From 02e50524132c3360f9f85ca1144399d55956d68a Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 4 Jul 2023 11:25:59 +0200 Subject: [PATCH 01/51] Initial work for v3 --- .editorconfig | 5 +- .gitattributes | 20 +- .github/FUNDING.yml | 1 - .github/ISSUE_TEMPLATE/bug.yml | 58 ++ .github/ISSUE_TEMPLATE/config.yml | 11 + .github/dependabot.yml | 12 + .github/workflows/dependabot-auto-merge.yml | 32 + ...ixer.yml => fix-php-code-style-issues.yml} | 20 +- .github/workflows/psalm.yml | 33 - .github/workflows/run-tests.yml | 25 +- .github/workflows/update-changelog.yml | 5 +- .gitignore | 13 +- .php_cs.dist.php | 40 -- CHANGELOG.md | 118 +--- LICENSE.md | 2 +- README.md | 66 +- UPGRADE.md | 24 - composer.json | 52 +- docs/_index.md | 6 - docs/about-us.md | 11 - docs/changelog.md | 8 - docs/dtos/_index.md | 4 - docs/dtos/transforming-dtos.md | 34 -- docs/dtos/typing-properties.md | 279 --------- docs/images/header.jpg | Bin 530140 -> 0 bytes docs/installation.md | 14 - docs/introduction.md | 45 -- docs/laravel/_index.md | 4 - .../executing-the-transform-command.md | 32 - docs/laravel/installation-and-setup.md | 100 --- docs/postcardware.md | 10 - docs/questions-and-issues.md | 8 - docs/transformers/_index.md | 4 - docs/transformers/getting-started.md | 122 ---- docs/transformers/type-processors.md | 134 ----- docs/transformers/type-reflectors.md | 79 --- docs/under-the-hood.md | 48 -- docs/usage/_index.md | 4 - docs/usage/annotations.md | 280 --------- docs/usage/formatters.md | 23 - docs/usage/general-overview.md | 567 ------------------ docs/usage/getting-started.md | 30 - .../selecting-classes-using-collectors.md | 43 -- docs/usage/using-transformers.md | 64 -- docs/usage/writers.md | 96 --- package.json | 5 + psalm.xml | 15 - src/Actions/AppendDefaultTypesAction.php | 36 ++ src/Actions/ConnectReferencesAction.php | 58 ++ src/Actions/DiscoverTypesAction.php | 32 + src/Actions/FindClassNameFqcnAction.php | 53 ++ src/Actions/FormatFilesAction.php | 25 + src/Actions/FormatTypeScriptAction.php | 26 - src/Actions/ParseUseDefinitionsAction.php | 46 ++ src/Actions/PersistTypesCollectionAction.php | 40 -- .../ReplaceSymbolsInCollectionAction.php | 19 - src/Actions/ReplaceSymbolsInTypeAction.php | 61 -- src/Actions/ResolveClassesInPhpFileAction.php | 48 -- src/Actions/ResolveRelativePathAction.php | 46 ++ src/Actions/ResolveTypesCollectionAction.php | 86 --- .../SplitTransformedPerLocationAction.php | 39 ++ src/Actions/TransformTypesAction.php | 67 +++ ...spilePhpStanTypeToTypeScriptTypeAction.php | 217 +++++++ ...leReflectionTypeToTypeScriptTypeAction.php | 127 ++++ .../TranspileTypeToTypeScriptAction.php | 140 ----- src/Actions/VisitTypeScriptTreeAction.php | 29 + src/Actions/WriteTypesAction.php | 26 + src/Attributes/Hidden.php | 10 - src/Attributes/InlineTypeScriptType.php | 10 - src/Attributes/RecordTypeScriptType.php | 27 - .../TypeScriptTransformableAttribute.php | 4 +- src/ClassReader.php | 64 -- src/Collections/ReferenceMap.php | 33 + src/Collectors/Collector.php | 19 - src/Collectors/DefaultCollector.php | 99 --- src/Collectors/EnumCollector.php | 38 -- .../DefaultTypesProvider.php | 11 + src/Exceptions/CircularDependencyChain.php | 13 - src/Exceptions/InvalidDefaultTypeReplacer.php | 13 - src/Exceptions/InvalidTransformerGiven.php | 19 - .../NoAutoDiscoverTypesPathsDefined.php | 13 - src/Exceptions/SymbolAlreadyExists.php | 22 - src/Exceptions/TransformerNotFound.php | 14 - .../UnableToTransformUsingAttribute.php | 15 - src/Formatters/EslintFormatter.php | 19 - src/Formatters/Formatter.php | 8 - src/Formatters/PrettierFormatter.php | 19 - .../Commands/TransformTypeScriptCommand.php | 35 ++ src/Laravel/LaravelDefaultTypesProvider.php | 158 +++++ .../LaravelTypeScriptTransformerConfig.php | 10 + .../SpatieLaravelDefaultTypesProvider.php | 43 ++ .../TypeScriptTransformerServiceProvider.php | 34 ++ src/References/ClassStringReference.php | 24 + src/References/FunctionReference.php | 22 + src/References/Reference.php | 10 + src/References/ReflectionClassReference.php | 14 + src/Structures/MissingSymbolsCollection.php | 36 -- src/Structures/TransformedType.php | 111 ---- src/Structures/TypesCollection.php | 106 ---- src/Support/Location.php | 18 + src/Support/TransformationContext.php | 12 + src/Support/TypeScriptTransformerLog.php | 24 + src/Support/WritingContext.php | 17 + src/Support/WrittenFile.php | 12 + src/Transformed/Transformed.php | 29 + src/Transformed/Untransformable.php | 17 + src/Transformers/ClassTransformer.php | 139 +++++ src/Transformers/DataClassTransformer.php | 57 ++ src/Transformers/DtoTransformer.php | 121 ---- src/Transformers/EnumTransformer.php | 114 ++-- src/Transformers/InterfaceTransformer.php | 72 --- src/Transformers/MyclabsEnumTransformer.php | 62 -- src/Transformers/SpatieEnumTransformer.php | 62 -- src/Transformers/Transformer.php | 6 +- src/Transformers/TransformsTypes.php | 72 --- .../DtoCollectionTypeProcessor.php | 43 -- src/TypeProcessors/ProcessesTypes.php | 82 --- .../ReplaceDefaultsTypeProcessor.php | 43 -- src/TypeProcessors/TypeProcessor.php | 18 - src/TypeReflectors/ClassTypeReflector.php | 155 ----- .../MethodParameterTypeReflector.php | 39 -- .../MethodReturnTypeReflector.php | 39 -- src/TypeReflectors/PropertyTypeReflector.php | 39 -- src/TypeReflectors/TypeReflector.php | 165 ----- src/TypeResolvers/Data/ParsedClass.php | 14 + src/TypeResolvers/Data/ParsedMethod.php | 17 + src/TypeResolvers/Data/ParsedNameAndType.php | 14 + src/TypeResolvers/DocTypeResolver.php | 127 ++++ src/TypeScript/TypeReference.php | 26 + src/TypeScript/TypeScriptAlias.php | 24 + src/TypeScript/TypeScriptAny.php | 13 + src/TypeScript/TypeScriptArray.php | 25 + src/TypeScript/TypeScriptBoolean.php | 13 + src/TypeScript/TypeScriptEnum.php | 27 + src/TypeScript/TypeScriptExport.php | 23 + src/TypeScript/TypeScriptFunction.php | 13 + src/TypeScript/TypeScriptGeneric.php | 35 ++ src/TypeScript/TypeScriptIdentifier.php | 19 + src/TypeScript/TypeScriptImport.php | 22 + src/TypeScript/TypeScriptIndexSignature.php | 24 + src/TypeScript/TypeScriptInterface.php | 38 ++ src/TypeScript/TypeScriptIntersection.php | 29 + src/TypeScript/TypeScriptLiteral.php | 17 + src/TypeScript/TypeScriptMethod.php | 33 + src/TypeScript/TypeScriptNamespace.php | 30 + src/TypeScript/TypeScriptNode.php | 11 + src/TypeScript/TypeScriptNodeWithChildren.php | 9 + src/TypeScript/TypeScriptNull.php | 13 + src/TypeScript/TypeScriptNumber.php | 13 + src/TypeScript/TypeScriptObject.php | 36 ++ src/TypeScript/TypeScriptParameter.php | 31 + src/TypeScript/TypeScriptProperty.php | 32 + src/TypeScript/TypeScriptRaw.php | 18 + src/TypeScript/TypeScriptString.php | 13 + src/TypeScript/TypeScriptUndefined.php | 13 + src/TypeScript/TypeScriptUnion.php | 41 ++ src/TypeScript/TypeScriptUnknown.php | 13 + src/TypeScript/TypeScriptVoid.php | 13 + src/TypeScriptTransformer.php | 56 +- src/TypeScriptTransformerConfig.php | 168 +----- src/Types/RecordType.php | 42 -- src/Types/StructType.php | 54 -- src/Types/TypeScriptType.php | 26 - src/Writers/ModuleWriter.php | 121 +++- src/Writers/NamespaceWriter.php | 75 +++ src/Writers/TypeDefinitionWriter.php | 73 --- src/Writers/Writer.php | 12 +- tests/Actions/FormatTypeScriptActionTest.php | 53 -- .../PersistTypesCollectionActionTest.php | 59 -- .../ReplaceSymbolsInCollectionActionTest.php | 42 -- .../ReplaceSymbolsInTypeActionTest.php | 104 ---- .../ResolveClassesInPhpFileActionTest.php | 37 -- .../Actions/ResolveRelativePathActionTest.php | 44 ++ .../ResolveTypesCollectionActionTest.php | 126 ---- ...ePhpStanTypeToTypeScriptTypeActionTest.php | 248 ++++++++ ...flectionTypeToTypeScriptTypeActionTest.php | 140 +++++ .../TranspileTypeToTypeScriptActionTest.php | 58 -- ...nTest__it_can_resolve_a_struct_type__1.txt | 1 - ...ctionTest__it_can_disable_formatting__1.ts | 1 - ...est__it_can_format_an_generated_file__1.ts | 2 - ...n_format_an_generated_file__1.ts_failed.ts | 1 - ...sist_multiple_types_in_one_namespace__1.ts | 6 - ...nActionTest__it_can_re_save_the_file__1.ts | 4 - ...ctionTest__it_will_persist_the_types__1.ts | 7 - .../Attributes/TransformAsTypescriptTest.php | 30 - tests/ClassReaderTest.php | 78 --- tests/Collectors/DefaultCollectorTest.php | 204 ------- tests/Collectors/EnumCollectorTest.php | 26 - tests/Datasets/ReflectionClasses.php | 102 ---- tests/Datasets/Types.php | 94 --- tests/ExampleTest.php | 5 + .../Annotations/FakeAnnotationsClass.php | 17 - ...thAlreadyTransformedAttributeAttribute.php | 12 - .../Attributes/WithTypeScriptAttribute.php | 13 - .../WithTypeScriptInlineAttribute.php | 15 - .../WithTypeScriptNamedAttribute.php | 13 - .../WithTypeScriptTransformerAttribute.php | 14 - .../BackedEnumWithoutAnnotation.php | 9 - .../FakeClasses/Collections/DtoCollection.php | 14 - .../Collections/NullableDtoCollection.php | 14 - .../Collections/StringDtoCollection.php | 13 - .../Collections/UntypedDtoCollection.php | 9 - tests/FakeClasses/Enum/RegularEnum.php | 10 - tests/FakeClasses/Enum/TypeScriptEnum.php | 11 - .../TypeScriptEnumWithCustomTransformer.php | 14 - .../Enum/TypeScriptEnumWithName.php | 11 - tests/FakeClasses/Finder/SomeClass.php | 7 - tests/FakeClasses/Finder/SomeEnum.php | 7 - tests/FakeClasses/Finder/SomeInterface.php | 7 - tests/FakeClasses/Finder/SomeTrait.php | 7 - tests/FakeClasses/IntBackedEnum.php | 12 - tests/FakeClasses/Integration/Dto.php | 77 --- .../Integration/DtoWithChildren.php | 16 - tests/FakeClasses/Integration/Enum.php | 12 - .../Integration/LevelUp/YetAnotherDto.php | 11 - tests/FakeClasses/Integration/OtherDto.php | 11 - .../Integration/OtherDtoCollection.php | 14 - tests/FakeClasses/SpatieEnum.php | 23 - tests/FakeClasses/StringBackedEnum.php | 12 - tests/FakeClasses/UnitEnum.php | 12 - tests/Fakes/FakeInterface.php | 10 - tests/Fakes/FakeReflectionClass.php | 46 -- tests/Fakes/FakeReflectionMethod.php | 10 - tests/Fakes/FakeReflectionProperty.php | 17 - tests/Fakes/FakeReflectionType.php | 64 -- tests/Fakes/FakeReflectionUnionType.php | 43 -- tests/Fakes/FakeTransformedType.php | 78 --- tests/Fakes/FakeTypeScriptCollector.php | 28 - tests/Fakes/FakeTypeScriptTransformer.php | 30 - tests/Fakes/FakedReflection.php | 77 --- tests/IntegrationTest.php | 57 -- tests/Pest.php | 18 - tests/Structures/TypesCollectionTest.php | 111 ---- tests/Stubs/PhpDocTypesStub.php | 108 ++++ tests/Stubs/PhpTypesStub.php | 46 ++ tests/Transformers/DtoTransformerTest.php | 148 ----- tests/Transformers/EnumTransformerTest.php | 115 ---- .../Transformers/InterfaceTransformerTest.php | 37 -- .../MyclabsEnumTransformerTest.php | 60 -- .../SpatieEnumTransformerTest.php | 58 -- ...rty_processor_can_remove_properties__1.txt | 1 - ...ype_processor_can_remove_properties__1.txt | 5 - ...o_optional_ones_according_to_config__1.txt | 3 - ..._ones_when_using_optional_attribute__1.txt | 3 - ...nsformerTest__it_will_replace_types__1.txt | 28 - ..._typescript_attributes_into_account__1.txt | 8 - ...formerTest__it_will_replace_methods__1.txt | 4 - .../DtoCollectionTypeProcessorTest.php | 75 --- tests/TypeProcessors/ProcessesTypesTest.php | 72 --- .../ReplaceDefaultsTypeProcessorTest.php | 51 -- .../TypeReflectors/ClassTypeReflectorTest.php | 23 - .../MethodParameterTypeReflectorTest.php | 116 ---- .../MethodReturnTypeReflectorTest.php | 143 ----- .../PropertyTypeReflectorTest.php | 103 ---- tests/TypeReflectors/TypeReflectorTest.php | 125 ---- tests/TypeScriptTransformerConfigTest.php | 70 --- tests/Types/RecordTypeTest.php | 46 -- tests/Types/StructTypeTest.php | 38 -- ...ype_processor_can_remove_properties__1.txt | 5 - ...ype_processor_can_remove_properties__1.txt | 5 - ...with_optional_attribute_to_optional__1.txt | 4 - ..._ones_when_using_optional_attribute__1.txt | 3 - ...en_ones_when_using_hidden_attribute__1.txt | 3 - ..._ones_when_using_optional_attribute__1.txt | 3 - ...nsformerTest__it_will_replace_types__1.txt | 29 - ..._typescript_attributes_into_account__1.txt | 8 - ...est__it_can_transform_to_es_modules__1.txt | 43 -- .../IntegrationTest__it_works__1.txt | 47 -- ...formerTest__it_will_replace_methods__1.txt | 4 - ...nTest__it_can_resolve_a_struct_type__1.txt | 1 - ...ctionTest__it_can_disable_formatting__1.ts | 1 - ...sist_multiple_types_in_one_namespace__1.ts | 6 - ...nActionTest__it_can_re_save_the_file__1.ts | 4 - ...ctionTest__it_will_persist_the_types__1.ts | 7 - yarn.lock | 8 + 275 files changed, 3478 insertions(+), 8490 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/dependabot-auto-merge.yml rename .github/workflows/{php-cs-fixer.yml => fix-php-code-style-issues.yml} (51%) delete mode 100644 .github/workflows/psalm.yml delete mode 100644 .php_cs.dist.php delete mode 100644 UPGRADE.md delete mode 100755 docs/_index.md delete mode 100755 docs/about-us.md delete mode 100755 docs/changelog.md delete mode 100755 docs/dtos/_index.md delete mode 100644 docs/dtos/transforming-dtos.md delete mode 100644 docs/dtos/typing-properties.md delete mode 100755 docs/images/header.jpg delete mode 100755 docs/installation.md delete mode 100755 docs/introduction.md delete mode 100755 docs/laravel/_index.md delete mode 100755 docs/laravel/executing-the-transform-command.md delete mode 100755 docs/laravel/installation-and-setup.md delete mode 100755 docs/postcardware.md delete mode 100755 docs/questions-and-issues.md delete mode 100755 docs/transformers/_index.md delete mode 100644 docs/transformers/getting-started.md delete mode 100644 docs/transformers/type-processors.md delete mode 100644 docs/transformers/type-reflectors.md delete mode 100644 docs/under-the-hood.md delete mode 100755 docs/usage/_index.md delete mode 100644 docs/usage/annotations.md delete mode 100644 docs/usage/formatters.md delete mode 100644 docs/usage/general-overview.md delete mode 100644 docs/usage/getting-started.md delete mode 100644 docs/usage/selecting-classes-using-collectors.md delete mode 100644 docs/usage/using-transformers.md delete mode 100644 docs/usage/writers.md create mode 100644 package.json delete mode 100644 psalm.xml create mode 100644 src/Actions/AppendDefaultTypesAction.php create mode 100644 src/Actions/ConnectReferencesAction.php create mode 100644 src/Actions/DiscoverTypesAction.php create mode 100644 src/Actions/FindClassNameFqcnAction.php create mode 100644 src/Actions/FormatFilesAction.php delete mode 100644 src/Actions/FormatTypeScriptAction.php create mode 100644 src/Actions/ParseUseDefinitionsAction.php delete mode 100644 src/Actions/PersistTypesCollectionAction.php delete mode 100644 src/Actions/ReplaceSymbolsInCollectionAction.php delete mode 100644 src/Actions/ReplaceSymbolsInTypeAction.php delete mode 100644 src/Actions/ResolveClassesInPhpFileAction.php create mode 100644 src/Actions/ResolveRelativePathAction.php delete mode 100644 src/Actions/ResolveTypesCollectionAction.php create mode 100644 src/Actions/SplitTransformedPerLocationAction.php create mode 100644 src/Actions/TransformTypesAction.php create mode 100644 src/Actions/TranspilePhpStanTypeToTypeScriptTypeAction.php create mode 100644 src/Actions/TranspileReflectionTypeToTypeScriptTypeAction.php delete mode 100644 src/Actions/TranspileTypeToTypeScriptAction.php create mode 100644 src/Actions/VisitTypeScriptTreeAction.php create mode 100644 src/Actions/WriteTypesAction.php delete mode 100644 src/Attributes/Hidden.php delete mode 100644 src/Attributes/InlineTypeScriptType.php delete mode 100644 src/Attributes/RecordTypeScriptType.php delete mode 100644 src/ClassReader.php create mode 100644 src/Collections/ReferenceMap.php delete mode 100644 src/Collectors/Collector.php delete mode 100644 src/Collectors/DefaultCollector.php delete mode 100644 src/Collectors/EnumCollector.php create mode 100644 src/DefaultTypeProviders/DefaultTypesProvider.php delete mode 100644 src/Exceptions/CircularDependencyChain.php delete mode 100644 src/Exceptions/InvalidDefaultTypeReplacer.php delete mode 100644 src/Exceptions/InvalidTransformerGiven.php delete mode 100644 src/Exceptions/NoAutoDiscoverTypesPathsDefined.php delete mode 100644 src/Exceptions/SymbolAlreadyExists.php delete mode 100644 src/Exceptions/TransformerNotFound.php delete mode 100644 src/Exceptions/UnableToTransformUsingAttribute.php delete mode 100644 src/Formatters/EslintFormatter.php delete mode 100644 src/Formatters/Formatter.php delete mode 100644 src/Formatters/PrettierFormatter.php create mode 100644 src/Laravel/Commands/TransformTypeScriptCommand.php create mode 100644 src/Laravel/LaravelDefaultTypesProvider.php create mode 100644 src/Laravel/LaravelTypeScriptTransformerConfig.php create mode 100644 src/Laravel/SpatieLaravelDefaultTypesProvider.php create mode 100644 src/Laravel/TypeScriptTransformerServiceProvider.php create mode 100644 src/References/ClassStringReference.php create mode 100644 src/References/FunctionReference.php create mode 100644 src/References/Reference.php create mode 100644 src/References/ReflectionClassReference.php delete mode 100644 src/Structures/MissingSymbolsCollection.php delete mode 100644 src/Structures/TransformedType.php delete mode 100644 src/Structures/TypesCollection.php create mode 100644 src/Support/Location.php create mode 100644 src/Support/TransformationContext.php create mode 100644 src/Support/TypeScriptTransformerLog.php create mode 100644 src/Support/WritingContext.php create mode 100644 src/Support/WrittenFile.php create mode 100644 src/Transformed/Transformed.php create mode 100644 src/Transformed/Untransformable.php create mode 100644 src/Transformers/ClassTransformer.php create mode 100644 src/Transformers/DataClassTransformer.php delete mode 100644 src/Transformers/DtoTransformer.php delete mode 100644 src/Transformers/InterfaceTransformer.php delete mode 100644 src/Transformers/MyclabsEnumTransformer.php delete mode 100644 src/Transformers/SpatieEnumTransformer.php delete mode 100644 src/Transformers/TransformsTypes.php delete mode 100644 src/TypeProcessors/DtoCollectionTypeProcessor.php delete mode 100644 src/TypeProcessors/ProcessesTypes.php delete mode 100644 src/TypeProcessors/ReplaceDefaultsTypeProcessor.php delete mode 100644 src/TypeProcessors/TypeProcessor.php delete mode 100644 src/TypeReflectors/ClassTypeReflector.php delete mode 100644 src/TypeReflectors/MethodParameterTypeReflector.php delete mode 100644 src/TypeReflectors/MethodReturnTypeReflector.php delete mode 100644 src/TypeReflectors/PropertyTypeReflector.php delete mode 100644 src/TypeReflectors/TypeReflector.php create mode 100644 src/TypeResolvers/Data/ParsedClass.php create mode 100644 src/TypeResolvers/Data/ParsedMethod.php create mode 100644 src/TypeResolvers/Data/ParsedNameAndType.php create mode 100644 src/TypeResolvers/DocTypeResolver.php create mode 100644 src/TypeScript/TypeReference.php create mode 100644 src/TypeScript/TypeScriptAlias.php create mode 100644 src/TypeScript/TypeScriptAny.php create mode 100644 src/TypeScript/TypeScriptArray.php create mode 100644 src/TypeScript/TypeScriptBoolean.php create mode 100644 src/TypeScript/TypeScriptEnum.php create mode 100644 src/TypeScript/TypeScriptExport.php create mode 100644 src/TypeScript/TypeScriptFunction.php create mode 100644 src/TypeScript/TypeScriptGeneric.php create mode 100644 src/TypeScript/TypeScriptIdentifier.php create mode 100644 src/TypeScript/TypeScriptImport.php create mode 100644 src/TypeScript/TypeScriptIndexSignature.php create mode 100644 src/TypeScript/TypeScriptInterface.php create mode 100644 src/TypeScript/TypeScriptIntersection.php create mode 100644 src/TypeScript/TypeScriptLiteral.php create mode 100644 src/TypeScript/TypeScriptMethod.php create mode 100644 src/TypeScript/TypeScriptNamespace.php create mode 100644 src/TypeScript/TypeScriptNode.php create mode 100644 src/TypeScript/TypeScriptNodeWithChildren.php create mode 100644 src/TypeScript/TypeScriptNull.php create mode 100644 src/TypeScript/TypeScriptNumber.php create mode 100644 src/TypeScript/TypeScriptObject.php create mode 100644 src/TypeScript/TypeScriptParameter.php create mode 100644 src/TypeScript/TypeScriptProperty.php create mode 100644 src/TypeScript/TypeScriptRaw.php create mode 100644 src/TypeScript/TypeScriptString.php create mode 100644 src/TypeScript/TypeScriptUndefined.php create mode 100644 src/TypeScript/TypeScriptUnion.php create mode 100644 src/TypeScript/TypeScriptUnknown.php create mode 100644 src/TypeScript/TypeScriptVoid.php mode change 100644 => 100755 src/TypeScriptTransformerConfig.php delete mode 100644 src/Types/RecordType.php delete mode 100644 src/Types/StructType.php delete mode 100644 src/Types/TypeScriptType.php create mode 100644 src/Writers/NamespaceWriter.php delete mode 100644 src/Writers/TypeDefinitionWriter.php delete mode 100644 tests/Actions/FormatTypeScriptActionTest.php delete mode 100644 tests/Actions/PersistTypesCollectionActionTest.php delete mode 100644 tests/Actions/ReplaceSymbolsInCollectionActionTest.php delete mode 100644 tests/Actions/ReplaceSymbolsInTypeActionTest.php delete mode 100644 tests/Actions/ResolveClassesInPhpFileActionTest.php create mode 100644 tests/Actions/ResolveRelativePathActionTest.php delete mode 100644 tests/Actions/ResolveTypesCollectionActionTest.php create mode 100644 tests/Actions/TranspilePhpStanTypeToTypeScriptTypeActionTest.php create mode 100644 tests/Actions/TranspileReflectionTypeToTypeScriptTypeActionTest.php delete mode 100644 tests/Actions/TranspileTypeToTypeScriptActionTest.php delete mode 100644 tests/Actions/__snapshots__/TranspileTypeToTypeScriptActionTest__it_can_resolve_a_struct_type__1.txt delete mode 100644 tests/Actions/__snapshots__/files/FormatTypeScriptActionTest__it_can_disable_formatting__1.ts delete mode 100644 tests/Actions/__snapshots__/files/FormatTypeScriptActionTest__it_can_format_an_generated_file__1.ts delete mode 100644 tests/Actions/__snapshots__/files/FormatTypeScriptActionTest__it_can_format_an_generated_file__1.ts_failed.ts delete mode 100644 tests/Actions/__snapshots__/files/PersistTypesCollectionActionTest__it_can_persist_multiple_types_in_one_namespace__1.ts delete mode 100644 tests/Actions/__snapshots__/files/PersistTypesCollectionActionTest__it_can_re_save_the_file__1.ts delete mode 100644 tests/Actions/__snapshots__/files/PersistTypesCollectionActionTest__it_will_persist_the_types__1.ts delete mode 100644 tests/Attributes/TransformAsTypescriptTest.php delete mode 100644 tests/ClassReaderTest.php delete mode 100644 tests/Collectors/DefaultCollectorTest.php delete mode 100644 tests/Collectors/EnumCollectorTest.php delete mode 100644 tests/Datasets/ReflectionClasses.php delete mode 100644 tests/Datasets/Types.php create mode 100644 tests/ExampleTest.php delete mode 100644 tests/FakeClasses/Annotations/FakeAnnotationsClass.php delete mode 100644 tests/FakeClasses/Attributes/WithAlreadyTransformedAttributeAttribute.php delete mode 100644 tests/FakeClasses/Attributes/WithTypeScriptAttribute.php delete mode 100644 tests/FakeClasses/Attributes/WithTypeScriptInlineAttribute.php delete mode 100644 tests/FakeClasses/Attributes/WithTypeScriptNamedAttribute.php delete mode 100644 tests/FakeClasses/Attributes/WithTypeScriptTransformerAttribute.php delete mode 100644 tests/FakeClasses/BackedEnumWithoutAnnotation.php delete mode 100644 tests/FakeClasses/Collections/DtoCollection.php delete mode 100644 tests/FakeClasses/Collections/NullableDtoCollection.php delete mode 100644 tests/FakeClasses/Collections/StringDtoCollection.php delete mode 100644 tests/FakeClasses/Collections/UntypedDtoCollection.php delete mode 100644 tests/FakeClasses/Enum/RegularEnum.php delete mode 100644 tests/FakeClasses/Enum/TypeScriptEnum.php delete mode 100644 tests/FakeClasses/Enum/TypeScriptEnumWithCustomTransformer.php delete mode 100644 tests/FakeClasses/Enum/TypeScriptEnumWithName.php delete mode 100644 tests/FakeClasses/Finder/SomeClass.php delete mode 100644 tests/FakeClasses/Finder/SomeEnum.php delete mode 100644 tests/FakeClasses/Finder/SomeInterface.php delete mode 100644 tests/FakeClasses/Finder/SomeTrait.php delete mode 100644 tests/FakeClasses/IntBackedEnum.php delete mode 100644 tests/FakeClasses/Integration/Dto.php delete mode 100644 tests/FakeClasses/Integration/DtoWithChildren.php delete mode 100644 tests/FakeClasses/Integration/Enum.php delete mode 100644 tests/FakeClasses/Integration/LevelUp/YetAnotherDto.php delete mode 100644 tests/FakeClasses/Integration/OtherDto.php delete mode 100644 tests/FakeClasses/Integration/OtherDtoCollection.php delete mode 100644 tests/FakeClasses/SpatieEnum.php delete mode 100644 tests/FakeClasses/StringBackedEnum.php delete mode 100644 tests/FakeClasses/UnitEnum.php delete mode 100644 tests/Fakes/FakeInterface.php delete mode 100644 tests/Fakes/FakeReflectionClass.php delete mode 100644 tests/Fakes/FakeReflectionMethod.php delete mode 100644 tests/Fakes/FakeReflectionProperty.php delete mode 100644 tests/Fakes/FakeReflectionType.php delete mode 100644 tests/Fakes/FakeReflectionUnionType.php delete mode 100644 tests/Fakes/FakeTransformedType.php delete mode 100644 tests/Fakes/FakeTypeScriptCollector.php delete mode 100644 tests/Fakes/FakeTypeScriptTransformer.php delete mode 100644 tests/Fakes/FakedReflection.php delete mode 100644 tests/IntegrationTest.php delete mode 100644 tests/Structures/TypesCollectionTest.php create mode 100644 tests/Stubs/PhpDocTypesStub.php create mode 100644 tests/Stubs/PhpTypesStub.php delete mode 100644 tests/Transformers/DtoTransformerTest.php delete mode 100644 tests/Transformers/EnumTransformerTest.php delete mode 100644 tests/Transformers/InterfaceTransformerTest.php delete mode 100644 tests/Transformers/MyclabsEnumTransformerTest.php delete mode 100644 tests/Transformers/SpatieEnumTransformerTest.php delete mode 100644 tests/Transformers/__snapshots__/DtoTransformerTest__a_property_processor_can_remove_properties__1.txt delete mode 100644 tests/Transformers/__snapshots__/DtoTransformerTest__a_type_processor_can_remove_properties__1.txt delete mode 100644 tests/Transformers/__snapshots__/DtoTransformerTest__it_transforms_nullable_properties_to_optional_ones_according_to_config__1.txt delete mode 100644 tests/Transformers/__snapshots__/DtoTransformerTest__it_transforms_nullable_properties_to_optional_ones_when_using_optional_attribute__1.txt delete mode 100644 tests/Transformers/__snapshots__/DtoTransformerTest__it_will_replace_types__1.txt delete mode 100644 tests/Transformers/__snapshots__/DtoTransformerTest__it_will_take_transform_as_typescript_attributes_into_account__1.txt delete mode 100644 tests/Transformers/__snapshots__/InterfaceTransformerTest__it_will_replace_methods__1.txt delete mode 100644 tests/TypeProcessors/DtoCollectionTypeProcessorTest.php delete mode 100644 tests/TypeProcessors/ProcessesTypesTest.php delete mode 100644 tests/TypeProcessors/ReplaceDefaultsTypeProcessorTest.php delete mode 100644 tests/TypeReflectors/ClassTypeReflectorTest.php delete mode 100644 tests/TypeReflectors/MethodParameterTypeReflectorTest.php delete mode 100644 tests/TypeReflectors/MethodReturnTypeReflectorTest.php delete mode 100644 tests/TypeReflectors/PropertyTypeReflectorTest.php delete mode 100644 tests/TypeReflectors/TypeReflectorTest.php delete mode 100644 tests/TypeScriptTransformerConfigTest.php delete mode 100644 tests/Types/RecordTypeTest.php delete mode 100644 tests/Types/StructTypeTest.php delete mode 100644 tests/__snapshots__/DtoTransformerTest__a_type_processor_can_remove_properties__1.txt delete mode 100644 tests/__snapshots__/DtoTransformerTest__it_a_type_processor_can_remove_properties__1.txt delete mode 100644 tests/__snapshots__/DtoTransformerTest__it_transforms_all_properties_of_a_class_with_optional_attribute_to_optional__1.txt delete mode 100644 tests/__snapshots__/DtoTransformerTest__it_transforms_nullable_properties_to_optional_ones_when_using_optional_attribute__1.txt delete mode 100644 tests/__snapshots__/DtoTransformerTest__it_transforms_properties_to_hidden_ones_when_using_hidden_attribute__1.txt delete mode 100644 tests/__snapshots__/DtoTransformerTest__it_transforms_properties_to_optional_ones_when_using_optional_attribute__1.txt delete mode 100644 tests/__snapshots__/DtoTransformerTest__it_will_replace_types__1.txt delete mode 100644 tests/__snapshots__/DtoTransformerTest__it_will_take_transform_as_typescript_attributes_into_account__1.txt delete mode 100644 tests/__snapshots__/IntegrationTest__it_can_transform_to_es_modules__1.txt delete mode 100644 tests/__snapshots__/IntegrationTest__it_works__1.txt delete mode 100644 tests/__snapshots__/InterfaceTransformerTest__it_will_replace_methods__1.txt delete mode 100644 tests/__snapshots__/TranspileTypeToTypeScriptActionTest__it_can_resolve_a_struct_type__1.txt delete mode 100644 tests/__snapshots__/files/FormatTypeScriptActionTest__it_can_disable_formatting__1.ts delete mode 100644 tests/__snapshots__/files/PersistTypesCollectionActionTest__it_can_persist_multiple_types_in_one_namespace__1.ts delete mode 100644 tests/__snapshots__/files/PersistTypesCollectionActionTest__it_can_re_save_the_file__1.ts delete mode 100644 tests/__snapshots__/files/PersistTypesCollectionActionTest__it_will_persist_the_types__1.ts create mode 100644 yarn.lock diff --git a/.editorconfig b/.editorconfig index 2a5ddbae..a7c44ddb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,3 @@ -; This file is for unifying the coding style for different editors and IDEs. -; More information at https://editorconfig.org - root = true [*] @@ -14,5 +11,5 @@ trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false -[*.yml] +[*.{yml,yaml}] indent_size = 2 diff --git a/.gitattributes b/.gitattributes index 7d1de53a..aa8ebc7f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,12 +2,14 @@ # https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html # Ignore all test and documentation with "export-ignore". -/.gitattributes export-ignore -/.gitignore export-ignore -/.travis.yml export-ignore -/phpunit.xml.dist export-ignore -/tests export-ignore -/.editorconfig export-ignore -/.php.cs export-ignore -/.github export-ignore - +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/phpunit.xml.dist export-ignore +/psalm.xml.dist export-ignore +/tests export-ignore +/.editorconfig export-ignore +/.php-cs-fixer.dist.php export-ignore +/art export-ignore +/docs export-ignore +/UPGRADING.md export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index fe5143b5..5ccc87cf 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1 @@ github: spatie -custom: https://spatie.be/open-source/support-us diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 00000000..a9933e19 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,58 @@ +name: Bug Report +description: Report an Issue or Bug with the Package +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + We're sorry to hear you have a problem. Can you help us solve it by providing the following details. + - type: textarea + id: what-happened + attributes: + label: What happened? + description: What did you expect to happen? + placeholder: I cannot currently do X thing because when I do, it breaks X thing. + validations: + required: true + - type: textarea + id: how-to-reproduce + attributes: + label: How to reproduce the bug + description: How did this occur, please add any config values used and provide a set of reliable steps if possible. + placeholder: When I do X I see Y. + validations: + required: true + - type: input + id: package-version + attributes: + label: Package Version + description: What version of our Package are you running? Please be as specific as possible + placeholder: 2.0.0 + validations: + required: true + - type: input + id: php-version + attributes: + label: PHP Version + description: What version of PHP are you running? Please be as specific as possible + placeholder: 8.2.0 + validations: + required: true + - type: dropdown + id: operating-systems + attributes: + label: Which operating systems does with happen with? + description: You may select more than one. + multiple: true + options: + - macOS + - Windows + - Linux + - type: textarea + id: notes + attributes: + label: Notes + description: Use this field to provide any other notes that you feel might be relevant to the issue. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..461d1551 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a question + url: https://github.com/spatie/typescript-transformer/discussions/new?category=q-a + about: Ask the community for help + - name: Request a feature + url: https://github.com/spatie/typescript-transformer/discussions/new?category=ideas + about: Share ideas for new features + - name: Report a security issue + url: https://github.com/spatie/typescript-transformer/security/policy + about: Learn how to notify us for sensitive bugs diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..30c8a493 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" \ No newline at end of file diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 00000000..4af8e6b6 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,32 @@ +name: dependabot-auto-merge +on: pull_request_target + +permissions: + pull-requests: write + contents: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v1.5.1 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Auto-merge Dependabot PRs for semver-minor updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Auto-merge Dependabot PRs for semver-patch updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/fix-php-code-style-issues.yml similarity index 51% rename from .github/workflows/php-cs-fixer.yml rename to .github/workflows/fix-php-code-style-issues.yml index f55d1fa8..2f32b5f5 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -1,21 +1,25 @@ -name: Check & fix styling +name: Fix PHP code style issues -on: [push] +on: + push: + paths: + - '**.php' + +permissions: + contents: write jobs: - php-cs-fixer: + php-code-styling: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: ref: ${{ github.head_ref }} - - name: Run PHP CS Fixer - uses: docker://oskarstark/php-cs-fixer-ga - with: - args: --config=.php_cs.dist.php --allow-risky=yes + - name: Fix PHP code style issues + uses: aglipanci/laravel-pint-action@1.0.0 - name: Commit changes uses: stefanzweifel/git-auto-commit-action@v4 diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml deleted file mode 100644 index a2c9a01d..00000000 --- a/.github/workflows/psalm.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Psalm - -on: - push: - paths: - - '**.php' - - 'psalm.xml' - -jobs: - psalm: - name: psalm - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.2' - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick - coverage: none - - - name: Cache composer dependencies - uses: actions/cache@v3 - with: - path: vendor - key: composer-${{ hashFiles('composer.lock') }} - - - name: Run composer install - run: composer install -n --prefer-dist - - - name: Run psalm - run: ./vendor/bin/psalm -c psalm.xml diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index f39087f2..25ca5537 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,4 +1,4 @@ -name: run-tests +name: Tests on: [push, pull_request] @@ -8,31 +8,30 @@ jobs: strategy: fail-fast: true matrix: - os: [ubuntu-latest] - php: [8.0, 8.1, 8.2] - dependency-version: [prefer-lowest, prefer-stable] + os: [ubuntu-latest, windows-latest] + php: [8.2, 8.1] + stability: [prefer-lowest, prefer-stable] - name: P${{ matrix.php }} - ${{ matrix.dependency-version }} - ${{ matrix.os }} + name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} steps: - name: Checkout code uses: actions/checkout@v3 - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: ~/.composer/cache/files - key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} - - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo coverage: none + - name: Setup problem matchers + run: | + echo "::add-matcher::${{ runner.tool_cache }}/php.json" + echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: Install dependencies - run: composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction + run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction - name: Execute tests run: vendor/bin/pest diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index fa56639f..8c12ba9e 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -4,13 +4,16 @@ on: release: types: [released] +permissions: + contents: write + jobs: update: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: ref: main diff --git a/.gitignore b/.gitignore index 192204ae..841e6e5a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,13 @@ +.idea +.php_cs +.php_cs.cache +.phpunit.result.cache build composer.lock -vendor coverage -.phpunit.result.cache -.idea -.php_cs.cache +docs +phpunit.xml +psalm.xml +vendor .php-cs-fixer.cache + diff --git a/.php_cs.dist.php b/.php_cs.dist.php deleted file mode 100644 index 3de28fd4..00000000 --- a/.php_cs.dist.php +++ /dev/null @@ -1,40 +0,0 @@ -in([ - __DIR__ . '/src', - __DIR__ . '/tests', - ]) - ->name('*.php') - ->notName('*.blade.php') - ->ignoreDotFiles(true) - ->ignoreVCS(true); - -return (new PhpCsFixer\Config()) - ->setRules([ - '@PSR2' => true, - 'array_syntax' => ['syntax' => 'short'], - 'ordered_imports' => ['sort_algorithm' => 'alpha'], - 'no_unused_imports' => true, - 'not_operator_with_successor_space' => true, - 'trailing_comma_in_multiline' => true, - 'phpdoc_scalar' => true, - 'unary_operator_spaces' => true, - 'binary_operator_spaces' => true, - 'blank_line_before_statement' => [ - 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], - ], - 'phpdoc_single_line_var_spacing' => true, - 'phpdoc_var_without_name' => true, - 'class_attributes_separation' => [ - 'elements' => [ - 'method' => 'one', - ], - ], - 'method_argument_space' => [ - 'on_multiline' => 'ensure_fully_multiline', - 'keep_multiple_spaces_after_comma' => true, - ], - 'single_trait_insert_per_statement' => true, - ]) - ->setFinder($finder); diff --git a/CHANGELOG.md b/CHANGELOG.md index dd6add0b..0faddee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,120 +1,4 @@ # Changelog -All notable changes to `typescript-transformer` will be documented in this file +All notable changes to `typescript-transformer` will be documented in this file. -## 2.2.0 - 2023-06-02 - -- Add support for hidden properties (#54) - -## 2.1.14 - 2023-04-07 - -- add support for record types (#51) - -## 2.1.13 - 2023-02-01 - -- Add EnumCollector (#42) -- Ensure transformed types are unique (#44) - -## 2.1.12 - 2022-11-18 - -- add support for optional attributes (#30) -- refactor tests to Pest (#39) - -## 2.1.11 - 2022-09-28 - -- fix: Support Collection with array-key key type (#38) - -## 2.1.10 - 2022-07-04 - -- Allow non fully qualified names within annotations - -## 2.1.9 - 2022-06-29 - -- allow transformation of interfaces (#32) - -## 2.1.8 - 2022-04-29 - -- add eslint formatter(#28) -- let prettier formatter use `npx` (#29) - -## 2.1.7 - 2022-04-06 - -- Allow whitespace in type definitions (#27 ) - -## 2.1.6 - 2022-01-05 - -- fix the transformation of PHP native enums - -## 2.1.5 - 2021-12-29 - -## What's Changed - -- Make compatible with Symfony 6.0 Process component by @firstred in https://github.com/spatie/typescript-transformer/pull/17 - -## New Contributors - -- @firstred made their first contribution in https://github.com/spatie/typescript-transformer/pull/17 - -**Full Changelog**: https://github.com/spatie/typescript-transformer/compare/2.1.4...2.1.5 - -## 2.1.4 - 2021-12-23 - -- allow interfaces in default type replacements - -## 2.1.3 - 2021-12-16 - -- add support for transforming to native TypeScript enums - -## 2.1.2 - 2021-12-16 - -- fix deprecations - -## 2.1.1 - 2021-12-08 - -- add support for PHP 8.1 (#15) - -## 2.1.0 - 2021-04-08 - -- Remove classtools dependency -- Add support for PHP 8.1 enums (#12) -- Add `declare` keyword by default to generated output (#13) - -## 2.0.3 - 2021-07-09 - -- Fix `ProcessTypes` to work with Collection types - -## 2.0.2 - 2021-06-30 - -- Fix default collector with missing symbols in attributes - -## 2.0.1 - 2021-04-14 - -- Allow spatie/temporary-directory v2 on dev - -## 2.0.0 - 2021-04-08 - -- The package is now PHP 8 only -- Added TypeReflectors to reflect method return types, method parameters & class properties within your transformers -- Added support for attributes -- Added support for manually adding TypeScript to a class or property -- Added formatters like Prettier which can format TypeScript code -- Added support for inlining types directly -- Updated the DtoTransformer to be a lot more flexible for your own projects -- Added support for PHP 8 union types - -## 1.1.2 - 2021-01-07 - -- Add support for `Writers` (#7) - -## 1.1.1 - 2020-11-26 - -- Add PHP8 support - -## 1.1.0 - 2020-11-26 - -- Fix some capitalization in namespace names -- Added `SpatieEnumTransformer` from the `laravel-typescript-transformer` package - -## 1.0.0 - 2020-09-02 - -- initial release diff --git a/LICENSE.md b/LICENSE.md index 59e5ec59..29f58637 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) Spatie bvba +Copyright (c) spatie Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 62619a05..8a36235c 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,10 @@ - -[](https://supportukrainenow.org) - -# Transform PHP types to TypeScript +# This is my package typescript-transformer [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/typescript-transformer.svg?style=flat-square)](https://packagist.org/packages/spatie/typescript-transformer) -[![Tests](https://github.com/spatie/typescript-transformer/workflows/run-tests/badge.svg)](https://github.com/spatie/typescript-transformer/actions?query=workflow%3Arun-tests) -[![Styling](https://github.com/spatie/typescript-transformer/workflows/Check%20&%20fix%20styling/badge.svg)](https://github.com/spatie/typescript-transformer/actions?query=workflow%3A%22Check+%26+fix+styling%22) -[![Psalm](https://github.com/spatie/typescript-transformer/workflows/Psalm/badge.svg)](https://github.com/spatie/typescript-transformer/actions?query=workflow%3APsalm) +[![Tests](https://img.shields.io/github/actions/workflow/status/spatie/typescript-transformer/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/spatie/typescript-transformer/actions/workflows/run-tests.yml) [![Total Downloads](https://img.shields.io/packagist/dt/spatie/typescript-transformer.svg?style=flat-square)](https://packagist.org/packages/spatie/typescript-transformer) -This package allows you to convert PHP classes to TypeScript. - -This class... - -```php -/** @typescript */ -class User -{ - public int $id; - public string $name; - public ?string $address; -} -``` - -... will be converted to this TypeScript type: - -```ts -export type User = { - id: number; - name: string; - address: string | null; -} -``` - -Here's another example. - -```php -class Languages extends Enum -{ - const TYPESCRIPT = 'typescript'; - const PHP = 'php'; -} -``` - -The `Languages` enum will be converted to: - -```tsx -export type Languages = 'typescript' | 'php'; -``` - -You can find the full documentation [here](https://docs.spatie.be/typescript-transformer/v2/introduction/). +This is where your description should go. Try and limit it to a paragraph or two. Consider adding a small example. ## Support us @@ -61,15 +16,22 @@ We highly appreciate you sending us a postcard from your hometown, mentioning wh ## Installation -You can install this package via composer: +You can install the package via composer: ```bash composer require spatie/typescript-transformer ``` +## Usage + +```php +$skeleton = new Spatie\TypescriptTransformer(); +echo $skeleton->echoPhrase('Hello, Spatie!'); +``` + ## Testing -``` bash +```bash composer test ``` @@ -81,9 +43,9 @@ Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed re Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. -## Security +## Security Vulnerabilities -If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. +Please review [our security policy](../../security/policy) on how to report security vulnerabilities. ## Credits diff --git a/UPGRADE.md b/UPGRADE.md deleted file mode 100644 index ba195dcb..00000000 --- a/UPGRADE.md +++ /dev/null @@ -1,24 +0,0 @@ -# Upgrading to v2 - -- The package is now PHP 8 only -- The `ClassPropertyProcessor` interface was renamed to `TypeProcessor` and now takes a union of reflection objects -- In the config: - - `searchingPath` was renamed to `autoDiscoverTypes` - - `classPropertyReplacements` was renamed to `defaultTypeReplacements` -- Collectors now only have one method: `getTransformedType` which should - - return `null` when the collector cannot find a transformer - - return a `TransformedType` from a suitable transformer -- Transformers now only have one method: `transform` which should - - return `null` when the transformer cannot transform the class - - return a `TransformedType` if it can transform the class -- In Writers the `replaceMissingSymbols` method was removed and a `replacesSymbolsWithFullyQualifiedIdentifiers` with `bool` as return type was added -- The DTO transformer was completely rewritten, please take a look at the docs how to create you own -- The step classes are now renamed to actions - -Laravel -- In the Laravel config: - - `searching_path` is renamed to `auto_discover_types` - - `class_property_replacements` is renamed to `default_type_relacements` - - `writer` and `formatter` were added -- You should replace the `DefaultCollector::class` with the `DefaultCollector::class` -- It is not possible anymore to convert one file to TypeScript via command diff --git a/composer.json b/composer.json index 53844ad9..5ce3ca69 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "spatie/typescript-transformer", - "description": "Transform your PHP structures to TypeScript types", + "description": "This is my package typescript-transformer", "keywords": [ "spatie", "typescript-transformer" @@ -11,48 +11,58 @@ { "name": "Ruben Van Assche", "email": "ruben@spatie.be", - "homepage": "https://spatie.be", "role": "Developer" } ], "require": { - "php": "^8.0", - "nikic/php-parser": "^4.13", - "phpdocumentor/type-resolver": "^1.6.2", - "symfony/process": "^5.2|^6.0" + "php": "^8.2", + "illuminate/contracts": "^10.0", + "spatie/laravel-package-tools": "^1.14.0", + "phpstan/phpdoc-parser": "^1.13", + "spatie/php-structure-discoverer": "^1.1" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.18", - "larapack/dd": "^1.1", - "myclabs/php-enum": "^1.7", - "pestphp/pest": "^1.22", - "phpunit/phpunit": "^9.0", - "spatie/data-transfer-object": "^2.0", - "spatie/enum": "^3.0", - "spatie/pest-plugin-snapshots": "^1.1", - "spatie/temporary-directory": "^1.2|^2.0", - "vimeo/psalm": "^4.2" + "laravel/pint": "^1.0", + "nunomaduro/collision": "^7.9", + "nunomaduro/larastan": "^2.0.1", + "orchestra/testbench": "^8.0", + "pestphp/pest": "^2.0", + "pestphp/pest-plugin-arch": "^2.0", + "pestphp/pest-plugin-laravel": "^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "spatie/laravel-ray": "^1.26" }, "autoload": { "psr-4": { - "Spatie\\TypeScriptTransformer\\": "src" + "Spatie\\TypeScriptTransformer\\": "src/" } }, "autoload-dev": { "psr-4": { - "Spatie\\TypeScriptTransformer\\Tests\\": "tests" + "Spatie\\TypeScriptTransformer\\Tests\\": "tests/" } }, "scripts": { + "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", + "analyse": "vendor/bin/phpstan analyse", "test": "vendor/bin/pest", "test-coverage": "vendor/bin/pest --coverage", - "psalm": "./vendor/bin/psalm -c psalm.xml", - "format": "./vendor/bin/php-cs-fixer fix --allow-risky=yes" + "format": "vendor/bin/pint" }, "config": { "sort-packages": true, "allow-plugins": { - "pestphp/pest-plugin": true + "pestphp/pest-plugin": true, + "phpstan/extension-installer": true + } + }, + "extra": { + "laravel": { + "providers": [ + "Spatie\\TypeScriptTransformer\\Laravel\\TypeScriptTransformerServiceProvider" + ] } }, "minimum-stability": "dev", diff --git a/docs/_index.md b/docs/_index.md deleted file mode 100755 index f12b7d80..00000000 --- a/docs/_index.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: v2 -slogan: Convert PHP types to TypeScript -githubUrl: https://github.com/spatie/typescript-transformer -branch: main ---- diff --git a/docs/about-us.md b/docs/about-us.md deleted file mode 100755 index 8c5f6c19..00000000 --- a/docs/about-us.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: About us -weight: 8 ---- - -[Spatie](https://spatie.be) is a webdesign agency based in Antwerp, Belgium. - -Open source software is used in all projects we deliver. Laravel, Nginx, Ubuntu are just a few of the free pieces of software we use every single day. For this, we are very grateful. -When we feel we have solved a problem in a way that can help other developers, we release our code as open source software [on GitHub](https://spatie.be/opensource). - -These typescript-transformer and laravel-typescript-transformer packages were made by [Ruben Van Assche](https://github.com/rubenvanassche). There are many other contributors for the [typescript-transformer](https://github.com/spatie/typescript-transformer/graphs/contributors) and [laravel-typescript-transformer](https://github.com/spatie/laravel-typescript-transformer/graphs/contributors) package who devoted time and effort to make this package better. diff --git a/docs/changelog.md b/docs/changelog.md deleted file mode 100755 index dfb94972..00000000 --- a/docs/changelog.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Changelog -weight: 7 ---- - -All notable changes to the `typescript-transformer` package will be documented in [the changelog on GitHub](https://github.com/spatie/typescript-transformer/blob/master/CHANGELOG.md). - -Changes to the `laravel-typescript-transformer` package are documented [here](https://github.com/spatie/laravel-typescript-transformer/blob/master/CHANGELOG.md). diff --git a/docs/dtos/_index.md b/docs/dtos/_index.md deleted file mode 100755 index 2a08fa9f..00000000 --- a/docs/dtos/_index.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Transforming PHP classes -weight: 3 ---- diff --git a/docs/dtos/transforming-dtos.md b/docs/dtos/transforming-dtos.md deleted file mode 100644 index 1253d320..00000000 --- a/docs/dtos/transforming-dtos.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Customization -weight: 2 ---- - -The package provides a `DtoTransformer` out of the box. This transformer will convert all public non-static properties of a class to TypeScript types. - -For the Laravel, a special `DtoTransformer` was written with some extra Laravel niceties. You can find this transformer in the [spatie/laravel-typescript-transformer](https://github.com/spatie/laravel-typescript-transformer) package. - -This transformer was built to be extended for specific use cases: - -**canTransform** - -returns a boolean whether the transformer can transform the class - -**transformProperties** - -Takes a collection of reflection properties and transforms them into TypeScript - -**transformMethods** - -Takes a collection of methods and transforms them into TypeScript (disabled by default) - -**transformExtra** - -Allows you to add extra definitions to the current type - -**typeProcessors** - -Initiates the type processors that will run in the transformer - -**resolveProperties** - -Collects the properties that will be transformed diff --git a/docs/dtos/typing-properties.md b/docs/dtos/typing-properties.md deleted file mode 100644 index 721201e0..00000000 --- a/docs/dtos/typing-properties.md +++ /dev/null @@ -1,279 +0,0 @@ ---- -title: Typing properties -weight: 1 ---- - -Let's take a look at how we can type individual properties of a PHP class. - -## Using PHP's built-in typed properties - -It's possible to use typed properties in a class. This package makes these types an A-class citizen. - -```php -class Dto -{ - public string $string; - - public int $integer; - - public float $float; - - public bool $bool; - - public array $array; - - public mixed $mixed; -} -``` - -It is also possible to use nullable types: - -```php -class Dto -{ - public ?string $string; -} -``` - -You can even use these union types: - -```php -class Dto -{ - public float|int $float_or_int; -} -``` - -Or use other types that can be replaced: - -```php -class Dto -{ - public DateTime $datetime; -} -``` - -## Using attributes - -You can use one of the two attributes provided by the package to transform them to TypeScript directly, more information about this [here](https://spatie.be/docs/typescript-transformer/v2/usage/annotations#using-typescript-within-php). - -## Using docblocks - -You can also use docblocks to type properties. You can find a more detailed overview of this [here](https://docs.phpdoc.org/latest/guides/types.html). While PHP's built-in typed properties are fine, docblocks allow for a bit more flexibility: - -```php -class Dto -{ - /** @var string */ - public $string; - - /** @var int */ - public $integer; - - /** @var float */ - public $float; - - /** @var bool */ - public $bool; - - /** @var array */ - public $array; - - /** @var array|string */ - public $arrayThatMightBeAString; -} -``` - -It is also possible to use nullable types in docblocks: - -```php -class Dto -{ - /** @var ?string */ - public $string; -} -``` - -And add types for your (custom) objects: - - -```php -class Dto -{ - /** @var \DateTime */ - public $dateTime; -} -``` - -Note: always use the fully qualified class name (FQCN). At this moment, the package cannot determine imported classes used in a docblock: - -```php -use App\DataTransferObjects\UserData; - -class Dto -{ - /** @var \App\DataTransferObjects\UserData */ - public $userData; // FCCN: this will work - - /** @var UserData */ - public $secondUserData; // Won't work, class import is not detected -} -``` - -It's also possible to add compound types: - -```php -class Dto -{ - /** @var string|int|bool|null */ - public $compound; -} -``` - -Or these unusual PHP specific types: - -```php -class Dto -{ - /** @var mixed */ - public $mixed; // transforms to `any` - - /** @var scalar */ - public $scalar; // transforms to `string|number|boolean` - - /** @var void */ - public $void; // transforms to `never` -} -``` - -You can even reference the object's own type: - -```php -class Dto -{ - /** @var self */ - public $self; - - /** @var static */ - public $static; - - /** @var $this */ - public $void; -} -``` - -These will all transform to a `Dto` TypeScript type. - -### Transforming arrays - -Arrays in PHP and TypeScript (JavaScript) are entirely different concepts. This poses a couple of problems we'll address. A PHP array is a multi-use storage/memory structure. In TypeScript, a PHP array can be represented both as an `Array` and as an `Object` with specified keys. - -Depending on how your annotations are written, the package will output either an `Array` or `Object`. Let's have a look at some examples that will transform into an `Array` type: - -```php -class Dto -{ - /** @var \DateTime[] */ - public $array; - - /** @var array<\DateTime> */ - public $another_array; - - /** @var array */ - public $you_probably_wont_write_this; -} -``` - -You can type objects as such: - -```php -class Dto -{ - /** @var array */ - public $object_with_string_keys; - - /** @var array */ - public $object_with_int_keys; -} -``` - -## Combining regular types and docblocks - -Whenever a property has a docblock, that docblock will be used to type the property. The 'real' PHP type will be omitted. - -If the property is nullable and has a docblock that isn't nullable, then the package will make the TypeScript type nullable. - -## Optional types - -You can make certain properties of a DTO optional in TypeScript as such: - -```php -class DataObject extends Data -{ - public function __construct( - #[Optional] - public int $id, - public string $name, - ) - { - } -} -``` - -This will be transformed into: - -```tsx -{ - id? : number; - name : string; -} -``` - -You can also transform all properties in a class to optional, by adding the attribute to the class: - -```php -#[Optional] -class DataObject extends Data -{ - public function __construct( - public int $id, - public string $name, - ) - { - } -} -``` - -Now all properties will be optional: - -```tsx -{ - id? : number; - name? : string; -} -``` - -## Hidden types - -You can make certain properties of a DTO hidden in TypeScript as such: - -```php -class DataObject extends Data -{ - public function __construct( - public int $id, - #[Hidden] - public string $hidden, - ) - { - } -} -``` - -This will be transformed into: - -```tsx -{ - id : number; -} -``` diff --git a/docs/images/header.jpg b/docs/images/header.jpg deleted file mode 100755 index 62f2604a327edf50a7d74dd06b12f58b7344122b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 530140 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 z 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

K<6tR&11BBd?+iJXgj)He=5z_x4;{kS3Sx9 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

%X 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%0AHf^pe%T(StP7fe!_@FM)wg~ zwGSM_4P<1qVxj?_AejkYD84EPn9fuWB8-7X(XJ>RLV@+pO!9kx6TZ!=mlXRg;t(KmckTITKT$rrAQ$^1(Wa<2bgC zm6y_t!_4B?mu>35t0OTc4n3?Y0#$44;^eOBO^6EKbBMI9gVL+%3oaO$)|d6_Gf6PR zS=}@|*9=fn`sjJ2{{Vn=sk`Y1mqd__0#Q2Y1}nmN$7*xf)O~3~A|X}(0B2JA*S~bi zfA;ksTKDdo-jDlwkD<3^v%OB<-oNEjN^It^b*_8v>;6^!ZhrNh>3`nTe_eO?tf&6~ zdr|#$(VWVu?`mJx@Xuu*_w|GN+&S_0qyGT7YJb;U{rr9Df8N*ppY_sDB*E5~{{V8< z59)jU$+W-wj-&dX{{V4f>qPyIqxy?)BE#0N{{Xma{z~(w275c!v)|YJr~2!oK0ofK z{{VYm@;|P+b68qY!kzau`rjpavrjM~x$m`&>u=R<=V#OYw*LSZxQTZ})6&Q*=p%0P z%H%W{@7A<&epGziLD9G;;pi)$tak9A7tZ`HV;?g|W6amIj>?9kTa9Z^V^6lWK8E9Y zsAk8CKy2oMzJ|Q7{^PE@;=V_(8#bxm?l_M9SFVr#<;KsJ6P-6MR47Ox1!KRnrOqm* zv?t_ThCFf0DL{agt~q}D%qq_eUy$aRmB~5k*E4Zw3ZQ|hKMMO_Tc6ih;ohIb$qbl2 zISVdP%{K&T{{RZ=W0dosHcJloZgam;)ck0)AkW9{RYX5C7NTl0qt5YoT*o=F7qq4I zKZ0pa>T_V1C*o-B&5c5${{W>dDpgdTBwXTSJ9b|B&}JRO-|eI8Z^2d3w1+JlYX zfy!&~THr}rZt8m4qKgwbCI_)PkQ5)`_|&4azaotvAz%fqvW-W2HCCbQK1b+%Mu#I^ z^RBg~6mW6@fkT3g6$uNWqz}h|Hd9$NvNe&o{{V@nR|0NRrj!M%fQ_tcFR&4`4O6WF z8fjXJ)oXf#(vVm2xaTvOV-s|3JBoEHp;}>mnU5FNlSYR)ARe75V3j#=7RFwL4G06J z5Y-X_3xcD1tx#~5FrX^Xe;d#pW6NICbGtN6jdjzDUHvnwARSf`u)gyL|x{vEXDVO)?bq0Wajj=>%)D3AGL_}CA+E#$$R^T-I z3We$@fK~R}jyYVE*{}CDKk>wRD7mF*ZyYu_5wInKTD6nGTDq8qL~1sq4Du|^k(V0s zy`9=?7&n#YjQ(ZL4&2yD7VlkNpFgg-{MBz&AdSYzzm;{MfWmI5LZP5J8CHNr$Wz=U z0p~P0Sb#M{r2)iqS^*y6aX?3V2Hv9j>p;z(Mcy;X3Gvtm{m8wpUa4GNwZ(n6>Aktr zo-fQ9VPp_NHX$uoVeM@tg1#>_0MNJVR~}D(LKxm0&_UOwR$g_OCTN=@96RYs!LHb$ zPIry{Ls8zjw~teYT*hDK+#52n9P$W~GjfQbYW;hP$M!zF-%O5v+4%b#ffIueHKD@2 zxp5y4+e}e^J0oC;#2uhDE1zn?koj*U3V?%h>tcf6#(P7G$yj}KXfC_J)8jxx$PO!|u0S*ebjS(=dZ<*2ai_l4!aFCn^=O=L^r{!t_J0srp+KM__kXd4J^EdNu^h%@*U(FfS$FLdOYY~lVL$?t&`TJzTW{Zb{F&nmjaDCDD|y$uQ%G)Ec>2? z``k-*l}_j@(B)q*?aWm)jc7xsO4-GSS{2$WPNslK2#qJ>K{%wSOAftg4mHkAP$dD; zAlOOym)bgfXbuS)NwSYhWLbDsBk-U+*R(sjt-DYPW=ott8hX;8udLSuc)Lh8pdMtQ zrrMLIr3P2jau%vK`deBF#>zvG^suH5$B>TI3Msz7g(E%thXU6U;8Q_CAA^uBe)Lxh ztucj$FEser;x7K{-`A}&z=Q4fT+(t`62yP^IVp_^KivBirFO!#kD+q-th_Ei9r6(G zqQ6Kc6siml*QP+=s$H~W>nDU4&AUGeF-~~zL zqH(@w!}2h(xolvO%ep~&n_0Yt*UOM z8|J>raGbt7j}M;7f?65K-TBo^v?~L=7l86VZ@DQsEba;1qZ-u5nh$%TI@xRmT;5-j z!Z$1b0LZ@Q^N?fA$aroy2%o3*kGtgTx+Y3Hg<(0%%76vXXIxj%a->@ z_iBKjX#53x-mJsQ_WX)CvRZ`PCvLu#>i!-DzsY+kA4qmRdsIf3%9Ag(e_oNgF-@$tMQ z`SK73BoC2K{H<3102=v@^ELH+?}yHL6H57|mE^if-07a~<>zt!b)VM_wK61)q_}FS z=$^HkNPJBeiVH~jwfs_pD1P0e>RohbX(IjYOCt%m9$DmA8)d-cXUA&_Ci39F^qK&x ziu=>&c%%vPG4o>={{SRmr}Lu)foB>20OJ9U92?2-?@&?FV>ln+X!?Oq9ruU9Gxmd( zCJcvG1>TZz|{Q%oR0;?=f;_sIQb1vo9VQ#;ss}U zfTtt&cffOZJN9zo%70!wg}3-p`q}VdeoO2YF;m5Iu88z*OLq^mWFU)u0MmTPwf~3avn3}oOF&EoK{3>ZNfJ=eqyS0l#n(^)3l^yUBTeD@Jg?= z1PIA+KZz?$JM`LKT-Vs^@t{5Q!-8}N@e~JK&Be-eI`^arbYz~3@JcWX$Aoj+r0rP9K=T>Y#LJZ0;O?d3qd;?ON$1CnyCuDmhdhhaegt! z;_XMiKip0K093cB+vEr`90WuItw>f*Q?8X1!o2*FO7>&0tEF&Y{7OY}yIw zw=us><8O7;P!aaE0bh%#2Lp!XCh30k95(j_L#s{Gs?cS%`#~26_)r~dRH&Dj*HW|tQt`0AX*!xRWR5L?8c0P|=|)29!?9T(kYQ=G$_XRmQPTea z8U-JCaE?EiN`x$|Y=}YxBly(uz7BzeG40%ep>2IqwfrQ3Wm}?+JJJpa+%2f;>H=;f zTo_1Z;%rZ9K`JK4yc_ie-|(OoHy0p2)}2U8Pz!4qM&}Y#*ot8LLsiT2_Uk}&HB}b+ z1L;6?2N!zZcdY^9#A*PE-%1VOrod`~^ugvSHXs5(_0p;nar;fh{Tx54=Af|xv+47I z)R|@)kgm&?FljKt#4LK;aLbf>S#U>-%7>G=tq ziV3&iYo`RKd2_b7-Q9t*QYV9$PQayqRRKE;xNr5g>J2fjXhl1T8&ly(9o*L|S@U-T zK|m$w2yq*U8VzU;fva8H+tDZmlW55de<29LB&4Igr~1%F zB;#fgIHKKeOCb_$GaEvWaTiZoGAuB0S|k9FdjU*gZ{zqMS0igu*FK; ztHI+qMB8eEap*lO_c%mP(?R@_L{9Rgc*ID38 zhvQgrMk8!nS^R5n`@7$oKdvUlz+o`^ViaFzWBNq6xX2i6zz1PV`>Fj>MkHxrPWJx* zlKQ&bW;Z|uXMKbEoDsANKkSd8z}A6NeZK?hL)Z%Ru1>vjxJZG6Q&`Q9C2fdN_a_h1 ztnTpKGDa#dU0}s?>%=N=v8nxQhblgmH`>(xl%L)(b*TRUx2XQQ;m>b+o9|EZKdhhM zx4lL8r};nYaPkaGx~BW^e_1DzVryCL`xtYPm-0QUpMTh8Q<1;-BU%3d-q~N)L%9d* zS>JDJf7Wf;?(bRN`>XoN{qgAPU){O=BcH!;{a?n9?%aJN$2s5K{Am8&{{YDPLO*=( z?livp{{WNv5d7}$qdWfqk@YwG(6=wI3){GMFL;hs=)qQBev&l4dqx$JMc_xiql>Y#(hritzNCEo#(fw2v{^rmZ zb*TDmJUjmE&^~2ae@%ynf87UHAn!}*xA`6m9H0WuqZ`l2#K2?AGK90S;^2fNcNO&C zq}trQZ`a?$oE)wCKyD}x=4D#w?_X}= zv3ZT;F8XDp08}6vd^^+1eycb+@x87F<5Sl9grH$_kITm+LOc$~)7FI8l^$S_kj}hqw;wa4E=n8Mnds z=0qDBE~9wWJDNVH6@1rm{6>=Pg z*WpbWNG8UK@n2T}hw%o878tp&k%7l(E!2UjH0VL(^PBY?0o=aQ1+Hlt%YuqLM#|?n z2_Dd(TGQiMz|^b4KPVi+jJ^V#(?1n(i%{8B7os7Edgxqpjv?A?o<=o<3Ked0d=qGq9p-0 zh0YjRO==`^5F|I&v+1s5zxBS&BjwaC`9+gALy#Q^ya6kN$0w!|X#qm0JSR#~*b zukFOqf3v;<5@;Q@!>MmWUdHxfJ}dTqz0;@sA^Emr8ppG85#GH0_Wn;Ti1}|K?eE)g zx_%X=GSgVhvmA-qQ|uW)ba)PopnyM$rPbK z(wRI1ZfFN$j9BVu3vNFZ&wYVgA?Rvo5H===+SF62C#^_wNSXpSUx!KqjgiCjoAv2T zJi_H$khBGy1%-p#X5FZ7_^k&Hy^#`ArNJF&2cjs}6$sPQxu6}$`h;z1wUuu`RmleBG#__BYzCWos1N@Hwr^2Stzn|!} zx78PE{{V$Zikz(W##})_mrj(dJgGDJ=Co={wLolX0M8}l_j5O*>elo%asa+xJ0s+7 z_Y$Vy`c+^l=j0ss7dZn7AT-e-p-r3`^xSMU_|*hCawB|*3J_Eir3PY5hX<-y-8z00 z%{zv2&L6w!p+G$kTAjWfUaiK>LxGS8m_aTUXdfEl=^tb3Ul4J|*`aKL%Utf=z67{0 zxa(>ogREP{^3}MuCm<^gDtdgVbC}YlFIm^2&W3py_Ne)Yxb7+)Q^;eLi~KJ?F$F*- zuUg{vCyE5;ajf!DLH2@0>%WbB&%2;|=srh&kTkt?Jhy*j_Bk35pIT%`?1~-14|A$e z9{s$A!ME%1pb+eFyaf2Jf`Dc50#Fq6_)t`lV`2W3$d|?(cL!W+Ks9lGo%EnPxf&A4 zZ%T)oC5R)c&EV@-7-pa!q`;$RC3981V5!a!HpWRRzr!1fI0+qar^b=lE_*jN`cYVc9V|q*66L zv|YR+z)vaR5zfgS9BNqaowpSet%K?l$nfq;AbyzM&{trw6}0Uc-4Nr%&BR#dV}S<2 z5Rb;LZ7E@EgUru}z|F+96|MgO$6CCBXA$Rl*af?C3nZ~?Nm1}M7ARsq>E~wR$Bj1D zKX&JAxpP3Nnif|4e?8^Xw+9ys1sN~6Hf9+2Hs@ZprSKie&+%+F<;TcC(Vk`xml`$~ z$X?w6H?Vjkg4`I%|PCiX#fUqF~iA-K3VoSACfXuErL6$de*Z-`#lGlb9jy{GydQNAUf*N zr{hJ?zRTV#?gli{52(jt&JoUYN)L^w`k&Zu!+pff3?EA_Jh`A>SriaKzKdIG;_9T7dq^5qmCqH~ zA1OB0v)R$1-Uj@XNE8F`TF0;Y*Jls+gYP`$20w+IMjv)_*$^6?toJ&;Z+V^HQLn=z zmEkNv2%slVg=e#U2cKy;pBs+{p7OsS#{Kpu3dXngsGv3f06WvJwawBsf|!2P_)jG4 zJgA+Khl7^a<;Ygz85};706J@G$27zIr`bM0WMm#MF|NfKGGoE}ixSA$yu6m$)pKwY zo56gd4nK^}{ioK8myyw6M-kljxr(Q6WD8Di6lPaM^2fBb?Qvt8;iG{|{3_rqP6gc6 zF6uke1ny`f+qyN>r(GzN9b`>{P0@nG{{Sf?`A|!|A2IT7Jhiwyu1q1nD|c^i~F z4J?x}_znZ*$zf<-z>8G>09eZR6Pw_3SU+dVV&ns(M6CY+^G!wKGL6Z2*8#|aGkEwi z1A>=r3P(~L+RpRG#W0?C*?$1Y4l&|QieG>6N=OIb>OYhLo;)?pp)tY5_X9mJO$?M`{DN4r<-Ne5d-*92Yu;AweDJ2`q)chZ}7j zO}`2O?c0M!uL<4O%>;~Yb|Um(EaNK=z5Lx=pW_c;DSuU~}M)Nn??16n<-0j>W4 zrJ)664dl2b?MN!;as$-`O)%~we|O}O^=2?KyW%WAjUxkn@8y24Ie~!_!eK&PB6NJE zZ}?XFLT53TMt6q|#YOr9Kq1P7s0)UIf;K`B(PHagy(4t19vm8^5$+wuGID2bE_9yl zv`QQ<+r_`?mj{8qkjH2RMhCZ4cXO7bjU z^9=y$RFX-uwE%alb?g9aM^7n@{ zBA9c|^P7vr&HdX*N_tNrf#yi1*94&@J!_{aMGi}Ww$@6J^lshgP08s%Sz=lt`c&MJ zYDSX9E)EJx^w!h@)r1zI0NnRl1B5v&Y;H8Akn`LK_Zw+_Xvic^97Xn&w{PK2gN%@F zQiKz7ZEs3ObeP`aRX5tlLMa=PwZB+7!CFy0XePtqUNOhyanZ>I-xL>soT^Hr1WGhyAGy3|k|~76+wo_GN=khm?03X;an+>p zump*XYBic2RM*S)+vV3EM2ui#Z@^b(-7RM$pR;MKcDXlOipH9Znb5|L!ms;qPOM|C zFYds@pH?pQKlW$&7$wOQuN8+Rhn`l$Y+!L!M<8=uCj`beMmMz76!KctB~ zpeydupY)bU{{V9*L-AU#=~N}$m4XrZ)0}}n?m+c6qxy`8@<4T}oPh_E7rvC|5_s}K zI~sq~c_8v*y&u$09#j5mf6@qlD8Nv>Q=CRg9IAjrlppC;{Ua_OTEM77r&=cwl3mM3 zuHpxJf73=yyPJn^&t>>ip1mxRdB_fqzhhPZ09uc+C!2t;ytEFzXr8?vWE|dktu~QU zMgFzZ%hS7l5q4HT%HsXQViF&$;s9W{B-c;<4YBZ#>=5PP^6*5*WI3@p`ZN_I;wnGs zZ$Ah3ima@h$WZ-IO}~f2iR$0XFT3CFd2DL9`%y_+Kk4Ls(c8`Bx^mE|-=>vc^yvGa zla|X7=v#D6YGHM&^ z;9s;BuehDSSGxB5L5?}z!O?42^7l8L2>N9Db*b7bmfl(Ck-$rw0^Dm{yGN(5n6L2; zRN-V#isQAUuj5`%?R{^yxp@BezD5k?!3()-=Whp?tmZtA=|tjO005n9tJd`X)g3mH zF>fm8CFFS-72AM1^sl=8Z>c$G^Ez*pFV-ys`?}Yn%={V8W17vz%9Q2EN{f|yNLwYz zTc5^>LcE7KcvQ{l|yoKSHa0zD}ZPJ8TvX7o)!noKS1&CXIG|LI_b1)D< zdz?`LAvN@%u$vBaPk!e-JOHlvcB3R)pM>*v4f+6kYNnVrczhWFAQ7Qa)lW(=c}>9M znU5fQ28Y@b=yjq&mE3^HcShiQv}jM`PYo5EkJM{WxQ8MTHNKUO`kp|3Lx(pYBsv3z4MGdw{Tw|1^Q2VHAy_$Hs@zi;@gEOwV8=XeCY6~ufhGQD8q zi{qITBrp4A2R_L zt}gvI(q|2H(qGFh^AEQ=ggBpdl|^If{>SgOEW)?riI!=~`1W2l_dYV<)9yj1 z#=Uv8u6!4X$l{JUoOdI;=tlD9^#-aydncd4iXj#xremXIa76V!v{OgJ z$aL8ZnnJB@+`UZ@E@R4bpY@vJ)1hi=niRDx9#&9i#~U1M&_Q#zXcM(8j485mj#STh zQ5ZH`FX2^Y0f&24Y#pH*Pz<^1;sb$8sI@RmZ`$F1kK;g3#d9N>trrTV-ju+*gXA_y z!~!=Gfl80zR+`c{Y@k@%mVu*spNXvOLhLKpKYG34BIGeqP1n$p)L!0SQflk=G0 zMc{%7)di3W7p+q`0yiC3N(_dJlc-cGC=P}+l2nV@fR!6UDc4&M;X$5^pmyM=KxqTm z7>>^6@o|#-hUTY0X;am|_}&X*V;aza00(PT>kn!(fuWJDI)x`Yl8klj*Wp}V#@JS| zi2cgt9OBG&yV_5?x>ql!%R60~ZQ=mh9N^N^any?SS-eYj{{RG_5*&7g7axrQz~Cc0 zY+QdepgdR*kWsV;T+piB8}i>;1IX0`H%`<8a>h_MaxKaDPz4@0${C_;4yHL z`)Q9aul+x}_ttBEV8at3N5fj?kD>X39y2i!4!tUzJ^AEOhms!Ds+HvQ(-iq`=8!Hr z)iak09$NrCrrOkB4*oq9JZ@~fPHUdu`)1Bz>K3|xw>sgUzw1t#sl3~jRFPG$Qq<^A zLsLAyN4F2I%#WLy^S#X^tGd<3YD2{m2_3FiO44i<**tV@JCKf*uC+A@*q3&l)YTF@ zphX?2ygrcjQ;>@4<<3Kbu1TBt~!owYb#JR32dL@(NLB!bOjQrrDdjRN^1gN778UfdD z^`r{@yYgNlj5+Tp0~#+(4g6>AvE0m_QMXOa$Jbl?jVzNwg9{oM!y|6iH%0&xPSrG> zn=OqFB!bq918ryuE>oM1DBgDtr*I>sFOUi3Jdbd5nxv9$PhW*phk(9sHfNCu+D^Mk z)YW}Ln`B)>Gl5r*nh)iZY^ztLTpam>zAZ`f33D; z9`PWPA;)?304d1)~Yymo=TMaY0}mybJ}m-v##z`HJZR)>nOec z6`gkK5#^(i?Qk7RdZx7Xa`o3f06s?;Y)&l~RV7#O72n3bbKcZ0WOITSCGHlw-ZVEn zlCi+DLe$Yp(SoP2pP7~Q4(A??eyKqs=VoW)J8Paxv|T797jrp^q1syYSS*&#Yb8N& z1vR#`urjg6%lgryngP^M5@C}W&J25Yp88M|hX7=2)xspa`EmHlEUDzaj{*nxu!|n(wpAy zq^^KhKKsnJ*IWjCw~~Zsvkx8N(l7>3AVN0{Q7LH-)BZV&k0xslPX7Sgs545%B{eBi zz3^CFa~U|ZWTq3gwTfCD{l6adj-eM5oz8|p#>nrb5dd97k5#9(un94Ivjyxi=0h8b zfxQrg>P=;IJe|to_?|%T#+9PVLAXAYP3#IDPs1cHGO{uO(d-HZYp-IusIq)^XkLHF zvKzFLgsp2jWfxT662^c303l(@YvhI&rq%^>-s9~boo#P~;k8SP&SrTp(dTl`4;hV* z^)dFmFtwlnn}Q|lDjeb)!ExAg%-2bimzObYYR$}*3O8$ydIef#KgcqI2O0M_Ga65)B!$Ol_c$tdBKD{CKeifgjQJz-;hT}hjaukG_hpN+ zKdJ3&oGd|GiiGQwk3h4RmPp`zf*xro^33`c{>Ov0XSlI)yc!&W%s_@&5qu ztQ}R!aaeZ$09R+PGtwr1{(YJlI@Q84{I!wRb47@KU!)#fY&rfXo)dl(r=ian}?5-;hGlb5?oXHjwqWTca z0Y@>It@a~_{PmiNegv5Q0fofFA(K7-0Q)g3j_DmF{{Z(SCshsBkXHEzD+d*ujOf4v z#yI013{Cl+m68J^1J$i=cvWy=HTH_7@USVmK@A! zpq2uXwpJGGJsRScTEDcWbKKI1Wa#u({~wn-xue8Zrz=fO0aT#KwiBME;NDP!P{wGaAZ-%{nGFZ^#1_gy9z`4 zG8$T+h_yxJc@tuCyiR*s><&r{sL1&lBVDg{x}x(LBR|Bve>^+)+*A{^>f*o+{Y6jG zz!lH?GvHYcCLCEXo^*5)6#oF}D@D71kQn5C!94FgXEqvtOj!&h6Y&)1TR#9f$o-k~ zTqctgQRlD)TxTJ-p-Q>eAT^ib{zZmJ&EfK5Xgaz#5&4RLco3e(FVk@PMYr1EpTO16 zAlBBH(W*_h?d*EdL$c$!&2!w+0t@vgwFG7;iAL@AojyIN38pyGAlY2|E7VvO(08Cb z-Oib;2#zQkf1Lv@E9|!p%#S3<oY;mDvbxN z=}@ZVB@BVrO==AT#NZ2)ohFh$ZsYx|yQ#=*56gLD%Xv2?oJxuE9@vjT8$GT50=gk9 z%S!AhNI<{UX;49EHoohE;nI;10Z>Udv0j8wZwCU778EM+!r%Tc!j$?Ng zw4hCB4ifDepf0^gpcNY&@$(fLEi+7pBkmhjO^L9eI?ch(0FA1b{*(uHF~ME!E$9kR z9Bg}9-AL4vKsrNOKx!cixR2pL5%?}I?sq>xC_EoG!<%buVMldr8H;N7`GkS}d7(tl}$|sdAm?NAP1oxwV*vS!$UwPx(gCJUVzcQ z-1)p1c;B?ie%8l!udk`DZkl;swRz-TO~>*_V;mmgaGkn}iKU9PALN{VE8{XojYL8f zNgZ^$R+?6H7mqRejt?LZu-yVwx1{BI|Wk$JO8HM?Y&$iXZH?qw7~gk6 zTpRoTSN(50k{u0aec}CN9!WaXf8MA4WIvKU>pi{Al{v#sMiNu4 zU-TE;hVoNNN7E`5Dc(|ijGf!ayr^OWQ*NN4kH{TDbrin(f?XaRQL&6VQE+v!?Q*6d&0 zb|=X9eB=;1R@(ljYM=WF5ikdPfpOQZ>wSMto9rd9#_d{!-nE~mh5rCxiA({cqOE}) z>URA!{15vI*;=RA^3p###{R2md=KwMnn__{cy{z7`0Gz!*Z%;K{i^A?lGiwHMboGQ zy>CBVsa~k(pI{Bz;^aLoV^{wG+hG3JVdRcONpMco{{Z`N{?wT>B4YvmDWz2XX@6=I z{L|BGNI`clsYm|+Ylr=)dAY+eiFUTyYHNNzl+ibGJYFWv<-AL0+FARU$sJG~4Rqd7 z8EU>x*&WGb2^@_-D~7&_T^C5pU6hexmo&NOa28Wp@5@|#FDO|!;y8B~5=pabk6v>v zB}l{K+bcx710C0>u8G4`-I8R$KJjmW>rV01eUbQn3`@&zb>nWM*Ria>qEfpGIsX6} zVd1{4OhXY4BegxaeSEt84*vjNkT1kMiT?nX`cu}v$8RuYg&-g`eO9&A<@Ja1(8Y83 zQ>f?i2l7bMaUG58lV_{W_-C0?yB{z~eXctH09wzdKfJ@w_qFi7#>&l~6_12(GA_oB z!>xJSdYHctpS>`p=Xg&p%#F^65WE#Y9edYzsjj+w$2U%y1!Z`@kYf}3tSKARoke>( zds<%^bKO;P!1&%v)j&rBWUifTzX`z|6abz2)e$lWIjWW=6jXvYy`P`LD z+Q&ZEHKb+2UvZ*YDdJh=r~~c`aT|`voB^Iofz=0E`JUXT_uMQ zfL!&Y4zbT5^z}NEL5jZJ@n&vDY{vy_Tq2170L+T((pI&(@URV_0c5pl6Kd*n^y2;% zG=aWHme#T4n-4_CbDq_9DOkdA9s z+JjQprnwaHd5dCJ)~b&yza`@w3lw#x;My3;i+f1?saVrZuomWU2)jkis&3eNT=&et zE6H%=b_>7-Sa%y#^6C7ZXY4&J&$MRwIL~yiE*IyCW6saVxxmbnjIu84#P=;^v-6<00xtUbJKEunVi?` zV*qKrsD|`!nj+V28r9VB7;x6=EGZiE z7BL$_jYhPBWK3hE1O*`Vpa^hh(m4vT6oE*{%MqK05zqoUel&s>^2sdgwEPyH0Wiv3 z;!wFwj-b#4ell_PO(slWwFq%@R3N&LDPZJlTwjRzl>RHl zt#Wksrg0v>FV*ce`)po!nMo_1j~M;DY4q6}Y=9I}Yh88mmz&eYu|o$P6qBTG5S)GBQm}lt zJ00hLzx3@Sp-%K(Vr;gX$@o0zcICp@<d_cc zZnv#vMP5r0$Zs#5%;mQw?S9xBG54!ME3{}g8&Q&29&?R~J_iF8ginA!aOXXwvv$%* zDRQf+r;?=NbDmAeW9Ra*O5A5m;g7H@T873=y}+X(V~c^nHYJ~6~*!exo9*?)U&f5M{bpcgUX0vR@s`)Wxo3)8JkmKA`WU&DB;rB;0@ zo(|GL+83>DuKP2gPDAbN@S|}aW{p8`Tcvbsu_&_qUyyD|aRfECEx0;Xu&o#Px0v}K z0mje8NyoSSw~%XFnJ{S?4?>~Md9t-FM$%C(9_Di2(p^aQ6_MNb6pg9#O;FtyrAaWZpLWnl9h%uSZQW z9#3AGrl*mCq&XY_I+aDOWOMC9{DYj)umlb$K)Qj~(z0{kBtE_ZaO>#1y%1Og@K=%CvLy za(48t9iE1yLCf-xy`l`MqY=C6#@0Nwta#DD3szWZk?)FmU)*`P@x$R}6Nc$Nscfuk zow%L%ak>+&M)pIVS~d(mT{bfZA&}7`JeRScI67aPwKTPbbKT=#s!V=29JwD`CI0|X z?r?jF)VKiOsPID^#2Jd$On<)@$-3U20UsvAR|NhR@&*4v`xl% z4<-9;%q5Bm03N5UE1}JU9LJ0De11Z-J99$;9ore)2jc3RaBw$^y-Wre0%!6AZ%pxy#X_!mH~tOuHGc zkhPKj0NhQ*ii!rPwMKYE3lxyRiZ=qJ`U(K&?FR@sUpm5;P+t!m$7RX8yFxG?-s&w2 zfByg&UTM>FTxS&5D{&%rOu@Y*mcf4kQK_;p_qgZ*&@I9aqO6=R1QJ}m&>;iypgPz0 zJFb9wdeJCJ$C=SQ>;@Nv0nz(^h^7P_Z=HEg0vtYPDU5Ehx%-%(2U4I*f%mt}Ax6&@ zHy{STQJ+ka@oi1g7^{u@<;BceISwL79o6ti*SGRFrF4TLJ~!@vf#wh!$^O;n^yD?n z{xu)eSrOy%I2r99w9SJphoc_X0zL+n=T9h^-Y3B3l5@C>d7MHQIy5)+6+cQc800?A zct&GfAjWqd^s}LHH~1Pql=uP8N9+TMF2Lr|qU?@n?hnPaQ+ohwk^491m~AeD9z5m^ zT@$vu@;&L!zwj^`&hd{U!7u&i7m*Ma2VpG(;wq-{mKCF8UgIe(aJXp%AnL1CFdP2> z*?uHqc#rP(`}rx%!1Xb-ZEY>?YFY${)*;rWnzttBwdqhE=jQrY?$tnAkc-acpC9lH z$?Vx%j6+xd0JlwRRcQ_Lv`x5bZqO2Y^{uHLbGCxSE3V$usgPW(C8V&ByMNY^6C>Me zT@}C{v;%DpO~=}{XgUKxa2D90)e?Y9oXZA;@A04z2xvat4X8NrAeP@-gVZNV1Hgho zb9A^$0l?kwHvlwUeP}X*09p!%1Ut1)^`Zk=K^h#;fau%Wm_jB)wMZlj=}d^bkG8{0 zYe*e)c7O)G0i+E*s!l$*=%UI-Um09oO>QXX|8Ux)t`Lg!)tiw#TbfwkY6ZhMI8yJ$K+Nv^51a{L0uIn z&fa#kr*d_qY1nw=knIpJr2#al9YUVE(j#vU-*7<|0)TQRkmPXPZ*V%_tpOr9n9=RH zUu%s9c)9z17UYUy#do>NHrxO`Xb%EEST4V|fN_CEx>*RIJtSnQ1K8bqsh~8Uw7gh; zNR)-@w6~<(VFj!f(xMtJTgnkkm98X;RKq?-=Q+WRy6P%nr}5D@ABk~C|X=dL{!p+ z9u7Z5SQWWXKpn3o?!w!f7GL|JZO0FnXJj-9ADpM}gz6yd?=drjB3;ZuOR(10;@c2&HwsSqS`pli4&utFVM9d-tsFrCXnY<3W)Z(_;jg z2Wp)ISN;_pWK5}_ad>=T9AoJ)%Epg#N@`TCdH&q+>f{dMU`MnNg`n5SdzTg8pvZCq zYi^dW67%xt!_LppEzK|M7$oPWrl0i!PI>;|J~aOTtc|}k^>Tz-sm#rs(udqMQBV58 zw`SVptyJa>yF3roYQO3%n|C76p>Ao;LnfWdFu41Ys&OA<59OGJ55}~d3+!S1yov1u z{{YQz%C^(hwLfPaE?j^Y0t32(T{?OjR9=xL3-yOzlD$ar zj4>`7B~{#}{p;ud0AtSSwjEsVV6PXAE=6xj@^qJDwCucFS`C-uPo}?EC06F(4WxjT z9Quk7=A-&Obk#MlYtjB&Mmx~MvfkH_EY%2w-azta=< zSm1Klt{_ebYW%<`6?2p;y%O>6;->dDLRQTutyUyXRtE{Y1DSr#)|}KClO8|_1s&f@ zQS})@%7Rw3i{D22(e*r_w75tcBTk4)^`IOWu*wTu3jS3$l%q8sPaVgUC}>9o4HmGe zynn&Xm3f{hbc^*APNU&j{VZ46$MCO%{9qtx1JseJsyv}i*S0Nymk^nEm>)=<*LgcAVnIT91!{* z^|n(@%a}t~a-AqByzxw8_L&`W@}$$!Wjj%v*uq{uSe@<;VFVi6u9?+5w#M<4v98DD zUl?izxQg>M+4r0_`*VVhIqHbIYh4k`+AUttV>aBO60W%=@)Ghm4%O?e^sY{iPlnzZ zmyyI;OUJd&AUE+TRmbNE_chL~3&1>euH$jvErJpOxxIQi`ug9O#rL=ES`>T~CmziJ zn^;zt*3$kQ*$?vmG3_DD+|m$(rE>N3b^I?~fL!0({0xU3*x{$R^saq%-%ra=6`%6{ z6%127kvnUFAcNAndH37$o2X|iikb(I@0aw0YUv@+K7Pzj7@uo1d!{&+@-jFWUn`#*f!Z8y0X+>X6*eb@#xZ#WPl3qU9MY@QANW%_wS7Gf9xOb5 z9L$J0?qghJOR4p)Zr;Av%%yLY-|wZz!ZJvAC52v>YpYKa$OFuHx8y}PD#;0vt=x__ zPfGDUoo_wmzn4cn{__m4CzqZ%OPT98)C5`h=9O~?xDumJ!kz<1@uLP`j^nkUwoEq|vLjSNLbb&!qn;CBlM@T} z+)O)I-Q~o{@&k(eYIotL{{S$hv*B0Dc;^wyVE*Ge5O)&R?EqQ{8zS!H$T)OS>#WS(x#mJH=bZd|u2jO1En@y%; zD==w}E+Bxts>wP7xLgg!z;s$+I|^eP!$1MX&Y~3=`0iQU)&h5e0e4Z>)aolo<8r>E z0%$-&e~nEglh{!LAsW31-(yNVAIRm?E{())u0j!fpT0PhJfe*w$}qy&`=6$5-<4lpcY9N2FIYFJm$9h zi93Rd0ix#yk*Gwy116r*2tlA-d(cbI9^}56Fi7hgv2K19&(~Mb{-55u-=!L1WRlk3 zOPb@$>~*ri7af}5MaT(d>s1`xx#U^Lk*yZH7t6(dq{N273uEx;u z;KaknY!S7s4YFt{3QW0;WNsm+X!&|4#*rwwE_6cR3AqRdS}<4y^8QNWxCu%+!3tZo zR2T^7@~@H30G*(M6Vuk2;YXLAp4BI}^`{_K%a-W?4sZYxf~X?yCBb^=Xa}Q?M+6#^ zf5w9!ct3!09K(Tz7d(;`$!3pIXpJ1VJ7RLrB|-QPZV<<5pzz^2(a&1bbTl5FNT-JJJ`~HR+YuS%iq)B zRmVJgP$ec^V)GgP(3Mf=?-E=>d}*_a?9idssIH!2zDbIszJ{`{{U~3PU?epL*4@it zdQi6o^Gz)zI4DwzZq}RWT^hW{k2?-#$&@52J^Pf04%Ve~g^>n7k@)`rEWr+DD;(f1_*IV| z8qVuy!IdwI;$)rNR%wmI1N1UA{3?F<`3j8QGnc}X{{VH2_Q!qGFc&obJu2qXmJo;S zW@Ch)BZ(WDZoMkrL6G27tcrG`5enK1Tnfa#fi=V z#xQR3zra-D%1E61{Bw-P(I{RMQFj7-W$#w;^;Ls<1L?@#9Ladm1*I)sa%gg)Sqak}%?<={Vf9j?~E- zK067ep~B|jx+(8ht*K0lA04d-+Y1|Y@9?iv*RDR&I~+`GhT8&e+$+@A;p6oG0Agkh zb3%Tj?Pxb9w2XF&PC?GlezM@@>0KH;{?$z>7aAV^m35A){D2c%bgcGtXp3VYXm+~( zHHQBH4vjun*uE)~l@xA+Y?=gA*1T_LX)EZxeS9M3cqbwANW##D%_^O%&f909tGq{m zW@P5FHv=3>0VHab8*hlxd2HM#1I6$hdbk56og{@7Q>Aflwc5HQz<0=g=y7-(n8yO< zlBTa-RJPndjrX^Xk>EHrsbpK$lZrm$P6x~IxrhhSlopUxr(aP=*2T;Yd|St3Zc{Rz zH#FV916>;2G2RcF(AI{udq#j!QX2Bs;xbsDC<lE(&?{{UL^ z?YV1kfyj7=1IRM*u($)X=t9*^_Jx3lE%v8-ohBQMWFKJE3e#Ty0OYU?h?sBiFtJ(%_<~jU_kok8j z$>X%X8Rm0fBdjs|Tl}d@gA3%tk&H`T!opAhd-kW2Sor6UzsP)x1BJ$S(UUE}O)t%{ ztZwOF_yKA&;0rmww_Y{%^74FRmwrbRfyWHFKc}9~`pC{{W&|*B4D2ZS@+O zwLz!$HB)9G<&Ha_dv*Z-0OHh8aJlS9A!r3c>p(imBrgM@Ra=$Tt0M=o^D?55V43mA z+pKOaasL46N-|hp@t%L>ym=o(mdUn^k>%t60J>Uu0-P_t{#lM%+`K$&#&vlGjz5UE z@TtHOV14%Br`!JH$IXbgbK-E2FTkZMs1V@&%A6i*ladssmH*vyej3N}dG!oKAMRoe)VTIV=|q-ZG_MUFs2h~20?sE}Ziz|smT zl%z){ZaKp(cz5muir3p{VqfuEFlfaZec@18e_8=0h(X+hY1)Dz%^T2J&Nn`Ttp@kf zJ42H908j~ZKsmdK2Sgp{2b$Ynr!GUqFhAwQ?qfa-z(1p1l*GPOz}UX&RK7@w$` zK&F;xq%U1)4d3mzmUy&2EX)p5A-L{|%I3IPz-d@-rKZom=J{iMhn|vrdtyM}rH|?x zWlGbYxGDx{aA_o{C4UOAdK^!v;P$mzECmNBEv-JlmpwpPf%=nB``~v!T|t<)eW`Qfa{tZ zfLL}Olm}vC98jA%el!H8i3n_F_vzT!if}hCwj5B$!4BGr1?xSoX*5lvWsNgd<&a9$ zgei{J6X{CR31zs?$TB@ackrbGTD+)K+)7PGBZq4T$pWIrd9$eGEsxkx(ubQQT( zG^CPTQC<4FprDMoulj&4;B2%|eg+cZbyA7ikXki>wXNK)Lu2Vk+z1xMP1E5(DOF0M zkBJl$pHL7Bj=KKDqvU zl;W-2Zb<<<3KD6qJBZwasG!m@ael0%f$by}RZ&1%=5f-)f%T+N!V#cZO-6EIIZ?Ck z7P^pXiD|{o*1J)--LB{t(v=N=!92o98tayqF$UWW&9F7g(_!@!^1Ouk&<9H5=SkPP zSCQ>|5=!agswW>3J9dHA0W2%z{jQm=?Hqq5=CMHp8tU8in?+90&-=TzHXX^V{{ZOh zf7uQH0K30wDh`@3?AlUJ^zP)R0e_cPpe`+(lg^KOO1onX5zO|(NbT92s;xm7DI{=ZS5`;K`Tl4h( zwKsf@W8vj=E!nh2gV*w}M@}-vTW?dbxc0U*B}wU5ImnB$$_QEjVGCCo)X@weDqgD) zvZdr>iPCHV6sLO5;PQdx1Y_|(O`!=FN{#V@%0Gn6AI8Gr@|Cy1*T(+p&$W77z6Y3v zV+me2uY4ZPu7j1U~EN{{18ked=tP6mU#xHJHN z(x)b$7cmL6?I3j>e>#6)+rq%1Aew52&ptaG_M zjHV=#zD2UQ>FZOpl;XY6koP!?lr}wUHN%_O7zBj0>PLEFqZx$xalq9rF1QD^GHCue zj@GfEEQk`e<*O~|qveS|I_cYA4CTekH1cde+w#tMYAckdV_puI`B?jZ)^z)FOXB3y zqSy-T*WsslzHiCWw0YO%L(aqt2^ZUX9vU`Yj~q_?hH>1oC|bl99^$2NA8!VaL8Fm0f%V{PUc}ixhD9FKn-P{{S?i?G5xI zxIK^ky)pQWbpA1(f3P-3l1+}E8rd&9#$s|=$~7SYq6Aapb3q`Q41yd9L{%sGQ^<79 z%i}e{&1|g&PUEkoJb?#|a=A>BT%1zqB-#X#xeFYc?(Qp1cBC}pyrVQd?c^g9;!FYC z5;U}r)Y_@i!G*+LL&Wj~;Ino!Z0KJo4Qr23wR-QHHoWj!t{5$AkU3ob_)A#ih7W7%a(EiFFUOcviEoUaC&G=40l#cad=wYI*;`6XoB z7cGWb=^#54lIGvnO6b?5pu^l_e&P}KD7B<2BH}aGZq@f$LAujRO7YyXVrk#(Zlu!M z5UMhxa@gs0s3Z<+3OS%nw38M0YuTd==# zfI5q`3IbDJK`Pb0qTj-Rox@`O`;vnbX>&`1oZy4M*|wrmLJ2VR-X#A zWsJ>{0(GjWs(x!AHa90#70K*eMw&(y_cN9fNgg9gsof{QX)dHbIU?;+)FCLKfgyrsNZ*;HA4H{QV)WnZw6JZ%@=Tf*y{-h0 zo8o#`2Kr*Y>-FB`>7)^2Waw2YC3<^7Hxr(B07|7^Tpq_EMl<)c6Xka}b*=rL8uZng@;DB}$$sIqHN-e+exHSU zJoo!Y#rr%zuVfJ>;6e~e1sz3chZ)stnh0*<5CB3U>S>Xkib!zwf~DP7f-?Bn;z=qr z39T?^_VB6zTDIJEppxucb~voXZ?- zaL_cbsidSV?ot?9@E#7*fDzR66cq{b-bfMc5^2%h2>$?DWdQuIlF2HzH+#Z|IGds9 zDd597T!fD#n~ha!uvk@3p4PR%5?oEiJQ})8m$;joX-@@6QsnGt3r;@*e5bLo$Gmqv z5|%?u@DH}I=VV8h$RF^uf*~KMR(Ejv7l-2T`1~eg;=5=DJ z5SCX-DSNJ5RMp0GgbPfLM;ee%;Yinb5ikXFQ~H`loLWc@Apz7-MX`=^>{Km|mY^lZ z9M=YcaC*`+vjtZ)Td6h+NEs&R55;t#IM51{+L#)6TY=-V;sWkr0dI0DwGX9!&1YfF z;CQbmafxG)b}h0|*A@+6FYb4aa=dRS?<>GYU=6O=1><{kJAG@Cw}@OG9wvTlT<4g} z9$T{*1`q^oZS->m#ZTD$v^;nCPA)n6Sh(#PovH3W&W{MiPRHbUe8`#fHe;4qk-`8^ z>qcRHXn&mZCg8<3Gv|%+i=iPyM!Stp`Kp?-j&6KkBFUCEa&d`VNF9J00OMJ6*6DK7 zYmnD7_VdcPmj3D%oVcCt62h(leigLqFUoF;)6eqw+hEL#{2+X?HtX@}S2WhAB}bWs zF<4`KF9Avw9}tx`P_e}3vmjt4{{U5sZua%4y2@g%GtOmW0Ca{(tPC<;sC%ttd4GjX zk7wlgwz%cTkZ>YkJAoWT)>9DpUaNZiZ@Q7#q%u_f;k1+_FzH{reS8qOBRu_2D zAHwj5$T4xU^W~Grurbll?{~gd{({l^eJiSZw#Ga6nyIsKc*yg)7h&LiX!3^GUhtb5 zJuA}n{R~Xkm+bz={sY+l0X{^oF9op@p+|c4zPf834d1@|e6A$>eFSljq)zexBvxO- zx5rueQRjH%xQ@FK!%E}!xXflfFHhOI#ZrkK5iL}BDo4XbuP1F@u1~|BP5q$}NwD|< zQlyXfCy)W|qp<9hiP4@5_cb>mRki70-WkszBmKuxbRPch40(#TIAB#N@ z61`e~omQ|c@v?vbIoq3kzmLMbJ{q`tQ>=NH>Np#!x2UgES>fbwe7lh4{R%!+9sB$% zNXKfT&J6`oLfN4~^{w=9c2myeb*=^#`-SqnCyUMwDvv3Cm#se7D*_zf+R0(w{BWcnZK=+MhdfoB zj{(WX$GEy8Q_!#Ss?JVq`nSP(CT3v7gWT4IqbGc>e1o575r*uSu*jVSZO=)yW4_pB zT(6MwUP+o=j~m?^27na!Ep6;`n(Kt}-q6P=%*Zm^428^b^h5Y9c^6ke%Yxvt=D}oz z#Fqd;H#HYbt0_`J$YH+i-qBHXJdyjnEP=Bw5FMq(JXmO1{wKlawZa4R-0(pSJ5_60 zy-pR_eg@t>p|Hjt(cbkJdqTIC@LUY3w@AiEAQtuSPw!yD2La(Z94DCZJmvvmixPH4 zJWL1N$-S<(!&6#m)4^eB!g%fv4J`2%CJ!UcWpg=a{{Zwjuw2GF(8@wnn`E%C@NOHM zZ@zHLCkTl_w$N-buyHK0hPJWJNAGZ`Nq}{~&QV9&pm8aTV(%S`? zd2SXoe3nBnBHcDrPJZ^ej2|ug(Px5v2OCD)1RVuHGnDUbsRdp^+Q%JDNvU{*HZd|R4GBTz*@skxKEaam3O0Nh|fFwu55`c&O$ zQ0M)x@a*EJ3_$bxqO}t<@^~PEF2dv`Vf`&|QB`)Z!yJd%FDHN(W0qxsq7W_k z)Bex%0B4i%4=KV0^I$!#)Rx@UO|=Rf<31>8=s)2__P?7_aL|rJra4>45 zxxk`}3M>Q!4K^jI?KFd|EHOC975@OEpaz};jdj8R(Wom(I$FRBjgNmyX7Fy%-KvCG zQV25v^@<{mPPE9|0dkE>p&g9?<`<_M^_SxhpLV>5X zP<*wBZ1!vny4S)Avqc5L;8P(1Z(aeBr$lo zK-9GdO4CSe7bqDdbKUbesQhbESGcs717qK%1VfrjhqNkg7o-v~N;o)+np7-W4bUhf z=3OWc86K@5YCk#wAxn#m*62G>2rg2(E~C<7pjSg~_gag*TU)lC{+1IYPTw${9DgOWp@;X+s069M-#*Lw&$v3KB`!b@RGmY0^xGDkNM{z$F;cf%k^OgYj<=V!L0V@ zkA-I=o-fStGJpyi{@rRdxAL4W;O8MFO^B^!O@}Auw?;!wy+=wu#A62_*9ZT{`Knd{{U8#iDnL!Dv}3zAT?0ao4!!oPrBS zs6Fd=iY>#y+{8LreQD%{JXaRu7WTJjB|XPag&?t)9LCV%cd5A0Qwpr+Fozv2Wd0O@ zQf*g=gc$szE+>NZbb33##+(8UJIb(fUfAb$$wiR$_)`MB7%^v!u8rZ{ zL0VxgxNMHwM^rl1NC}q>!L~rrtpNskLmP5_9<+jskdy0wjS0U`N{XnemkucpxG6U! zH1w&!%C94cb7m5;T9+5s)KuM7Dr|7b_JR}v(CW1&u!2b%)`>_d{3+NEO_du|kU%5& zdQ@RbZ2m!wB@W%l0aZVRJQYjv2qnwp9gs({KxsGl(IGZ&6C5@mfd^{r=c^s6(n3Q@ zbQOCf59X|ErDUVrQsTaUr;d-R{WktEui_-#wLd!T)$4bj1n}N4Q{!4km$%BhmViDZ z*lSqzj*c67W`0o!Z`Dy^ZF<8`m!mwvr`rx#PryYIZTc=nUdFxdcRId5_I&ZXM#so* z!+ZKwW#ucmT!0u#l}?qG(RzZpJil|Cwz`sQ3mQOoE6qL_%fr>6l7~z7thd2ollgBj z?D^zkpQh7wJ$stI8mDRq%glI?HHYC{k~2qUbgU$ePN`1>mk%Ty-*G~;1pHj*rr(=& zu5VLJYs2^?`H)8}u5&jrKnCC*^^Z>0Paa7&Pqn;+!2Z?y-P7S*TYkpt^A$X+IUl!v zM~F8%5ax^fP1cP+YtQrHU)CAQrs-lKC{VQ*LDsZOjeCQY zrD+4P<-9jbf~J6RxsGxZXB}w-i1T)ZC2e!?rvqL4J;Vv|a&xCm@&UEILv4GUBi&p zH{Q@_)<4%ds_aNPvcHt8=Djots%g5X$kmIfTzgAB4)KLY#f*TYH%InkV_+X zBCA||dnu}u0do;-u(Yt3eKD}KX;euQKY!9|T2L@G!1OdwTiDp3k8p0{FLG_^Ops;7 z!;skm*n?%G0TNFif#?(30<&(H)~3i6xjr=@++#&Iw{0#dS^fYI&OB;NXxp6&cC>qD zEgBw%w&l0^0lq$G#_$>eEi0{+61Pa-$9VwR<2#BFS`gSCwyLnJN8@r@>~Xa2QE;Tz zX-$+l%up5NBGLeVE#sJAe5Itg*{wb3ILf&UU#Br@BZd7Eom4{&M;8OQCcg?wN{@5E zvI0J83~lzuhyBiMxle8X0PVEYUy$B7j7^a+CBPM=gJs4;wngbmkT1!z10a&<03@38 zeXZ^>8fy9LpUgRYob&G70W5E18rN5^uh&>MsCZaz$&>;JXbQEy-V(Tr#te!|h5{S7 z_dNwg+#zQ8_FNL1by7&Ztxj(X3i)y7V~B8bmf9t%rN|-9XaPz95P?qsxWh=)DZj0# z2Oa_x)F0tU7?8XmAp+A567T^Bs#tWS2vYBgq3lHf+|6}?(%Wb>&M!UKlI8zLpjMJq}i_?5iuP1~9+^zV5SnnRU9+`lDD&ao7Z<02U=7mwBdew8^>f`;7 zr}x(}ol6s;o=D!in`P7TuS#6ZzB{I$U9nzkm5AV%Rc@#$vw0flxPiD4pfnL8HxN(h z+>QJxhYV~lUW;l(nG1j*0*yB-r7(5v4hTXJR1S$iM*2!f7ALI`Is0OVV~d~Ie;Q=A z;Jjlul*j@-!VrQ3U?Uvb#aW^L@L_iJ-RS!c* z)I6U$#x>hyXisXmF1qa$;e)wEJ1H z^ICfQx_mm*H9kgJFR=xh01lMG8=URr?oxCWP>y57=UJZTDkrJ!MnJcO@i}H^*3AYe zMZiyS_*Qpgz={lJ8KJ<&V`6Ns4I4s|-lb|S$+vG0;+WXr8&R!j01W^ZrL4k6QUJyf zENV_wW^==X)807l}ifdNpS-I1oM-q8v=(_KC-~%)9n$k604?$cz$JlY( zFi(J>fIzI9bogK5I22KFRU926#j2O8j#^_R1X@+DiVXI2_}a6tL!AopgF)L$J_)`r|SmEdLq{fyY?j|ArMz~r>f5NicOsZBz=AUfxpvuU};23AZ z1nq&2Nh5I7x1kk}#lrr~>o1)04ix<QhlRsa^c?O3%{{Xj`?I8B2{hLpie^B;sj&s~g#CiCRj41*} zf>?Vv8o9PpW(kjno>J2p+~SMr@k;b~dY5SC>E3JdDB!$bksC=ZKi?yQhL1pc*Qd+V zn*6-(-tYKmyeAQlkBcK;3cg0|jGGn#{k@5;FBvPIdls!Bz-(}cTF@A~oKoxYt#KJ` zwWkIgY|sD&SKBPUkmva08qwtEgHOs}1XzxMQ8iE6C^`1E(7oUVOLsMT>zRfY` zUComUND}I&*R^?m-#k~b>Gk{SrTzH?g*%f<;KRQKo=MIPSEx_xK*8ah){r*>7p(K zXGf!|P~{jLN`guDv~*g_3r&NNfVcoiZl%q+QF>M?EqMI)Iz}|zroM$fTaS*8MHF$~1vbyWDmhcu?g>7;No$ zSB+6?JS}(Oe?nd+2Z9JK30TSGqV`@p9$Pt})m>t;t<3RkNr70!P0;00W_VkeOW=E& zdUmYtxK>q?!_DD)LrjhS?R2c}v>`eyx!?6CVQP03KTu0DqHMckDDLB>Gf0XCO6NLQ z#ITs11EF=6H#>8i{{X1#S1AXl=kol2n{IsAuY5-)eC(G%6gZ4?4lDq4Y-+Wvu{c8G ze4~ZrJZmS#;W4s&pQ97G369An%FdWenp(Ostq8SWPzU?RhnJ1#+0My)Plk5K{@)xA z5(Z+%-=`Z%A1<0xnGij}aPo4T=O4s@{!HoPg*Dn7;H&TyN-J-X__A`pUPgg~Hz6)g z(o_Ec!M;Bh#<4pY?kN(x3qL0t$T*4eCVYSF+-7%_^LIk|vzO?!r(K(Ag|Clg`5q5} zx0mJ;Q_kbhmB{5zj(n2kAESki#p=eDN}H*uIc_7z@Pv%Y<08#&K35y#SP@15FPt<< zmmojfOYUl+6`SBcZ24az&5syM!|=RTOL)w@hP_1D(tNC8Ix&OhJ?UJVAkT^zIZWJW zrv1h~JkmuB;MweBh-<$QR|Cxq06B_419J5#tsvTd#Bi*N9#fQXQui_l4`2r8r7BHy z0@&81A;{Nz~9B4A3hbBI%HkzrVq$%V6 z_V5yf&c&A;{{V9a;I*C9#Qb-ic%FXWqn_Byt{W$?)O`(k9!3s8{{S4(PTs-Ibt|Yx z%i(yOkVN=U$fK(TN6-ejAGN+Cnx)vkMaQgVDDw<4$o+}q9;XK_>>U{v^v))a#Lu8d&{IH$^an^gKo>fK zE%;M`=QM9YW1#rc2X~eYy+*A%I)H4D;n1d^@Sr;z6qeZhZ$LQsNstZ0*8K$m=0_p8 zwXD);17ElNU+%dUJT-Q>29ct5T6=s4&{$B0LdJ^#0)ADTBC-k!C|65zw2lV+ig4H* z(fYKgGhQtS`Vve<+*9(RfFIqCOLp95kg8NIt9>A>wC+Qrzva;Yt0XxbL309tY-tg< zH{8$;ni>LDFSX-9;i>r23GDq%%)GdS ztYLF@t%rWKp4;JAIm&&19*p8T;FNLD`caIX0cU1ACRrt2&T$}v{3`I3Z1-XU2^~FX zB<>pv>dY9?a+`XAS$6C6*DBU#w5}@7;vi?Y;d) z8qACg{@5S?07q~aQpK-^{qO$(Eqa`%@r^eT%Fwmr`t(zileB?V>|X^HS!#y3v^{{H zQc^NkQhljj=j;Cf+A-aj@=>}>kj9h>bUIhwasL2V;;T*y5A!w{zyy_RPkP=av`#mP zbDa@zKz~W-wRi&aj?8$Q6MH6|MOA@O$+l0Xttu5rr~$4*c>`WuVgdMCva|xZK25I# zgK=6yQOS~BAOI*&=UPZfcu~GdkT-BpPM6V`xF z?cQ99fi(XBjR%zcOm={dYAx+`TAQfKTd`t$3IM2qZhs1k4l4N$K6j0D1=x;1Ob)eiXh&nUz%X=w@?Ym+#yS4)_S_Un{5Sy?!aBk8&OqhU4ddi&-)FWd3_wQPP(f&k@B z-7BWPZtAP$c}?wbBo5U(9sxYxAOvKD_@1e)a`fK|Wxi3*lH%B$q>WWVR($lkT-4o} z{%&~&L@Mj6`Wo!lR(7dMJnl9clUk7k7dW1j6U~egns=ZGygv>+ob}t zsL9En4X5qCVZ_BTI3Xl8Z z-7Z&4Q`f;*avpHQApzupt>~iy9zV&Wm7q)jcSEg46Jo*HFuWH=j@&JDUs?$cdGZu=y_$tgJxB*u;5uRhi2ojTK=ODUKf z+-^^!D^p#C-9H+5qbrDc5%l*tf<>~9lnk2WF}Uta25fm-v`WI%cSju!9~78(E61KY zI!t*AeKLW=t?X;6d4#aw^4KI{x|K&*{z9%5X+Al~InjXcXf9Ovij9a&$25@AdejmR zGlD0H%xDM!xu=lGe4mW^Q@x-gEE^(SM!HkU4y zr$jX7;Z=|TJB7)y9<@YZsSO6-+|{5uLr=6Vy3ichNVq=V>p(XBt`<^Flh>sOoHjoi z@kt%yaLk05*t3(=k*#v{=J&r}@8iW*IDXqD%yNJ`*E{PuZ#J0_aaqBoqjt1N&#yee zjut~+(0f&7icVlwYTUJ{=JvW<7R%zwD z2hzwB#?)xkjXxUe;nzMNIs0Er7wrO#XbM8y_pmC7U2mSQ$8dyqTL1w;{=QH}w?qv9 zrPR<3?t6(q1HVY`Op>5YfcHZ{J-x4YxFeK;okakxjCv96Ap2D6DZnG~?-O=@*`{Ng zhzK^ZC-I;(984TsEOx^P4Qn)RgVktBIeE;5r*KCSfvb8(7UemoI5`g6f`G1{%8USi zJI;dB^v>rwOKuG_2<2wT<$R7ij+d$>US@JMp+}ILX@ykT1?OvmEy5isnSiKDSm?9` zhZn=;X3W;Hg$}MF!j?euhViUC0Lfg@Hb4qoY#UK_ITo1{df5t%Dor!Nc+;298QeEV zj?jua(!VgP=uaKQ`gpA=&?RZSO2;P4-m#!L&P%cv1?h8K{ddG%U|wyWb2<4V-To|p zXkPtl{artfO;Kktp``#PXf@JBZTtWiI#3Nr)E0G!*R=+JkCX>AYj6^l{{Rl3TWoAS z;$%MC`$I<6O>2*M`tMh75L1UC+SI>#=dSIS*iJnu#}`<&k6YEp9XB`|O)9M_k3EE` z8rF5+%7$~}aC+G;wGNmZ=3!k$`8BvP!8j;C_Sz~=x7PE%HY<`5!&d1Uxg@Q z9|7`n$N2^<=1->^%UdJPfu+|!g=qup9KJ`7i;9jeE>=v;fC-j20uYOp6*~x}%ACUz zH=xq6!qi#Ze;didS;*qYlEd>Cv=sOqYOc5uLnqrz0UVheA0w5;Owx>Kd3-1SQrC2; zx}2d-bHurvSe-YJ@i_dp8vzG57>M4`KT#DEE3u#Cc*l)rIj`ZlBaJyFaWcKTN$l}g zHnZgzJ&iw{_~0T)TbYJ+-URTtdOGx3Hva&D<{W1`;yjduXW_ew$~Sbd6ZF^qfm8Iq zgZo6hZx`gZ44x|}Ww58yd~e$FHL%vzzMiey<}%-`*YY~?`TUGS`#i}XGY8JeS{?BU zYt!TC>pZx+`=UHQ7USOWAb>>U^9BS2*KV?D@bmg~yRY#$SRnx#sd{&#J$WV=85q8` zGi72Kl6c$Jw3?#rA_V+{T8Ua~&5qqwxg5hD+(tzpYnhSl(Mxui>{#;$UG~WH_A|1&&8@TQQ$|>X=!_P@~ zA92t)6|H%Eqi=y`25!jO@Ndzhs`}Tt>uT|R-SGt6RK>$}-mX^N5p`SFcGD5$^V>CR zmJ!;KwHLSLQLA12#TOvvx#Qd%w+Yk#06OT^=l1zkNVfoKR?Z8}Sk3s-i^Lxj&>fZ(@&keA>t`v&$xA}V7l@~DW-~s|PSX_J-p;jls z%_0>6$5W}dr7HwmU~@k92RIdS?j8MVY*t3lc_Ukl0V?$Nod7{x_g^ZNNyujj826Gl zJOj4cEp4Pt9R0P+V?$rL53i+byzp4dLFD-vGNS#nT!HpI$gK78PU~yXbN>MB`0at$ zjkaxVakVU20A6|IQ}S}g$1l?k9?^9Mr+FaKr1;l2orMEn1PI&m{3{&8w*ziB%G9;9 z*qu#O6JZQ|)&z|JJTJzDWT#}n;b#IWXv#a=j{QKWu;nw6fr}(A8%CC?`luI`#WFcw zSS}5!Hq}bmKD#AGIEIgzHK-+y1bFkGrIOaUjyF)DC=YVJ3*uaZ3tbd@x)clfin+Cf z2IaoUhdt79^Afikj^s@MT0gR!f!I7d%KTHD!sW4X#O~we`mvA{IGsa=wYIy#Q{wxW zT-Ti|`l;jKEJD!KjKJ#&yc$rU=#`vP&_RLc4uuA+mk%wXG_uNrnT)DAf`Cl7> znC49H62{N+9(C~*jOJt)IyMvi%?K|_1vii3a_swOpX|frX8Sy zY&3Uz)SYv7wWYxY%AT}jIvfLr@LG-b0b)H?i9>>U0U#1w0Z+z@LB{62k+rv9)JgvU zN)0y?oy^BAc0BKi&Cwj;f6jm;<9_S;UI=RjS&^UG?{X)prGgXj{{Ve6zymuKvJT+^ zN`F8l!TZtU8387aNDorrqNei#Odm1vy!JKN`O)6tu5bpk`cqlQCN%6=VKsO{n9gh6K zpd4l?_Pg|tn9vfM$29_|)P7W`IAd#u=M#Rt0i+8-wN}Ti=mk09tq6K2;3>dH_BoC{ zM^8@lgWHKhQkOx_c06pubPXdJY%2-f>P;Mio zGBYG#6fXC5C*eqpvMtV41O61KO^mPr)~>pg6ascn@g+W0+V=x!niu=wa=Ye&jAcdkvE)=~FncuqqT=H7OWwjVI9`KNYT`>>rC zri<%ZR2X|hoD#)AT2zIeImW%NW57o$uA~n2wJ&lkxLhjIo(xRwk;9M!qn9H)w`CN3qwUwS!((*XS@v-A;SR1=f zL*BH;6O|7m3mv#QsDH0h_|{u$xEjrXA8zECTi(6D*Z%;s4gML%Kx-W%$x36R=j5iB#&uvyOCOZRo#H^gWzHfXblLLCXg9naB(%D2Jg{&T`9qo ziFkx*C7>wMo&a~3w2H2RrVclJbM`vU9}BqrkFvyZvhB{f?h)2Dm;z@r*vJkbs1^q5SK! z?)d%n!R(%1KcaU#{FjTrAxG~ zjdey;Q$5dir@0*~SdvICOARkUlIFESTcrg`j4PVAE-68ad;&LrySf*RqaZPk{2UMT zsoLTzS(nuwD~{OM0~(A(XsXtD8xA0k*Z|g@^3CjrK@$t4L;+CN8}B|BuCytglLlPw z+`!nytDi~bvWJ=C@n>%KJUEbBx2hU_Jfke@l;Uxjp2kk*7Ab2drQR;h&uc>7_8<&-&9mKtD>+4FxUf_8^j4(>=tW+g#rj`k4IrwI8vF$;i z>+z!ikofW($e7|~l!3K1U-(fWU{{41g^g)@LmC8ab1oM|poC4u!*1Mj6Qr4*;`bX8 zXuv3AyvMpdv2J&`9Z*q#8}hmSn_gpads4^L+M>|MIsSflIS0fZY^_7k*4(C3W0*cy zwH)aPa813|weXt-9ABGG#{`{E>fQeU^wD%7Ru`JZAZur1TDr*I{{RZk^DzGan!$sd zKYNQKT;~sa65%@4^8p+QzMO%8-B5abD_KD2$GI@qSx^?QdMql?@Uj^8D^}zx-6|?l zreeo*3#|qM^6n|_U-8_o+w0tXMzvZ`@)bXCIoLQaAq|}Gz{)>IA&$|iQ*iuiqra)A zUTpQ=&6<}p;+!6AjdZxtyJ_4U0xwmyc+=JTUlrB%%JYQ6#DE6G(8i!ZI#+i+aSG8q zVU3Q*rD;&0=W;`1c?a>XKVxh~>cPI{a(`1bn5*qw&L^oID<4-(+gEM@CQoXUw4a$Y zuJnq0e~^YD83+7s4Oa9$4Mqb<;JIQoC>v{@_Y?)=v!aq9N(Khp{Ap7UOAb8QKT)V! zNC1wAd(>boFnE|9IdDS5V$gg406NlWNsjUOTPlI>A1YtrPv`+${wrBK%YyFjajI53 zcnsH@#R9O5_f$a9VfH}PgB9{92Eq40WRp$I#`0i#{zemi+_i4ZIuRh1v zey`g5Oe=W&r!~oB7Y3&JNnTd^;+&Eo_Mcv~CT)c_ zejq|XTE}f&ym|%sel?j8z88fK7AL)DpR2E~lctzuK1Ia44Hd$;tnn#sRRR{PSg-bl;+;! zBg@EQ6(w%B6c9g==3ghZjv<->rBpbi%gu$XAUpiTp>JW{nv^*?rN-v{C?pByFE-S6 zsszIseZVEufR};sNwVGbvVD*??Fn;JXc^*S<6$w3g{ao`cT?7*CX+0d0YGw+;piwe zbomWk=iJ7s+v7lKziRk*HvnWv(e-j&zW)H*(w@I5t(y)Z4m(;qQn$$&$#SWz@0xHC z<(z{c#>kn)8yg(rx>#2avwXsdf5ME0?~xjQ;?djgD-NaC4a8KyOic?d4t>M^D%cRcm4$lOM(y&;c5bGQ9ED_K|XWJFmi!GxX$^+BhHw{uF{#IKeiT0PS28_|OTH+}AY; z4g@*I+E8lNPGCUSC(HT4!TgP0G=Oif<(QkK=wl1n^6e*S^kdZ7NN=Rc)?f<-;EGU%7mbkrzhn zhhp)>+Z}}*_V;nDr#M?~6PJ^UQuaW?_cSK*mKCJco=$OIU&&xSt%EJ#0)?M2LDOlq zUcRs1_)gvA`5o~u<9voTo==a3&Li0*1L?_rt`*A~mp z_bvESKl=}XuL~o9 zjib46Cc8KIc|32G)UP3g>fWvOJ;h_Is?`o-k>m$_s(}#Szrwn8dEI_h&p;FSEmcfG znKrxrUWcV+vwcUyfY%#y>qcl{!~j-M}YL^kFT z?cZ*{k4nca53@E+!5n}MJ`{a|eh(|P?c8jtP~M(K7bBf6+SZpAlyL>f6V|5zE-qXa zkMaT4M}E{E8VB3l+?Sn^tDF>&Z`PxyMd_&g<xjV`-|pn&h%_T+{^{ zW$3hP)V9<+pC;KF{D+4CZaM?no!czFxIxYHJZv{KMDS3B6iN9j7knR@;${HX6md?y zYQES{)J+ytbAQ^E9q4GiAi*+C%R)CpJDgU)-D-cfAJraTGUo8!k~F*mx_)(2X+|(1 z;=KO=5`1FmK@LlVtnS9D^OCe4ya&(LUK zAFhC=<+;Iu5Rvoyx|b9D1y-{(9O8c92Q+P*IDcuf(YAt77B89LP5{VYEB)0t!PA*M zM~d?dA@Q&{^+8VUQXJ(z%J5l$yBXV8`)SSS0rLL=4=NUKA0t?gth*NovQWd z0dw*GRe=_6JK2#O8K4eC1^)muLH38gt?OA~EP1cE-doA|);Eu3&c^YWt!!^(ZAm3- zK1Rsy&7%N08V{Hf6r=dDhIEONT6sil-3O2Ch^HH#cWCQ9y|)nrMJKz za9i~ zF)mUT#0R5F(V7E(OZKNXg=SozyJXSgs0^{bl&UoGICPsQ?o?U^{j zOwo_~zoQWj+XxC=+(S%9F`!qo<<9cHddpW z$U}pEKe%s@ZJm=a>utq9i>*=%Acr{1Mdfw;mFpUkt=&UwPj+2ym#N6fwj4q zZIzC8V;mT4hYy4qVxiZjY)Ar)TKj?DiLf`X`~*~!{s(lg#fn19O3Q-E&(*)craW6 zt;zUwpgXoQ2Hu^@-k2rG2m~vI0D@h200I4dg#p;c=LNe@S^@A`837HJ=|C#vsY1Gw zKuLxT^0AEvaUdY>dsEb_=ufu%ckTG7+~9Dq#cPacR2SMC!0$<&w=ml?Y({p}TJ45N|Kq1AgTXna30kCWo zsau)@&mnuMHuW7S1QOuT1&fBJfalZ~2SZ@ADdF#FBjpyib4(Au+wp9lDkpRf8kD_8 z{{ROY23x+A1;u@V2^GygS8D?MiO4!`V6Z$Eu~7P&>ep0lLB-8fAdf@qPLcc^c_fvs zB~I%Kj+{w$6wp#fTuX05z{arD+zh zu{5q-sm4>u_+AsEC4jWFe8%_fdj0R%_ja!jhv@n1@E6PRrelM%RJTok(!Bow?X}gv zqx7B-n}S<+gigKcT-IQgz|y2P>GAw;LPs7G$U%(8Fpj8=@9SCT$&0h{=9o3WsRO-$ z{aCJS>wbxR#mhw`x z2R<tGTz$&-~todqS1oN`@lH4vU3he1Tw*+rK)y@Jo32&c=5v0 zzfGv_n(Im>hxr_vrjVO$H&2QRdcfJf+1tnX^(uawe}#U1`@jBep0_{spZIB5ZaOV^ zoIXWAF*``DJ6nQ^%%Vd{u3ZHK`4nbCSOV?_)$abk{i7|J&nt~9Tvey7(Gc_ozUz<9wvwEGAeZZc*vuuZJr=ju&j})^-N}02+L>!BlP=jxKnW{{Y1+c}hX&2gBq}_iSl# zx4}=Z!mAlPG#(#>Fvj0)g^KG>SX4ueg|}!#6}A#iTr6d_+pQ}MHOn!ZUf`A&2c==L zv%es2(EXtTf(cLQMy3^%yLlnXO@+TZR-}6kSgvUxiy+tVsj$R+UoXi*{l&jfC6A$} z0IvhcA(5##EF=1SY2=n-OgUM;Hw8~lv`R36k&Fpe_0cp0(SUP><0d&GM|`>ia6 zE~BV4s1^8Kj^s6wGBbA+C(%V*J)qJoh}jqX$xCbDn0uPel2`pO zZCrEZwD?eEWCklixIODkMMWPY%57u7vMrCNtzp+}6^wVwyoy(Hu*%mXVtFn@bnQ=% zKM2kmr-}2poSzv84jYR?J#JR2wU;8eJZ>!b(rJi{n+DNz$j&PJi}3Cz5ep;UHMI6_FuTa(;$}E&00BA*+xlj)k50CL{K1%bIFFYp2}Embr21DT`)1-4tP`Be ze7R3^PqB3P*JnL^CaXlw4)-_O1-><8>v98JTpWZhr}$7ZlOH3HeSyjexa~#;S9o!; zu4$3`3A!p(O-~{|ApIP;G8gKI$`IFF3SU%hw1Qj=xge48$iXf-CPqw6fj#?Dx(teV z-dBXqY)s`d-;bKxZDMSDMu_22bffH=YUH`%zIY9VnHs?$8aQ<6ko!q zlQF!5CK3sAn25Jp3oGs`$T6}@;&Cf0cB_#T-D#Q}q{Wg+0YO5&J%1W-2zYF<9E>}^ zQhJ|L=~V+v@%+N}ZFAZ|D2jyC?Ghylh_+6(0awl8{d{Kv&`MjmpqUqt;>n3S?n91= zWM5k8)31WDtoSFHVe`H{3{H2^Ct#u zo@y6FQNX6DRd*|>KgoQNu*C8nsi0cp?fkwK7*{o%?A}65mvtx@f0(WLYxUG!kVI~H zO?0@eNd7#zc&$x_MsD_jpeswk3wX@_N?^wZ2!l<5AH{1Os)bnj8E?jPGQ7F&3-u3F z^z3SW!wUZZ_Xoi#4_2WEupa?NK$n5$T5=d{SeELhwwrh=AfK9vu!OmJMX>y8yflWUO1E70cI#UIR!Vu$U zALU3ipS1iB)RXsbXjz@Pw$ZJytu-p$;thu!17Oj5eLfQzSKTR@HZYtId~QbcC~M8y zd3rvdvHg2W&v@*@_u>`y7`n zm9B28Iv+~671snw3tkucQn2;g3NB9p1BkFU6_)YnaOwy0eg(O>?sFKDu=Lc_^6B)b z_x)MdR%yv$Nr@<9)HN(R*Qci6XUBUSZ##rGl?Vbjl?3*yj1EN`KrL`ktLZ>J6kFP) zXhzoho|F*t#ynxcjLObn+T*+F^=e!bOvVBG9k`(CJ}IRHtK_+cz&7tYu0ugu3>V4s zDqQD0IE5-Y8WTd9+_U6sh;@m%H#eqmIhC47_PJK)r7)?RE!QCHs?!LC&Noy}hJbq$ z5`2jRV+!M^`q7X$;Ji{OLuShazTVTIsk90Mqdw5o3<%cx3WGH^z(Y%{P=V_}MVJET zmA0+;(}2`)$(NMOeW!HKYYRo2au$_3`WwUefdmmSHqC25`~_aO5uF)GcCxdfSD7M4 z>)h#CZSbhbKIrn2GmjYYpLRru{{Z?$a`FCMGSQngs~*=7Q~7kSM=5#80bOsmS_6*I zxfC9kS^=lGbAeLiC^}FKFsp{PUB0VLQ{-vmk3UVW7YK!Q1b?k_b$vhU_Wm$q#=Pul zAqQ&X+3sn42=LrJs7g^7bc>u~u0IVbqla_v0I?3esn&5>n$K;&jYW0xq1@=o7LQX| z*M7IaTatwy@@}I}fYP5V`oKpwjT>Sj>P7ljS3j4D?oR*=zC8Z%&iq1}^kwk*js8iC z6&|fu6V{eel*}jD#^XSpXvj_wIiM=Jy)D*^gWp87{KE8Po%(ESisDF1(+44oe4!43 zO#vaXEB^quuuD*Bk1l*|+?!jgQq-s`9Kyl5%_tOl1?XuRCP;$qw%&nC{&WH&F}oyh zKGIO8iA*MQz8T4XwzrnTV93Kd7|N0AMJc!rU}Q@cIYQ=589?n3I{a%`7Fb;0BMwK_ z84qy&@8_jDNx42r%VfD*lPQf5w7j4+U}ZzgVhLPj`i0IYTpUSCc**Aq{CFguLdKtH z2%?rDU6PDaIR4TAg%RbwYp>V!w(Ec4&D*@vc^P=$fy*#tk~a3!RIhW_^|j+37q#tP zpACb?{4W{+#f?KiI$YHC_CLc8eztCC_Y)2{nAV1mXtjC!ZI`F6m`fxy6{Nb0K3Hfs z6?0a5Y^crT0|C9&61USwKKq5aj%(?IxZg&%sqJ0-c|6}^*}zXTkaUhE#ZIZ(yS3Hh zZ*KKdK4}G22YPwKT{$bI&r`4G`PAK`t6nHGXV?(ssJ$EO$z7MoDubrA$=kPwUoJVZ zSDlz{HvmAjeBZIZm(p?55k)Z}>TXT_E0xe>%1SMclmd61zw-Y88Uw}lx%f~|7cG|k zDTK^7AE?`*>0MsG%RavYE-l-PV{tCEd)~9>zhF1v97~!45_M8Pt#P9) zTMMG}tT%6>qagcSM>iWMbOy>j&0U+yMKVO%*&*4|TM4Z3uHmn1JX6VGD-SMB;BL3+9~xo5eV*f^INx36Ty9i;0=WBME}bWV zUy)_w48}tl3!(#Y+}2GYoR(v@CWkmQC71frn`Edb%W*!VLm_sGgmosh%#7@3dEP&s zeg5Sa-E{1=roEyrs958b!*EwP0dS>ts?4dthl}%WM-|O$$#4m{bof+l%w?hY@1Ejj z0M|QbLf3!Bu-jZKBG`u)$TQurlXN;#lo-h3xcnBnXv0B2E&l+ORoJko%ANT#24gfa znmNEOl&v%0NXG#Bvu{m7*3@Jo$<5^L5aY%`KP!#WjDY_Dl?JnjozwfW4Z#ISx2;z_ zyo$-NJewmLS2U9H3J|EKyYP&N@jUw>qz|Zet(r|N9z^_K++1e{Z!$Id0yM1eBgor_ z^7-WrIQbxhqe-lG*U5?Tyq6&Za##x;ZQUwxcvwdV0hqAT+XM&zlcf#cBqNis4XEa?M5?gh;dFb}vqzx$t-a`|AF zNA-S`2Hz}WZL44$c1T0@9_s+03$JPeMEgzRnVi;Syyg}`izkYf0R#@vjo;)hJ+`eX z2hPI6%j7doBby{%NzDhoM(FH(7Vdc+!EmTELy6-j^G+|FE|f_zpOMOi=lo(gkJHCH zY5oG&pW{Y%mvMN-pHm+MznIUA51MBH$U*g`tswsZmGUmc{{ZIRMei zpPdqUH_y>QeTB&7BWAr~TIac+G7s$(r$L1eKj6MW!g62j*|53u zmVH222@YV?gy<@#L4-_x4ayq!A9kCpZ`P~t3k@g3KHOnv#3albjjfHP6QKnuZP&qA zD#`mx!N$=K?A$@tX6smgWky-wmHocS;WL28FLV+g@$J-pRj#$9LVq9USP+biH)O{c z3I)&NMLgv2(7w=fzCp?U@N(0-NT5bAICV8UK(+F}x)@vsH~!rCKBIPrK1I-;l%zA< zZ{430&M0O2E;@%%^dSa6Gr?xG7;>X}+yxaEMP(%5;?5V^_3D7sT>?D5Da6jwC4s0# z$N*CMjHrVidCG{aS zAI^*iqiA;GZd#`Lk-f+JEzpN)`kkfd z1iS?x>diY)33EvZpEjLnI8BRC{sho(IS3#d_Dv`b2Dm6&_v=mpuYvQh;N*|0-~l4$ zqj+$4^AfL-#Gi$45QOHw$~Oe}A4(`*aCkeX zX8X9MfwET*wbS^t(ll-}7`3fH07Okqe2=@omu`?x#z5c|#Xq!Hjn`(;eT@}?)3~l3 z>f**WB32ft6_{H6x)J@U)hqUXy>jo@*9`J#Sa+p~381W|%sq~N19^{)yO3$q<6CKR z@H=aYfL_8}6;K=KJ|7y5pRVTr93mvd&!%YPtzx9H~xgydh_#|sZ$QM$3zq#tTW&wkNuNH3~(^jGvUC zU(8d5Q6Go6ps^$djdisI)Vwc^$HB#7xDq?m$&*!ihHD;V_Yw+}UW2VGFidllk+Qm1 z?y*0g!m1l_`*70U1C31#K`Zm$-Ov5IJzjt78ZI#aiuwIMMM#u{*0k+JS7n=4T+3je zAm+Cv&vo{KQoa8G*39o1@9+fY^Is&6LWcX04GxyCx#v7MYBvXzF(fj$F8=^(=t9<( z7Ds$BYwD;l4QzKM@X0$6=6fYjq`h*GC<5s&}}eM!|~R7Try2^+ryy ztm#q}+@qQaCqYV>N12c{kGcb`bL?#Ao1L;s_O;5=-Kf24wT7$4E-MuF_iZ z++2IURBG6z{VGj+jfWd9J*U)JXviv9qmiT#LW_?70F_M$IbU7~_d_X zL^CpY>O@^|U8Su~QddpEyabQj2U@AHSI>zgM`|JTs>vVY;CqYQ;BDG~z2Gt(4oHzc zR{}o+Nm2CQ1C<=&9GIL;QZ=Qm_<{VY;`aC%43F@Ib&&xz8%)MnQfAHpF5CgspM_=g z!(Bq0uOQMv4gn2;_12e$fNv`2W5&aiVLR>`hUE4lxOSI){2tT8Cgn1pGuqVwW-r!6JD9pJYOkzbyJ5Zfzm~FgeHTg?@P*1yUb5R4h;$hr2G=F>%G4RHJ_F%<^10z&Sn=!6W38ifn(TG26VL-d5FL^fX`p0C4aB0PhsT#d3IzpbSop zu0J>gw2do^+w`Wo<~sc^*zdOAUE;qsXG4%+5;!vBXnF?#R`us=9==PDC0ZF9wuK;t zr-78*UP~(-%ExWn?O|!UQo5_`dn1sQ<7P(re^cGLZhsn!&L7y!EZ&8Ew>wcRX-#0t zE=La=&gCe(PpQ2>sLHxu6J(_v32D(ar-J(;WcUjUcQIT-65m5c4=VTgc1AOU+6e4D zC_K5BhdTsi>f!&C6O`6S*EBGe`&aX>jW+v4ShK|Pc4RO)(yAd1aZ20pNtq?wk`+vf> z6A_jk)`o=i6|99t9KKJ{#(l!{tol^Q#XjQlVa$AF*)8z~xn#{&v;o6%d*yRppn|Oe z!76%bhtz(b-v0pUqW(WD=t_WxaICWWt$e{x6`!TnrOn%(MSOlxa|Q3(u2+-Wo)&G| z;Z)?Na>_S^6BCVPrKlqE2G!xi-(W< zUrOIF5a_l@_B=WarA**%>RUY4IkNp`!49J%`7{&_I%M z5=lrMdJ3COXyVZF!i0nlU-(pnckWUGH4{r(fQnoY2Lo@^N}2=GD*+&jYV8i4s0*(G z_?q=@I6&T;+;2{_Oej%V5JYjgAwpq0mEQ^*(|>f)sIN`e_hk?JmT-QN~XUGEH>-ao+L>4R|HQ8t>O{50>q&HZzNk8il_%RqSfA!D7r^ zh(nq-+%B|(q7qmRnOiP9hUmyPBo2nB0bQKd&estTX>cXmd)3Me!-t4C>1A}zW>jb08L%zx zDUzw2#1OoHB(xl)MXRX)-O^Qy<3BmP&scNAHU*ke~c{{T5P=z8C;yZNp! zW8T-C8+gZtNhFPMh!Wxyo0P9>T{Yz~A3NW+{kHM&*L;7)Va5$|Dr@nsF1_;U;p*e$ zUyqE~m=Gm#?X|B)YYKbX!P#JFZ9nSSnK5Ghq?Zi|2IyB_0%FHKsy2qtw zzeis_YWZ?Yv?uYdeY(o%*P1NZWuz2aX<2R84YlU1pCGuYI$FHXYuNlfKUat40%z_X z;kfiQ~&%x&y6s zdiTpNp91b7mt*dt!1fjIde!s({2Oqi{DVfOq>TZt?Y>y@1&QrII>H*M{C#LQUps@WPTr%X85m9B z-uLB3HE1Z6P&Z#6@@V*eBt*iJ6b>GVT%9IsqhEzE^4zSxNXSXv3Y62Sxuv$ahZdQ> zL&!?N*1T*zdWyzO3ssNhGDN}ho!fiaY1xtzaomp=n~v6{=9269!b|kybGV^C{Jb=jU+ihzNY1!c;4mWYfj5VdZuIt{W8hh{|BkD-~ z$Q&zSx+JOBAI*cgSLjJ}Q5#H@)pf?m~pq*5ZoBGW<(5@0NI#3yA|t?VY0> zDROZ$aYzo_Hk1I1)%~SA4UR^5#h&)vdh0U~k#d}L;b?!m!Mb;`&&lC4sE+;XP zJL*Og+UkqVUv{12KXkZ>w!<(Ht;>!704k57t%q|wkN*IA=`d$>alETnL;2ApB$;^R{pEa>SKA1+c9v5#h#?ktodeXqir;Pj0#XFaNT+q7?T*=$} z*&pu_;=zlf?No}azBdL_ zBVUZdfCe}K2n`>mrdrbxH^Zf79%CXXB5RcTt!=9=k*57vSpblugT2ljmbA+NN0SUm z=VJqR7M;M0-mYl5;WvQsjLeuxkZoiC0JwCj{-oWmQf2Vjc(Ek-5rlEwBN`EEZi|)k z_ndh5F_X<@=E~plx#w$dX8tu;Fl6{yc&FaPKinx~Q@EVYW-taho)f4kK`*%PxSaMn zY|D@Vp;Z)pMnybl-i~4**33pg>U5*%Sr&1>dT=EnxiEb)bs~$;KL?R658PiH%0czo z#~lGZYA-7WV&V>8ipdKM?07z+qUlCbeugZ=MxV%vkDxO*k8zWNUkhBlg(<1kHgDUW z3n8T-Yf1E~@&lRw0B3MyC+MR)phHjZu~5e$_HQ|gVSAMj8gr!}&Uf2iBF3{@^)~*J z2w%poZ7HfI@xEUjsV+O&?)g9@1of)$5hTNvr0x!3WzdRv1fdMr&_e1+xT653JO+W+ zr%`%X1FyCmXV&F6xGvHIe+n(|RrDS!3nFV#5~2rXDL_D(^o%p)Q;(;#8j~-_AQ9mi z91yum(xH|8+~q^En~%9r>09XqW+lxsIJp8{5A&<3AzA{D!QJT_icQcjRTQW_j>y$X zRnR7si6knP^#1^sfabu8K_vAw2ZKrtL;HGA9e=r?2C8?_Xb94-;1y6`r2)@>>2e^k zKC~Qg4{yHi{!|;6LRWB24|-sxf{S399_c_hC2!10Y>Bl7pBh0ECniU!d#}^I7#T$+ z*9BUl?w}loAm;@xCd1=J_yc|m0I;>?qj6~361*RCtEZl$jyD|bYO1!wwd>`*o|&$_ zV3!gxZmF$#eJfcP5HNrZa&1(Y(OFYv(|HCy)p7y_o7??s^^4K;-`6Bl^E}ouk9gYG z3!)))R<_rl?MWHuFU}9#I1LW2I@fP0;qt!S{z~Zix1COc$4}0-%aWaMHMyHhu76gV z(hj>aISYp10wSGOkVcm;Y4;UDV18f9mK15+f!YB(fzs7P^n8ZakOH2R1c>vQ-3dhW zpk*=~)@~chkz$k#ra_qIk?vHxh|ms-DEgjPIJwPpSl3CY_bJfyN{gbV$$weT+(8AU z)*9GSz{}g7*EG4P6LUcCMhhb^ALc>AdrOIM0BzAvR6iP6^1OyTpF41s>W;YE+e;dI z#AnMt%P##y7Mq0{d@5GmzTCNddycoa>9+p>g?@Ycul`-X^7VQD0IX=3we$LXoj)$r z^{VPoUzefmm7aw`oZHJ@T$W8gt$W|DzsfP+;19|~*`+Pg*Y)dPbbA8RHAfJ`mCD8m_OSfEwWdR9@tj?eFo1zATXTroiJsyb z2SHmQRmo(C<8eI!u1{m8BBLDpgUi{`wf^E$Y4xu=*VpZ~TYed2p#gDj;i7uiv4&Y_ z-Z7TxFyJJr00HPd7QFtG8$R##oj%-P+l$C8A*!IFB`sU`)4;nC%NQiqvEy`@rnv~j zkhpaf8~iVh*sSNC$1Ii20aoQZ*BtqKeC}WioWo=< zmJ$mAHcv`(W{m~hjLu*?m-$tqsyTj8F@>L+Xe&Oq!nJ^2d&sfz8u!T{F6cTm*A>n9 zYnaP^bTAXq*oL$~-n%@?@39?P&xG=-2f(^5_2m?$Z1W zF*r#}L6PLNS-YKa+}BQ@uII1(u-o0~v|91}KOV=Ui2MN7y5_T4;zupFYv!$Afyczw zhBPsMjVZMko~ZKA3oOyQuuhev%Pp?45?)RgErWa3vr5Nq+Qd0|_?S>g5cKM5=N21# zk|UoWVYrb>{GkCtf2CC5%KNB%hc?XYa@8upN-l(Y>0BQF06b{v^!<;&Sj@SuQ~v;a zU>aO*Kpph2oz?adqs7TEfwk9C5dQ#LqDhaFXlqV~s()HQ@Z_Eqz-n~s_|PH7^JS^; zC8t*U{Aw&SjOR)hJ;aby4#tt-rt=6(+VAoX#+brXUO-qzJ7gDYg|A%K zzTEcm-f1jnIlQMS#?2&i!}Vf}`-?tc1$S%KZRHkVN#fsju$v$4-zka83Q6rJ@vUYF zqVYaw$K}S7Owta9qokNPuI6UrUL1EQc_bRb>zJ&v&o|(q$%*shUEqAag0ttX(v_^w zW1A_}plUjU>0Oe<9&;Q)E+mpJb+s_#<8lxQ3*9IqE@|2xrF3vh+KijfkP4M6a&)4B zM}=`iGnkH6`-=$Lqe51%Sesu*aQKbJ!)rkOAqS#Xl`20e&TAU`plGgcwBrHuPDkry zJZcue+v8EDs(~RZ;gPaOW7E~Oduy26k4vW6gAOB+`jKyAfpAZ6r)uNceSALO_1^61 zpr4P+a3iHeuU5>#KN-oP2vGK_aQ5SoRwi3y1EsEM%JMpM!fFQu=~Y;(`3xZT8+0ot}8d^_y@(7;~PuVt4b|)@af|f^FOlm{<~Wa@LVi# z5s@+avJa7t004W}MR?d0M?7(`o!}hHRAxqPt8zI+n)>J@$m2xb`#gO$C? zS9X&c#MfMXpfPQrsss=kmk4k0rVYFh6IVLJ&+8TB`SfOIysb@P1BwXQ0*D8ZDHE_>GQ6t)JD?QN3lui-#E7#ud69?qaR zSX@K&4k!>(; zGH%@EBd_$J7~AtS6W;wPf};-lWE}|xfb&}7(t&P5>1r|wo+zc<$pVLHg6NtWaD&;t zCo`rpMDpm!R}Q1&TI&j}_)i^@BO8oIcj}She*i1i^gU^}n)3bMXX!jN-yHFAlOtqw zL^T43YE63IX{5w>TkU$*rNMagIFU3lsG6;Cb?ujT4`1pfh8D&V5O%Ixa_!bVH0@(n zi85BXNK29Fp=AoWPoi0}~$WDL??bDOY>+M~< zK;CyE_Z54!w(n0`+F~-AUA|QLd4Ubob@)_WojP(>$uW34S6d}>bnPR*mkUft*$En7 zO2YS*zx`c_+N{(f6g_K`ezH3`@XEU|2DFBSarLe~p3Qb?>re2FA0Irg1hLz7E($>P zHO0SNy7bc?BI^&qGI<#OrXz&gR6T%z~@iadVio#rxo#Q}LUs^x8Uqqa*1}%)Z0l5KlKhmfB&-&G_ z@s=ebIx<+ou5kz}em!nk@27!B0mNfuuGMQnN4VGmYuw@A z4=>sErtn(_4ULBo{{S$WbMA0KVbBEWUWUEy(d2FF%_*FVH*gmYM=((X*{X5emZ@0Sk(w0>b&(wRv zKr7Wc&{S#S94Y#(o^3jIqU^n3la-!W+w|DtUJ5Rj^sJ}LQ=T`9`A^j9y@%qfS@zok z2;{_;C<}vAcYE0@2}TiNfMly&yGa41DcgaEr{Hp6f7Qn%mj0fmrgW5Gm^r>%1?5A5 z1q(`=FF8h6SYC42!EDVx^RAT_iT#(@-a)}+#7uwyQ$#w}S4#-acM-&Ar|sXm#lyej zRX4D#QjZmGHe_?N^c}Olh0Gh>@ARjTXC^;s z5e>?9Jt-QCo8u*xA9+_f0|C8_A7B}qhswr}{?mgU(5a*4KUF6o#be6CB#Uc}fi^8# zdfPuKuqnuRj80cEP%1m&T^jaA1KIvRnF-r+dkWrdlR|5!g}@Ny7Bxg8@qT6T=R(_< z_qXZ-6zyBo+H$to8T0Ucw@uqQ@F%9xONsw1ov><4$Cc%K_@!X#qH`B<0 z?JWhTcqCr5%%rY-k0daZAg#1o21y{GChlKzse+v%xH3Ra+Ya}ji=DFLkqAFc#O`7J zSM{U_hF5{N=Ql1LN%+tb93*?%HhjM1JrdB2G zWouX33kraKGzOo-{mIJ6%@by0mdllt%&fPz#KjX)u|_H`s)A-`yOGDw%4mE!9Hjf) z(_+|~5v;Xgw)knRE*yCAvBYimgpryH62A54A1UQ2aWrN%Gkf|1SDFEuY;ABW_a{ct4jfBDjG(ft6W>#D@6`pgh3;V zn$M;Z+jRV?&ZFw0%J8hn+T#zn1ubc@UgEW#p?y_yary8$9T@~!KjT{Kw5h=lmS(C1 zO9QRx=N|#svgc!zDCkC^XtyPWh9^JBV-<==u{sA4PJ9@HkNcI*<2Aqgg7NC7^QASE zQD+(VpE@)dEDi`1)!s71{lz#RcetrQ`s3-XV|igxi7>q1g3AMLLqYX5A4)A+!^q&w zn`CR3LI|n4na9D2AUlIx55kw!B2U0^b1vwbR+cDJXMMimvXr_or>z;V!@1vQ_}M}F zV7I7F^Oe%y4i75>YQ|Fh?K_C)!Z;1YIc_Y+m87 zwNp#b9NL;X+HIgdUkMUlr_TfHz*Rsz)}r>9bsf*i4OrOUIN7V((w00XJjYv=oH=z=U*gHUy^KYF;b ziMJKcwNddB@!L*4S&Me!2Zmdw%l>S$xahz^)__sXxZI(@Z>nZ`e?0Ws>Wa z?6uLW!0onbC+?60Ql0ds$n04!0e32mI#G~#<*Q2C{V2dC`Cxq{iS(Fd} zQEo|~Q_H>B?cG4~{ui;E#Dn3w&EbVRRw6Ydhyt9}U&Lu*T(u@{X zu=0^eQc^%8+S7WI%4RPo96%O0wl@G29^GhzD&BvRj&raRv=t;2*SMo&fsD5j*C9|o zP)|zDrd!|Jrzel`>20w!MSgwzss8}CZ~VP}f9o11E37+pub$Rogozxqy7>qhh*;&( zDtVT`zaT>(9Tf_8uXpwH`NkXk2Qo3?E;|m#ixA+wJuB}yf2?rqr_07y6(|(k3tQt` zM5OrMSJBL492JGY4@K`zhM~vDBcg!{Fu1g}qpehgxMU3w0773wPHLR1Im|d+5NFc` z#3&->vg^)RabRsM%S0V*L|AnFv_`a zFqE~yr*mC)yR~&`t`lm8EbNTAzNGFUrr|CL6+lVj5zokSF<`+Nbg~^=qAeiS`2PS4 zelrI24^=8u-j$T9@w_Z?Gid_U{k1Jc<$|CAKF~-nh2; zdw#v}&$;H8Mu;~Ddyt~qyS;0Nll!iD*D4*)2uR$}fZ1!Y@^rAm*C}XoeJCT1loSO5 zT|GJq0z)Tj+3sfduHf#}15b(v#A9~8z5RR81KIp^jeDGQ1S5x93EVgu=Dna2=IF~& z0zN5xc| ztvOHUMxH(PuOlq6(?t1GR<_Nq{eM@FJ3Nj50C8=`lRy4Zu0PwT@R|Ntn-pOYpnqL! z(Bs3NzYjORZrwh7E%5Moj8_H*hf3R$LAU%XvfGdF<(g?eZffDVMX{_wtR#Hii+a!N zpUbLkiSUm%G{y%F%^`m>npIA0w)S!PavvgvuW579$#$y$01;Jre}sSApOn0>CxpZP zIT+$D6W68cHRE!wp5(`X_D_>!Jau=jLnEB&N|SI6X*G=g zn{0^wS)}~O)lEFZp1HSjjoaScD7Gqk=3mUP6YfF+q0+ps+3!A&NA)mTwJfP zI3Nn7o7hvqn8=svEbJvoM**PtP!8c`7PtZkIs^FCN<*(7&j`rcNexGC3Zv50Z3&@a zWs|~;ys0H`xz8$F z-{Q3#@h5_|=`NF(XE1^zXEU$Sx`uSd8HJBjGCYQop8;8P z*L^+)yMnn-wLFd+T;HqjD(1JK^jgnfYj4a|nFaEkR(=Dv4f0^kaR*)^-xaRKf37g(JVscAf3WAS7SRAa2+>sj^Z_Kx4z`*)Vu z{0uO20b^VbO5(cWzU$WSH`GIem`nEpfFcU1T^nndE8{c402f-OyuSQIc++let4UsF z`eL7WYnx4+@TTPOL&AolU*lQr9lSc=Ey`ehMi2r#U8OYmROz-Wr`Y8>>42>5{h2P1 zD+kO6WlPtgrulrAwd-DWU7O0Zt5|)64c~=m;i0}qlXHu#3UxNqN(n)olu0ZvE+D90 zi`uFS6}b|Rq)zMq0B?F^sh(}f-n7Cf1@1x#C#^6e;5=enwzheJZaYe$u|4W;z-NVv z3=#b{H)~VcI*-Dl(3)dwT7eM)^w+gXr9$R4u3@+zDOJ;22#Fo_5zQe5nh8RGI$;WT z0Bpo5V00YR=MC=E<>+anrqLwK&N>>2l7GkNN=r0Fx#AS_ZpC&2V#xLzr(ZCzsP^ZIEJ+oX#o)Tu1%j^>uumygg|g4lj~SlX8!;S z%oDi!cdCvKv26&j0`+mlvzZJ88dX|VHe_HbYt+Z*b{1e=^K`b&-d4RlwQl^Y5NIH3Z^y)r64 zaka|obw3JVrh*Hc)}R*re;Naf0I4cSHf;doT;0R~8V6bf&1f5mDi6x;{{V#my`Wft z685=T0pJCX>b3O(fM+1NuJ)=EWzv9&?cC=ZH$W+eNEdh>-4JO&dviU(t^?t!9+bh@ z*z$K-U8k*9lG579y5jiRWi2lr(u&@R+}Ek<`nmI59?!8gAMv~ca$dq%8xv5LrOEGJ zp19I@c)hLZN1R6y95J*8pc*Z5@7C8xE;2>rQMH3km4&XUFhuzcjCysaH7&MM=kh_V zHz%;^T^e(8^y(GmIYKv(>w~R!Y02jH?iJ)YV<2z`Q*ty)>(fj;-kaTn`FZJMVOss7 z`SDZaW|HC+Q%dLQz8&27p%)jv`jEeca&Ot$rwW%IAQA2#CsI#-wZqr1hoi@i6}OWc)udx-d{fy*`m75 z4cdbG1WM9OmGDOm%yC??bQv;!+a!bya)5gZowTq`isg6%iUW(CuwA=Cg1TOoyM7MD zZa<62mnEw(b}&EXdKC+1f)7>gTWM^l+738uHu%F=!_4V29>{cM z#jOe6x^;P*{GrM78|iPtig6B2#1fYaQCn(*Dv__Eb*wgTsi=ay5)dwx=J$I3t4i@6 zAB)bz0!Z8SN|Jp&E9bqdWqltBEIrSCw#1Sb9CmL@gt5JH>%DYMV>jgf+;R+^!Z@73 zbQ;#vt39+2X81=T8^|pv(Q(?V`(b}oSn%ErJ6>$AONc81ku(?rOMwg}XtfIVQP77YZszD;v zUSU|R%iv)l&W~PzTqmtwy%Y6Q&5f0f{{U%yzo(_^No7rrVe^^zvap9arN5HVb%l{< zAI;618FR~*LTf*$TjFot4fg15p!{foV-`tc(A*n?*i$6dW;C(3RQ_}Yj~Np(^8;lH z0vpo6i;hQv#F{O;0Uef$s)0P`h%_XcvGkpxOw?H1&f}`U?Nd+5Ql1OqYxdBo!I8Njx2j_*Da}iMG<)Sm+}XYgb8=M za6(l-8n3Lva^E53__M}EBcU@+7z_7&ia^mWYo5i$#}58qkmBDqJDH7&#l3(D@x3E4 ziI5D2Jc*3*ID0@Ul&QoFw&1fT_WZ6pXNcXdaJm{KqX^l*Z2VK2iPGUUu{^HdrR^)~ z4OHHXt3T&H(en6jkm9&b)~RfI6ds1IbWPL`o5ONAj&PZejo|Ge?Lt2~xwNSw9zP;b zW_p&k#oavsr&USkT2+~_K9(hLYOzSjAK{`Ya^tvYUa2fuos z?1zW4e%tw$3jv#rl*HSY#6!XOfqFjh&8YmdZxwmtc~M#;8&%^&0qDG-i^3lEN)iTxQqO!{{S5-Y;C7T!}iWh zJnX21j=LOou!0o)Od;Sz5U$zU#whd29bXu_v2f(>#{2x-wPE0V%2o+-yk1C~9{aj>eJQu?a$ zKG|_nt&0jS=65|SO?pvvAbpr);B1me16+Ne0<>P#uo&_$wmgR$DRczQ9c!mtozNSV z$K~V6^yY5TEE`+On&TkK5tk#&NVn~!WM8di^)W{s_gjaZ1dl^U z_2k$1zHh>0G;Np#=xT2}1X#t(ar36hTH<;rsrrhr#$RWz~15Dq}!^rR5ej-095?}!wvrfvKe88AZ|Nd8qg3jDtY z;_@1Q#?k1Mpu&Sc?N1*y+Rz*gYdT=CsLcC69@UR^y7m4ERsE+TmoM#aB@+pW<$F;& z1s+d>b5UFA5`|W@`$9mUgydocuY`nMtEDW#g>emsYU^( zL_rEQHc#b74BKO918~%+G=W5rfxYf11)piSRkF|?A)-CS$6b3+5oQq!5`>B%MT0JjSGy$Mlk3J_XBGAA8)Q}vq&oAp%-^mC@aS8vsk;w zVj&{9x2uWKwJRG`b4rl-zbZ$WN_01Ee%t*{U+<>lX_duBg)d9!%YK<&_}7O?&1Nx$ zzQ;RgPvcRxe-2&Rd%n~7wiXzCxSvv1{M(#vJ{8Tk9^d9N`>1&@2jOF3{k|Y*T>#J; zTJz~Inp13qv;1A5+hdP!ri<{c=GHF?4Bi7G7Z#R~Nhp7%X*S7NuXh=e!Es^u)ntu7 z4rY&)FVs~LvmBryS^?I%%>w!g0m!nRI($t4<7Wh@_bIrf5+dXr)2)#J)|gpA%Mjp% z0(;bjyjkV=ps+L_xvAP$=M&MAWKN{01u@{lU`jGD*r9n>AsbB-n zz!|x%A1G8_*RE~nmYRl}p61L8RgP|s7Cw~jinqv?KQYWWzeagqBSHYzIO;X{)ZKEg zy$tg?CpF6X1=J9Tl>U+yLOC2eWgrJ1&db>=LM&ipM$py*V`{2UZ71wc9!D?8G+yhiFgWoZG%zrbn^0+>t$FI2(O_tf5HzmH&ZFf@0}1I_@3(-?eBYEX z!6|9F5Iz;;@BFsIU09ZEf?S6wRV#9-UZ$GjV5%~E!;p@GsF%JUJLBXY!=Vb+_dRP5 zVt*I4`c9bZkHw8MkCdoDr{mJOHGO{bz{`n{77nMVt)s^FCzYF;ttU>@IRTzc2`?@I zP?KC+{JpPJZw&97g<9f(04$eN+}7N7^2_#mXP8@QF87e5^>?pMJ|AdP+z}N(xEfxB zkmdkFSdZ&KFM9yg?$UsN4Uc&;IjSQ*!Pw9YaxjOup;2-ebq0W^hG%2XxuJJi;ckao z3F+Mn#`X)XsgsO&dr;VDrsX=N5*^7ICl4MajzHqSDk-N5qTuk^+_pnTGYi#At?g+4 z0IIrqa@&81$9~%BGiyQd&$JUyD(cdPiL#>0%VCblpIkRhz@Yh7g5;YH-!gkv4W zS5-B?4LrF0uG0+p{E-sxB&h!YUbX1VVdQzfPCJ^F!tX#-Ef|XX?ytz?l%3Dq?zDZd zCo522%7EfWO;2h+vNxPEM4ZinoAgRQvMPGWb8`d|0oSEQ-J@-NWJmIE>Yt58?%HrP z?n%URP+o7m?hVaN##gpwGDF16%)-G1I9()76Ehja2t$SJ5j zYsh;400`$L!(Pcib-51PZ)q1n)_`;OFDbYw7eU^bbL$ru5`D)(=|Cvty$T(tp;~wZ zN0yc^_Y0a{0pXh?y}`uwT6iOj%^-WOPOjE~lFatGP!(;+r-7B1AD*%XJRf$A?MlPS z!GZ04+?~_brXBXivs^IUUffzAVV?wy4bz+~P>#J&lS_|dDc<6RoiAn^R?!tRl@0?$(yDAjo)^e5WX1iq zYnua)xso|idV|`!H0v?tA5_CW7F?3Y5Nw5m*zgoP^Q`QfUn9&?^WHUun;X8f0dB(Q zv90v3UY@U?;(4qO&HPE+OaB0OgukjW+@P*eolp}zJPW6XM7`K}8#z|cpTy-c#Ve?lR+_B|3=10as zTOc}CnTkepMegE2vGqOahaD2J0)o+Wrv@^);IhxR9J#^5>b7aaqdLD!#szQRDaJ<#!Br34AXH9b%e=9VQz07ef2U2Xl zg1sGf%zU4*!)-RjQcXE7D&;2OYMk~f-#b|4c@A-1Ye@-Fk&xr3ds8Zo*1hsr-=zdB z@!uFPEz8RVuz>)`ygWY;9b)<;Lv=;%mpXES0J)>|c9ng!fr2*rx4aEp0 zNT3^b#OPW{1ZhkoJXkb*5NScZiubZ%vd>VpJ4XCA#>1{2cNqhEb!hHulW6*{PkTmK zG8YuvqF%Y{)xQE96Vsr$6yuAk>KlM-Na5J2n5`*uMTMX#-hhxDj&{?s^D9H zTFSi*E?|F>Ylv_iT8ZgZt#kL+z$2Q7I6Ycb0(@()pUdYx?R+Q%V|M)zY3p527YPk< z*`~Jd{3s&hXlWkhMF4#p`*|4Y`3eXD-bDjXRLX+0?1FeiT%Hd)@ zVXXnsn}O-utpNW3(tgm+uC~$J<3L2oj0P5hy#dzr1VY;&C_w)J&Y3*6PMtpr4D?v) z6%?Pwt6eSlE4CM0cN%3Oa@e0uK(8~eYV|pCiP%yM zm`fvjl>DK39<|AOTHb3So0Q^bpEvf9V-Wxm97RyIiy@Uoj(e9 zSx!;+jQz~=K0U00Ia|nLKF1d$+)txUqO#wNttUZbuJAu7V`Ry~%Hg>Ib30>mS{>-& zVk;XgUQh8pJ>>W~&BTwA8Ca5gvE`AVdjP)oJLR=T6_GX<3x|^yR}B<04D-H+ z8IH^U0Ek9^Q;lgOEr`Lz<#`S>GaDe}zmwxi<>zF|&`gde*%g%E)aEN%*24vxsYahM36V+>v4y>YO;E7y}X(akyqQln3HgAc|JD2EuN>Y*0H`VMv0?3)b zIj9Ac>s9qm-Dlx<$9=2gnZEFl|E>b$yAA2hG_yO@A zLjE%+=|=wmVG3Ffx700bwwZ(^&-2bYEPbLv=@R!_`c_k&1~`v6h0OQ6bCowDABE`K zl{rK(@_bHnHj=>NP=VOiH(L)Tr}1nohLan%u8s-Pl-j|SMn{O|LLB(Yi=#oh1+6B* zOP9xFMoV21fvt|})>xXTJai6_283}tKsOb6NP`Op=|s@RDqQ?UD?-_U#xUoS04uau z{AofdpGY(otB#5Wr1up;;Vh)9M%}jjXaWukE-1>EE04|iRM04gIynLk*7c%ODrlJ_ zQ5AdDU@3Vn3ldfpVl6L#4&}T<9`26PY3WrrD+3vxBbLzO$j$CXqwuXW8QoR!;qn+V zeOSR^ZIvia72LeJdGfAF`k)(!^k0oVZVJ^*o;`;ulEQ~o3ZICo{>uv}S)Ks-8crjF zlmY;)Ct;O#Hy6rq{EJ-pj^+?rTn&$U*X=AS-@H4O-N7^K#Mk9b?%yb1L;|s^)J+c) z%QqP^Vm9dBl<;G2Gn3=_jA0byHgrb6M*~w~DLjLPz=}~9EsY*rM(mluY1493 z)L_;({{S1G7+}ebj{Io;SoVOQl~t6NSk2>cF@rG~91y3qv48@86`ho1lgi_=&vJjI zFQMd?^Vsb>nId|F+LzR2Srw#%q0dfhZXN*T-UXzSmhq? z^r*Vpxm@IVmi-N(KqIB-oXYIZ{EHHN=~YPGa?xZ0x^(c%Y7xotW!>#p8{Yo_#yHd4N!)*Iko+ah+Un86t9yYOTN_MN(WLo+Y z*S#toLq!Vw&ORqi^#cOC<|%qMv}#Z7qhKD;D;pYN$3Yu~fUN8pRC1hrPhs59?$w?= zt^>IU#w+zet5bC;O~J^pmY1c71t^eFUM%oT);UJjgaG%gGK%JB83sslq8(}2G6eEZ z3B}2JwkGZ;R6?EUOJG>yc((_Q#5)>XcFQxEuP(j%0Z8x|$@^!+$mW90vN&#WHvxN7 z`_N;O`#Xm0WYo%<?xihS ze`F+Q;%DPI!dP6_19FC(+6NhtByYD~siC7dP7~tfvSTOd$l;L6RQ|SZ=%9Bc*QgafPLgdvFvo&XpW3EHwH%_#| zlkTTCCI=jTl$#uEpw;OGWVrr&&11Pl#OPDuTG&|=ald!68e8>Bl=lLuS>90cDdRru zangVs;MW~Nss5pnFCFI`PFVm<0X+%|o6f^ukae%*4gd!ttr`CKXeqHw2|#(9W|5q2}L#YHAQ$iGr3Si3tGBv4kdWu2~xC~I(nip$L^y(|;KWEnE)68$ivBfM% zKr7}uZ0pofhY$=Ke=74jR7YH&U+&VTv90>Ieyuim%j`Mo`i zpTU98ctj-H^@iHICi!k(Yq%)m%7m7J_O(LwwzbLgvF-3W=CB02fC|0TUb;4nLlf}Q zJvYWm{K=|~D?PNoC90b*fZF2uS+(~K+!XUw9PTnNsOa@YR~~>?(h7=vemALW zEK_TZl{-*aLdxJiU>L3nTHdtsSW-{HZ7x=Y{U+n^rwLg<65!)x>d0KvN*JWfDva<8;{$J!0r!^{?@cLeipBT{nnW4v)SXf@r@%JBSqJ( zeBGF>DsyC3>Z?`u%p_cgdt41;tn!_K9#ZGBH$8&l{-`V3^gr4;_koE#qIg;K9TOd7 z4|b-$^N;$+4$8UtC|=h%3$&HoEn7%}_;wIEmpKC6 zQ%a8@OOKTxHhCTIYCOV7e>IjFvOS9JOM=t;(o^yrPH!TmuV{Z%04mmdY;uJ2{6m<= zki8n@_W}(QwD>%$` zXVEgNvN6a%P!v#sYnHSLbNpUhu6NQqm~|jjOMnl9gU){@$sZe<)*Vup-zSEX64 z8-?{J*1LI64CsaA0G%tL3Vv2`94;WCdjS5vq3XZ^12 zICwVxRiV0)JJEWodgwVEgoi@5toG>NPB9VLuH2xZ>w3%kTTW&lo_naMy;@F4&(3)# z_|$#ce^Z~ka2os;m-Zj^yfS2Putg|J-kjc#q$XUGvtp+@FFUJ1=)hP(zb zM`K+Y`kU-%n6{OA7w$h*V)Q^-nNxz2Iw8WUREUqf%ra^Kxc{<8l7`Gq`B*_J>5 z0KPu3R8kxUm+eO0oRj5x{>P@@kN*H$o=x`dZXN9UZjiwvN6~YTxkpD@x%Tzsj?O;i zw-hlHj*B6wVhB;7PPNY?&>Q&gCH_D+k+i1XpPenC4{;_?0MoCfVM5F42e9l3ymjwE z0Q|R$b8OLa3OlFcSD*xOv&qHsV|guxQ}pGGXhAl2;agZ5&y4xh{7xKr(lE=930R*W z%ma^ZwL2WE%-^~EUP}&1Y^};*A5R_3D68`q4J!|)^2)Bu49~lqM7%MLNXvNGx0*618>RWG9upDV|;E4s8BkZ=l+|og@xw%6kO8}H zzVp_z#QS6cd{@UMJg9S;f-;+k>!OP9=Zjg^@5aR3Tt+km4FLsS-U>xeG%~WIkVezA zown~>eK*SFXWVayWOF?HoP6VCu5vZRdv^6TGd6fT7d*Gy0mc@>qF9~|*%+Y+)EdX_ zx5YRO@(%$KK=~OOxLlA(DgL#3b^ib-fF@Q0*;uksa!@#=8v zpB0(;kB>3!e64NuR8qa^^6h2ApWgJQ`sOmM=F0@7uTVmRYV`EEe3trMZYrxS%H#*s z_cgqXYB0xqj@r?2MWsd_IpQ+4jLyr)cGLzI5%YSQ%I-orBydKC{XM7;`X@?2*5kC; zDLb6KN2LKHd%dq?wy#PJ(J1*{^jm4`K@lUGT2jMJsn(b^ zBX+o1T9#h)gU)FI-`s%~fYW}{aV5CV%$1|+WVuKEEwHC2ky}FAqiJzpO5*l6{4~>FFQ4bg{oYnaTdro2&k4|ImDkH(uDC6}QC#M_ z$aa8Iv?mdxOD4Uj4@MeyAh|1g0msDqk{0Q2Wt4QG9B)D4C*~M;KEP5J?9X#rTIxy+FN0uTN-dZ6mF1!NE>V_wbwMQ5b)eo`30_X5dhop zuVdBqzcs<^cJR>rcgDdjGBriLsjtSn?%!GA<5_px$omJ6jxasCXgeixZ@aIbITL^l z79Wje^4H-PGb>|BCd0L9rl{Y1K<;NK3hgWYmDQ&=7X5=hI;x%2p>D?63hl2{vjf zlWvve_3e)@Yo94%`+&iN9&?Q;Z`64pmuQ+=y#C#8-wxe5$al$nPa0Tek(*f7w7Ghc z-rpML``hx=A>$lwPG3Kd#p96I%JeUB_K%G_@Mpf~VMpSeoN+TJ_ZbC&!Ql#?h&l?L zHny87%+8-KXURD)neI|e?^+UfjPRfD8IobP(-s#;@f7U?f3*30M;)8VaehSc4h~dr zggN8d*)&u6RBh%8XT&^)PZjd!=V3}+hYZDzI)}nsojoe0w_H<`6jlONZ&{2gN)hX=~>tY`1>B*!yeGWL)h4eV=ucbCB;oAmb%etzD8{CuO4iX#@<2) zULfD`OWoy7?Ou+1!m}z?%F^XartYeJ z1$S#O^7`DxHdxwAQtH3>7168D-g!Zg0!RS|QR_~WM3icKbf~ShL75Rv&uYVF`nd-n z_yBVVR-Fj1JJ^p~)T?}dxICNzkxR4b3m^%tc{}oz?rFI9it>DJLl`?AMz%(c&mH{* zdEGU|b?b8k{7;ZNIT$h0U@!=vZWna#^GnCa#nWc^e z$`Byb)4?CY&U%8Utj&q9Y1{m1VAOdr$rw^qy@dp6u;Szmvc?=wM5@Yg zgFlXOQ$63M#E?fr=~u4Fx~X#fDneZRNm?33+I6ayR(2{U;CT!;xfomU3KUiSAsdyF zOjz04ByjFJioIqNMLsRaNyURIFsM5nCBrGIRQ!%tkmM(hIOcSYct7NEKTKT(Dl+S$ z;keds%$eD_%Au|Q0JINEa#}vdc^@yACO0y2Nfu;l>>mMD{bTN-XE^ciF52t+HC3wm z54yt($=wXCdhB@B3X4}aIa6Nt$eIb3Gm&!7q?mpeRmAMDCA4q>=AN+?f8@qE;=IFIQSTbz$ z^M;o0RIvi<>w2H8Jdn;_QbQKtDjgK|qw5FRG8~_*R{IT&)~3OekBy~4AZ|7XuVF|d zI}!ro-3L`oDG?9Eh=SdNQGUtS;Z6llC5MjP&IFc#t!-4HdEy*|xGsulfcmXK!j=`c zEwM_*@8SMZ?96tyeN$6gZ>+vbQA=}UbM}wUuw16{rvVdArj|}c2PNcK_~&Nuqfoc0 zx>DPw4P$vGESBH=YYnA>ZthPl6PxWIp4FXP+!y3|v6i@laHO8pL*j;5hmb&hPM8{YIl zm^h&;4xqSRo(URGHfH5U;da6jHqyW+CO@bZ{^u}gdzvn%lqN%o4B|N;YumKB#WbaW zhm*wcSvb;WWWdHkP&QRg9n`2#Vd=5mG$f5G(;m(W*Ko(iBb?otR#q5LQ&tM-SB z`k4-#wM&hO?NhjJ@H1-wse#FCD4|_3xv8fEUO_#!gXvIdd;}j$2YCoxbNwg`hwld@ z>2d}rA;?KSg1U6q!CK0taBI7QH5GP|ru|phs#E2?A}nNN#qHj0qU%AQgPY8Rf*$W~ zq#6iK#`(u93tAf@r$wU3c@W|K)8yk#_DcjSO6vfhjr+Ds8}-flrA^ix5nz4ILSC7 z+7`Utn>AU)5G8Z%RSy7Cpc$3cs*pY|oD(Yb?O)IvzCW0c#PA)JpA!I3(5^jou0GY@ z$-Q`_O~rd&2GG!k*H2pJlgn%5WI&n4@uHwmYiP>{Wr2){mo$2tTC(f5H8}u&Ps&3R z-WrE!Ci+|Y*PW)htpXSrZp(3%%}wgyKtZpqcWVNBuziZ*?iwILYODlIjxe~RtfQc~ zK9sN!GB^$FO+e|^^kA^0$>3gBb4yd&7XJW^EQ~3#_*X`#&`XY@)bcQ{myB?D1&46x zD9K@<_*aRC5I7XlErz@DE9xJsP0+5y#7X@=AG)<=J_U|PTuPvc(|`?>!Bvb|m(5u)SdNH40_ z&26h}tsuw_8rMCT8G9weV~I9OT9pNPBi1LKQ|3P!_I*DVG2NQ-&yVVt?nV-#_XfV_ z)!RC_cb-(T@($hra1C4ETI*!I;%6IP@$C*m(n=xtP-FfNGDk;rrAeL0I3H_?Cg6HjTXKaei#wK) zgo#=l-lYEkBAqDLOfM=Ke1na2i6lr0g<S1U!rl|kB#cDjwc zu5vlyAr21Q+Z5??lv`?WT)8qC9!axPYjxJHX$vntU_-a5V?~-ZCae14WdzR{ceXaG zwH2TBNHVvxY|~o#k?5e4mto_ zO?lh=zW)Hz+dQDVh@H)QZ)0m#npQr}o&_-yfkvXOrEg<{XqW!I=2(gc>t&Cu@n*fCbhCT z#^InZ#(-S&X}-jT3q}S^7dL4x2!TS~PvJxg$s8Qw<6wVIloH>^cpR=vBYPTI7ziXy zPh_sX9% z$fc8b(S=3sO>eI(IJfP#;f&sCjbR&rJ?P(jE0?X1&m+#cz@b7##c=fO^!WO+Dczdq z5LWev-P<`}w`LI6TeYgcu``*8a?8K1M%%R4nTJU2RRv{sX*D~>;=ey7xa@EaeZfD{ zv)iPRuAK95D~Ij;!Y%LI>-^d%_*NhGo8MBaxjz{gu0riJ9-S*Y?tj9ksHl6PZs1$B zI{MWA0NUAq^|e8sR=MD`AvQ|KeeTV5(rXB8YDj3lhqXeAW&>OkQfSC`{jI{lDAj3# zM-el=OCh=V&`!IH$c|5MxD8DZRLnmdk(@o}V;N#eKEP zmP`qwbMWl@g z(QKVh#*749mt&Hyy9flH{{R|gRgyysPt=A@tMfHV8Za_KxxUdMRTo<&EJ2cw)N5Q7 zZ)EF7)*enXA?4u!B#k=TpsBha*!&#UUL=9dbAwxV0BvJS`k~E)c)nvBnjbDfWQ{wm zWbNLvys*}8BF6JxGG>6rgE_ODbS9?hPcT7;%VOj-{{WSZ4z|S#qgql7yUk?6L*pDMg}|brU*l@ujlyWqz8IrP929HmRnp~J>a8AC+khtG zw3=tlXSapFH{rZ=xNiDE68E?@$?SUHn7wvn9Jhet9JyKY?oyUFtU2AHtO{~}Z1UR~ zmIks!7HXlcoqHxgzDJI7ynMUuAKI8cn{)X{_M=0gf!{U%08^_ywnb-=eqg%AV)pIIoS}j5ZPuYW3)g!(Kf-DkHY4C+FoAM^AZZUkL6W&zu;ip z{u9cvix$V08*U_Rr8Kz#|}JX3LSvEdE*7#e@AUcUuL4hYm108~*Ba@>q@ z>!39OlYPfQI7tJeK?JQG3-$OMWMb zc0+=+jS!{l)b#yq`L15waMC<`fN?AUBXm$NUar<_9yeXC)u8eGdm>-}T{W&f_sf2I zVPUsu1FdE9(GM#iFc3{%ksED*zc0%%fP!pnFV?#`a(VsxgtNJH4rw4eP}Um)z3aP9 zZ!>>kN1dKsV&!l3Jxy(lIlS#uIk~Fh-Pc}~8*J+3$yE2rAyP;hR}Ws#SBJ0uNRiBr zCcVP#HwU$Oy}M)b^!V_f;bp~hc>CiFA51jpstR?lKYrh5cD(+p?tAHoS(}F=HZ%?C zsV7>?TZQ3UKb%1y8zw$mfKg`>wyC=LRA)K9W%RNf`~v9YRTGOEcnV%;k;~*9%&dpH zS>BEMuOy%2M2fEB@sh#KVZ)FRO!k6Kp6V?u3Gy)|@ZUO$S0Ra;WIz#PcC|Uo2;Nhd z$LDi1WMG1EraSHt3D(qWJOn&=$t~MwHq8e@J5>gy_M?FIzjf3t)5o+koeKXTcu&=BlnQ2q)+iMk-yD?mYGT|wZ@pxswb zN_|L!h;J8NZBMD)0^|g8Zn+53T)A8{UPqVU zaiJn<21Z@fFsKB6l0{=HOQsa?bKHxawl{0?fOH15`$S-=`Tj-_9wpx+NFWs-olV{z z6`9^u%jTB{wZ#yv&=E?W3&4&U%by`$pc@tJNvyUJHz$hYWsJE7muMY+H9u-V-ebh0 zE-rznwl}SH-8WF)R}qIg~W72U{a>)5}BOG zsEh#1E7$o_z*%7Oc>uAoxR(Bqq%BQ@BCa-9mVHS*z1P;IOAK7F;z=L|n-v!nuuC{{ z8ikdi^cR)2$f`xN=z(U@x#Bzj|2-F=b z?mG4dr8}(|dy>U_+4Zqx4FD<=s_R#xc2oZME;F&^1C6`)v90EkvCF^iGL{lb1w)GB9n~%uoxEb`*V& z2LAwbr2~e|6!+~*BMuy}3b?2Wpqmec88RQ8VSU|g(w0FdG=fT?Z_T#!2M-|BJBn@4 zrlx_CmoJf zQ6z6KjHD9gj+M`Mm4DWOljS-1!OeJZEf*AhWh@5ypDoFeT68KxE&l)tQ(P!5m(ED$ zxGo_~28h31rE;#=HKILEt zCm}3Wiy#r;ebmcg8{BlcQ%m~b`%xz!_i`sVHc~zptgkg>M~C+>j+p_GttXwr&*}KQYNmJnweoR}wF4bxN%?`o-0+1(g#b zLX9L1-}4IBQlbVj7qB&;I6Cg#uccWyaJNnaT1y#wk7%SaiIa+Mh!m@kX+{lImy0VS z+~r2<*LU=#fE&zd4j}tVSRF0vN}9V99L<{?K&y+cqV}Xsuf_P*MI2%54QT?vb^Iz} z)L$*ZW@&H)2Qb;ELW^phgDLVh7-lSAa6aYq6&lh8%%h%cv9ubJ7ZRBF-^vY_nJtYmsplgUtblQ?|| zK_~#PThyLeZppdF7RW~AOE~m3_I{80xOxYGl+E7xb91cH=$3rOJR=xuvIL(5F+aZy{jDHh2<%*IeocRHFvC$K_ncv^GaC z&F=n`zNeM0HN@?o;8Dd$M$$Wqy(_Q08DgP`F1psw6Q{}_GZ20`3W<8lwD8;4 zxUAorj1u6V6+X4cugl)n<^XwCHaWwa?G7IBHC3-$*V{4jAGzg?@`hBDCv%m3J~g|= zdqM~(4ZEVyMMC6zg0AZlZ9sIi?kG{CMfIm(AotuRT6*hmh@G~?%@ugp z+pNsDvTUY%jrM+#>TA^Xy(zDqo?o%<+x&EDJYNUK@o<2F^0aDD04vb@--+C^GpeBqd4`npY0}ZF(AV zkpc3#Z=#hoj$INI$fa`&+R~lsZt^N=Qe(iC%r9|Pk#6&B)aUw4N05Ll| zde=VQ#dPcI!gHAnM3&ec1x4*uIY^Jd=e`be9nL#gUAFpEU9Dca$y=5NNShFC_OZbe zQY>#!)`L8&Qs7hq14z31n$I3c3z-pgu3ojQMi2}L?blwOl)>1eEBM}mO@|!ZleIJ? zr4%H^x!^Q0Nw6LLY3noaTU>mJIG+&ZxggdEOe`sKFI~L-J@#<*_w?7}%^YyQX_}0F zOFiG-Zf)sahdvr}`FXv2b?g5C5NpSHP6jAzBaM5w-n6}>_+_W5KO$@&8pmN9$ZHO$ z1#|TFHm>e|wzKfGINUA+H#=ZK^AxNl-)h>m2l7!81|PAfd2rcbZP7DT%A4M zbMhDD{Q3~TL;_8MY53P~KT`f)cVF0lfZj)#X1+q^yJ+jxR=V`{qzM9vAgJo|qd@LydQ zIFbmS+lM%#hvTR;6&&y!gl^g{*E_dQjYT2O1l!mi;)STS6sp|D>7d%6omP3RqKaBzhkKN1cFsLP@YPFYL z{{V@6R(HwrxpVSuEgLjm#cPXMi&}lWE^+a)rGz~L_gQo{tNMvve;vG*y^Fg*fUReB zR#|`CY>pR=&14ZUw=sXn_SgmKQ?ER|4@=tmY^LKg8sHw+dqs%;w7N__-_`B+*CKo@ zuX9|wXliA2@2+64isS-@xZa$vE7;^F#mNqrhd`HBS{Fo5Y%BHzD>q_=^UQG1Q56dKyFQE ztny;+7X~L{vPqf)_WMUdp#^Vn77+0mxeivN3r@rqsQStfxgQmVKttFZsNKXWn$K=W zkS~;YmJ3@XaY7<~p~K*&rsrBr@wnzW%RjNjp$dNr=$=AEuJ7$|9V`%OKw5ZL9&`MT zB810|46gKXvqc+Bljx2M7wN{+&y(rl?J8eC)op&RJY?;cH0`9V1IJ?2jz3| z;(`k?o4L&&+;m#Y)1Nl4V6&PFRV)bCzO~ku9^g*xR+xFaMZ5JH&hSdFE9V@#^`Hyd0R2Y;v z7agTKX<9gsIJ~s1Ck#6hWnH=MwbeSI3f;t3M71!$VyH;I# zO=Cgv&LI6r{i;r#KRWm1#CW~>+gfK6$B38;d%CAeqNkCQlHtJWsa z-!Giju$3UE=C#$!M;CqZ0Q|Qp8@Ft5B>de_*Q3i!JZ{}(@Egg>YeJF~-8a1dLHq&CUvRgy@GG)9qrmZK)t{>Au- z8Rwi`8``J^b@2IP)8Uo#Cr1K+v-Li8K0jw(#6MopxHn`J|x$h?A$#+Pg`o{FA&V* zvKf-*<3A;Y5GD69uGLeiuM@uSpyeXNljgjhcvv%e0VGZ+Va8~Us5_Bfe%j2tb>7IS zVak!hJaM+i-t%w$N3|=3c`80`Xy$Gv6U^_v{DGp5jr65;@Oc<@cIv@h#1H zh~Hg1RnCdA77hn8DHgq(NoBfXT=;+&z}%(<9%HRk17g!HYnksBbpo6H4Nk`3!mq-S<0 zbJm2wVe5_Np&>(rTZzKNQZAQ@M5%Zt2>K22u{jglRpN_N5@U zmNK#fLj;y@v;{y{<44jL*o>Ln=?Ib94(nB{D^c9`G;V8(*#Od+BTJVUYk@wlV$cw! z%q74Rr^-jIRDljm;?hXb0+m!W@C-}Hu(Sgv)w-^f5+uBgWT`4aOB>K=?&K}F5*kjr zibo9`@LEZ7GCRM(Spei&j5l@ZW;oh&<(pdAtc=D z)|>n&A`c+lz%7N*deCIMkOZ|!P^H77nhOtqxDWtU2lV!!VCy+oMB5Tt@0d_{KHh2W z0B!^up44DEd6%dluImsiR*VOaD&;LB+oh~c7!MrxzisX`2|yb7jzT?w<8z&H=#`G^ z)u(Y~!Xy{c)s@}6iCNB_(GVPVntJrYupgBhA`~4~x0_}n#fKG{5Prv{bMy=GkB^Ye zsjjupkgMeUV;*?O5%*L4D)s4Lx`DaR1(CaqZ8}*iS=TK7pq@*`@*`{9_>$;LZ}^(I z(5|XHt`xE5R5Wy|o2LuU8OJeB)l?1v#VR(Z8kZ9Dus9nVg{-pHMfNm=w|1WjWre;L zQ_99llwZQ0Mjqtc(~h8`T&)(64qqcUneSuYXBBT@@vS7x3rIY!CkBw~bRg3Dn&TVE zvEJRK!F0ZhQ;JDLFAjFyJ|gur(K2}4DQ>_J*(u6G-yX+@7Y(dx1Jx^CQn}vJ7o}K7 zn6%QStK=M!)KDG3gZCVnkc%|hk)(N-AlyD4SE>*Y55life8j#~!3vd-{7;6art7L0f)d$!* z3alF6FOG!bhn52H)Lm|A;FY;NCNRL2S_F@7bDqtEz^92{U{8)k6|+TWGQR(#|R zKrgSxwM(#pETN3m`ZkfT@%&RJcm%Cu>=Ca*gwp%SFpoEmXU6gxAMLRuTC43SMajbD zF;jF!Ke!z!WMIQG^JB8O0GjDkl=wWKkbvzH5IdEngIi*G{{SfpB#ZsD@B~~}-2OmF z*2;FC`qXW#IY)dqpUIFLy%6LmEgO(^~Vlo?f<}7#O{(080+F3VF}j0S(ts4{@#QJMdut04dER zIfcmJECOG(aqH%(pJYnYBE5F5r0Ax)MO^Wd`SxM(+_I5qX4$bQ7JJ6ume-h)Bqv07T<54TNv_Y@dMHM>+EmkE0w z^)%AFJB)rtZ`RsFT1Z3u>S$~DF$`=|A|2!(2TK0SrsU~e9LCgngdY=Sb6f+bkR!m= zy)V+L$jKu;?)ol!)zUh{ap*f%d+Mmq`P_#}T=osyfemA{UiHhgHh;=;+=dj+bTT-1 z17E_Px=N%-eCL>rQzl0Bvxuv_p*=sC@)=+K@bf4(zNkHFwda7Un~?g)aR~naQPQgf zX^#$hc9!^42OEn5Nxi;=_ z7DLf0DT;Wk+>fcTTGG}kl6NXNP#P8qX(z1~(7`!D|YFm#1mrcFrIso1FLD~&?=?R6s$gYU+`)lN^ z_?+lo6Q;F0%);eyS?`eJG<`xErM3W96_|UxukmNR^lISZY|?{T?)7WH{oa|9s@MQK z7eb@4TJC(y#e@}3_R(k}9NBknU^`dkU-hOF_}(u+lbN{qkr#h+%8=jq*8Dwt(y{B> zX)@G&GwrCx4am*-xPGHv#=2?FVtl+!w&~Ms+lW4!3LJc6^w=8KHp!?JAA4_>n`tEQ z3gx-69L``QF+IuC@vlRlEXSGK+4_d_d3a=LEp>>oB|*NTx@MKb)7X{=^79mzXf*?+ zZ9F+y)0*0w zWcd+rz9GqSk7Gj%W04iaDXz{wme=LU+uhe+6U}PgE%wU?5C>#6t|vz#l6u_quSZ^5 zX*@jJ)28>&!1EW1!p37;9ATgjN-p>CxymI6<3iPWOLeUAm1aa7uOr5c9f|GKn+nrj zR_#Fv#_YA0&+q*+e-VbtxZZdKMYQa zM-WOj$=xW1$^46rEiRC-w3~uO*0hx%Zbj1LxR5|nxAmaWA>`!*g5*^?sh}htX;5q2 zohgEU-Pm2Y6md&*?M#ou%Cw-}{{T^@7SQF!=I(R0gt8iG!4KqvT#M}*FM0+KGm*Gc zeGzP?kVu1(A5jg4iP3sdLdDZD8*-YbW9pQQ{xslD5P(Kx(>6))#(W%zCw3z8-X=m|O@jr0jfgzcsFlyiy_0-QPT$kN` zI~AKTvHrZ|Avy-qnyV~#%kwAX@*kBmmFDdouDz4=f0Btjg(`nIvI>)a( z$I$+(_7hw6*m;GN*AQBIBJ{O#{c!s&cAI<%IL!An4aI+@7BlP1BHlAKKv!S25#)E| zAjh0I1S?uf^S9Rr-p$QQY~{kMkicY(B`ZC(b@AT8XvRyY|;!y5fO( zJnmPCl(E5MF0+OI06OgF(=V9!eM_ZDentBw};P8DpG2 zuJdqK$E`%y%33x*H-yYsV;k7+%G%_e$UgBQ&5^wbnbJDiQfPK&QAu0$rI?)41 z`$NMY*}IT%_hmem_YGx-=SIG1HryzFoDCIMp_Jd`vzFK{CkZg@1a<6Od^sPw4+%}ENwBKmxX$Lp3l5U`D z(I^EElpyzxiiIc+wT&PJ*5nEUqA{viS!f2FE}Dc*$Er{b$lc7*3Y&URaVLzFMIhQy z`ULn^d&g(1zt>^64Dwpi2pfvD8JA#UxjC0hMxhhMlxAs_Y=7^9~yeOeTWg| zV&fn zE4}cI_nL}09x0@?!M~SU_q}dESn<8TV?HBK;CT4iw3M(Kd@8za{v6!(&&abKp|OB9 z>s(tqHF9YKML0INQSy1=tvAxTG||n|e*ivnnLC`;l~o0F?OvBISa{ygvG@pOWrQhV z+PW>nk6kUQY`nE0tA2*FySq8_S4}B%+=JRWo7X2^uKql7NtbQKc^=1iwd?Rfo>v!; zT%e0~0=(U}wdl!&H#d1CgumjI21B7~X&V7+<4ksGug;IzUDbWJN@zP2KOmB#0CJbRvSa^=K)joTrK#k-r>r5OmV zW5ZtQ3c2Kx;&&GltBE~mo&mU+OM@M8b8~7k561~>6rQ)XzLW&WK~*4MP`V{J8_)j$ zi}ws+;Szc!bKG=ZcmWQ`mY{&2i?lyLrSB_}>7`Jb35Ha~e^u zl4n>ky5IdP9rn+N=+j(JBECj!47^5S@smIHFK_hq5C zXEsnW{mnlz^eLiLUPt#49&d@t0w~|GmffUoY43W%X=6{cFAt2$%KD$OA6&F5w3{}S zxFX`pWW-z+w5N;>>Qj}PU z{{Y9so3*EI+nBYdlB&$!J2B4K?Yme#txEtt6;O~S4uS_6g1izK+u13SzblZHxNdda z;dF681ym{l#oY5^oYr#=CnSLx20KtP=AZk~?~c*|HVD0FGh;2qpb_}30mo*Q_iA-Y1{{8AP(di2Xc#Iy#}KNg)3pNy ze*4|6v_NPWI5MOJxF5r?pkV0DBn#P4cm6c;Fqw;#uWr@aE>`rYz~{wel;lV5Ay0mV zD=pU>Hwe<`nxreNp9e)E`8xwb>g2ALP>GP8#EIPrYoF ztw3niHcUt|8iWt)K`6Oz6^2mf zHNKTu6a~)si{fj8T2k%{{S8#vHYqsXI#}o1BT6c6^{UQFLGK$0bZrf4q66p!Dy6Y4 zV|KSw`A`v~&y|@0q}>3>EZ&|ern(v z*OxV;Q)R8NSrK4)q=W=|g$t&XePPNcwf1OR9-j)&Z6U?}FOtaQ4k69`CrVS9 zudqDF>5ZF+15vKEr_n3SUoyvgZtD$AFRdknenX2V>bH7U9t>ylaXvG(!1SrXyW{XQ zmrM%lQb6Ck{GZX|3{AV=Vd+|F1Ly3C99_EhKIPW7Bo|5V1XzMC*3}`Rc$b=B@t8Yh z4Sb301fn?VYYp_ZQiyo}0Jt1{*$ruMKtbMZtw;4(c@)9qJX@rQ$2(uNg6s7)I0ATi zJW1edHw4tvz{9z($C3uMuHACthqWx=F<-hSLzc!|HtwzY6=!b>N21oLT6o?&9hM8m z@B)A}Q_`brh;N{Ldx(f}`0%Ic-1Qwuu3D^>o#R+;kbjH-=PQl>0Hrx>2zmY=#!D8~ zL>qgcT7P;2a(%tUGCj>;D&hxQRex<5MC3l%Mj~Cl_h?1+qx-192gy8EekMcpLAo8o zx5Bj7u1o5N#K8AR-)f*Kf8$frtcNx7(0CkT*ke6G1v-?kpZ2}4msXmL`10l;xK6e6 z-p9S4hfLH#g!kJ201ETJ>}{3(M40YKTD_~&-W9>9~a8`eI4QvHN12VaUb@Y(PSz`dD}fr7xAz;Tne7G zooI{5$ksqgpN~OUb=wM&nfX3=l3%9Nw5Z;-k2s*E1qSfXKW?K-q3H{1fUttUj--)d>g;Y6OJ9p~Rwn?rrpI0%Ca*wC(+wh`@-aH+f_}p)5 zg>hd(YvVt8al6yyd=K&Aus;P|YvuRZu2KYNrj?dPNu7u3lC<@r(1(z)eHUB{l1cR% zSF7sZEcRwR!vmxNES}Qgsjss1Kh?$2TMjrf19p-8>(FK^@lJ5@Q^?GS{{V5uH%~rXf=+~}^T5ZukRfbMBMouH65jroVRkKU& zW7?lHU8PUoC4dK&eF$6hqLs7C;c*+c1?oW+&$r63%(-8D<*hr2{X*fsm3kVe;{s3= zXwd0;(el%+Wt^AZboTq9Vb_7ut_S_Tl*GH**yzkuUo?-IvTzi`V{cF>&j;ERdPphc5 z;ps*K--GdN{$h37Obd6SS8uQCUHt9v|rUZyL0o020ZJNw# z;MZ%norLJdwKVKL6_?7?9z)KEAbSA~+!OGwjy$tmp2vBtWgMp>>1TR8?c8j1{{YIp zetP0QbGFxQl(~7qi*SQlyRg%iR2+vSPqkH`C`+ftxwq)_IdH<4BRT4U#ZU6CZMS!6 z$}8o3vXp7uPeEM!-}33xRJ<-5b4%tZ4ujUb{@%~m;or;G&&t+3IHO=8pj-~$!npS7 zwRP*ySe=iG*w`enRV{G-Rh7AEB#y}A^%GXQ^hgl%n+sx6O0T!DS_M(Q2E9w1jT905 zemH*AyK1Y^DT1Gb8pqiQ>wPE*=KO@_YB+r^7ezvvy zNQa%|3~Nrpzm<}&URh()yn#Mb%nJ!@ZzO;;>-d`M)7Op;p5}f6nf^tSGriJU{@;(n zw%44FOTPPld@5(nC#nU|Rp$xUk{r3HzUA%KqV#XCBuO*KRW>~;FIL*|LX(jsAT~Pp z6_(wb&M0!_y8i2m$8NQ(=8ke)sG(h0Z_?qmx@W<@Z;%ea7~rdk+LqG1Ub|;!waZ*< zB8E1QcQm7#m+nO+iuhhg%Z}E#kF3+LaZ}-Oe~&N{1RID&AfN-aGoxP`3#N?boCPe`1I@7>hyOUwWa}0nK(CC#G z8aLZlGG8LfdRj+@fPdw^UA_)CKO5!$0A-xkJ>TGzqrvF47Z>HU)So2fhaKdw@)*z9 zOnU+!&@Xz!pX9cl8Ry)dbYy*KTi_Lsl^dMbv)qbWdKIpT z5^lt~&O4N;Jy*R_kaEzp29i}x`ck9FCz@~4$r7-ux(5FMI=SX5TwIDgU&v$1V_=c# zGX~MSLu%&f$@YJ!_x}L1#rcCgT$0id>V4gkxvS^|oOXF>ZPY3#=}FDko<+QNXmAL7 zQKUb=(%FSg!GjDDsZOw52?Bh@`yU3|T%x69>ypIhrg`M9}wP9|(Qac9u` z96z0JH`e}KKU!8VnDTh;YaT?xJ2h;B)K;EKRhAE%$rej;BqS9dQFK4~P-8wb%%M2b z&gp~4>GuUBi&~JtBg(mF=dw71x60{0quM<>RLSHi+YoQJM>|k71lE}%9Nq7Z7G3f@ zHK)|sX$R3h1H>~EjmF0fPo}^R$E{Y7hc0f`pwLlC2QB+pTDnnyoxpKtljUQ^di^G=SHB<4XAs@i+@ABpMH0#j~G`i(NLDy42a?Jsw{{T{6nG}9ua2hlw z)B>2|)|Hd+)Lwv+z-(GU3GMZu6vhVv)%SYU>#lAJ)!z-p8J^MtgR1CKy`NKu*X8GX zU79xy!^Ip;YgiF3Z?9_ZyJ6+;KGao*8)Ildo}DY3YnIw;g_C3rZo2DO;l5S5*^L0F zMXhwzhWmoK&Q*;1joha|(5inL^f>Z)e#d+Pxh&A)T%-Zhpsv~B=E?S<{M^F2oAj)@ zZ0+UESt)y*3jQ_6*QeFl%bnTByXmm{i{8AR{?GV(p1-e&*U?Ms;E03wUrAd1`k%pv$e6UEKR`u1!vgz>4e2!o_Kc)H` zcMi4me!u#@bKAejIg$w^y-5V8_UqESb+03=@`C%62$gja8bNo-Ob~V*` z;vB>{+04htaBClC$WESyyzg_5+g!bW*LRxjir3-9`tY(yT-Op8YS@F=*UNOiabHc( z{Ki%HYn$cWA>mnzk∋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πj7E86%FBH3aW0UXkb}@yrOsnqCj64S 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 Yb;SK$j78eLdr=HeiN9!6tssB@*-Vwrr2qf` diff --git a/docs/installation.md b/docs/installation.md deleted file mode 100755 index 29609d7a..00000000 --- a/docs/installation.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Installation -weight: 3 ---- - -## Basic installation - -You can install this package via composer: - -```bash -composer require spatie/typescript-transformer -``` - -We also created a Laravel specific package, you can find the installation instructions: [here](https://docs.spatie.be/typescript-transformer/v2/laravel/installation-and-setup/). diff --git a/docs/introduction.md b/docs/introduction.md deleted file mode 100755 index 2c250ee7..00000000 --- a/docs/introduction.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: Introduction -weight: 1 ---- - -This package allows you to convert PHP classes to TypeScript. - - -This class... - -```php -/** @typescript */ -class User -{ - public int $id; - public string $name; - public ?string $address; -} -``` - -... will be converted to this TypeScript type: - -```ts -export type User = { - id: number; - name: string; - address: string | null; -} -``` - -Here's another example. - -```php -class Languages extends Enum -{ - const TYPESCRIPT = 'typescript'; - const PHP = 'php'; -} -``` - -The `Languages` enum will be converted to: - -```tsx -export type Languages = 'typescript' | 'php'; -``` diff --git a/docs/laravel/_index.md b/docs/laravel/_index.md deleted file mode 100755 index ecad49eb..00000000 --- a/docs/laravel/_index.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Laravel -weight: 4 ---- diff --git a/docs/laravel/executing-the-transform-command.md b/docs/laravel/executing-the-transform-command.md deleted file mode 100755 index 5ce42d85..00000000 --- a/docs/laravel/executing-the-transform-command.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Executing the transform command -weight: 2 ---- - -After configuring the package in the `typescript-transformer` config file, you can run this command to write the typescript output file: - -```bash -php artisan typescript:transform -``` - -## Command options - -There are some extra commands you can use when running the command. It is also possible to transform classes in a specified path: - -```bash -php artisan typescript:transform --path=app/Enums -``` - -Or you can define another output file than the default one: - -```bash -php artisan typescript:transform --output=types.d.ts -``` - -This file will be stored in the resource's path of your Laravel application. - -It is also possible to automatically format the generated TypeScript with Prettier, ESLint, or a custom formatter: - -```bash -php artisan typescript:transform --format -``` diff --git a/docs/laravel/installation-and-setup.md b/docs/laravel/installation-and-setup.md deleted file mode 100755 index df224eed..00000000 --- a/docs/laravel/installation-and-setup.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: Installation and setup -weight: 1 ---- - -## Basic installation - -You can install this package via composer: - -```bash -composer require spatie/laravel-typescript-transformer -``` - -The package will automatically register a service provider. - -You can publish the config file with: - -```bash -php artisan vendor:publish --provider="Spatie\LaravelTypeScriptTransformer\TypeScriptTransformerServiceProvider" -``` - -This is the default content of the config file: - -```php - [ - app_path() - ], - - /* - * Collectors will search for classes in the `auto_discover_types` paths and choose the correct - * transformer to transform them. By default, we include a DefaultCollector which will search - * for @typescript annotated and ![TypeScript] attributed classes to transform. - */ - - 'collectors' => [ - Spatie\TypeScriptTransformer\Collectors\DefaultCollector::class, - ], - - /* - * Transformers take PHP classes(e.g., enums) as an input and will output - * a TypeScript representation of the PHP class. - */ - - 'transformers' => [ - Spatie\LaravelTypeScriptTransformer\Transformers\SpatieStateTransformer::class, - Spatie\TypeScriptTransformer\Transformers\SpatieEnumTransformer::class, - Spatie\TypeScriptTransformer\Transformers\DtoTransformer::class, - ], - - /* - * In your classes, you sometimes have types that should always be replaced - * by the same TypeScript representations. For example, you can replace a - * Datetime always with a string. You define these replacements here. - */ - - 'default_type_replacements' => [ - DateTime::class => 'string', - DateTimeImmutable::class => 'string', - Carbon\CarbonImmutable::class => 'string', - Carbon\Carbon::class => 'string', - ], - - /* - * The package will write the generated TypeScript to this file. - */ - - 'output_file' => resource_path('types/generated.d.ts'), - - /* - * When the package is writing types to the output file, a writer is used to - * determine the format. By default, this is the `TypeDefinitionWriter`. - * But you can also use the `ModuleWriter` or implement your own. - */ - - 'writer' => Spatie\TypeScriptTransformer\Writers\TypeDefinitionWriter::class, - - /* - * The generated TypeScript file can be formatted. We ship two formatters by - * default: a Prettier and an ESLint one. You can also implement your own. - * The generated TypeScript will not be formatted if none is configured. - */ - - 'formatter' => null, - - /* - * Enums can be transformed into types or native TypeScript enums, by default - * the package will transform them to types. - */ - - 'transform_to_native_enums' => false, -]; -``` diff --git a/docs/postcardware.md b/docs/postcardware.md deleted file mode 100755 index a019fe35..00000000 --- a/docs/postcardware.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Postcardware -weight: 2 ---- - -You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. - -Our address is: Spatie, Kruikstraat 22 Box 12, 2018 Antwerp, Belgium. - -The best postcards will get published on the [open source section](https://spatie.be/en/opensource/postcards) on our website. diff --git a/docs/questions-and-issues.md b/docs/questions-and-issues.md deleted file mode 100755 index 3d41fd08..00000000 --- a/docs/questions-and-issues.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Questions & issues -weight: 6 ---- - -Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving the backup package? Feel free to [create an issue on GitHub](https://github.com/spatie/typescript-transformer/issues), we'll try to address it as soon as possible. - -If you've found a bug regarding security please mail [freek@spatie.be](mailto:freek@spatie.be) instead of using the issue tracker. diff --git a/docs/transformers/_index.md b/docs/transformers/_index.md deleted file mode 100755 index 584509e4..00000000 --- a/docs/transformers/_index.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Writing transformers -weight: 2 ---- diff --git a/docs/transformers/getting-started.md b/docs/transformers/getting-started.md deleted file mode 100644 index 752bd1c2..00000000 --- a/docs/transformers/getting-started.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: Getting started -weight: 1 ---- - -A transformer is a class that implements the `Transformer` interface: - -```php -use Spatie\TypeScriptTransformer\Transformers\Transformer; - -class EnumTransformer implements Transformer -{ - public function transform(ReflectionClass $class, string $name): ?TransformedType - { - } -} -``` - -In the `transform` method, you should transform a PHP `ReflectionClass` into a `TransformedType`. This `TransformedType` will -include the TypeScript representation of the PHP class and some extra information. - -When a transformer cannot transform the given `ReflectionClass` then the method should return `null`, indicating that the transformer is not suitable for the type. - -### Creating transformed types - -A `TransformedType` always has three properties: the `ReflectionClass` of the type you're transforming, the name of the -type and, of course, the transformed TypeScript code: - -```php -TransformedType::create( - ReflectionClass $class, // The reflection class - string $name, // The name of the Type - string $transformed // The TypeScript representation of the class -); -``` - -For types that depend on other types a fourth argument can be passed to the `create` method: - -```php -TransformedType::create( - ReflectionClass $class, - string $name, - string $transformed, - MissingSymbolsCollection $missingSymbols -); -``` - -A `MissingSymbolsCollection` will contain references to other types. The package will replace these references with -correct TypeScript types. - -Consider the following class as an example: - -```php -/** @typescript **/ -class User extends DataTransferObject -{ - public string $name; - - public RoleEnum $role; -} -``` - -As you can see, it has a `RoleEnum` as a property, which looks like this: - -```php -/** @typescript **/ -class RoleEnum extends Enum -{ - const GUEST = 'guest'; - const ADMIN = 'admin'; -} -``` - -When transforming the `User` class we don't have any context or types for the `RoleEnum`. The transformer can register -this missing symbols for a property using the `MissingSymbolsCollection` as such: - -```php -$type = $missingSymbols->add(RoleEnum::class); // Will return {%RoleEnum::class%} -``` - -The `add` method will return a token that can be used in your transformed type. It's a link between the two types and -will later be replaced by the actual type implementation. - -When no type was found (for example: because it wasn't converted to TypeScript), the type will default to -TypeScript's `any` type. - -In the end, the package will produce the following output: - -```tsx -export type RoleEnum = 'guest' | 'admin'; - -export type User = { - name: string; - role: RoleEnum; -} -``` - -#### Inline types - -It is also possible to create an inline type. These types are replaced directly in other types. You can read more about -inline types [here](/docs/typescript-transformer/v2/usage/annotations#inlining-types). - -Inline types can be created like a regular `TransformedType` but they do not need a name: - -```php -TransformedType::createInline( - ReflectionClass $class, - string $transformed -); -``` - -When required you can also add a `MissingSymbolsCollection`: - -```php -TransformedType::createInline( - ReflectionClass $class, - string $transformed, - MissingSymbolsCollection $missingSymbols -); -``` - -When you create a new transformer, don't forget to add it to the list of transformers in your configuration! diff --git a/docs/transformers/type-processors.md b/docs/transformers/type-processors.md deleted file mode 100644 index 46b54293..00000000 --- a/docs/transformers/type-processors.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: Type processors -weight: 3 ---- - -You can use type processors to change an entity's internal `Type` before it is transpiled into TypeScript. - -## Default type processors - -- `ReplaceDefaultsTypeProcessor` replaces some types defined in the configuration -- `DtoCollectionTypeProcessor` replaces `DtoCollections` from the `spatie/data-transfer-object` package with their - TypeScript equivalent - -Specifically for Laravel, we also include the following type processors in the Laravel package: - -- `LaravelCollectionTypeProcessor` handles Laravel's `Collection` classes like `array`s - -## Using type processors in your transformers - -When you're using the `TransformsTypes` [trait](https://github.com/spatie/typescript-transformer/blob/master/src/Transformers/TransformsTypes.php) in your transformer and use -the `reflectionToTypeScript` then you can additionally pass type processors: - -```php -$this->reflectionToTypeScript( - $reflection, - $missingSymbolsCollection, - new ReplaceDefaultsTypeProcessor(), - new DtoCollectionTypeProcessor(), - // and so on ... -); -``` - -## Writing type processors - -A class property processor is any class that implements the `ClassPropertyProcessor` interface: - -```php -class MyClassPropertyProcessor implements TypeProcessor -{ - public function process( - Type $type, - ReflectionProperty|ReflectionParameter|ReflectionMethod $reflection, - MissingSymbolsCollection $missingSymbolsCollection - ): ?Type - { - // Transform the types of the property - } -} -``` - -### Returning a type - -You can either return a PHPDocumenter type or a `TypeScriptType` instance for literal TypeScript types. - -Let's take a look at an example. With this type processor, it will convert each property type into a `string`. - -Using a `TypeScriptType`: - -```php -class MyClassPropertyProcessor implements TypeProcessor -{ - public function process( - Type $type, - ReflectionProperty|ReflectionParameter|ReflectionMethod $reflection, - MissingSymbolsCollection $missingSymbolsCollection - ): ?Type - { - return TypeScriptType::create('SomeGenericType'); - } -} -``` - -Or using a PHPDocumenter type: - -```php -class MyClassPropertyProcessor implements TypeProcessor -{ - public function process( - Type $type, - ReflectionProperty|ReflectionParameter|ReflectionMethod $reflection, - MissingSymbolsCollection $missingSymbolsCollection - ): ?Type - { - return new String_(); - } -} -``` - -You can find all the possible PHPDocumenter -types [here](https://github.com/phpDocumentor/TypeResolver/tree/1.x/src/Types). - -### Walking over types - -Since any type can exist of arrays, compound types, nullable types, and more, you'll sometimes need to walk (or loop) -over these types to specify types case by case. This can be done by including the `ProcessesTypes` trait into your type -processor. - -This trait will add a `walk` method that takes an initial type and closure. - -Let's say you have a compound type like `string|bool|int`. The `walk` method will run a `string`, `bool` and `int` type -through the closure. You can then decide a type to be returned for each type given to the closure. Finally, the updated -compound type will also be passed to the closure. - -You can remove a type by returning `null`. - -Let's take a look at an example where we only keep `string` types and remove any others: - -```php -class MyClassPropertyProcessor implements TypeProcessor -{ - use ProcessesTypes; - - public function process( - Type $type, - ReflectionProperty|ReflectionParameter|ReflectionMethod $reflection, - MissingSymbolsCollection $missingSymbolsCollection - ): ?Type - { - return $this->walk($type, function (Type $type) { - if ($type instanceof _String || $type instanceof Compound) { - return $type; - } - - return null; - }); - } -} -``` - -As you can see, we check in the closure if the type is a `string` or a `compound` type. If it is none of these two -types, we remove it by returning `null`. - -Why checking if the given type is a compound type? In the end, the compound type will be given to the closure. If we -removed it, the whole property could be removed from the TypeScript definition. diff --git a/docs/transformers/type-reflectors.md b/docs/transformers/type-reflectors.md deleted file mode 100644 index e2b2c0c0..00000000 --- a/docs/transformers/type-reflectors.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: Type reflectors -weight: 2 ---- - -Writing transformers can be complicated since there's a lot to keep in mind when trying to resolve the types within PHP -classes. - -TypeReflectors can help you with this. They will take a `ReflectionMethod`, `ReflectionProperty` -or `ReflectionParameter` and convert it into a `Type` which can be easily transpiled to TypeScript. - -A type reflector uses the following information to deduce a type: - -- attributes added to the PHP definition -- an annotation was added with the PHP definition -- the type is written in PHP, and if it is nullable - -It will use all this information and creates a `Type` object from -the [phpDocumentor/TypeResolver](https://github.com/phpDocumentor/TypeResolver) package, examples of such types are: - -- Array_ -- Boolean -- Compound -- Object_ -- Void -- [and many more](https://github.com/phpDocumentor/TypeResolver/tree/1.x/src/Types) - -These types can be easily transpiled to TypeScript. Let's take a look at an example: - -```php -class Properties{ - #[LiteralTypeScriptType('unknown')] - public $propertyWithAttribute; - - /** @var int */ - public $propertyWithAnnotation; - - public bool $propertyWithType; - - public ?string $propertyWithNullableType; -}; -``` - -We can now write a transformer that uses the `TransformsTypes` trait. This trait adds the `reflectionToTypeScript` method to your transformer, which takes a reflected entity and a missing symbols collection and transforms it to Typescript. - -```php - -class PropertyTransformer implements Transformer{ - use TransformsTypes; - - public function transform(ReflectionClass $class, string $name) : ?TransformedType - { - $missingSymbols = new MissingSymbolsCollection(); - - $properties = array_map( - fn(ReflectionProperty $reflection) => "{$reflection->name}: {$this->reflectionToTypeScript($reflection, $missingSymbols)};", - $class->getProperties() - ); - - return TransformedType::create( - $class, - $name, - '{'. join($properties) . '}', - $missingSymbols - ); - } -} -``` - -This transformer will transform the `Properties` class into: - -```tsx -{ - propertyWithAttribute: unknown; - propertyWithAnnotation: number; - propertyWithType: boolean; - propertyWithNullableType: ?string; -} -``` diff --git a/docs/under-the-hood.md b/docs/under-the-hood.md deleted file mode 100644 index 9396e462..00000000 --- a/docs/under-the-hood.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: Under the hood -weight: 5 ---- - -Reading this page is not required for knowing how to use the package. We recommend to first read through the other documentation and then come back to read this page. - -## Step 0: configuring the package - -In the package configuration a couple important values are defined: - -- the path where your PHP classes are stored, -- the file where the TypeScript definition will be written, -- the collectors that will find relevant PHP classes that can be transformed, -- and the transformers required to convert PHP to TypeScript. - -## Step 1: Collecting classes - -We start by iterating over the PHP classes in the specified directory and create a `ReflectionClass` for each class. If a collector can collect one of these classes, it will try to find a suitable transformer. - -For example, the `DefaultCollector` will collect each class with a `@typescript` annotation or a `#[TypeScript]` attribute and feed it to all the registered transformers to hopefully find a transformer that can generate a type definition for the class. - -## Step 2: Transforming classes - -We've created a set of classes and their suitable transformers in step 1. We're now going to transform these types to TypeScript. For the enum example, this is relatively simple, but for a complex data transfer object (DTO) this process is a bit more complicated. - -Each property of the DTO will be checked: does it have a PHP type and/or does it have an annotated type? The package creates a unified `Type` from this and feeds it to the type processors. These will transform the type or completely remove it from the DTO's TypeScript definition. - -A good example of a type processor is the `ReplaceDefaultsTypeProcessor`. This one will replace some default types you can define in the configuration with a TypeScript representation. For example transforming `DateTime` or `Carbon` types to `string`. - -DTO's often have properties that contain other DTO's, or even other custom types. This is why we'll also keep track of the missing symbols when transforming a DTO. -Let's say your DTO has a property that contains another DTO. At the moment of transformation, the package will not know how that other DTO should be transformed. We'll temporarily use a missing symbol that can be replaced by a reference to the correcty DTO type later. - -## Step 3: Replacing missing symbols - -The classes we started with in step 2 are now transformed into TypeScript definitions, although some types are still missing references to other types. Thanks to the missing symbols collections that each transformer constructed, we can replace these references with the correct type. - -If a reference cannot be replaced because it cannot be found the package will default to the `any` type, as it doesn't know how to reference it. - -It's recommended to try to avoid these `any` types as much as possible. - -## Step 4: persisting types - -Our set of transformed classes is now ready. All missing symbols are replaced, so it's time to write them out. A writer will take the entire set of transformed types and write them down into a TypeScript type defintion file you configured. - -## Step 5: formatting the output - -The package tries to output readable TypeScript code without adhering to any code style. Using tools like Prettier the output can be formatted in a code style of your choice. diff --git a/docs/usage/_index.md b/docs/usage/_index.md deleted file mode 100755 index 7700d360..00000000 --- a/docs/usage/_index.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Usage -weight: 1 ---- diff --git a/docs/usage/annotations.md b/docs/usage/annotations.md deleted file mode 100644 index d5fc0843..00000000 --- a/docs/usage/annotations.md +++ /dev/null @@ -1,280 +0,0 @@ ---- -title: Describing types -weight: 3 ---- - -PHP classes will only be converted to TypeScript when they are annotated, there are quite a few ways to do this, let's take a look. - -When using the `@typescript` annotation, the PHP class's name will be used as name for the TypeScript type: - -```php -/** @typescript */ -class Language extends Enum{ - const en = 'en'; - const nl = 'nl'; - const fr = 'fr'; -} -``` - -The package will produce the following TypeScript: - -```tsx -export type Language = 'en' | 'nl' | 'fr'; -``` - -It is also possible to use a PHP8 attribute like this: - -```php -#[TypeScript] -class Language extends Enum{ - const en = 'en'; - const nl = 'nl'; - const fr = 'fr'; -} -``` - -You can also give the type another name: - -```php -/** @typescript Talen **/ -class Language extends Enum{} -``` - -Which also can be done using attributes: - -```php -#[TypeScript('Talen')] -class Language extends Enum{} -``` - -Now the transformed TypeScript looks like this: - -```tsx -export type Talen = 'en' | 'nl' | 'fr'; -``` - -## Inlining types - -It is also possible to annotate types as an inline type. These types will not create a whole new TypeScript type but replace a type inline in another type. Let's create a class containing the `Language` enum: - -```php -/** @typescript **/ -class Post -{ - public string $name; - public Language $language; -} -``` - -The transformed version of a `Post` would look like this: - -```tsx -export type Language = 'en' | 'nl' | 'fr'; - -export type Post = { - name : string; - language : Language; -} -``` - -We could inline the `Language` enum as such: - -```php -/** - * @typescript - * @typescript-inline - */ -class Language extends Enum{} -``` - -Or using an attribute: - -```php -#[TypeScript] -#[InlineTypeScriptType] -class Language extends Enum{} -``` - -And now our transformed TypeScript would look like this: - -```ts -export type Post = { - name : string; - language : 'en' | 'nl' | 'fr'; -} -``` - -## Using TypeScript to write TypeScript - -It is possible to directly represent a type as TypeScript within your PHP code: - -```php -#[TypeScript] -#[LiteralTypeScriptType("string | null")] -class CustomString{} -``` - -Now when `Language` is being transformed, the TypeScript respresentation is used: - -```tsx -export type CustomString = string | null; -``` - -You can even provide an array of types: - -```php -#[TypeScript] -#[LiteralTypeScriptType([ - 'email' => 'string', - 'name' => 'string', - 'age' => 'number', -])] -class UserData{ - public $email; - public $name; - public $age; -} -``` - -This would transform to: - -```tsx -export type UserData = { - email: string; - name: string; - age: number; -}; -``` - -This attribute can also be used with properties in a class, for example: - -```php -#[TypeScript] -class Post -{ - public string $name; - - #[LiteralTypeScriptType("'en' | 'nl' | 'fr'")] - public Language $language; -} -``` - -## Using PHP types to write TypeScript - -When you have a very specific type you want to describe in PHP then you can use the `TypeScriptType` which can transform every type [phpdocumentor](https://www.phpdoc.org) can read. For example, let's say you have an array that always has the same keys as this one: - -```php -$user = [ - 'name' => 'Ruben Van Assche', - 'email' => 'ruben@spatie.be', - 'age' => 26, - 'language' => Language::nl() -]; -``` - -When we put that array as a property in a class: - -```php -#[TypeScript] -class UserRepository{ - public array $user; -} -``` - -The transformed type will look like this: - -```tsx -export type UserRepository = { - user: Array; -}; -``` - -We can do better than this, since we know the keys of the array: - -```php -use Spatie\TypeScriptTransformer\Attributes\TypeScript;#[TypeScript] -class UserRepository{ - #[TypeScriptType([ - 'name' => 'string', - 'email' => 'string', - 'age' => 'int', - 'language' => Language::class - ])] - public array $user; -} -``` - -Now the transformed TypeScript will look like this: - -```tsx -export type UserRepository = { - user: { - name: string; - email: string; - age: number; - language: 'en' | 'nl' | 'fr'; - }; -}; -``` - -As you can see, the package is smart enough to convert `Language::class` to an inline enum we defined earlier. - -## Generating `Record` types - -If you need to generate a `Record` type, you may use the `RecordTypeScriptType` attribute: - -```php -use Spatie\TypeScriptTransformer\Attributes\RecordTypeScriptType; - -class FleetData extends Data -{ - public function __construct( - #[RecordTypeScriptType(AircraftType::class, AircraftData::class)] - public readonly array $fleet, - ) { - } -} -``` - -This will generate a `Record` type with a key type of `AircraftType::class` and a value type of `AircraftData::class`: - -```ts -export type FleetData = { - fleet: Record -} -``` - -Additionally, if you need the value type to be an array of the specified type, you may set the third parameter of `RecordTypeScriptType` to `true`: - -```php -class FleetData extends Data -{ - public function __construct( - #[RecordTypeScriptType(AircraftType::class, AircraftData::class, array: true)] - public readonly array $fleet, - ) { - } -} -``` - -This will generate the following interface: - -```ts -export type FleetData = { - fleet: Record> -} -``` - -## Selecting a transformer - -Want to define a specific transformer for the file? You can use the following annotation: - -```php -/** - * @typescript - * @typescript-transformer \Spatie\TypeScriptTransformer\Transformers\SpatieEnumTransformer::class - */ -class Languages extends Enum{} -``` - -It is also possible to transform types without adding annotations. You can read more about it [here](https://spatie.be/docs/typescript-transformer/v2/usage/selecting-classes-using-collectors). diff --git a/docs/usage/formatters.md b/docs/usage/formatters.md deleted file mode 100644 index aeda7cbd..00000000 --- a/docs/usage/formatters.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Formatters -weight: 7 ---- - -This output file with all the transformed types can be formatted using tools like Prettier. We ship an ESLint and a Prettier formatter, which will run after the output file is generated. For instance, you can configure the Prettier formatter as such: - -```php -$config = TypeScriptTransformerConfig::create() - ->formatter(PrettierFormatter::class) - ... -``` - -You could also implement your own formatter by implementing the `Formatter` interface: - -```php -interface Formatter -{ - public function format(string $file): void; -} -``` - -Within the `format` method, a path to the output file is given, which should be formatted. diff --git a/docs/usage/general-overview.md b/docs/usage/general-overview.md deleted file mode 100644 index a4df7a8d..00000000 --- a/docs/usage/general-overview.md +++ /dev/null @@ -1,567 +0,0 @@ ---- -title: General overview -weight: 1 ---- - -Let's look at a real-world use case of how the package can transform PHP types to TypeScript. We're not going to use the default Laravel resources because they cannot be typed. Instead, we're going to use the [spatie/data-transfer-object](https://github.com/spatie/data-transfer-object) package. - -Let's first create a `UserResource`: - -```php -class UserResource extends DataTransferObject implements Arrayable -{ - public ?int $age = null; - - public ?string $name = null; - - public ?string $email = null; - - public ?AddressResource $address = null; -} -``` - -Here is the code of `AddressResource`: - -```php -class AddressResource extends DataTransferObject implements Arrayable -{ - public ?string $street = null; - - public ?string $number = null; - - public ?string $city = null; - - public ?string $postal = null; - - public ?string $country = null; -} -``` - -Each property is nullable, so it's easy to send an empty instance to the front end where necessary. - -To easily convert a user to a `UserResource`, we're going to add a static `make` function to it. We'll also implement `Illuminate\Contracts\Support\Arrayable` so the resource can be converted to an array when sending it to the front end. This interface requires the object to have a `toArray` method. The implementation of the `toArray` method lives in the `DataTransferObject` base class, which will use the object's public properties. - -When applying the changes described, the `UserResource` will now look like this: - -```php -use Illuminate\Contracts\Support\Arrayable; - -class UserResource extends DataTransferObject implements Arrayable -{ - public ?int $age = null; - - public ?string $name = null; - - public ?string $email = null; - - public ?AddressResource $address = null; - - public static function make(User $user): self - { - return new static([ - 'age' => $user->age, - 'name' => "{$user->first_name} {$user->last_name}", - 'email' => $user->email, - 'address' => AddressResource::make($user->address ?? new Address()), - ]); - } -} -``` - -Let's also apply the same changes to the `AddressResource`. - -```php -class AddressResource extends DataTransferObject implements Arrayable -{ - public ?string $street = null; - - public ?string $number = null; - - public ?string $city = null; - - public ?string $postal = null; - - public ?string $country = null; - - public static function make(Address $address): self - { - return new self([ - 'street' => $address->street, - 'number' => $address->number, - 'city' => $address->city, - 'postal' => $address->postal, - 'country' => $address->country, - ]); - } -} -``` - -When using DTO's, it's impossible to assign a `string` to an `int` type. Another benefit is IDE completion. You can now construct your resource with all the information hinted by your IDE. - -## Using resources in your project - -Let's use the `UserResource` in a controller. - -```php -class UserController -{ - public function create() - { - return UserResource::make(new User()); - } - - public function update(User $user) - { - return UserResource::make($user); - } -} -``` - -## Transforming DTOs to TypeScript - -```php -/** @typescript */ -class UserResource extends DataTransferObject implements Arrayable -{ - // ... -} - -/** @typescript */ -class AddressResource extends DataTransferObject implements Arrayable -{ - // ... -} -``` - -With that annotation in place, we can generate the typescript equivalents by executing this command: - - -```bash -php artisan typescript:transform -``` - -Then we get the following output: - -```bash -+------------------------------------+------------------------------------+ -| PHP class | TypeScript entity | -+------------------------------------+------------------------------------+ -| App\Http\Resources\UserResource | App.Http.Resources.UserResource | -| App\Http\Resources\AddressResource | App.Http.Resources.AddressResource | -+------------------------------------+------------------------------------+ -Transformed 2 PHP types to TypeScript - -``` - -A new file was created in the `resources/js` directory of our application. `generated.ts` contains two types: - -```ts -namespace App.Http.Resources { - export type AddressResource = { - street: string | null; - number: string | null; - city: string | null; - postal: string | null; - country: string | null; - } - - export type UserResource = { - age: number | null; - name: string | null; - email: string | null; - address: App.Http.Resources.AddressResource | null; - } -} -``` - -These types can now be used in TypeScript code. Referencing a `UserResource` can now be done using `App.Http.Resource.UserResource`. - -## Using collectors to find resources - -Instead of manually adding `@typescript` to each class, we can use a [collector](https://spatie.be/docs/typescript-transformer/v2/usage/selecting-classes-using-collectors). - -Let's first create an abstract class Resource: - -```php -abstract class Resource extends DataTransferObject implements Arrayable -{ -} -``` - -Next, the `UserResource` and `AddressResource` should extend `Resource`: - -```php -class UserResource extends Resource -{ - // ... -} - - -class AddressResource extends Resource -{ - // ... -} -``` - -With that in place, we can create a collector that will process all classes that extend `Resource` - -```php -class ResourceCollector extends Collector -{ - public function shouldCollect(ReflectionClass $class): bool - { - return $class->isSubclassOf(Resource::class); - } - - public function getTransformedType(ReflectionClass $class): ?TransformedType - { - if(! $class->isSubclassOf(Resource::class)) - { - return null; - } - - $transformer = new DtoTransformer($this->config); - - return $transformer->transform( - $class, - Str::before($class->getShortName(), 'Resource') - ); - } -} -``` - -Finally, `ResourceCollector` should be added to the list of collectors in the configuration file `typescript-transformer.php`: - -```php - ... - - /* - * Collectors will search for classes in your `searching_path` and choose the correct - * transformer to transform them. By default, we include an AnnotationCollector - * which will search for @typescript annotated classes to transform. - */ - - 'collectors' => [ - Spatie\TypeScriptTransformer\Collectors\AnnotationCollector::class, - App\Support\TypeScriptTransformer\ResourceCollector::class, - ], - - ... -``` - -Now you can run `php artisan typescript:transform` to create the TypeScript definitions. - -### Using default type replacements - -You can specify to which TypeScript type a PHP type should be converted. - -Let's add a `$birthday` property to the `UserResource`, which is of type `Carbon`. - -```php -class UserResource extends DataTransferObject implements Arrayable -{ - public ?int $age = null; - - public ?string $name = null; - - public ?string $email = null; - - public ?AddressResource $address = null; - - public ?Carbon $birthday = null; - - public static function make(User $user): self - { - return new self([ - 'age' => $user->age, - 'name' => "{$user->first_name} {$user->last_name}", - 'email' => $user->email, - 'address' => AddressResource::make($user->address ?? new Address()), - 'birthday' => $user->birthday, - ]); - } -} -``` - -Since `Carbon` isn't a primitive type like a `string`, `int`, `bool`, `array`, we actually cannot send it directly to the front. Using the `Resource` class, we can convert the `Carbon` instance to a string: - -```php -abstract class Resource extends DataTransferObject implements Arrayable -{ - public function toArray(): array - { - return collect(parent::toArray())->map(function ($value) { - if ($value instanceof Carbon) { - return $value->toAtomString(); - } - - return $value; - })->toArray(); - } -} -``` - -This class will transform it to the `any` TypeScript type, but you can make it more specific. You can specify to which TypeScript type the PHP type should be converted to in the' typescript-transformer' config file. - -``` - ... - - 'default_type_replacements' => [ - // ... - Carbon::class => 'string', - ], - - ... -``` - -### Using transformer to manually convert a type - -In the previous section, we converted a `Carbon` type to a string. If you want to have fine-grained control over how a PHP type should be converted to a TypeScript type, you can use a `Transformer`. Let's convert `Carbon` to a type that has a day, month, and year. - -First, we need to create a PHP class that has the desired structure. - -```php -@typescript -class CustomDate -{ - private int $year; - - private int $month; - - private int $day; - - public function __construct(int $year, int $month, int $day) - { - $this->year = $year; - $this->month = $month; - $this->day = $day; - } - - public function get(): array - { - return [ - 'year' => $this->year, - 'month' => $this->month, - 'day' => $this->day, - 'is_today' => date('Y/m/d') === "{$this->year}/{$this->month}/{$this->day}" - ]; - } -} -``` - -Next, the `UserResource` needs to use the `CustomDate` type: - -```php -class UserResource extends Resource -{ - public ?int $age = null; - - public ?string $name = null; - - public ?string $email = null; - - public ?AddressResource $address = null; - - public ?CustomDate $birthday = null; - - public static function make(User $user): self - { - return new self([ - 'age' => $user->age, - 'name' => "{$user->first_name} {$user->last_name}", - 'email' => $user->email, - 'address' => AddressResource::make($user->address ?? new Address()), - 'birthday' => new CustomDate( - $user->birthday->year, - $user->birthday->month, - $user->birthday->day - ), - ]); - } -} -``` - -And the base `Resource` needs to be aware of the `CustomDate` as well. - -```php -abstract class Resource extends DataTransferObject implements Arrayable -{ - public function toArray(): array - { - return collect(parent::toArray()) - ->map(function ($value) { - if ($value instanceof CustomDate) { - return $value->get(); - } - - return $value; - }) - ->toArray(); - } -} -``` - -If we would run `php artisan typescript:transform` now, this would be the result. - -```ts -export type User = { - age: number | null; - name: string | null; - email: string | null; - address: App.Http.Resources.Address | null; - birthday: any | null; -} -``` - -That `any` does not describe our homemade `CustomDate` type. Let's fix that by using a `Transformer`: - -```php -class CustomDateTransformer implements Transformer -{ - public function transform(ReflectionClass $class, string $name): ?TransformedType - { - if(!$class->getName() === CustomDate::class) - { - return null; - } - - $type = << [ - App\Support\TypeScriptTransformer\CustomDateTransformer::class, - // ... - ], - - ... -``` - - - -Running `php artisan typescript:transform` will generate these TypeScript types: - -```ts -namespace App.Http.Resources { - export type Address = { - street: string | null; - number: string | null; - city: string | null; - postal: string | null; - country: string | null; - } - - export type User = { - age: number | null; - name: string | null; - email: string | null; - address: App.Http.Resources.Address | null; - birthday: App.Support.CustomDate | null; - } -} - -namespace App.Support { - export type CustomDate = { - year: number; - month: number; - day: number; - is_today: boolean; - } -} -``` - -If you don't need a separate `App.Support.CustomDate` type, you can choose to inline it in the types where it is used. To do that, use the `createInline` function in the `Transformer`. - -```php -class CustomDateTransformer implements Transformer -{ - public function transform(ReflectionClass $class, string $name): ?TransformedType - { - if(!$class->getName() === CustomDate::class) - { - return null; - } - - $type = << [ - // ... - CustomDate::class => TypeScriptType::create(<<autoDiscoverTypes(__DIR__ . '/src') - // list of transformers - ->transformers([MyclabsEnumTransformer::class]) - // file where TypeScript type definitions will be written - ->outputFile(__DIR__ . '/js/generated.d.ts'); -``` - -This is the minimal required configuration that should get you started. There are some more configuration options, but we'll go over these later in the documentation. - -Let's use this configuration to start the transformation process: - -```php -TypeScriptTransformer::create($config)->transform(); -``` - -That's it! Each class with a `@typescript` annotation or `#[TypeScript]` are now transformed to TypeScript if a suitable transformer can be found. - -## Laravel - -Are you using Laravel? Then you can use a Laravel config file, more info about that [here](https://docs.spatie.be/typescript-transformer/v2/laravel/installation-and-setup/). diff --git a/docs/usage/selecting-classes-using-collectors.md b/docs/usage/selecting-classes-using-collectors.md deleted file mode 100644 index f035e339..00000000 --- a/docs/usage/selecting-classes-using-collectors.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: Collectors -weight: 5 ---- - -In some cases, you'll want to transform classes without an attribute or annotation. For example, Laravel's API resources are almost always sent to the front and should always have a TypeScript definition ready to be used. - -Collectors allow you to specify which PHP classes should be transformed to TypeScript and what transformer should be used. - -The package comes out of the box with the pre-configured `DefaultCollector` to find and transform classes marked with the `@typescript` annotation and `#[TypeScript]` attributes. - -A collector is a class that extends the abstract `Collector` class and implements the `getTransformedType` method: - -```php -class EnumCollector extends Collector -{ - public function getTransformedType(ReflectionClass $class): ?TransformedType - { - } -} -``` - -The `getTransformedType` will return a `TransformedType` object if it can transform a `ReflectionClass` to TypeScript. When not possible, the method should return `null`. - -Don't forget to add the collector to your configuration: - -```php -$config = TypeScriptTransformerConfig::create() - ->collectors([EnumCollector::class]) - ... -``` - -Collectors will be checked in order if a perfect collector fit was found for a type. Then all the other collectors after that collector will be ignored for that type. - -## Difference between Collectors and Transformers - -Although the two concepts share a very similar interface, they are indeed different. - -A collector takes Types and gives them to a specific transformer that the collector decided. For example, the `DefaultCollector` will run a type through each transformer you've configured to find the right one. - -Collectors can also change names for specific Types. For example, a ResourceCollector could strip the Resource prefix of each class it collects. - -Transformers, on the other hand, transform types. They take a `ReflectionClass` and try to transform it to TypeScript. diff --git a/docs/usage/using-transformers.md b/docs/usage/using-transformers.md deleted file mode 100644 index 312dda4d..00000000 --- a/docs/usage/using-transformers.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Using transformers -weight: 4 ---- - -Transformers are the heart of the package. They take a PHP class and try to make a TypeScript definition out of it. - -## Default transformers - -The package comes with a few transformers out of the box: - -- `EnumTransformer`: this transforms a PHP 8.1 native enum -- `MyclabsEnumTransformer`: this transforms an enum from the [`myclabs\enum`](https://github.com/myclab/enum) package -- `SpatieEnumTransformer`: this transforms an enum from the [`spatie\enum`](https://github.com/spatie/enum) package -- `DtoTransformer`: a powerful transformer that transforms entire classes and their properties, you can read more about - it [here](/docs/typescript-transformer/v2/dtos/typing-properties) -- `InterfaceTransformer`: this transforms a PHP interface and its functions to a Typescript interface. If used, this - transformer should always be included before the `DtoTransformer`. - -[The laravel package](/docs/typescript-transformer/v2/laravel/installation-and-setup) has some extra transformers: - -- `SpatieStateTransformer`: this transforms a state from - the [`spatie\laravel-model-states`](https://github.com/spatie/laravel-model-status) package -- `DtoTransformer`: a more Laravel specific transformer based upon the default `DtoTransformer` - -There are also some packages with community transformers: - -- A [transformer](https://github.com/wt-health/laravel-enum-transformer) for `bensampo/laravel-enum` enums - -If you've written a transformer package, let us know, and we add it to the list! - -You should supply a list of transformers the package should use in your config. The order of transformers matters and can lead to unexpected results if in the wrong order. A PHP declaration (e.g. classes, enums) will go through each transformer and stop once a transformer is able to handle it; this is a problem if `DtoTransformer` is listed before an enum transformer since `DtoTransformer` will incorrectly handle an enum as a class and never allow `MyclabsEnumTransformer` to handle it. - -```php -$config = TypeScriptTransformerConfig::create() - ->transformers([MyclabsEnumTransformer::class, DtoTransformer::class]) - ... -``` - -### Transforming enums - -The package ships with three enum transformers out of the box, by default these enums are transformed to TypeScript types like this: - -```tsx -type Language = 'JS' | 'PHP'; -``` - -It is possible to transform them to native TypeScript enums by changing the config: - -```php -$config = TypeScriptTransformerConfig::create() - ->transformToNativeEnums() - ... -``` - -A transformed enum now looks like this: - -```tsx -enum Language {'JS' = 'JS', 'PHP' = 'PHP'}; -``` - -## Writing your own transformers - -We've added a whole section in the docs about [writing transformers](../transformers/getting-started). diff --git a/docs/usage/writers.md b/docs/usage/writers.md deleted file mode 100644 index 6b7f3094..00000000 --- a/docs/usage/writers.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: Writers -weight: 6 ---- - -When all types are transformed, the package will write them out in one big output file. A writer will determine how this output will look. - -You can configure a writer as such: - -```php -$config = TypeScriptTransformerConfig::create() - ->writer(TypeDefinitionWriter::class) - ... -``` - -By default, the `TypeDefinitionWriter` is used when no writer was configured. You can use one of the Writers shipped with the package out of the box or write your own one. - -## TypeDefinitionWriter - -The `TypeDefinitionWriter` will group types into namespaces that follow the structure of the PHP namespaces. - -Let's take a look at an example with two PHP classes: - -```php -namespace App\Enums; - -#[TypeScript] -class Language extends Enum -{ - public const nl = 'nl'; - public const en = 'en'; - public const fr = 'fr'; -} -``` - -and - -```php -namespace App\Models; - -#[TypeScript] -class User -{ - public string $name; - public \App\Enums\Language $language; -} -``` - -The written TypeScript will look like this: - -```tsx -namespace App.Enums { - export type Language = 'nl' | 'en' | 'fr'; -}; - -namespace App.Models { - export type User = { - name: string; - language: App.Enums.Language - }; -}; -``` - -## ModuleWriter - -The `ModuleWriter` will ignore namespaces and list all the types as individual modules. - -When we use the same classes from the previous example, the written TypeScript now looks like this: - -```tsx -export type Language = 'nl' | 'en' | 'fr'; -export type User = { - name: string; - language: Language -}; -``` - -## Building your own writer - -A writer is a class implementing the `Writer` interface: - -```php -interface Writer -{ - public function format(TypesCollection $collection): string; - - public function replacesSymbolsWithFullyQualifiedIdentifiers(): bool; -} -``` - -The `format` method takes a `TypesCollection` and outputs a string containing the TypeScript representation. - -In the `replacesSymbolsWithFullyQualifiedIdentifiers` method, a boolean is returned that indicates whether to use fully qualified identifiers when replacing symbols or not. - -The `TypeDefinitionWriter` uses fully qualified identifiers with a namespace, whereas the `ModuleWriter` doesn't. - diff --git a/package.json b/package.json new file mode 100644 index 00000000..3f60d288 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "typescript": "^5.1.6" + } +} diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index 3e4e3d08..00000000 --- a/psalm.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - diff --git a/src/Actions/AppendDefaultTypesAction.php b/src/Actions/AppendDefaultTypesAction.php new file mode 100644 index 00000000..02b54251 --- /dev/null +++ b/src/Actions/AppendDefaultTypesAction.php @@ -0,0 +1,36 @@ + $transformed + * + * @return array + */ + public function execute(array $transformed): array + { + $defaults = []; + + foreach ($this->config->defaultTypeProviders as $defaultTypeProviderClass) { + /** @var DefaultTypesProvider $defaultTypeProvider */ + $defaultTypeProvider = new $defaultTypeProviderClass; + + array_push($defaults, ...$defaultTypeProvider->provide()); + } + + return array_merge($transformed, $defaults); + } +} diff --git a/src/Actions/ConnectReferencesAction.php b/src/Actions/ConnectReferencesAction.php new file mode 100644 index 00000000..ac8bf73d --- /dev/null +++ b/src/Actions/ConnectReferencesAction.php @@ -0,0 +1,58 @@ + $transformed + */ + public function execute(array $transformed): ReferenceMap + { + $referenceMap = new ReferenceMap(); + + foreach ($transformed as $transformedItem) { + if ($transformedItem->reference) { + $referenceMap->add($transformedItem); + } + } + + foreach ($transformed as $transformedItem) { + $references = []; + + $this->visitTypeScriptTreeAction->execute( + $transformedItem->typeScriptNode, + function (TypeReference $typeReference) use ($referenceMap, &$references, $transformedItem) { + $reference = $typeReference->reference; + + if (! $referenceMap->has($reference)) { + $this->log->warning("Tried replacing reference to `{$reference->humanFriendlyName()}` in `{$transformedItem->reference->humanFriendlyName()}` but it was not found in the transformed types"); + + return; + } + + $references[] = $reference; + $typeReference->connect($referenceMap->get($reference)); + }, + [TypeReference::class] + ); + + $transformedItem->references = $references; + } + + return $referenceMap; + } +} diff --git a/src/Actions/DiscoverTypesAction.php b/src/Actions/DiscoverTypesAction.php new file mode 100644 index 00000000..1d1c21ea --- /dev/null +++ b/src/Actions/DiscoverTypesAction.php @@ -0,0 +1,32 @@ + */ + public function execute(): array + { + // Idea / TODO : make it possible for other packages to hook in to find types in other directories, like their vendor dir + + return Discover::in(...$this->config->directories) + ->types( + DiscoveredStructureType::ClassDefinition, + DiscoveredStructureType::Enum, + DiscoveredStructureType::Interface + ) + ->get(); + } +} diff --git a/src/Actions/FindClassNameFqcnAction.php b/src/Actions/FindClassNameFqcnAction.php new file mode 100644 index 00000000..a2224326 --- /dev/null +++ b/src/Actions/FindClassNameFqcnAction.php @@ -0,0 +1,53 @@ + */ + protected static array $cache = []; + + public function execute(ReflectionClass $reflectionClass, string $className): ?string + { + $usages = $this->loadUsages($reflectionClass); + + $className = $this->cleanupClassname($className); + + if ($usage = $usages->findForAlias($className)) { + return $this->cleanupClassname($usage->fcqn); + } + + if (! $reflectionClass->inNamespace() && class_exists($className)) { + return $this->cleanupClassname($className); + } + + $guessedFqcn = "{$reflectionClass->getNamespaceName()}\\{$className}"; + + if(class_exists($guessedFqcn)){ + return $this->cleanupClassname($guessedFqcn); + } + + return $className; + } + + protected function loadUsages(ReflectionClass $reflectionClass): UsageCollection + { + $filename = $reflectionClass->getFileName(); + + if (! array_key_exists($filename, static::$cache)) { + static::$cache[$filename] = (new ParseUseDefinitionsAction())->execute($filename); + } + + return static::$cache[$filename]; + } + + protected function cleanupClassname( + string $classname + ):string + { + return ltrim($classname, '\\'); + } +} diff --git a/src/Actions/FormatFilesAction.php b/src/Actions/FormatFilesAction.php new file mode 100644 index 00000000..31e696e0 --- /dev/null +++ b/src/Actions/FormatFilesAction.php @@ -0,0 +1,25 @@ + $writtenFiles + */ + public function execute(array $writtenFiles): void + { + + } +} diff --git a/src/Actions/FormatTypeScriptAction.php b/src/Actions/FormatTypeScriptAction.php deleted file mode 100644 index 17e977f7..00000000 --- a/src/Actions/FormatTypeScriptAction.php +++ /dev/null @@ -1,26 +0,0 @@ -config = $config; - } - - public function execute(): void - { - $formatter = $this->config->getFormatter(); - - if ($formatter === null) { - return; - } - - $formatter->format($this->config->getOutputFile()); - } -} diff --git a/src/Actions/ParseUseDefinitionsAction.php b/src/Actions/ParseUseDefinitionsAction.php new file mode 100644 index 00000000..0405805b --- /dev/null +++ b/src/Actions/ParseUseDefinitionsAction.php @@ -0,0 +1,46 @@ +reject(fn (PhpToken $token) => $token->is([T_COMMENT, T_DOC_COMMENT, T_WHITESPACE])) + ->values() + ->pipe(fn (Collection $collection): TokenCollection => new TokenCollection($collection->all())); + } catch (ParseError) { + return new UsageCollection(); + } + + $usages = new UsageCollection(); + + for ($index = 0; $index < $tokens->count(); $index++) { + if ($tokens->get($index)->is(T_USE)) { + $usages->add(...$this->useResolver->execute($index + 1, $tokens)); + } + } + + return $usages; + } +} diff --git a/src/Actions/PersistTypesCollectionAction.php b/src/Actions/PersistTypesCollectionAction.php deleted file mode 100644 index 271638d5..00000000 --- a/src/Actions/PersistTypesCollectionAction.php +++ /dev/null @@ -1,40 +0,0 @@ -config = $config; - } - - public function execute(TypesCollection $collection): void - { - $this->ensureOutputFileExists(); - - $writer = $this->config->getWriter(); - - (new ReplaceSymbolsInCollectionAction())->execute( - $collection, - $writer->replacesSymbolsWithFullyQualifiedIdentifiers() - ); - - file_put_contents( - $this->config->getOutputFile(), - $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); - } - } -} diff --git a/src/Actions/ReplaceSymbolsInCollectionAction.php b/src/Actions/ReplaceSymbolsInCollectionAction.php deleted file mode 100644 index 9c76f0ac..00000000 --- a/src/Actions/ReplaceSymbolsInCollectionAction.php +++ /dev/null @@ -1,19 +0,0 @@ -transformed = $replaceSymbolsInTypeAction->execute($type); - } - - return $collection; - } -} diff --git a/src/Actions/ReplaceSymbolsInTypeAction.php b/src/Actions/ReplaceSymbolsInTypeAction.php deleted file mode 100644 index 12605944..00000000 --- a/src/Actions/ReplaceSymbolsInTypeAction.php +++ /dev/null @@ -1,61 +0,0 @@ -collection = $collection; - $this->withFullyQualifiedNames = $withFullyQualifiedNames; - } - - public function execute(TransformedType $type, array $chain = []): string - { - if (in_array($type->getTypeScriptName(), $chain)) { - $chain = array_merge($chain, [$type->getTypeScriptName()]); - - throw CircularDependencyChain::create($chain); - } - - foreach ($type->missingSymbols->all() as $missingSymbol) { - $this->collection[$type] = $this->replaceSymbol($missingSymbol, $type, $chain); - } - - return $type->transformed; - } - - protected function replaceSymbol(string $missingSymbol, TransformedType $type, array $chain): TransformedType - { - $found = $this->collection[$missingSymbol]; - - if ($found === null) { - $type->replaceSymbol($missingSymbol, 'any'); - - return $type; - } - - if (! $found->isInline) { - $type->replaceSymbol($missingSymbol, $found->getTypeScriptName($this->withFullyQualifiedNames)); - - return $type; - } - - $transformed = $this->execute( - $found, - array_merge($chain, [$type->getTypeScriptName()]) - ); - - $type->replaceSymbol($missingSymbol, $transformed); - - return $type; - } -} diff --git a/src/Actions/ResolveClassesInPhpFileAction.php b/src/Actions/ResolveClassesInPhpFileAction.php deleted file mode 100644 index 7dd7ef45..00000000 --- a/src/Actions/ResolveClassesInPhpFileAction.php +++ /dev/null @@ -1,48 +0,0 @@ -parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); - } - - public function execute(SplFileInfo $file): array - { - $statements = $this->parser->parse($file->getContents()); - - $nodeFinder = new NodeFinder; - - $namespace = $nodeFinder->findFirst( - $statements, - fn ($node) => $node instanceof Namespace_ - ); - - $classes = $nodeFinder->find( - $statements, - fn ($node) => $node instanceof Class_ || $node instanceof Interface_ || $node instanceof Trait_ || $node instanceof Enum_ - ); - - return array_map(function (Class_|Interface_|Trait_|Enum_ $item) use ($namespace) { - $className = $namespace instanceof Namespace_ - ? "{$namespace->name}\\{$item->name}" - : $item->name; - - return preg_replace('/^\\\*/', '', (string) $className); - }, $classes); - } -} diff --git a/src/Actions/ResolveRelativePathAction.php b/src/Actions/ResolveRelativePathAction.php new file mode 100644 index 00000000..222e3976 --- /dev/null +++ b/src/Actions/ResolveRelativePathAction.php @@ -0,0 +1,46 @@ +finder = $finder; - - $this->config = $config; - - $this->collectors = $config->getCollectors(); - } - - public function execute(): TypesCollection - { - $collection = new TypesCollection(); - - $paths = $this->config->getAutoDiscoverTypesPaths(); - - if (empty($paths)) { - throw NoAutoDiscoverTypesPathsDefined::create(); - } - - foreach ($this->resolveIterator($paths) as $class) { - $transformedType = $this->resolveTransformedType($class); - - if ($transformedType === null) { - continue; - } - - $collection[] = $transformedType; - } - - return $collection; - } - - 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/Actions/SplitTransformedPerLocationAction.php b/src/Actions/SplitTransformedPerLocationAction.php new file mode 100644 index 00000000..a5aa75f5 --- /dev/null +++ b/src/Actions/SplitTransformedPerLocationAction.php @@ -0,0 +1,39 @@ + $transformedTypes + * + * @return array + */ + public function execute(array $transformedTypes): array + { + $split = []; + + foreach ($transformedTypes as $transformedType) { + $splitKey = count($transformedType->location) > 0 + ? implode('.', $transformedType->location) + : ''; + + if (! array_key_exists($splitKey, $split)) { + $split[$splitKey] = new Location($transformedType->location, []); + } + + $split[$splitKey]->transformed[] = $transformedType; + } + + ksort($split); + + foreach ($split as $splitConstruct) { + usort($splitConstruct->transformed, fn (Transformed $a, Transformed $b) => $a->name <=> $b->name); + } + + return array_values($split); + } +} diff --git a/src/Actions/TransformTypesAction.php b/src/Actions/TransformTypesAction.php new file mode 100644 index 00000000..7530327b --- /dev/null +++ b/src/Actions/TransformTypesAction.php @@ -0,0 +1,67 @@ + $types + * + * @return array + */ + public function execute(array $types): array + { + $transformedTypes = []; + + foreach ($types as $type) { + try { + $reflection = new ReflectionClass($type); + } catch (ReflectionException) { + // TODO: maybe add some kind of log? + + continue; + } + + foreach ($this->config->transformers as $transformer) { + $transformed = $transformer->transform( + $reflection, + $this->createTransformationContext($reflection), + ); + + if ($transformed instanceof Transformed) { + $transformedTypes[] = $transformed; + + break; + } + } + } + + return $transformedTypes; + } + + protected function createTransformationContext( + ReflectionClass $reflection + ): TransformationContext { + $name = $reflection->getShortName(); + + $nameSpaceSegments = explode('\\', $reflection->getNamespaceName()); + + return new TransformationContext( + $name, + $nameSpaceSegments, + ); + } +} diff --git a/src/Actions/TranspilePhpStanTypeToTypeScriptTypeAction.php b/src/Actions/TranspilePhpStanTypeToTypeScriptTypeAction.php new file mode 100644 index 00000000..a79a770d --- /dev/null +++ b/src/Actions/TranspilePhpStanTypeToTypeScriptTypeAction.php @@ -0,0 +1,217 @@ + $this->identifierNode($type, $reflectionClass), + ArrayTypeNode::class => $this->arrayTypeNode($type, $reflectionClass), + GenericTypeNode::class => $this->genericNode($type, $reflectionClass), + ArrayShapeNode::class, ObjectShapeNode::class => $this->arrayShapeNode($type, $reflectionClass), + NullableTypeNode::class => $this->nullableNode($type, $reflectionClass), + UnionTypeNode::class => $this->unionNode($type, $reflectionClass), + IntersectionTypeNode::class => $this->intersectionNode($type, $reflectionClass), + default => new TypeScriptUnknown(), + }; + } + + protected function identifierNode( + IdentifierTypeNode $node, + ?ReflectionClass $reflectionClass + ): TypeScriptNode { + if ($node->name === 'mixed') { + return new TypeScriptAny(); + } + + if ($node->name === 'string' || $node->name === 'class-string') { + return new TypeScriptString(); + } + + if ($node->name === 'float' || $node->name === 'double' || $node->name === 'int' || $node->name === 'integer') { + return new TypeScriptNumber(); + } + + if ($node->name === 'bool' || $node->name === 'boolean' || $node->name === 'true' || $node->name === 'false') { + return new TypeScriptBoolean(); + } + + if ($node->name === 'void') { + return new TypeScriptVoid(); + } + + if ($node->name === 'array') { + return new TypeScriptArray(null); + } + + if ($node->name === 'callable') { + return new TypeScriptFunction(); + } + + if ($node->name === 'null') { + return new TypeScriptNull(); + } + + if ($node->name === 'self' || $node->name === 'static') { + return new TypeReference(new ClassStringReference($reflectionClass->getName())); + } + + if ($node->name === 'object') { + return new TypeScriptObject([]); + } + + if (class_exists($node->name) || interface_exists($node->name)) { + return new TypeReference(new ClassStringReference($node->name)); + } + + $referenced = $this->findClassNameFqcnAction->execute( + $reflectionClass, + $node->name + ); + + if (class_exists($referenced) || interface_exists($referenced)) { + return new TypeReference(new ClassStringReference($referenced)); + } + + return new TypeScriptUnknown(); + } + + protected function arrayTypeNode( + ArrayTypeNode $node, + ?ReflectionClass $reflectionClass + ): TypeScriptNode { + return new TypeScriptArray( + $this->execute($node->type, $reflectionClass) + ); + } + + protected function arrayShapeNode( + ArrayShapeNode|ObjectShapeNode $node, + ?ReflectionClass $reflectionClass + ): TypeScriptObject { + return new TypeScriptObject(array_map( + function (ArrayShapeItemNode|ObjectShapeItemNode $item) use ($reflectionClass) { + return new TypeScriptProperty( + (string) $item->keyName, + $this->execute($item->valueType, $reflectionClass), + isOptional: $item->optional + ); + }, + $node->items + )); + } + + protected function nullableNode( + NullableTypeNode $node, + ?ReflectionClass $reflectionClass + ): TypeScriptNode { + $type = $this->execute($node->type, $reflectionClass); + + if (! $type instanceof TypeScriptUnion) { + return new TypeScriptUnion([$type, new TypeScriptNull()]); + } + + if ($type->contains(fn () => new TypeScriptNull())) { + $type->types[] = new TypeScriptNull(); + } + + return $type; + } + + protected function unionNode( + UnionTypeNode $node, + ?ReflectionClass $reflectionClass + ): TypeScriptUnion { + return new TypeScriptUnion(array_map( + fn (TypeNode $type) => $this->execute($type, $reflectionClass), + $node->types + )); + } + + protected function intersectionNode( + IntersectionTypeNode $node, + ?ReflectionClass $reflectionClass + ): TypeScriptIntersection { + return new TypeScriptIntersection(array_map( + fn (TypeNode $type) => $this->execute($type, $reflectionClass), + $node->types + )); + } + + protected function genericNode( + GenericTypeNode $node, + ?ReflectionClass $reflectionClass + ): TypeScriptNode { + $type = $this->execute($node->type, $reflectionClass); + + if ($type instanceof TypeScriptString) { + return $type; // class-string case + } + + if ($type instanceof TypeScriptArray) { + return match (count($node->genericTypes)) { + 0 => $type, + 1 => new TypeScriptArray( + $this->execute($node->genericTypes[0], $reflectionClass) + ), + 2 => new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [ + $this->execute($node->genericTypes[0], $reflectionClass), + $this->execute($node->genericTypes[1], $reflectionClass), + ] + ), + default => throw new Exception('Invalid number of generic types for array'), + }; + } + + return new TypeScriptGeneric( + $type, + array_map( + fn (TypeNode $type) => $this->execute($type, $reflectionClass), + $node->genericTypes + ) + ); + } +} diff --git a/src/Actions/TranspileReflectionTypeToTypeScriptTypeAction.php b/src/Actions/TranspileReflectionTypeToTypeScriptTypeAction.php new file mode 100644 index 00000000..3c3a1d5c --- /dev/null +++ b/src/Actions/TranspileReflectionTypeToTypeScriptTypeAction.php @@ -0,0 +1,127 @@ +resolveType($reflectionType, $reflectionClass); + + if ( + ! $reflectionType->allowsNull() + || $type instanceof TypeScriptAny + || $type instanceof TypeScriptNull) { + return $type; + } + + if ($type instanceof TypeScriptUnion && $type->contains(fn (TypeScriptNode $node) => $node instanceof TypeScriptNull)) { + return $type; + } + + if ($type instanceof TypeScriptUnion) { + $type->types[] = new TypeScriptNull(); + + return $type; + } + + return new TypeScriptUnion([$type, new TypeScriptNull()]); + } + + protected function resolveType( + ReflectionType $reflectionType, + ReflectionClass $reflectionClass, + ): TypeScriptNode { + return match ($reflectionType::class) { + ReflectionNamedType::class => $this->reflectionNamedType($reflectionType, $reflectionClass), + ReflectionUnionType::class => $this->reflectionUnionType($reflectionType, $reflectionClass), + ReflectionIntersectionType::class => $this->reflectionIntersectionType($reflectionType, $reflectionClass), + default => new TypeScriptUndefined(), + }; + } + + protected function reflectionNamedType( + ReflectionNamedType $type, + ReflectionClass $reflectionClass, + ): TypeScriptNode { + if ($type->getName() === 'string') { + return new TypeScriptString(); + } + + if ($type->getName() === 'float' || $type->getName() === 'int') { + return new TypeScriptNumber(); + } + + if ($type->getName() === 'bool' || $type->getName() === 'true' || $type->getName() === 'false') { + return new TypeScriptBoolean(); + } + + if ($type->getName() === 'array') { + return new TypeScriptArray(null); + } + + if ($type->getName() === 'null') { + return new TypeScriptNull(); + } + + if ($type->getName() === 'mixed') { + return new TypeScriptAny(); + } + + if ($type->getName() === 'self' || $type->getName() === 'static') { + return new TypeReference(new ClassStringReference($reflectionClass->getName())); + } + + if ($type->getName() === 'object') { + return new TypeScriptObject([]); + } + + if (class_exists($type->getName()) || interface_exists($type->getName())) { + return new TypeReference(new ClassStringReference($type->getName())); + } + + return new TypeScriptUnknown(); + } + + protected function reflectionUnionType( + ReflectionUnionType $type, + ReflectionClass $reflectionClass, + ): TypeScriptNode { + return new TypeScriptUnion(array_map( + fn (ReflectionType $type) => $this->resolveType($type, $reflectionClass), + $type->getTypes() + )); + } + + protected function reflectionIntersectionType( + ReflectionIntersectionType $type, + ReflectionClass $reflectionClass, + ): TypeScriptNode { + return new TypeScriptIntersection(array_map( + fn (ReflectionType $type) => $this->resolveType($type, $reflectionClass), + $type->getTypes() + )); + } +} diff --git a/src/Actions/TranspileTypeToTypeScriptAction.php b/src/Actions/TranspileTypeToTypeScriptAction.php deleted file mode 100644 index 87b2ebdc..00000000 --- a/src/Actions/TranspileTypeToTypeScriptAction.php +++ /dev/null @@ -1,140 +0,0 @@ -missingSymbolsCollection = $missingSymbolsCollection; - $this->currentClass = $currentClass; - } - - public function execute(Type $type): string - { - return 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 Self_, $type instanceof Static_, $type instanceof This => $this->resolveSelfReferenceType(), - $type instanceof Scalar => 'string|number|boolean', - $type instanceof Mixed_ => 'any', - $type instanceof Void_ => 'void', - default => throw new Exception("Could not transform type: {$type}") - }; - } - - private function resolveCompoundType(Compound $compound): string - { - $transformed = array_map( - fn (Type $type) => $this->execute($type), - iterator_to_array($compound->getIterator()) - ); - - return join(' | ', array_unique($transformed)); - } - - private function resolveListType(AbstractList $list): string - { - if ($this->isTypeScriptArray($list->getKeyType())) { - return "Array<{$this->execute($list->getValueType())}>"; - } - - return "{ [key: {$this->execute($list->getKeyType())}]: {$this->execute($list->getValueType())} }"; - } - - private function resolveNullableType(Nullable $nullable): string - { - return "{$this->execute($nullable->getActualType())} | null"; - } - - private function resolveObjectType(Object_ $object): string - { - if ($object->getFqsen() === null) { - return 'object'; - } - - return $this->missingSymbolsCollection->add( - (string) $object->getFqsen() - ); - } - - private function resolveStructType(StructType $type): string - { - $transformed = "{"; - - foreach ($type->getTypes() as $name => $type) { - $transformed .= "{$name}:{$this->execute($type)};"; - } - - return "{$transformed}}"; - } - - private function resolveRecordType(RecordType $type): string - { - return "Record<{$this->execute($type->getKeyType())}, {$this->execute($type->getValueType())}>"; - } - - private function resolveSelfReferenceType(): string - { - if ($this->currentClass === null) { - return 'any'; - } - - return $this->missingSymbolsCollection->add($this->currentClass); - } - - private function isTypeScriptArray(Type $keyType): bool - { - if (! $keyType instanceof Compound) { - return false; - } - - if ($keyType->getIterator()->count() !== 2) { - return false; - } - - if (! $keyType->contains(new String_()) || ! $keyType->contains(new Integer())) { - return false; - } - - return true; - } -} diff --git a/src/Actions/VisitTypeScriptTreeAction.php b/src/Actions/VisitTypeScriptTreeAction.php new file mode 100644 index 00000000..701408ce --- /dev/null +++ b/src/Actions/VisitTypeScriptTreeAction.php @@ -0,0 +1,29 @@ +children() as $child) { + $this->execute($child, $walker, $allowedNodes); + } + } + } +} diff --git a/src/Actions/WriteTypesAction.php b/src/Actions/WriteTypesAction.php new file mode 100644 index 00000000..5b74feea --- /dev/null +++ b/src/Actions/WriteTypesAction.php @@ -0,0 +1,26 @@ + */ + public function execute( + array $transformed, + ReferenceMap $referenceMap + ): array + { + return $this->config->writer->output($transformed, $referenceMap); + } +} diff --git a/src/Attributes/Hidden.php b/src/Attributes/Hidden.php deleted file mode 100644 index 8e6dbdab..00000000 --- a/src/Attributes/Hidden.php +++ /dev/null @@ -1,10 +0,0 @@ -keyType = $keyType; - $this->valueType = $valueType; - $this->array = $array; - } - - public function getType(): Type - { - return new RecordType($this->keyType, $this->valueType, $this->array); - } -} diff --git a/src/Attributes/TypeScriptTransformableAttribute.php b/src/Attributes/TypeScriptTransformableAttribute.php index fd09e3da..80f0ba53 100644 --- a/src/Attributes/TypeScriptTransformableAttribute.php +++ b/src/Attributes/TypeScriptTransformableAttribute.php @@ -2,9 +2,9 @@ namespace Spatie\TypeScriptTransformer\Attributes; -use phpDocumentor\Reflection\Type; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; interface TypeScriptTransformableAttribute { - public function getType(): Type; + public function getType(): TypeScriptNode; } diff --git a/src/ClassReader.php b/src/ClassReader.php deleted file mode 100644 index b96bf7d8..00000000 --- a/src/ClassReader.php +++ /dev/null @@ -1,64 +0,0 @@ - $this->resolveTransformable($class), - 'name' => $this->resolveName($class), - 'transformer' => $this->resolveTransformer($class), - 'inline' => $this->resolveInline($class), - ]; - } - - protected function resolveTransformable(ReflectionClass $class): bool - { - return str_contains($class->getDocComment(), '@typescript'); - } - - protected function resolveName(ReflectionClass $class): string - { - $annotations = []; - - preg_match( - '/@typescript\s*([\w\/\.]*)\s*/', - $class->getDocComment(), - $annotations - ); - - $name = $annotations[1] ?? null; - - if (count($annotations) !== 2 || empty($name)) { - return $class->getShortName(); - } - - return $name; - } - - protected function resolveTransformer(ReflectionClass $class): ?string - { - $annotations = []; - - preg_match( - '/@typescript-transformer\s+([\w\\\]*)/', - $class->getDocComment(), - $annotations - ); - - if (count($annotations) !== 2 || empty($annotations[1])) { - return null; - } - - return $annotations[1]; - } - - private function resolveInline(ReflectionClass $class): bool - { - return str_contains($class->getDocComment(), '@typescript-inline'); - } -} diff --git a/src/Collections/ReferenceMap.php b/src/Collections/ReferenceMap.php new file mode 100644 index 00000000..40b899b7 --- /dev/null +++ b/src/Collections/ReferenceMap.php @@ -0,0 +1,33 @@ +reference === null) { + throw new Exception('Can only add transformed items with a reference'); + } + + $this->references[$transformed->reference->getKey()] = $transformed; + } + + public function has(Reference $reference): bool + { + return array_key_exists($reference->getKey(), $this->references); + } + + public function get( + Reference $reference + ): Transformed { + return $this->references[$reference->getKey()]; + } +} diff --git a/src/Collectors/Collector.php b/src/Collectors/Collector.php deleted file mode 100644 index bde78941..00000000 --- a/src/Collectors/Collector.php +++ /dev/null @@ -1,19 +0,0 @@ -config = $config; - } - - abstract public function getTransformedType(ReflectionClass $class): ?TransformedType; -} diff --git a/src/Collectors/DefaultCollector.php b/src/Collectors/DefaultCollector.php deleted file mode 100644 index 46261517..00000000 --- a/src/Collectors/DefaultCollector.php +++ /dev/null @@ -1,99 +0,0 @@ -isTransformable()) { - return null; - } - - $transformedType = $reflector->getType() - ? $this->resolveAlreadyTransformedType($reflector) - : $this->resolveTypeViaTransformer($reflector); - - if ($reflector->isInline()) { - $transformedType->name = null; - $transformedType->isInline = true; - } - - return $transformedType; - } - - protected function resolveAlreadyTransformedType(ClassTypeReflector $reflector): TransformedType - { - $missingSymbols = new MissingSymbolsCollection(); - $name = $reflector->getName(); - - $transpiler = new TranspileTypeToTypeScriptAction( - $missingSymbols, - $name - ); - - return TransformedType::create( - $reflector->getReflectionClass(), - $reflector->getName(), - $transpiler->execute($reflector->getType()), - $missingSymbols - ); - } - - protected function resolveTypeViaTransformer(ClassTypeReflector $reflector): ?TransformedType - { - $transformerClass = $reflector->getTransformerClass(); - - if ($transformerClass !== null) { - return $this->resolveTypeViaPredefinedTransformer($reflector); - } - - foreach ($this->config->getTransformers() as $transformer) { - $transformed = $transformer->transform( - $reflector->getReflectionClass(), - $reflector->getName() - ); - - if ($transformed !== null) { - return $transformed; - } - } - - throw TransformerNotFound::create($reflector->getReflectionClass()); - } - - protected function resolveTypeViaPredefinedTransformer(ClassTypeReflector $reflector): ?TransformedType - { - if (! class_exists($reflector->getTransformerClass())) { - throw InvalidTransformerGiven::classDoesNotExist( - $reflector->getReflectionClass(), - $reflector->getTransformerClass() - ); - } - - if (! is_subclass_of($reflector->getTransformerClass(), Transformer::class)) { - throw InvalidTransformerGiven::classIsNotATransformer( - $reflector->getReflectionClass(), - $reflector->getTransformerClass() - ); - } - - $transformer = $this->config->buildTransformer($reflector->getTransformerClass()); - - return $transformer->transform( - $reflector->getReflectionClass(), - $reflector->getName() - ); - } -} diff --git a/src/Collectors/EnumCollector.php b/src/Collectors/EnumCollector.php deleted file mode 100644 index 7ccca5c4..00000000 --- a/src/Collectors/EnumCollector.php +++ /dev/null @@ -1,38 +0,0 @@ -config->getTransformers()); - - if (! \in_array(EnumTransformer::class, $transformers, true)) { - return null; - } - - $reflector = ClassTypeReflector::create($class); - - if (! $reflector->getReflectionClass()->implementsInterface(BackedEnum::class)) { - return null; - } - - $transformedType = $reflector->getType() - ? $this->resolveAlreadyTransformedType($reflector) - : $this->resolveTypeViaTransformer($reflector); - - if ($reflector->isInline()) { - $transformedType->name = null; - $transformedType->isInline = true; - } - - return $transformedType; - } -} diff --git a/src/DefaultTypeProviders/DefaultTypesProvider.php b/src/DefaultTypeProviders/DefaultTypesProvider.php new file mode 100644 index 00000000..b939c4f9 --- /dev/null +++ b/src/DefaultTypeProviders/DefaultTypesProvider.php @@ -0,0 +1,11 @@ + */ + public function provide(): array; +} diff --git a/src/Exceptions/CircularDependencyChain.php b/src/Exceptions/CircularDependencyChain.php deleted file mode 100644 index 2676d7aa..00000000 --- a/src/Exceptions/CircularDependencyChain.php +++ /dev/null @@ -1,13 +0,0 @@ - ', $chain)); - } -} diff --git a/src/Exceptions/InvalidDefaultTypeReplacer.php b/src/Exceptions/InvalidDefaultTypeReplacer.php deleted file mode 100644 index 1423b0dc..00000000 --- a/src/Exceptions/InvalidDefaultTypeReplacer.php +++ /dev/null @@ -1,13 +0,0 @@ -getName()}) does not exist!"); - } - - public static function classIsNotATransformer(ReflectionClass $reflectionClass, string $transformerClass) - { - return new self("The transformer ({$transformerClass}) defined in ({$reflectionClass->getName()}) does not implement the Transformer interface!"); - } -} diff --git a/src/Exceptions/NoAutoDiscoverTypesPathsDefined.php b/src/Exceptions/NoAutoDiscoverTypesPathsDefined.php deleted file mode 100644 index 32eee3ed..00000000 --- a/src/Exceptions/NoAutoDiscoverTypesPathsDefined.php +++ /dev/null @@ -1,13 +0,0 @@ - $kind, 'value' => $value] = $existing; - - return new self("Tried adding namespace: {$namespace} but a {$kind} already exists: $value"); - } - - public static function whenAddingType(string $type, array $existing): self - { - ['kind' => $kind, 'value' => $value] = $existing; - - return new self("Tried adding type: {$type} but a {$kind} already exists: $value"); - } -} diff --git a/src/Exceptions/TransformerNotFound.php b/src/Exceptions/TransformerNotFound.php deleted file mode 100644 index f4a611b1..00000000 --- a/src/Exceptions/TransformerNotFound.php +++ /dev/null @@ -1,14 +0,0 @@ -getName()}!"); - } -} diff --git a/src/Exceptions/UnableToTransformUsingAttribute.php b/src/Exceptions/UnableToTransformUsingAttribute.php deleted file mode 100644 index 04fbcc29..00000000 --- a/src/Exceptions/UnableToTransformUsingAttribute.php +++ /dev/null @@ -1,15 +0,0 @@ -run(); - - if (! $process->isSuccessful()) { - throw new ProcessFailedException($process); - } - } -} diff --git a/src/Formatters/Formatter.php b/src/Formatters/Formatter.php deleted file mode 100644 index 34cc6a13..00000000 --- a/src/Formatters/Formatter.php +++ /dev/null @@ -1,8 +0,0 @@ -run(); - - if (! $process->isSuccessful()) { - throw new ProcessFailedException($process); - } - } -} diff --git a/src/Laravel/Commands/TransformTypeScriptCommand.php b/src/Laravel/Commands/TransformTypeScriptCommand.php new file mode 100644 index 00000000..a83785e2 --- /dev/null +++ b/src/Laravel/Commands/TransformTypeScriptCommand.php @@ -0,0 +1,35 @@ +execute(); + + if (! empty($log->infoMessages)) { + foreach ($log->infoMessages as $infoMessage) { + $this->info($infoMessage); + } + } + + if (! empty($log->warningMessages)) { + foreach ($log->warningMessages as $warningMessage) { + $this->warn($warningMessage); + } + } + + $this->comment('All done'); + + return self::SUCCESS; + } +} diff --git a/src/Laravel/LaravelDefaultTypesProvider.php b/src/Laravel/LaravelDefaultTypesProvider.php new file mode 100644 index 00000000..210fe29f --- /dev/null +++ b/src/Laravel/LaravelDefaultTypesProvider.php @@ -0,0 +1,158 @@ +collection(), + $this->eloquentCollection(), + $this->lengthAwarePaginator(), + $this->lengthAwarePaginatorInterface(), + ]; + } + + protected function collection(): Transformed + { + return new Transformed( + new TypeScriptExport( + new TypeScriptAlias( + new TypeScriptGeneric( + new TypeScriptIdentifier('Collection'), + [new TypeScriptIdentifier('T')], + ), + new TypeScriptArray( + new TypeScriptIdentifier('T'), + ), + ) + ), + new ClassStringReference(Collection::class), + 'Collection', + true, + ['Illuminate', 'Support'], + ); + } + + protected function eloquentCollection(): Transformed + { + return new Transformed( + new TypeScriptExport( + new TypeScriptAlias( + new TypeScriptGeneric( + new TypeScriptIdentifier('Collection'), + [new TypeScriptIdentifier('T')], + ), + new TypeScriptArray( + new TypeScriptIdentifier('T'), + ), + ) + ), + new ClassStringReference(EloquentCollection::class), + 'Collection', + true, + ['Illuminate', 'Database', 'Eloquent', 'Collection'], + ); + } + + protected function lengthAwarePaginator(): Transformed + { + return new Transformed( + new TypeScriptExport( + new TypeScriptAlias( + new TypeScriptGeneric( + new TypeScriptIdentifier('LengthAwarePaginator'), + [new TypeScriptIdentifier('T')], + ), + new TypeScriptObject([ + new TypeScriptProperty('data', new TypeScriptArray(new TypeScriptIdentifier('T'))), + new TypeScriptProperty('links', new TypeScriptObject([ + new TypeScriptProperty('url', new TypeScriptUnion([ + new TypeScriptIdentifier('string'), + new TypeScriptIdentifier('null'), + ])), + new TypeScriptProperty('label', new TypeScriptString()), + new TypeScriptProperty('active', new TypeScriptBoolean()), + ])), + new TypeScriptProperty('meta', new TypeScriptObject([ + new TypeScriptProperty('total', new TypeScriptNumber()), + new TypeScriptProperty('current_page', new TypeScriptNumber()), + new TypeScriptProperty('first_page_url', new TypeScriptString()), + new TypeScriptProperty('from', new TypeScriptUnion([ + new TypeScriptNumber(), + new TypeScriptNull(), + ])), + new TypeScriptProperty('last_page', new TypeScriptNumber()), + new TypeScriptProperty('last_page_url', new TypeScriptString()), + new TypeScriptProperty('next_page_url', new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNull(), + ])), + new TypeScriptProperty('path', new TypeScriptString()), + new TypeScriptProperty('per_page', new TypeScriptNumber()), + new TypeScriptProperty('prev_page_url', new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNull(), + ])), + new TypeScriptProperty('to', new TypeScriptUnion([ + new TypeScriptNumber(), + new TypeScriptNull(), + ])), + ])), + ]), + ) + ), + new ClassStringReference(LengthAwarePaginator::class), + 'LengthAwarePaginator', + true, + ['Illuminate', 'Pagination'], + ); + } + + protected function lengthAwarePaginatorInterface(): Transformed + { + return new Transformed( + new TypeScriptExport( + new TypeScriptAlias( + new TypeScriptGeneric( + new TypeScriptIdentifier('LengthAwarePaginator'), + [new TypeScriptIdentifier('T')], + ), + new TypeScriptGeneric( + new TypeReference(new ClassStringReference(LengthAwarePaginator::class)), + [new TypeScriptIdentifier('T')], + ), + ) + ), + new ClassStringReference(LengthAwarePaginatorInterface::class), + 'LengthAwarePaginator', + true, + ['Illuminate', 'Contracts', 'Pagination'], + ); + } +} diff --git a/src/Laravel/LaravelTypeScriptTransformerConfig.php b/src/Laravel/LaravelTypeScriptTransformerConfig.php new file mode 100644 index 00000000..6bdbf5a4 --- /dev/null +++ b/src/Laravel/LaravelTypeScriptTransformerConfig.php @@ -0,0 +1,10 @@ +name('typescript-transformer') + ->hasCommand(TransformTypeScriptCommand::class); + } + + public function bootingPackage(): void + { + // TODO: use a laravel config file or something better here + + $this->app->singleton( + TypeScriptTransformerConfig::class, + fn () => LaravelTypeScriptTransformerConfig::$defined + ); + + $this->app->singleton( + TypeScriptTransformerLog::class, + fn () => new TypeScriptTransformerLog(), + ); + } +} diff --git a/src/References/ClassStringReference.php b/src/References/ClassStringReference.php new file mode 100644 index 00000000..e761392e --- /dev/null +++ b/src/References/ClassStringReference.php @@ -0,0 +1,24 @@ +classString = trim($classString, '\\'); + } + + public function getKey(): string + { + return "class_string_{$this->classString}"; + } + + public function humanFriendlyName(): string + { + return "class {$this->classString}"; + } +} diff --git a/src/References/FunctionReference.php b/src/References/FunctionReference.php new file mode 100644 index 00000000..19365fd2 --- /dev/null +++ b/src/References/FunctionReference.php @@ -0,0 +1,22 @@ +name}"; + } + + public function humanFriendlyName(): string + { + return "function {$this->name}"; + } +} diff --git a/src/References/Reference.php b/src/References/Reference.php new file mode 100644 index 00000000..2e77d0e2 --- /dev/null +++ b/src/References/Reference.php @@ -0,0 +1,10 @@ +getName()); + } +} diff --git a/src/Structures/MissingSymbolsCollection.php b/src/Structures/MissingSymbolsCollection.php deleted file mode 100644 index 4ad0ec90..00000000 --- a/src/Structures/MissingSymbolsCollection.php +++ /dev/null @@ -1,36 +0,0 @@ -missingSymbols; - } - - public function remove(string $symbol) - { - if (in_array($symbol, $this->missingSymbols)) { - unset($this->missingSymbols[array_search($symbol, $this->missingSymbols)]); - } - } - - public function isEmpty(): bool - { - return empty($this->missingSymbols); - } - - public function add(string $symbol): string - { - $symbol = ltrim($symbol, '\\'); - - if (! in_array($symbol, $this->missingSymbols)) { - $this->missingSymbols[] = $symbol; - } - - return "{%{$symbol}%}"; - } -} diff --git a/src/Structures/TransformedType.php b/src/Structures/TransformedType.php deleted file mode 100644 index 49322a33..00000000 --- a/src/Structures/TransformedType.php +++ /dev/null @@ -1,111 +0,0 @@ -reflection = $class; - $this->name = $name; - $this->transformed = $transformed; - $this->missingSymbols = $missingSymbols; - $this->isInline = $isInline; - $this->keyword = $keyword; - $this->trailingSemicolon = $trailingSemicolon; - } - - public function getNamespaceSegments(): array - { - if ($this->isInline === true) { - return []; - } - - $namespace = $this->reflection->getNamespaceName(); - - if (empty($namespace)) { - return []; - } - - return explode('\\', $namespace); - } - - public function getTypeScriptName($fullyQualified = true): string - { - if (! $fullyQualified) { - return $this->name ?? ''; - } - - $segments = array_merge( - $this->getNamespaceSegments(), - [$this->name] - ); - - return implode('.', $segments); - } - - public function replaceSymbol(string $class, string $replacement): void - { - $this->missingSymbols->remove($class); - - $this->transformed = str_replace( - "{%{$class}%}", - $replacement, - $this->transformed - ); - } - - public function toString(): string - { - $output = match ($this->keyword) { - 'enum' => "enum {$this->name} { {$this->transformed} }", - 'interface' => "interface {$this->name} {$this->transformed}", - default => "type {$this->name} = {$this->transformed}", - }; - - return $output . ($this->trailingSemicolon ? ';' : ''); - } -} diff --git a/src/Structures/TypesCollection.php b/src/Structures/TypesCollection.php deleted file mode 100644 index d1758334..00000000 --- a/src/Structures/TypesCollection.php +++ /dev/null @@ -1,106 +0,0 @@ -types); - } - - public function offsetGet($class): ?TransformedType - { - return $this->types[$class] ?? null; - } - - public function getIterator(): ArrayIterator - { - return new ArrayIterator($this->types); - } - - /** - * @param null|string|\Spatie\TypeScriptTransformer\Structures\TransformedType $class - * @param \Spatie\TypeScriptTransformer\Structures\TransformedType $type - * - * @throws \Spatie\TypeScriptTransformer\Exceptions\SymbolAlreadyExists - */ - public function offsetSet($class, $type): void - { - $class ??= $type->reflection->getName(); - - $class = $class instanceof TransformedType - ? $class->reflection->getName() - : $class; - - if (array_key_exists($class, $this->types) === false && $type->isInline === false) { - $this->ensureTypeCanBeAdded($type); - } - - $this->types[$class] = $type; - } - - public function offsetUnset($class): void - { - unset($this->types[$class]); - } - - public function count(): int - { - return count($this->types); - } - - protected function ensureTypeCanBeAdded(TransformedType $type): void - { - $namespace = array_reduce($type->getNamespaceSegments(), function (array $checkedSegments, string $segment) { - $segments = array_merge($checkedSegments, [$segment]); - - $namespace = join('.', $segments); - - if (array_key_exists($namespace, $this->structure)) { - if ($this->structure[$namespace]['kind'] !== 'namespace') { - throw SymbolAlreadyExists::whenAddingNamespace( - $namespace, - $this->structure[$namespace] - ); - } - } - - $this->structure[$namespace] = [ - 'kind' => 'namespace', - 'value' => str_replace('.', '\\', $namespace), - ]; - - return $segments; - }, []); - - $namespacedType = join('.', array_merge($namespace, [$type->name])); - - if (array_key_exists($namespacedType, $this->structure)) { - throw SymbolAlreadyExists::whenAddingType( - $type->reflection->getName(), - $this->structure[$namespacedType] - ); - } - - $this->structure[$namespacedType] = [ - 'kind' => 'type', - 'value' => $type->reflection->getName(), - ]; - } -} diff --git a/src/Support/Location.php b/src/Support/Location.php new file mode 100644 index 00000000..8ed839bf --- /dev/null +++ b/src/Support/Location.php @@ -0,0 +1,18 @@ + $segments + * @param array $transformed + */ + public function __construct( + public array $segments, + public array $transformed, + ) { + } +} diff --git a/src/Support/TransformationContext.php b/src/Support/TransformationContext.php new file mode 100644 index 00000000..0ff41ca6 --- /dev/null +++ b/src/Support/TransformationContext.php @@ -0,0 +1,12 @@ +infoMessages[] = $message; + + return $this; + } + + public function warning(string $message): self + { + $this->warningMessages[] = $message; + + return $this; + } +} diff --git a/src/Support/WritingContext.php b/src/Support/WritingContext.php new file mode 100644 index 00000000..0dd312e0 --- /dev/null +++ b/src/Support/WritingContext.php @@ -0,0 +1,17 @@ + $location + * @param array $references + */ + public function __construct( + public TypeScriptNode $typeScriptNode, + public ?Reference $reference, // Not always referenceable + public ?string $name, // Not always needs a name + public bool $export, // Not always exportable + public array $location, + public array $references = [], + ) { + } +} + +// Niet per se tied aan een ReflectionClass +// Heeft niet per se een naam -> enkel indien exportable en dus referencable +// Location duid een structuur aan, maar is niet per se een namespace, kan evengoed een collectie aan files zijn diff --git a/src/Transformed/Untransformable.php b/src/Transformed/Untransformable.php new file mode 100644 index 00000000..1cd372a4 --- /dev/null +++ b/src/Transformed/Untransformable.php @@ -0,0 +1,17 @@ +shouldTransform($reflectionClass)) { + return Untransformable::create(); + } + + $classAnnotations = $this->docTypeResolver->class($reflectionClass)?->properties ?? []; + + $constructorAnnotations = $reflectionClass->hasMethod('__construct') + ? $this->docTypeResolver->method($reflectionClass->getMethod('__construct'))?->parameters ?? [] + : []; + + $properties = []; + + foreach ($this->getProperties($reflectionClass) as $reflectionProperty) { + $properties[] = $this->createProperty( + $reflectionClass, + $reflectionProperty, + $classAnnotations, + $constructorAnnotations, + ); + } + + return new Transformed( + new TypeScriptExport(new TypeScriptAlias(new TypeScriptIdentifier($context->name), new TypeScriptObject($properties))), + new ReflectionClassReference($reflectionClass), + $context->name, + true, + $context->nameSpaceSegments, + ); + } + + abstract public function shouldTransform(ReflectionClass $reflection): bool; + + protected function getProperties(ReflectionClass $reflection): array + { + return array_values(array_filter( + $reflection->getProperties(ReflectionProperty::IS_PUBLIC), + fn (ReflectionProperty $property) => ! $property->isStatic() + )); + } + + protected function createProperty( + ReflectionClass $reflectionClass, + ReflectionProperty $reflectionProperty, + array $classAnnotations, + array $constructorAnnotations, + ): TypeScriptProperty { + $propertyAnnotation = $this->docTypeResolver->property($reflectionProperty); + + $type = $this->resolveTypeForProperty( + $reflectionClass, + $reflectionProperty, + $classAnnotations[$reflectionProperty->getName()] + ?? $constructorAnnotations[$reflectionProperty->getName()] + ?? $propertyAnnotation, + ); + + return new TypeScriptProperty( + $reflectionProperty->getName(), + $type, + $this->isPropertyOptional($reflectionProperty, $reflectionClass, $type), + $this->isPropertyReadonly($reflectionProperty, $reflectionClass, $type) + ); + } + + protected function resolveTypeForProperty( + ReflectionClass $reflectionClass, + ReflectionProperty $reflectionProperty, + ?ParsedNameAndType $annotation, + ): TypeScriptNode { + if ($annotation) { + return $this->transpilePhpStanTypeToTypeScriptTypeAction->execute( + $annotation->type, + $reflectionClass, + ); + } + + if ($reflectionProperty->hasType()) { + return $this->transpileReflectionTypeToTypeScriptTypeAction->execute( + $reflectionProperty->getType(), + $reflectionClass + ); + } + + return new TypeScriptUnknown(); + } + + protected function isPropertyOptional( + ReflectionProperty $reflectionProperty, + ReflectionClass $reflectionClass, + TypeScriptNode $type, + ): bool { + return count($reflectionProperty->getAttributes(Optional::class)) > 0; + } + + protected function isPropertyReadonly( + ReflectionProperty $reflectionProperty, + ReflectionClass $reflectionClass, + TypeScriptNode $type, + ): bool { + return $reflectionProperty->isReadOnly() || $reflectionClass->isReadOnly(); + } +} diff --git a/src/Transformers/DataClassTransformer.php b/src/Transformers/DataClassTransformer.php new file mode 100644 index 00000000..0ef2e46d --- /dev/null +++ b/src/Transformers/DataClassTransformer.php @@ -0,0 +1,57 @@ +implementsInterface(\Spatie\LaravelData\Contracts\BaseData::class); + } + + protected function createProperty( + ReflectionClass $reflectionClass, + ReflectionProperty $reflectionProperty, + array $classAnnotations, + array $constructorAnnotations, + ): TypeScriptProperty { + $property = parent::createProperty( + $reflectionClass, + $reflectionProperty, + $classAnnotations, + $constructorAnnotations, + ); + + $this->replaceLazy($property); + + return $property; + } + + protected function replaceLazy( + TypeScriptProperty $property, + ): void { + if (! $property->type instanceof TypeScriptUnion) { + return; + } + + for ($i = 0; $i < count($property->type->types); $i++) { + $subType = $property->type->types[$i]; + + if ($subType instanceof TypeReference && is_a($subType->reference, \Spatie\LaravelData\Lazy::class, true)) { + $property->isOptional = true; + + unset($property->type->types[$i]); + } + } + + $property->type->types = array_values($property->type->types); + } +} diff --git a/src/Transformers/DtoTransformer.php b/src/Transformers/DtoTransformer.php deleted file mode 100644 index 512554a6..00000000 --- a/src/Transformers/DtoTransformer.php +++ /dev/null @@ -1,121 +0,0 @@ -config = $config; - } - - public function transform(ReflectionClass $class, string $name): ?TransformedType - { - if (! $this->canTransform($class)) { - return null; - } - - $missingSymbols = new MissingSymbolsCollection(); - - $type = join([ - $this->transformProperties($class, $missingSymbols), - $this->transformMethods($class, $missingSymbols), - $this->transformExtra($class, $missingSymbols), - ]); - - return TransformedType::create( - $class, - $name, - "{" . PHP_EOL . $type . "}", - $missingSymbols - ); - } - - protected function canTransform(ReflectionClass $class): bool - { - return true; - } - - protected function transformProperties( - ReflectionClass $class, - MissingSymbolsCollection $missingSymbols - ): string { - $isOptional = ! empty($class->getAttributes(Optional::class)); - - return array_reduce( - $this->resolveProperties($class), - function (string $carry, ReflectionProperty $property) use ($isOptional, $missingSymbols) { - $isHidden = ! empty($property->getAttributes(Hidden::class)); - - if ($isHidden) { - return $carry; - } - - $isOptional = $isOptional || ! empty($property->getAttributes(Optional::class)); - - $transformed = $this->reflectionToTypeScript( - $property, - $missingSymbols, - ...$this->typeProcessors() - ); - - if ($transformed === null) { - return $carry; - } - - return $isOptional - ? "{$carry}{$property->getName()}?: {$transformed};" . PHP_EOL - : "{$carry}{$property->getName()}: {$transformed};" . PHP_EOL; - }, - '' - ); - } - - protected function transformMethods( - ReflectionClass $class, - MissingSymbolsCollection $missingSymbols - ): string { - return ''; - } - - protected function transformExtra( - ReflectionClass $class, - MissingSymbolsCollection $missingSymbols - ): string { - return ''; - } - - protected function typeProcessors(): array - { - return [ - new ReplaceDefaultsTypeProcessor( - $this->config->getDefaultTypeReplacements() - ), - new DtoCollectionTypeProcessor(), - ]; - } - - protected function resolveProperties(ReflectionClass $class): array - { - $properties = array_filter( - $class->getProperties(ReflectionProperty::IS_PUBLIC), - fn (ReflectionProperty $property) => ! $property->isStatic() - ); - - return array_values($properties); - } -} diff --git a/src/Transformers/EnumTransformer.php b/src/Transformers/EnumTransformer.php index 8f7a5a4f..236ea6b6 100644 --- a/src/Transformers/EnumTransformer.php +++ b/src/Transformers/EnumTransformer.php @@ -2,73 +2,89 @@ namespace Spatie\TypeScriptTransformer\Transformers; +use BackedEnum; use ReflectionClass; -use ReflectionEnum; -use ReflectionEnumBackedCase; -use Spatie\TypeScriptTransformer\Structures\TransformedType; -use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; +use Spatie\TypeScriptTransformer\References\ReflectionClassReference; +use Spatie\TypeScriptTransformer\Support\TransformationContext; +use Spatie\TypeScriptTransformer\Transformed\Transformed; +use Spatie\TypeScriptTransformer\Transformed\Untransformable; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptExport; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptEnum; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptLiteral; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; +use UnitEnum; class EnumTransformer implements Transformer { - public function __construct(protected TypeScriptTransformerConfig $config) - { + public function __construct( + public bool $useNativeEnums = false, + ) { } - public function transform(ReflectionClass $class, string $name): ?TransformedType - { - // If we're not on PHP >= 8.1, we don't support native enums. - if (! method_exists($class, 'isEnum')) { - return null; + public function transform( + ReflectionClass $reflectionClass, + TransformationContext $context + ): Transformed|Untransformable { + if (! $this->isEnum($reflectionClass)) { + return Untransformable::create(); } - if (! $class->isEnum()) { - return null; - } + $cases = $this->resolveCases($reflectionClass); - $enum = (new ReflectionEnum($class->getName())); - - if (! $enum->isBacked()) { - return null; - } - - return $this->config->shouldTransformToNativeEnums() - ? $this->toEnum($enum, $name) - : $this->toType($enum, $name); + return new Transformed( + $this->useNativeEnums + ? $this->transformAsNativeEnum($context->name, $cases) + : $this->transformAsUnion($context->name, $cases), + new ReflectionClassReference($reflectionClass), + $context->name, + true, + $context->nameSpaceSegments, + ); } - protected function toEnum(ReflectionEnum $enum, string $name): TransformedType + protected function isEnum(ReflectionClass $reflection): bool { - $options = array_map( - fn (ReflectionEnumBackedCase $case) => "'{$case->getName()}' = {$this->toEnumValue($case)}", - $enum->getCases() - ); - - return TransformedType::create( - $enum, - $name, - implode(', ', $options), - keyword: 'enum' - ); + return $reflection->isEnum(); } - protected function toType(ReflectionEnum $enum, string $name): TransformedType + protected function resolveCases(ReflectionClass $reflection): array { - $options = array_map( - fn (ReflectionEnumBackedCase $case) => $this->toEnumValue($case), - $enum->getCases(), - ); + /** @var class-string $enumClass */ + $enumClass = $reflection->getName(); - return TransformedType::create( - $enum, - $name, - implode(' | ', $options) - ); + $cases = []; + + foreach ($enumClass::cases() as $case) { + $cases[$case->name] = $case instanceof BackedEnum + ? $case->value + : $case->name; + } + + return $cases; } - protected function toEnumValue(ReflectionEnumBackedCase $case): string - { - $value = $case->getBackingValue(); + protected function transformAsNativeEnum( + string $name, + array $cases + ): TypeScriptNode { + return new TypeScriptExport(new TypeScriptEnum($name, $cases)); + } - return is_string($value) ? "'{$value}'" : "{$value}"; + protected function transformAsUnion( + string $name, + array $cases + ): TypeScriptNode { + return new TypeScriptExport(new TypeScriptAlias( + new TypeScriptIdentifier($name), + new TypeScriptUnion( + array_map( + fn (string $case) => new TypeScriptLiteral($case), + $cases, + ), + ), + )); } } diff --git a/src/Transformers/InterfaceTransformer.php b/src/Transformers/InterfaceTransformer.php deleted file mode 100644 index 9e2303dd..00000000 --- a/src/Transformers/InterfaceTransformer.php +++ /dev/null @@ -1,72 +0,0 @@ -isInterface()) { - return null; - } - - $transformedType = parent::transform($class, $name); - $transformedType->keyword = 'interface'; - $transformedType->trailingSemicolon = false; - - return $transformedType; - } - - protected function transformMethods( - ReflectionClass $class, - MissingSymbolsCollection $missingSymbols - ): string { - return array_reduce( - $class->getMethods(ReflectionMethod::IS_PUBLIC), - function (string $carry, ReflectionMethod $method) use ($missingSymbols) { - $transformedParameters = \array_reduce( - $method->getParameters(), - function (string $parameterCarry, \ReflectionParameter $parameter) use ($missingSymbols) { - $type = $this->reflectionToTypeScript( - $parameter, - $missingSymbols, - ...$this->typeProcessors() - ); - - $output = ''; - if ($parameterCarry !== '') { - $output .= ', '; - } - - return "{$parameterCarry}{$output}{$parameter->getName()}: {$type}"; - }, - '' - ); - - $returnType = 'any'; - if ($method->hasReturnType()) { - $returnType = $this->reflectionToTypeScript( - $method, - $missingSymbols, - ...$this->typeProcessors() - ); - } - - return "{$carry}{$method->getName()}({$transformedParameters}): {$returnType};" . PHP_EOL; - }, - '' - ); - } - - protected function transformProperties( - ReflectionClass $class, - MissingSymbolsCollection $missingSymbols - ): string { - return ''; - } -} diff --git a/src/Transformers/MyclabsEnumTransformer.php b/src/Transformers/MyclabsEnumTransformer.php deleted file mode 100644 index cab53b62..00000000 --- a/src/Transformers/MyclabsEnumTransformer.php +++ /dev/null @@ -1,62 +0,0 @@ -isSubclassOf(Enum::class) === false) { - return null; - } - - return $this->config->shouldTransformToNativeEnums() - ? $this->toEnum($class, $name) - : $this->toType($class, $name); - } - - protected function toEnum(ReflectionClass $class, string $name): TransformedType - { - /** @var \MyCLabs\Enum\Enum $enum */ - $enum = $class->getName(); - - $options = array_map( - fn ($key, $value) => "'{$key}' = '{$value}'", - array_keys($enum::toArray()), - $enum::toArray() - ); - - return TransformedType::create( - $class, - $name, - implode(', ', $options), - keyword: 'enum' - ); - } - - protected function toType(ReflectionClass $class, string $name): TransformedType - { - /** @var \MyCLabs\Enum\Enum $enum */ - $enum = $class->getName(); - - $options = array_map( - fn (Enum $enum) => "'{$enum->getValue()}'", - $enum::values() - ); - - return TransformedType::create( - $class, - $name, - implode(' | ', $options) - ); - } -} diff --git a/src/Transformers/SpatieEnumTransformer.php b/src/Transformers/SpatieEnumTransformer.php deleted file mode 100644 index 5824c67b..00000000 --- a/src/Transformers/SpatieEnumTransformer.php +++ /dev/null @@ -1,62 +0,0 @@ -isSubclassOf(Enum::class) === false) { - return null; - } - - return $this->config->shouldTransformToNativeEnums() - ? $this->toEnum($class, $name) - : $this->toType($class, $name); - } - - protected function toEnum(ReflectionClass $class, string $name): TransformedType - { - /** @var \Spatie\Enum\Enum $enum */ - $enum = $class->getName(); - - $options = array_map( - fn ($key, $value) => "'{$key}' = '{$value}'", - array_keys($enum::toArray()), - $enum::toArray() - ); - - return TransformedType::create( - $class, - $name, - implode(', ', $options), - keyword: 'enum' - ); - } - - private function toType(ReflectionClass $class, string $name): TransformedType - { - /** @var \Spatie\Enum\Enum $enum */ - $enum = $class->getName(); - - $options = array_map( - fn ($enum) => "'{$enum}'", - array_keys($enum::toArray()) - ); - - return TransformedType::create( - $class, - $name, - implode(' | ', $options) - ); - } -} diff --git a/src/Transformers/Transformer.php b/src/Transformers/Transformer.php index 0a834792..ddbf4b57 100644 --- a/src/Transformers/Transformer.php +++ b/src/Transformers/Transformer.php @@ -3,9 +3,11 @@ namespace Spatie\TypeScriptTransformer\Transformers; use ReflectionClass; -use Spatie\TypeScriptTransformer\Structures\TransformedType; +use Spatie\TypeScriptTransformer\Support\TransformationContext; +use Spatie\TypeScriptTransformer\Transformed\Transformed; +use Spatie\TypeScriptTransformer\Transformed\Untransformable; interface Transformer { - public function transform(ReflectionClass $class, string $name): ?TransformedType; + public function transform(ReflectionClass $reflectionClass, TransformationContext $context): Transformed|Untransformable; } diff --git a/src/Transformers/TransformsTypes.php b/src/Transformers/TransformsTypes.php deleted file mode 100644 index 6863daed..00000000 --- a/src/Transformers/TransformsTypes.php +++ /dev/null @@ -1,72 +0,0 @@ -reflectionToType( - $reflection, - $missingSymbolsCollection, - ...$typeProcessors - ); - - if ($type === null) { - return null; - } - - return $this->typeToTypeScript( - $type, - $missingSymbolsCollection, - $reflection->getDeclaringClass()?->getName() - ); - } - - protected function reflectionToType( - ReflectionMethod | ReflectionProperty | ReflectionParameter $reflection, - MissingSymbolsCollection $missingSymbolsCollection, - TypeProcessor ...$typeProcessors - ): ?Type { - $type = TypeReflector::new($reflection)->reflect(); - - foreach ($typeProcessors as $processor) { - $type = $processor->process( - $type, - $reflection, - $missingSymbolsCollection - ); - - if ($type === null) { - return null; - } - } - - return $type; - } - - protected function typeToTypeScript( - Type $type, - MissingSymbolsCollection $missingSymbolsCollection, - ?string $currentClass = null, - ): string { - $transpiler = new TranspileTypeToTypeScriptAction( - $missingSymbolsCollection, - $currentClass, - ); - - return $transpiler->execute($type); - } -} diff --git a/src/TypeProcessors/DtoCollectionTypeProcessor.php b/src/TypeProcessors/DtoCollectionTypeProcessor.php deleted file mode 100644 index e70e5d24..00000000 --- a/src/TypeProcessors/DtoCollectionTypeProcessor.php +++ /dev/null @@ -1,43 +0,0 @@ -walk($type, function (Type $type) { - if (! $type instanceof Object_) { - return $type; - } - - $fqs = ltrim((string) $type->getFqsen(), '\\'); - - if (! is_subclass_of($fqs, DataTransferObjectCollection::class)) { - return $type; - } - - $reflection = new ReflectionClass($fqs); - - return new Array_( - TypeReflector::new($reflection->getMethod('current'))->reflect() - ); - }); - } -} diff --git a/src/TypeProcessors/ProcessesTypes.php b/src/TypeProcessors/ProcessesTypes.php deleted file mode 100644 index b90cf609..00000000 --- a/src/TypeProcessors/ProcessesTypes.php +++ /dev/null @@ -1,82 +0,0 @@ - $this->walk($type, $closure), - iterator_to_array($type->getIterator()) - ); - - $walkedTypes = array_filter($walkedTypes); - - if (empty($walkedTypes)) { - return null; - } - - if (count($walkedTypes) === 1) { - return current($walkedTypes); - } - - return $closure(new Compound($walkedTypes)); - } - - if ($type instanceof AbstractList) { - $walkedValueType = $this->walk($type->getValueType(), $closure); - $walkedKeyType = $this->walk($type->getKeyType(), $closure); - - if ($walkedValueType === null || $walkedKeyType === null) { - return null; - } - - return $closure( - $this->updateListType($type, $walkedValueType, $walkedKeyType) - ); - } - - if ($type instanceof Nullable) { - $walkedType = $this->walk($type->getActualType(), $closure); - - if ($walkedType === null) { - return null; - } - - return $closure(new Nullable($walkedType)); - } - - return $closure($type); - } - - protected function updateListType( - AbstractList $type, - Type $valueType, - ?Type $keyType = null - ): Type { - $keyType = $type->getKeyType(); - - if ((string) $keyType === (string) new Compound([new String_(), new Integer()])) { - $keyType = null; - } - - if ($type instanceof Collection) { - return new Collection($type->getFqsen(), $valueType, $keyType); - } - - $typeClass = get_class($type); - - return new $typeClass($valueType, $keyType); - } -} diff --git a/src/TypeProcessors/ReplaceDefaultsTypeProcessor.php b/src/TypeProcessors/ReplaceDefaultsTypeProcessor.php deleted file mode 100644 index 9407121b..00000000 --- a/src/TypeProcessors/ReplaceDefaultsTypeProcessor.php +++ /dev/null @@ -1,43 +0,0 @@ - */ - private array $mapping; - - public function __construct(array $mapping) - { - $this->mapping = $mapping; - } - - public function process( - Type $type, - ReflectionProperty | ReflectionParameter | ReflectionMethod $reflection, - MissingSymbolsCollection $missingSymbolsCollection - ): ?Type { - return $this->walk($type, function (Type $type) { - if (! $type instanceof Object_) { - return $type; - } - - foreach ($this->mapping as $replacementClass => $replacementType) { - if (ltrim((string) $type->getFqsen(), '\\') === $replacementClass) { - return $replacementType; - } - } - - return $type; - }); - } -} diff --git a/src/TypeProcessors/TypeProcessor.php b/src/TypeProcessors/TypeProcessor.php deleted file mode 100644 index 4140879a..00000000 --- a/src/TypeProcessors/TypeProcessor.php +++ /dev/null @@ -1,18 +0,0 @@ -reflect(); - } - - public function isTransformable(): bool - { - return $this->transformable; - } - - public function getType(): ?Type - { - return $this->type; - } - - public function getName(): ?string - { - return $this->name; - } - - public function getTransformerClass(): ?string - { - return $this->transformerClass; - } - - public function isInline(): bool - { - return $this->inline; - } - - public function getReflectionClass(): ReflectionClass - { - return $this->class; - } - - private function reflect(): void - { - [ - 'transformable' => $this->transformable, - 'name' => $this->name, - 'transformer' => $this->transformerClass, - 'inline' => $this->inline, - ] = (new ClassReader())->forClass($this->class); - - $attributes = $this->class->getAttributes(); - - $this->reflectName($attributes) - ->reflectInline($attributes) - ->reflectType($attributes) - ->reflectTransformer($attributes); - } - - private function reflectName(array $attributes): self - { - $nameAttributes = array_values(array_filter( - $attributes, - fn (ReflectionAttribute $attribute) => is_a($attribute->getName(), TypeScript::class, true) - )); - - if (! empty($nameAttributes)) { - /** @var \Spatie\TypeScriptTransformer\Attributes\TypeScript $nameAttribute */ - $nameAttribute = $nameAttributes[0]->newInstance(); - - $this->transformable = true; - $this->name = $nameAttribute->name ?? $this->name; - } - - return $this; - } - - private function reflectInline(array $attributes): self - { - $inlineAttributes = array_values(array_filter( - $attributes, - fn (ReflectionAttribute $attribute) => is_a($attribute->getName(), InlineTypeScriptType::class, true) - )); - - if (! empty($inlineAttributes)) { - $this->transformable = true; - $this->inline = true; - } - - return $this; - } - - private function reflectType(array $attributes): self - { - $transformableAttributes = array_values(array_filter( - $attributes, - fn (ReflectionAttribute $attribute) => is_a($attribute->getName(), TypeScriptTransformableAttribute::class, true) - )); - - if (! empty($transformableAttributes)) { - /** @var \Spatie\TypeScriptTransformer\Attributes\TypeScriptTransformableAttribute $transformableAttribute */ - $transformableAttribute = $transformableAttributes[0]->newInstance(); - - $this->transformable = true; - $this->type = $transformableAttribute->getType(); - } - - return $this; - } - - private function reflectTransformer(array $attributes): self - { - if ($this->type) { - return $this; - } - - $transformerAttributes = array_values(array_filter( - $attributes, - fn (ReflectionAttribute $attribute) => is_a($attribute->getName(), TypeScriptTransformer::class, true) - )); - - if (! empty($transformerAttributes)) { - /** @var \Spatie\TypeScriptTransformer\Attributes\TypeScriptTransformer $transformerAttribute */ - $transformerAttribute = $transformerAttributes[0]->newInstance(); - - $this->transformable = true; - $this->transformerClass = $transformerAttribute->transformer; - } - - return $this; - } -} diff --git a/src/TypeReflectors/MethodParameterTypeReflector.php b/src/TypeReflectors/MethodParameterTypeReflector.php deleted file mode 100644 index 458143c7..00000000 --- a/src/TypeReflectors/MethodParameterTypeReflector.php +++ /dev/null @@ -1,39 +0,0 @@ -reflection->getDeclaringFunction()->getDocComment(); - } - - protected function docblockRegex(): string - { - return "/@param ((?:\\s?[\\w?|\\\\<>,-]+(?:\\[])?)+) \\\${$this->reflection->getName()}/"; - } - - protected function getReflectionType(): ?ReflectionType - { - return $this->reflection->getType(); - } - - protected function getAttributes(): array - { - return []; - } -} diff --git a/src/TypeReflectors/MethodReturnTypeReflector.php b/src/TypeReflectors/MethodReturnTypeReflector.php deleted file mode 100644 index e63c4f96..00000000 --- a/src/TypeReflectors/MethodReturnTypeReflector.php +++ /dev/null @@ -1,39 +0,0 @@ -reflection->getDocComment(); - } - - protected function docblockRegex(): string - { - return '/@return ((?:\s?[\w?|\\\\<>,-]+(?:\[])?)+)/'; - } - - protected function getReflectionType(): ?ReflectionType - { - return $this->reflection->getReturnType(); - } - - protected function getAttributes(): array - { - return $this->reflection->getAttributes(); - } -} diff --git a/src/TypeReflectors/PropertyTypeReflector.php b/src/TypeReflectors/PropertyTypeReflector.php deleted file mode 100644 index be7a16da..00000000 --- a/src/TypeReflectors/PropertyTypeReflector.php +++ /dev/null @@ -1,39 +0,0 @@ -reflection->getDocComment(); - } - - protected function docblockRegex(): string - { - return '/@var ((?:\s?[\\w?|\\\\<>,-]+(?:\[])?)+)/'; - } - - protected function getReflectionType(): ?ReflectionType - { - return $this->reflection->getType(); - } - - protected function getAttributes(): array - { - return $this->reflection->getAttributes(); - } -} diff --git a/src/TypeReflectors/TypeReflector.php b/src/TypeReflectors/TypeReflector.php deleted file mode 100644 index 7f16e80a..00000000 --- a/src/TypeReflectors/TypeReflector.php +++ /dev/null @@ -1,165 +0,0 @@ -reflectionFromAttribute()) { - return $type; - } - - if ($type = $this->reflectFromDocblock()) { - return $type; - } - - if ($type = $this->reflectFromReflection()) { - return $type; - } - - return new TypeScriptType('any'); - } - - public function reflectionFromAttribute(): ?Type - { - $attributes = array_filter( - $this->getAttributes(), - fn (ReflectionAttribute $attribute) => is_a($attribute->getName(), TypeScriptTransformableAttribute::class, true) - ); - - if (empty($attributes)) { - return null; - } - - /** @var \Spatie\TypeScriptTransformer\Attributes\TypeScriptTransformableAttribute $attribute */ - $attribute = current($attributes)->newInstance(); - - return $attribute->getType(); - } - - public function reflectFromDocblock(): ?Type - { - preg_match( - $this->docblockRegex(), - $this->getDocblock(), - $matches - ); - - $docDefinition = $matches[1] ?? null; - - if ($docDefinition === null) { - return null; - } - - $type = (new TypeResolver())->resolve( - $docDefinition, - (new ContextFactory())->createFromReflector($this->reflection) - ); - - return $this->nullifyType($type); - } - - public function reflectFromReflection(): ?Type - { - $reflectionType = $this->getReflectionType(); - - if ($reflectionType === null) { - return null; - } - - if ($reflectionType instanceof ReflectionUnionType) { - $type = new Compound(array_map( - fn (ReflectionNamedType $reflectionType) => (new TypeResolver())->resolve( - $reflectionType->getName(), - ), - $reflectionType->getTypes() - )); - - return $this->nullifyType($type); - } - - if (! $reflectionType instanceof ReflectionNamedType) { - return null; - } - - $type = (new TypeResolver())->resolve( - $reflectionType->getName(), - ); - - return $this->nullifyType($type); - } - - private function nullifyType(Type $type): Type - { - $reflectionType = $this->getReflectionType(); - - if ($reflectionType === null || $reflectionType->allowsNull() === false) { - return $type; - } - - if ($type instanceof Mixed_) { - return $type; - } - - if ($type instanceof Nullable) { - return $type; - } - - if ($type instanceof Compound && $type->contains(new Null_())) { - return $type; - } - - if ($type instanceof Compound) { - /** @psalm-suppress InvalidArgument */ - return new Compound(array_merge( - iterator_to_array($type->getIterator()), - [new Null_()], - )); - } - - return new Nullable($type); - } -} diff --git a/src/TypeResolvers/Data/ParsedClass.php b/src/TypeResolvers/Data/ParsedClass.php new file mode 100644 index 00000000..696df3dc --- /dev/null +++ b/src/TypeResolvers/Data/ParsedClass.php @@ -0,0 +1,14 @@ + $properties + */ + public function __construct( + public array $properties, + ) { + } +} diff --git a/src/TypeResolvers/Data/ParsedMethod.php b/src/TypeResolvers/Data/ParsedMethod.php new file mode 100644 index 00000000..122bda20 --- /dev/null +++ b/src/TypeResolvers/Data/ParsedMethod.php @@ -0,0 +1,17 @@ + $parameters + */ + public function __construct( + public array $parameters, + public ?TypeNode $returnType, + ) { + } +} diff --git a/src/TypeResolvers/Data/ParsedNameAndType.php b/src/TypeResolvers/Data/ParsedNameAndType.php new file mode 100644 index 00000000..265aefdd --- /dev/null +++ b/src/TypeResolvers/Data/ParsedNameAndType.php @@ -0,0 +1,14 @@ +typeParser = new TypeParser($constExprParser); + + $this->docParser = new PhpDocParser($this->typeParser, $constExprParser); + $this->lexer = new Lexer(); + } + + public function class(ReflectionClass $class): ?ParsedClass + { + $parsed = $this->parseDocComment($class); + + if ($parsed === null) { + return null; + } + + $properties = []; + + foreach ($parsed->getPropertyTagValues() as $propertyTag) { + $name = ltrim($propertyTag->propertyName, '$'); + + $properties[$name] = new ParsedNameAndType($name, $propertyTag->type); + } + + if (empty($properties)) { + return null; + } + + return new ParsedClass($properties); + } + + public function method(ReflectionMethod $method): ?ParsedMethod + { + $parsed = $this->parseDocComment($method); + + if ($parsed === null) { + return null; + } + + $parameters = []; + + foreach ($parsed->getParamTagValues() as $paramTag) { + $name = ltrim($paramTag->parameterName, '$'); + + $parameters[$name] = new ParsedNameAndType($name, $paramTag->type); + } + + $return = null; + + foreach ($parsed->getReturnTagValues() as $returnTag) { + $return = $returnTag->type; + } + + if (empty($parameters) && $return === null) { + return null; + } + + return new ParsedMethod($parameters, $return); + } + + public function property(ReflectionProperty $property): ?ParsedNameAndType + { + $parsed = $this->parseDocComment($property); + + if ($parsed === null) { + return null; + } + + $var = null; + + foreach ($parsed->getVarTagValues() as $varTag) { + $var = $varTag->type; + } + + if ($var === null) { + return null; + } + + return new ParsedNameAndType($property->name, $var); + } + + public function type(string $type): TypeNode + { + return $this->typeParser->parse( + new TokenIterator($this->lexer->tokenize($type)) + ); + } + + protected function parseDocComment( + ReflectionClass|ReflectionMethod|ReflectionProperty $reflection + ): ?PhpDocNode { + if ($reflection->getDocComment() === false) { + return null; + } + + return $this->docParser->parse( + new TokenIterator($this->lexer->tokenize($reflection->getDocComment())) + ); + } +} diff --git a/src/TypeScript/TypeReference.php b/src/TypeScript/TypeReference.php new file mode 100644 index 00000000..d11492a8 --- /dev/null +++ b/src/TypeScript/TypeReference.php @@ -0,0 +1,26 @@ +referenced = $transformed; + } + + public function write(WritingContext $context): string + { + return ($context->referenceWriter)($this->reference); + } +} diff --git a/src/TypeScript/TypeScriptAlias.php b/src/TypeScript/TypeScriptAlias.php new file mode 100644 index 00000000..1bb33129 --- /dev/null +++ b/src/TypeScript/TypeScriptAlias.php @@ -0,0 +1,24 @@ +identifier->write($context)} = {$this->type->write($context)};"; + } + + public function children(): array + { + return [$this->identifier, $this->type]; + } +} diff --git a/src/TypeScript/TypeScriptAny.php b/src/TypeScript/TypeScriptAny.php new file mode 100644 index 00000000..23aff39a --- /dev/null +++ b/src/TypeScript/TypeScriptAny.php @@ -0,0 +1,13 @@ +type + ? "Array<{$this->type->write($context)}>" + : 'Array'; + } + + public function children(): array + { + return $this->type ? [$this->type] : []; + } +} diff --git a/src/TypeScript/TypeScriptBoolean.php b/src/TypeScript/TypeScriptBoolean.php new file mode 100644 index 00000000..f5fdc1eb --- /dev/null +++ b/src/TypeScript/TypeScriptBoolean.php @@ -0,0 +1,13 @@ +name.' {'.PHP_EOL; + + foreach ($this->cases as $case) { + $output .= ' '.$case.','.PHP_EOL; + } + + $output .= '}'.PHP_EOL; + + return $output; + } +} diff --git a/src/TypeScript/TypeScriptExport.php b/src/TypeScript/TypeScriptExport.php new file mode 100644 index 00000000..a0884210 --- /dev/null +++ b/src/TypeScript/TypeScriptExport.php @@ -0,0 +1,23 @@ +node->write($context)}".PHP_EOL; + } + + public function children(): array + { + return [$this->node]; + } +} diff --git a/src/TypeScript/TypeScriptFunction.php b/src/TypeScript/TypeScriptFunction.php new file mode 100644 index 00000000..9c522d2e --- /dev/null +++ b/src/TypeScript/TypeScriptFunction.php @@ -0,0 +1,13 @@ + $genericTypes + */ + public function __construct( + public TypeScriptNode $type, + public array $genericTypes, + ) { + } + + public function write(WritingContext $context): string + { + $generics = implode(', ', array_map( + fn (TypeScriptNode $type) => $type->write($context), + $this->genericTypes + )); + + return "{$this->type->write($context)}<{$generics}>"; + } + + public function children(): array + { + return [ + $this->type, + ...$this->genericTypes, + ]; + } +} diff --git a/src/TypeScript/TypeScriptIdentifier.php b/src/TypeScript/TypeScriptIdentifier.php new file mode 100644 index 00000000..a490eb7e --- /dev/null +++ b/src/TypeScript/TypeScriptIdentifier.php @@ -0,0 +1,19 @@ +name; + } +} diff --git a/src/TypeScript/TypeScriptImport.php b/src/TypeScript/TypeScriptImport.php new file mode 100644 index 00000000..f1691345 --- /dev/null +++ b/src/TypeScript/TypeScriptImport.php @@ -0,0 +1,22 @@ +names); + + return "import { {$names} } from '{$this->path}';" . PHP_EOL; + } +} diff --git a/src/TypeScript/TypeScriptIndexSignature.php b/src/TypeScript/TypeScriptIndexSignature.php new file mode 100644 index 00000000..f8fd55ec --- /dev/null +++ b/src/TypeScript/TypeScriptIndexSignature.php @@ -0,0 +1,24 @@ +name}: {$this->type->write($context)}]]"; + } + + public function children(): array + { + return [$this->type]; + } +} diff --git a/src/TypeScript/TypeScriptInterface.php b/src/TypeScript/TypeScriptInterface.php new file mode 100644 index 00000000..afa3862f --- /dev/null +++ b/src/TypeScript/TypeScriptInterface.php @@ -0,0 +1,38 @@ + $properties + * @param array $methods + */ + public function __construct( + public string $name, + public array $properties, + public array $methods, + ) { + } + + public function write(WritingContext $context): string + { + $combined = [...$this->properties, ...$this->methods]; + + $items = array_reduce( + $combined, + fn (string $carry, TypeScriptProperty|TypeScriptMethod $item) => $carry.$item->write($context).PHP_EOL, + empty($combined) ? '' : PHP_EOL + ); + + return "interface {$this->name} {{$items}}"; + } + + public function children(): array + { + return [...$this->properties, ...$this->methods]; + } +} diff --git a/src/TypeScript/TypeScriptIntersection.php b/src/TypeScript/TypeScriptIntersection.php new file mode 100644 index 00000000..cd9fdeee --- /dev/null +++ b/src/TypeScript/TypeScriptIntersection.php @@ -0,0 +1,29 @@ + $types + */ + public function __construct( + public array $types, + ) { + } + + public function write(WritingContext $context): string + { + return implode(' & ', array_map( + fn (TypeScriptNode $type) => $type->write($context), + $this->types + )); + } + + public function children(): array + { + return $this->types; + } +} diff --git a/src/TypeScript/TypeScriptLiteral.php b/src/TypeScript/TypeScriptLiteral.php new file mode 100644 index 00000000..2d678a42 --- /dev/null +++ b/src/TypeScript/TypeScriptLiteral.php @@ -0,0 +1,17 @@ +value); + } +} diff --git a/src/TypeScript/TypeScriptMethod.php b/src/TypeScript/TypeScriptMethod.php new file mode 100644 index 00000000..725ec4a0 --- /dev/null +++ b/src/TypeScript/TypeScriptMethod.php @@ -0,0 +1,33 @@ + $parameters + */ + public function __construct( + public string $name, + public array $parameters, + public TypeScriptNode $returnType, + ) { + } + + public function write(WritingContext $context): string + { + $parameters = implode(', ', array_map( + fn (TypeScriptParameter $parameter) => $parameter->write($context), + $this->parameters + )); + + return "{$this->name}({$parameters}): {$this->returnType->write($context)};"; + } + + public function children(): array + { + return [$this->returnType, ...$this->parameters]; + } +} diff --git a/src/TypeScript/TypeScriptNamespace.php b/src/TypeScript/TypeScriptNamespace.php new file mode 100644 index 00000000..5af4a7db --- /dev/null +++ b/src/TypeScript/TypeScriptNamespace.php @@ -0,0 +1,30 @@ + $types + */ + public function __construct( + public array $namespaceSegments, + public array $types, + ) { + } + + public function write(WritingContext $context): string + { + $output = 'declare namespace '.implode('.', $this->namespaceSegments).'{'.PHP_EOL; + + foreach ($this->types as $type) { + $output .= $type->write($context).PHP_EOL; + } + + $output .= '}'.PHP_EOL; + + return $output; + } +} diff --git a/src/TypeScript/TypeScriptNode.php b/src/TypeScript/TypeScriptNode.php new file mode 100644 index 00000000..5205e87e --- /dev/null +++ b/src/TypeScript/TypeScriptNode.php @@ -0,0 +1,11 @@ + */ + public function children(): array; +} diff --git a/src/TypeScript/TypeScriptNull.php b/src/TypeScript/TypeScriptNull.php new file mode 100644 index 00000000..9638d356 --- /dev/null +++ b/src/TypeScript/TypeScriptNull.php @@ -0,0 +1,13 @@ + $properties + */ + public function __construct( + public array $properties + ) { + } + + public function write(WritingContext $context): string + { + if (empty($this->properties)) { + return 'object'; + } + + $output = '{'.PHP_EOL; + + foreach ($this->properties as $property) { + $output .= $property->write($context).PHP_EOL; + } + + return $output.'}'; + } + + public function children(): array + { + return $this->properties; + } +} diff --git a/src/TypeScript/TypeScriptParameter.php b/src/TypeScript/TypeScriptParameter.php new file mode 100644 index 00000000..456ad0cc --- /dev/null +++ b/src/TypeScript/TypeScriptParameter.php @@ -0,0 +1,31 @@ +name) + ? "'{$this->name}'" + : $this->name; + + return $this->isOptional + ? "{$name}?: {$this->type->write($context)}" + : "{$name}: {$this->type->write($context)}"; + } + + public function children(): array + { + return [$this->type]; + } +} diff --git a/src/TypeScript/TypeScriptProperty.php b/src/TypeScript/TypeScriptProperty.php new file mode 100644 index 00000000..81aff03a --- /dev/null +++ b/src/TypeScript/TypeScriptProperty.php @@ -0,0 +1,32 @@ +name = is_string($name) ? new TypeScriptIdentifier($name) : $name; + } + + public function write(WritingContext $context): string + { + $readonly = $this->isReadonly ? 'readonly ' : ''; + $optional = $this->isOptional ? '?' : ''; + + return "{$readonly}{$this->name->write($context)}{$optional}: {$this->type->write($context)}"; + } + + public function children(): array + { + return [$this->name, $this->type]; + } +} diff --git a/src/TypeScript/TypeScriptRaw.php b/src/TypeScript/TypeScriptRaw.php new file mode 100644 index 00000000..b25b9722 --- /dev/null +++ b/src/TypeScript/TypeScriptRaw.php @@ -0,0 +1,18 @@ +typeScript; + } +} diff --git a/src/TypeScript/TypeScriptString.php b/src/TypeScript/TypeScriptString.php new file mode 100644 index 00000000..53d35321 --- /dev/null +++ b/src/TypeScript/TypeScriptString.php @@ -0,0 +1,13 @@ + $types + */ + public function __construct( + public array $types, + ) { + } + + public function write(WritingContext $context): string + { + return implode(' | ', array_map( + fn (TypeScriptNode $type) => $type->write($context), + $this->types + )); + } + + public function contains(Closure $closure): bool + { + foreach ($this->types as $type) { + if ($closure($type)) { + return true; + } + } + + return false; + } + + public function children(): array + { + return $this->types; + } +} diff --git a/src/TypeScript/TypeScriptUnknown.php b/src/TypeScript/TypeScriptUnknown.php new file mode 100644 index 00000000..e70cacb6 --- /dev/null +++ b/src/TypeScript/TypeScriptUnknown.php @@ -0,0 +1,13 @@ +config = $config; - } + // Parallelize + // - discovering types + // - transforming types - public function transform(): TypesCollection - { - $typesCollection = (new ResolveTypesCollectionAction( - new Finder(), - $this->config, - ))->execute(); + // Cant't do parallel + // - replace type references + + $discovered = $this->discoverTypesAction->execute(); + + $transformed = $this->transformTypesAction->execute($discovered); + + $transformed = $this->appendDefaultTypesAction->execute($transformed); + + $referenceMap = $this->connectReferencesAction->execute($transformed); - (new PersistTypesCollectionAction($this->config))->execute($typesCollection); + $writtenFiles = $this->writeTypesAction->execute($transformed, $referenceMap); - (new FormatTypeScriptAction($this->config))->execute(); + $this->formatFilesAction->execute($writtenFiles); - return $typesCollection; + return $this->log; } } diff --git a/src/TypeScriptTransformerConfig.php b/src/TypeScriptTransformerConfig.php old mode 100644 new mode 100755 index 0d0c763c..92de833d --- a/src/TypeScriptTransformerConfig.php +++ b/src/TypeScriptTransformerConfig.php @@ -2,164 +2,22 @@ namespace Spatie\TypeScriptTransformer; -use phpDocumentor\Reflection\Type; -use phpDocumentor\Reflection\TypeResolver; -use Spatie\TypeScriptTransformer\Collectors\DefaultCollector; -use Spatie\TypeScriptTransformer\Exceptions\InvalidDefaultTypeReplacer; -use Spatie\TypeScriptTransformer\Formatters\Formatter; +use Spatie\TypeScriptTransformer\DefaultTypeProviders\DefaultTypesProvider; use Spatie\TypeScriptTransformer\Transformers\Transformer; -use Spatie\TypeScriptTransformer\Writers\TypeDefinitionWriter; use Spatie\TypeScriptTransformer\Writers\Writer; -class TypeScriptTransformerConfig +readonly class TypeScriptTransformerConfig { - private array $autoDiscoverTypesPaths = []; - - private array $transformers = []; - - private array $collectors = [DefaultCollector::class]; - - private string $outputFile = 'types.d.ts'; - - private array $defaultTypeReplacements = []; - - private string $writer = TypeDefinitionWriter::class; - - private ?string $formatter = null; - - private bool $transformToNativeEnums = false; - - public static function create(): self - { - return new self(); - } - - public function autoDiscoverTypes(string ...$paths): self - { - $this->autoDiscoverTypesPaths = array_merge($this->autoDiscoverTypesPaths, $paths); - - return $this; - } - - public function transformers(array $transformers): self - { - $this->transformers = $transformers; - - return $this; - } - - public function collectors(array $collectors) - { - $this->collectors = array_merge($collectors, [DefaultCollector::class]); - - return $this; - } - - public function writer(string $writer): self - { - $this->writer = $writer; - - return $this; - } - - public function outputFile(string $defaultFile): self - { - $this->outputFile = $defaultFile; - - return $this; - } - - public function defaultTypeReplacements(array $defaultTypeReplacements): self - { - $this->defaultTypeReplacements = $defaultTypeReplacements; - - return $this; - } - - public function formatter(?string $formatter): self - { - $this->formatter = $formatter; - - return $this; - } - - public function transformToNativeEnums(bool $transformToNativeEnums = true): self - { - $this->transformToNativeEnums = $transformToNativeEnums; - - return $this; - } - - public function getAutoDiscoverTypesPaths(): array - { - return $this->autoDiscoverTypesPaths; - } - - /**@return \Spatie\TypeScriptTransformer\Transformers\Transformer[] */ - public function getTransformers(): array - { - return array_map( - fn (string $transformer) => $this->buildTransformer($transformer), - $this->transformers - ); - } - - public function buildTransformer(string $transformer): Transformer - { - return method_exists($transformer, '__construct') - ? new $transformer($this) - : new $transformer; - } - - public function getWriter(): Writer - { - return new $this->writer; - } - - public function getOutputFile(): string - { - return $this->outputFile; - } - - /** @return \Spatie\TypeScriptTransformer\Collectors\Collector[] */ - public function getCollectors(): array - { - return array_map( - fn (string $collector) => new $collector($this), - $this->collectors - ); - } - - public function getDefaultTypeReplacements(): array - { - $typeResolver = new TypeResolver(); - - $replacements = []; - - foreach ($this->defaultTypeReplacements as $class => $replacement) { - if (! class_exists($class) && ! interface_exists($class)) { - throw InvalidDefaultTypeReplacer::classDoesNotExist($class); - } - - $replacements[$class] = $replacement instanceof Type - ? $replacement - : $typeResolver->resolve($replacement); - } - - return $replacements; - } - - public function getFormatter(): ?Formatter - { - if ($this->formatter === null) { - return null; - } - - return new $this->formatter; - } - - public function shouldTransformToNativeEnums(): bool - { - return $this->transformToNativeEnums; + /** + * @param array $directories + * @param array $transformers + * @param array> $defaultTypeProviders + */ + public function __construct( + public array $directories, + public array $transformers, + public array $defaultTypeProviders, + public Writer $writer, + ) { } } diff --git a/src/Types/RecordType.php b/src/Types/RecordType.php deleted file mode 100644 index d385c5ce..00000000 --- a/src/Types/RecordType.php +++ /dev/null @@ -1,42 +0,0 @@ -keyType = (new TypeResolver())->resolve($keyType); - - if ($array) { - $this->valueType = new Array_((new TypeResolver())->resolve($valueType)); - } else { - $this->valueType = is_array($valueType) - ? StructType::fromArray($valueType) - : (new TypeResolver())->resolve($valueType); - } - } - - public function __toString(): string - { - return 'record'; - } - - public function getKeyType(): Type - { - return $this->keyType; - } - - public function getValueType(): Type - { - return $this->valueType; - } -} diff --git a/src/Types/StructType.php b/src/Types/StructType.php deleted file mode 100644 index 72e1067e..00000000 --- a/src/Types/StructType.php +++ /dev/null @@ -1,54 +0,0 @@ - */ - private array $types; - - public static function fromArray(array $properties): self - { - $resolver = new TypeResolver(); - - $types = []; - - foreach ($properties as $name => $property) { - if (is_string($property)) { - $types[$name] = $resolver->resolve($property); - - continue; - } - - if (is_array($property)) { - $types[$name] = self::fromArray($property); - - continue; - } - - throw UnableToTransformUsingAttribute::create($property); - } - - return new self($types); - } - - public function __construct(array $types) - { - $this->types = $types; - } - - public function __toString(): string - { - return 'struct'; - } - - public function getTypes(): array - { - return $this->types; - } -} diff --git a/src/Types/TypeScriptType.php b/src/Types/TypeScriptType.php deleted file mode 100644 index 5d5b5efd..00000000 --- a/src/Types/TypeScriptType.php +++ /dev/null @@ -1,26 +0,0 @@ -typeScript = $typeScript; - } - - public function __toString(): string - { - return $this->typeScript; - } -} diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index e0c0c462..ae2d5cf0 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -2,35 +2,122 @@ namespace Spatie\TypeScriptTransformer\Writers; -use Spatie\TypeScriptTransformer\Structures\TransformedType; -use Spatie\TypeScriptTransformer\Structures\TypesCollection; +use Spatie\TypeScriptTransformer\Actions\ResolveRelativePathAction; +use Spatie\TypeScriptTransformer\Actions\SplitTransformedPerLocationAction; +use Spatie\TypeScriptTransformer\Collections\ReferenceMap; +use Spatie\TypeScriptTransformer\References\Reference; +use Spatie\TypeScriptTransformer\Support\Location; +use Spatie\TypeScriptTransformer\Support\WritingContext; +use Spatie\TypeScriptTransformer\Support\WrittenFile; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptImport; class ModuleWriter implements Writer { - public function format(TypesCollection $collection): string + protected string $path; + + protected SplitTransformedPerLocationAction $transformedPerLocationAction; + + protected ResolveRelativePathAction $resolveRelativePathAction; + + public function __construct( + string $path, + protected string $filename = 'types', + protected string $extension = 'ts', + ) { + $this->path = rtrim($path, '/'); + $this->transformedPerLocationAction = new SplitTransformedPerLocationAction(); + $this->resolveRelativePathAction = new ResolveRelativePathAction(); + } + + public function output(array $transformedTypes, ReferenceMap $referenceMap): array { - $output = ''; + $locations = $this->transformedPerLocationAction->execute( + $transformedTypes + ); - /** @var \ArrayIterator $iterator */ - $iterator = $collection->getIterator(); + $writtenFiles = []; + + foreach ($locations as $location) { + $writtenFiles[] = $this->writeLocation($location, $referenceMap); + } - $iterator->uasort(function (TransformedType $a, TransformedType $b) { - return strcmp($a->name, $b->name); + return $writtenFiles; + } + + protected function writeLocation( + Location $location, + ReferenceMap $referenceMap, + ): WrittenFile { + $imports = $this->resolveImports($location, $referenceMap); + + $path = "{$this->path}/".implode('/', $location->segments)."/"; + + $output = ''; + + $writingContext = new WritingContext(function (Reference $reference) use ($referenceMap) { + return $referenceMap->get($reference)->name; }); - foreach ($iterator as $type) { - if ($type->isInline) { - continue; - } + foreach ($imports as $import) { + $output .= $import->write($writingContext); + } + + $output .= PHP_EOL; + + foreach ($location->transformed as $transformedItem) { + $output .= $transformedItem->typeScriptNode->write($writingContext); + } - $output .= "export {$type->toString()}".PHP_EOL; + if (is_dir($path) === false) { + mkdir($path, recursive: true); } - return $output; + file_put_contents("{$path}/{$this->filename}.{$this->extension}", $output); + + return new WrittenFile($path, $location->transformed); } - public function replacesSymbolsWithFullyQualifiedIdentifiers(): bool - { - return false; + /** + * @return array + */ + protected function resolveImports( + Location $location, + ReferenceMap $referenceMap, + ): array { + /** @var array, names: array}> $imports */ + $imports = []; + + foreach ($location->transformed as $transformedItem) { + foreach ($transformedItem->references as $reference) { + $transformedReference = $referenceMap->get($reference); + + if (! array_key_exists(implode($transformedReference->location), $imports)) { + $imports[implode($transformedReference->location)] = [ + 'location' => $transformedReference->location, + 'names' => [], + ]; + } + + $imports[implode($transformedReference->location)]['names'][] = $transformedReference->name; + } + } + + return array_filter(array_map(function (array $import) use ($location) { + $names = array_values(array_unique($import['names'])); + $path = $this->resolveRelativePathAction->execute( + $location->segments, + $import['location'], + ); + + if ($path === null) { + // current path + return null; + } + + return new TypeScriptImport( + "{$path}/{$this->filename}", + $names + ); + }, $imports)); } } diff --git a/src/Writers/NamespaceWriter.php b/src/Writers/NamespaceWriter.php new file mode 100644 index 00000000..49e263e2 --- /dev/null +++ b/src/Writers/NamespaceWriter.php @@ -0,0 +1,75 @@ +splitTransformedPerLocationAction = new SplitTransformedPerLocationAction(); + } + + public function output(array $transformedTypes, ReferenceMap $referenceMap): array + { + $split = $this->splitTransformedPerLocationAction->execute( + $transformedTypes + ); + + $output = ''; + + $writingContext = new WritingContext(function (Reference $reference) use ($referenceMap) { + $transformable = $referenceMap->get($reference); + + return implode('.', $transformable->location).'.'.$transformable->name; + }); + + foreach ($split as $splitConstruct) { + if (count($splitConstruct->segments) === 0) { + foreach ($splitConstruct->transformed as $transformable) { + $output .= $transformable->typeScriptNode->write($writingContext); + } + + continue; + } + + $namespace = new TypeScriptNamespace( + $splitConstruct->segments, + array_map( + fn (Transformed $transformable) => $transformable->typeScriptNode, + $splitConstruct->transformed, + ), + ); + + $output .= $namespace->write($writingContext); + } + + file_put_contents( + $this->filename, + $output, + ); + + return [ + new WrittenFile( + $this->filename, + $transformedTypes, + ), + ]; + } + + public function replaceReference( + Transformed $transformable + ): string { + return implode('.', $transformable->location).'.'.$transformable->name; + } +} diff --git a/src/Writers/TypeDefinitionWriter.php b/src/Writers/TypeDefinitionWriter.php deleted file mode 100644 index 1a029894..00000000 --- a/src/Writers/TypeDefinitionWriter.php +++ /dev/null @@ -1,73 +0,0 @@ -execute($collection); - - [$namespaces, $rootTypes] = $this->groupByNamespace($collection); - - $output = ''; - - foreach ($namespaces as $namespace => $types) { - asort($types); - - $output .= "declare namespace {$namespace} {".PHP_EOL; - - $output .= join(PHP_EOL, array_map( - fn (TransformedType $type) => "export {$type->toString()}", - $types - )); - - - $output .= PHP_EOL."}".PHP_EOL; - } - - $output .= join(PHP_EOL, array_map( - fn (TransformedType $type) => "export {$type->toString()}", - $rootTypes - )); - - return $output; - } - - public function replacesSymbolsWithFullyQualifiedIdentifiers(): bool - { - return true; - } - - protected function groupByNamespace(TypesCollection $collection): array - { - $namespaces = []; - $rootTypes = []; - - foreach ($collection as $type) { - if ($type->isInline) { - continue; - } - - $namespace = str_replace('\\', '.', $type->reflection->getNamespaceName()); - - if (empty($namespace)) { - $rootTypes[] = $type; - - continue; - } - - array_key_exists($namespace, $namespaces) - ? $namespaces[$namespace][] = $type - : $namespaces[$namespace] = [$type]; - } - - ksort($namespaces); - - return [$namespaces, $rootTypes]; - } -} diff --git a/src/Writers/Writer.php b/src/Writers/Writer.php index 639200f6..dea785dc 100644 --- a/src/Writers/Writer.php +++ b/src/Writers/Writer.php @@ -2,11 +2,15 @@ namespace Spatie\TypeScriptTransformer\Writers; -use Spatie\TypeScriptTransformer\Structures\TypesCollection; +use Spatie\TypeScriptTransformer\Collections\ReferenceMap; +use Spatie\TypeScriptTransformer\Support\WrittenFile; +use Spatie\TypeScriptTransformer\Transformed\Transformed; interface Writer { - public function format(TypesCollection $collection): string; - - public function replacesSymbolsWithFullyQualifiedIdentifiers(): bool; + /** @return array */ + public function output( + array $transformedTypes, + ReferenceMap $referenceMap, + ): array; } diff --git a/tests/Actions/FormatTypeScriptActionTest.php b/tests/Actions/FormatTypeScriptActionTest.php deleted file mode 100644 index b5d82768..00000000 --- a/tests/Actions/FormatTypeScriptActionTest.php +++ /dev/null @@ -1,53 +0,0 @@ -temporaryDirectory = (new TemporaryDirectory())->create(); - - $this->outputFile = $this->temporaryDirectory->path('types.d.ts'); -}); - -it('can format an generated file', function () { - $formatter = new class implements Formatter { - public function format(string $file): void - { - file_put_contents($file, 'formatted'); - } - }; - - $action = new FormatTypeScriptAction( - TypeScriptTransformerConfig::create() - ->formatter($formatter::class) - ->outputFile($this->outputFile) - ); - - file_put_contents( - $this->outputFile, - "export type Enum='yes'|'no';export type OtherDto={name:string}" - ); - - $action->execute(); - - assertEquals('formatted', file_get_contents($this->outputFile)); -}); - -it('can disable formatting', function () { - $action = new FormatTypeScriptAction( - TypeScriptTransformerConfig::create()->outputFile($this->outputFile) - ); - - file_put_contents( - $this->outputFile, - "export type Enum='yes'|'no';export type OtherDto={name:string}" - ); - - $action->execute(); - - assertMatchesFileSnapshot($this->outputFile); -}); diff --git a/tests/Actions/PersistTypesCollectionActionTest.php b/tests/Actions/PersistTypesCollectionActionTest.php deleted file mode 100644 index dacaf4f5..00000000 --- a/tests/Actions/PersistTypesCollectionActionTest.php +++ /dev/null @@ -1,59 +0,0 @@ -temporaryDirectory = (new TemporaryDirectory())->create(); - - $this->action = new PersistTypesCollectionAction( - TypeScriptTransformerConfig::create() - ->autoDiscoverTypes(__DIR__ . '/../FakeClasses') - ->transformers([MyclabsEnumTransformer::class]) - ->outputFile($this->temporaryDirectory->path('types.d.ts')) - ); -}); - -it('will persist the types', function () { - $collection = TypesCollection::create(); - - $collection[] = FakeTransformedType::fake('Enum')->withoutNamespace(); - $collection[] = FakeTransformedType::fake('Enum')->withNamespace('test'); - $collection[] = FakeTransformedType::fake('Enum')->withNamespace('test\test'); - - $this->action->execute($collection); - - assertMatchesFileSnapshot($this->temporaryDirectory->path("types.d.ts")); -}); - -it('can persist multiple types in one namespace', function () { - $collection = TypesCollection::create(); - - $collection[] = FakeTransformedType::fake('Enum')->withTransformed('transformed Enum')->withoutNamespace(); - $collection[] = FakeTransformedType::fake('OtherEnum')->withTransformed('transformed OtherEnum')->withoutNamespace(); - $collection[] = FakeTransformedType::fake('Enum')->withTransformed('transformed test\Enum')->withNamespace('test'); - $collection[] = FakeTransformedType::fake('OtherEnum')->withTransformed('transformed test\OtherEnum')->withNamespace('test'); - - $this->action->execute($collection); - - assertMatchesFileSnapshot($this->temporaryDirectory->path("types.d.ts")); -}); - -it('can re save the file', function () { - $collection = TypesCollection::create(); - - $collection[] = FakeTransformedType::fake('Enum')->withoutNamespace(); - - $this->action->execute($collection); - - $collection[] = FakeTransformedType::fake('Enum')->withNamespace('test'); - - $this->action->execute($collection); - - assertMatchesFileSnapshot($this->temporaryDirectory->path("types.d.ts")); -}); diff --git a/tests/Actions/ReplaceSymbolsInCollectionActionTest.php b/tests/Actions/ReplaceSymbolsInCollectionActionTest.php deleted file mode 100644 index 7d273615..00000000 --- a/tests/Actions/ReplaceSymbolsInCollectionActionTest.php +++ /dev/null @@ -1,42 +0,0 @@ -withNamespace('enums'); - $collection[] = FakeTransformedType::fake('Dto') - ->withTransformed('{enum: {%enums\Enum%}, non-existing: {%non-existing%}}') - ->withMissingSymbols([ - 'enum' => 'enums\Enum', - 'non-existing' => 'non-existing', - ]); - - $collection = $action->execute($collection); - - assertEquals('{enum: enums.Enum, non-existing: any}', $collection['Dto']->transformed); -}); - -it('can replace missing symbols without fully qualified names', function () { - $action = new ReplaceSymbolsInCollectionAction(); - - $collection = TypesCollection::create(); - - $collection[] = FakeTransformedType::fake('Enum')->withNamespace('enums'); - $collection[] = FakeTransformedType::fake('Dto') - ->withTransformed('{enum: {%enums\Enum%}, non-existing: {%non-existing%}}') - ->withMissingSymbols([ - 'enum' => 'enums\Enum', - 'non-existing' => 'non-existing', - ]); - - $collection = $action->execute($collection, false); - - assertEquals('{enum: Enum, non-existing: any}', $collection['Dto']->transformed); -}); diff --git a/tests/Actions/ReplaceSymbolsInTypeActionTest.php b/tests/Actions/ReplaceSymbolsInTypeActionTest.php deleted file mode 100644 index 3991fa6b..00000000 --- a/tests/Actions/ReplaceSymbolsInTypeActionTest.php +++ /dev/null @@ -1,104 +0,0 @@ -collection = TypesCollection::create(); - - $this->action = new ReplaceSymbolsInTypeAction($this->collection); -}); - -it('can replace symbols', function () { - $typeC = FakeTransformedType::fake('C') - ->isInline() - ->withTransformed('This is type C'); - - $typeB = FakeTransformedType::fake('B') - ->isInline() - ->withMissingSymbols(['C' => 'C']) - ->withTransformed('Depends on type C: {%C%}'); - - $typeA = FakeTransformedType::fake('A') - ->isInline() - ->withMissingSymbols(['B' => 'B']) - ->withTransformed("Depends on type B: {%B%}"); - - $this->collection[] = $typeA; - $this->collection[] = $typeB; - $this->collection[] = $typeC; - - $transformed = $this->action->execute($typeA); - - assertEquals('Depends on type B: Depends on type C: This is type C', $transformed); - assertEquals('Depends on type C: This is type C', $this->collection['B']->transformed); - assertEquals('This is type C', $this->collection['C']->transformed); -}); - -it('will throw an exception when doing circular dependencies', function () { - $this->expectException(CircularDependencyChain::class); - - $typeA = FakeTransformedType::fake('A') - ->isInline() - ->withMissingSymbols(['B' => 'B']) - ->withTransformed("Depends on type B: {%B%}"); - - $typeB = FakeTransformedType::fake('B') - ->isInline() - ->withMissingSymbols(['A' => 'A']) - ->withTransformed('Depends on type A: {%A%}'); - - $this->collection[] = $typeA; - $this->collection[] = $typeB; - - $this->action->execute($typeA); -}); - -it('can replace non inline types circular', function () { - $typeB = FakeTransformedType::fake('B') - ->withMissingSymbols(['A' => 'A']) - ->withTransformed('Links to A: {%A%}'); - - $typeA = FakeTransformedType::fake('A') - ->withMissingSymbols(['B' => 'B']) - ->withTransformed('Links to B: {%B%}'); - - $this->collection[] = $typeA; - $this->collection[] = $typeB; - - $transformedA = $this->action->execute($typeA); - $transformedB = $this->action->execute($typeB); - - assertEquals('Links to B: B', $transformedA); - assertEquals('Links to A: A', $transformedB); -}); - -it('can inline multiple dependencies', function () { - $typeC = FakeTransformedType::fake('C') - ->isInline() - ->withTransformed('This is type C'); - - $typeB = FakeTransformedType::fake('B') - ->isInline() - ->withMissingSymbols(['C' => 'C']) - ->withTransformed('Depends on type C: {%C%}'); - - $typeA = FakeTransformedType::fake('A') - ->isInline() - ->withMissingSymbols(['B' => 'B', 'C' => 'C']) - ->withTransformed('Depends on type B: {%B%} | depends on type C: {%C%}'); - - $this->collection[] = $typeA; - $this->collection[] = $typeB; - $this->collection[] = $typeC; - - $transformed = $this->action->execute($typeA); - - assertEquals( - 'Depends on type B: Depends on type C: This is type C | depends on type C: This is type C', - $transformed - ); -}); diff --git a/tests/Actions/ResolveClassesInPhpFileActionTest.php b/tests/Actions/ResolveClassesInPhpFileActionTest.php deleted file mode 100644 index 4924f471..00000000 --- a/tests/Actions/ResolveClassesInPhpFileActionTest.php +++ /dev/null @@ -1,37 +0,0 @@ -action = new ResolveClassesInPhpFileAction(); -}); - -it('can find classes', function () { - assertEquals([SomeClass::class,], $this->action->execute( - new SplFileInfo(__DIR__ . '/../FakeClasses/Finder/SomeClass.php', '', '') - )); -}); - -it('can find interfaces', function () { - assertEquals([SomeInterface::class,], $this->action->execute( - new SplFileInfo(__DIR__ . '/../FakeClasses/Finder/SomeInterface.php', '', '') - )); -}); - -it('can find traits', function () { - assertEquals([SomeTrait::class,], $this->action->execute( - new SplFileInfo(__DIR__ . '/../FakeClasses/Finder/SomeTrait.php', '', '') - )); -}); - -it('can find enums', function () { - assertEquals([SomeEnum::class,], $this->action->execute( - new SplFileInfo(__DIR__.'./../FakeClasses/Finder/SomeEnum.php', '', '') - )); -}); diff --git a/tests/Actions/ResolveRelativePathActionTest.php b/tests/Actions/ResolveRelativePathActionTest.php new file mode 100644 index 00000000..b5786a5d --- /dev/null +++ b/tests/Actions/ResolveRelativePathActionTest.php @@ -0,0 +1,44 @@ +execute( + $current, + $requested + ))->toBe($expected); +})->with( + [ + [ + [], + [], + null, + ], + [ + ['a', 'b', 'c'], + ['a', 'b', 'c'], + null, + ], + [ + ['a', 'b', 'c'], + ['a', 'd', 'e'], + './../../d/e', + ], + [ + ['a', 'b', 'c', 'd'], + ['a', 'd', 'e'], + './../../../d/e', + ], + [ + ['a', 'b', 'c'], + ['a', 'd', 'e', 'f'], + './../../d/e/f', + ], + [ + ['a'], + ['b'], + './../b', + ], + ] +); diff --git a/tests/Actions/ResolveTypesCollectionActionTest.php b/tests/Actions/ResolveTypesCollectionActionTest.php deleted file mode 100644 index 91be8326..00000000 --- a/tests/Actions/ResolveTypesCollectionActionTest.php +++ /dev/null @@ -1,126 +0,0 @@ -action = new ResolveTypesCollectionAction( - new Finder(), - TypeScriptTransformerConfig::create() - ->autoDiscoverTypes(__DIR__ . '/../FakeClasses/Enum') - ->transformers([MyclabsEnumTransformer::class]) - ->collectors([DefaultCollector::class]) - ->outputFile('types.d.ts') - ); -}); - -it('will construct the type collection correctly', function () { - $typesCollection = $this->action->execute(); - - assertCount(3, $typesCollection); -}); - -it('will check if auto discover types paths are defined', function () { - $this->expectException(NoAutoDiscoverTypesPathsDefined::class); - - $action = new ResolveTypesCollectionAction( - new Finder(), - TypeScriptTransformerConfig::create() - ); - - $action->execute(); -}); - -it('parses a typescript enum correctly', function () { - $type = $this->action->execute()[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 () { - $type = $this->action->execute()[TypeScriptEnumWithName::class]; - - 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 () { - $type = $this->action->execute()[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 ResolveTypesCollectionAction( - new Finder(), - TypeScriptTransformerConfig::create() - ->autoDiscoverTypes( - __DIR__ . '/../FakeClasses/Enum/', - __DIR__ . '/../FakeClasses/Integration/' - ) - ->transformers([MyclabsEnumTransformer::class, DtoTransformer::class]) - ->collectors([DefaultCollector::class]) - ->outputFile('types.d.ts') - ); - - $types = $this->action->execute(); - - 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 an collector for types', function () { - $this->action = new ResolveTypesCollectionAction( - new Finder(), - TypeScriptTransformerConfig::create() - ->autoDiscoverTypes(__DIR__ . '/../FakeClasses/Enum') - ->collectors([FakeTypeScriptCollector::class]) - ->outputFile('types.d.ts') - ); - - $types = $this->action->execute(); - - assertCount(4, $types); - assertArrayHasKey(RegularEnum::class, $types); - assertArrayHasKey(TypeScriptEnum::class, $types); - assertArrayHasKey(TypeScriptEnumWithCustomTransformer::class, $types); - assertArrayHasKey(TypeScriptEnumWithName::class, $types); -}); diff --git a/tests/Actions/TranspilePhpStanTypeToTypeScriptTypeActionTest.php b/tests/Actions/TranspilePhpStanTypeToTypeScriptTypeActionTest.php new file mode 100644 index 00000000..7f32795d --- /dev/null +++ b/tests/Actions/TranspilePhpStanTypeToTypeScriptTypeActionTest.php @@ -0,0 +1,248 @@ +execute( + $docTypeResolver->property(new ReflectionProperty(PhpDocTypesStub::class, $property))->type, + new ReflectionClass(PhpDocTypesStub::class) + ); + + expect($typeScriptNode)->toBeInstanceOf($expectedTypeScriptNode::class); + expect($typeScriptNode)->toEqual($expectedTypeScriptNode); +})->with(function () { + yield [ + 'string', + new TypeScriptString(), + ]; + + yield [ + 'bool', + new TypeScriptBoolean(), + ]; + + yield [ + 'boolean', + new TypeScriptBoolean(), + ]; + + yield [ + 'int', + new TypeScriptNumber(), + ]; + + yield [ + 'integer', + new TypeScriptNumber(), + ]; + + yield [ + 'float', + new TypeScriptNumber(), + ]; + + yield [ + 'double', + new TypeScriptNumber(), + ]; + + yield [ + 'mixed', + new TypeScriptAny(), + ]; + + yield [ + 'void', + new TypeScriptVoid(), + ]; + + yield [ + 'callable', + new TypeScriptFunction(), + ]; + + yield [ + 'false', + new TypeScriptBoolean(), + ]; + + yield [ + 'true', + new TypeScriptBoolean(), + ]; + + yield [ + 'null', + new TypeScriptNull(), + ]; + + yield [ + 'nullable', + new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNull(), + ]), + ]; + + yield [ + 'union', + new TypeScriptUnion([ + new TypeScriptNumber(), + new TypeScriptString(), + ]), + ]; + + yield [ + 'intersection', + new TypeScriptIntersection([ + new TypeScriptNumber(), + new TypeScriptString(), + ]), + ]; + + yield [ + 'bnf', + new TypeScriptUnion([ + new TypeScriptIntersection([ + new TypeScriptNumber(), + new TypeScriptString(), + ]), + new TypeScriptNull(), + ]), + ]; + + yield [ + 'self', + new TypeReference(new ClassStringReference(PhpDocTypesStub::class)), + ]; + + yield [ + 'static', + new TypeReference(new ClassStringReference(PhpDocTypesStub::class)), + ]; + + yield [ + 'parent', + new TypeScriptUnknown(), + ]; + + yield [ + 'object', + new TypeScriptObject([]), + ]; + + yield [ + 'objectShape', + new TypeScriptObject([ + new TypeScriptProperty('a', new TypeScriptNumber()), + new TypeScriptProperty('b', new TypeScriptNumber()), + new TypeScriptProperty('c', new TypeScriptNumber()), + new TypeScriptProperty('d', new TypeScriptNumber(), isOptional: true), + ]), + ]; + + yield [ + 'array', + new TypeScriptArray(null), + ]; + + yield [ + 'arrayGeneric', + new TypeScriptArray(new TypeScriptString()), + ]; + + yield [ + 'arrayGenericWithKey', + new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [ + new TypeScriptNumber(), + new TypeScriptString(), + ] + ), + ]; + + yield [ + 'typeArray', + new TypeScriptArray(new TypeScriptString()), + ]; + + yield [ + 'nestedArray', + new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [ + new TypeScriptNumber(), + new TypeScriptArray(new TypeScriptString()), + ] + ), + ]; + + yield [ + 'arrayShape', + new TypeScriptObject([ + new TypeScriptProperty('a', new TypeScriptNumber()), + new TypeScriptProperty('b', new TypeScriptNumber()), + new TypeScriptProperty('c', new TypeScriptNumber()), + new TypeScriptProperty('d', new TypeScriptNumber(), isOptional: true), + ]), + ]; + + yield [ + 'classString', + new TypeScriptString(), + ]; + + yield [ + 'classStringGeneric', + new TypeScriptString(), + ]; + + yield [ + 'reference', + new TypeReference(new ClassStringReference(Collection::class)), + ]; + + yield [ + 'referenceWithImport', + new TypeReference(new ClassStringReference(Collection::class)), + ]; + + yield [ + 'generic', + new TypeScriptGeneric( + new TypeReference(new ClassStringReference(Collection::class)), + [ + new TypeScriptNumber(), + new TypeScriptString(), + ] + ), + ]; +}); diff --git a/tests/Actions/TranspileReflectionTypeToTypeScriptTypeActionTest.php b/tests/Actions/TranspileReflectionTypeToTypeScriptTypeActionTest.php new file mode 100644 index 00000000..8a08aa7c --- /dev/null +++ b/tests/Actions/TranspileReflectionTypeToTypeScriptTypeActionTest.php @@ -0,0 +1,140 @@ +execute( + (new ReflectionProperty(PhpTypesStub::class, $property))->getType(), + new ReflectionClass(PhpTypesStub::class) + ); + + expect($typeScriptNode)->toBeInstanceOf($expectedTypeScriptNode::class); + expect($typeScriptNode)->toEqual($expectedTypeScriptNode); +})->with(function () { + yield [ + 'string', + new TypeScriptString(), + ]; + + yield [ + 'bool', + new TypeScriptBoolean(), + ]; + + yield [ + 'int', + new TypeScriptNumber(), + ]; + + yield [ + 'float', + new TypeScriptNumber(), + ]; + + yield [ + 'mixed', + new TypeScriptAny(), + ]; + + yield [ + 'false', + new TypeScriptBoolean(), + ]; + + yield [ + 'true', + new TypeScriptBoolean(), + ]; + + yield [ + 'null', + new TypeScriptNull(), + ]; + + yield [ + 'nullable', + new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNull(), + ]), + ]; + + yield [ + 'union', + new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNumber(), + ]), + ]; + + yield [ + 'intersection', + new TypeScriptIntersection([ + new TypeReference(new ClassStringReference(Collection::class)), + new TypeReference(new ClassStringReference(Arrayable::class)), + ]), + ]; + + yield [ + 'bnf', + new TypeScriptUnion([ + new TypeScriptIntersection([ + new TypeReference(new ClassStringReference(Collection::class)), + new TypeReference(new ClassStringReference(Arrayable::class)), + ]), + new TypeScriptNull(), + ]), + ]; + + yield [ + 'self', + new TypeReference(new ClassStringReference(PhpTypesStub::class)), + ]; + + // @todo figure out this one +// yield [ +// 'static', +// new TypeReference(new ClassStringReference(PhpTypesStub::class)), +// ]; + + yield [ + 'parent', + new TypeScriptUnknown(), + ]; + + yield [ + 'object', + new TypeScriptObject([]), + ]; + + yield [ + 'array', + new TypeScriptArray(null), + ]; + + yield [ + 'reference', + new TypeReference(new ClassStringReference(Collection::class)), + ]; +}); diff --git a/tests/Actions/TranspileTypeToTypeScriptActionTest.php b/tests/Actions/TranspileTypeToTypeScriptActionTest.php deleted file mode 100644 index 55db7f1b..00000000 --- a/tests/Actions/TranspileTypeToTypeScriptActionTest.php +++ /dev/null @@ -1,58 +0,0 @@ -missingSymbols = new MissingSymbolsCollection(); - - $this->typeResolver = new TypeResolver(); - - $this->action = new TranspileTypeToTypeScriptAction( - $this->missingSymbols, - 'fake_class' - ); -}); - -it('can resolve types', function (string $input, string $output) { - $resolved = $this->action->execute( - $this->typeResolver->resolve($input), - ); - - assertEquals($output, $resolved); -})->with('types'); - -it('can resolve self referencing types without current class', function () { - $action = new TranspileTypeToTypeScriptAction($this->missingSymbols); - - assertEquals('any', $action->execute(new Self_())); - assertEquals('any', $action->execute(new Static_())); - assertEquals('any', $action->execute(new This())); -}); - -it('can resolve a struct type', function () { - $transformed = $this->action->execute(StructType::fromArray([ - 'a_string' => 'string', - 'a_float' => 'float', - 'a_class' => RegularEnum::class, - 'an_array' => 'int[]', - 'a_self_reference' => '$this', - 'an_object' => [ - 'a_bool' => 'bool', - 'an_int' => 'int', - ], - ])); - - assertMatchesSnapshot($transformed); - assertContains(RegularEnum::class, $this->missingSymbols->all()); - assertContains('fake_class', $this->missingSymbols->all()); -}); diff --git a/tests/Actions/__snapshots__/TranspileTypeToTypeScriptActionTest__it_can_resolve_a_struct_type__1.txt b/tests/Actions/__snapshots__/TranspileTypeToTypeScriptActionTest__it_can_resolve_a_struct_type__1.txt deleted file mode 100644 index d15eb552..00000000 --- a/tests/Actions/__snapshots__/TranspileTypeToTypeScriptActionTest__it_can_resolve_a_struct_type__1.txt +++ /dev/null @@ -1 +0,0 @@ -{a_string:string;a_float:number;a_class:{%Spatie\TypeScriptTransformer\Tests\FakeClasses\Enum\RegularEnum%};an_array:Array;a_self_reference:{%fake_class%};an_object:{a_bool:boolean;an_int:number;};} \ No newline at end of file diff --git a/tests/Actions/__snapshots__/files/FormatTypeScriptActionTest__it_can_disable_formatting__1.ts b/tests/Actions/__snapshots__/files/FormatTypeScriptActionTest__it_can_disable_formatting__1.ts deleted file mode 100644 index 0144c8c2..00000000 --- a/tests/Actions/__snapshots__/files/FormatTypeScriptActionTest__it_can_disable_formatting__1.ts +++ /dev/null @@ -1 +0,0 @@ -export type Enum='yes'|'no';export type OtherDto={name:string} \ No newline at end of file diff --git a/tests/Actions/__snapshots__/files/FormatTypeScriptActionTest__it_can_format_an_generated_file__1.ts b/tests/Actions/__snapshots__/files/FormatTypeScriptActionTest__it_can_format_an_generated_file__1.ts deleted file mode 100644 index d212a8a6..00000000 --- a/tests/Actions/__snapshots__/files/FormatTypeScriptActionTest__it_can_format_an_generated_file__1.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type Enum = "yes" | "no"; -export type OtherDto = { name: string }; diff --git a/tests/Actions/__snapshots__/files/FormatTypeScriptActionTest__it_can_format_an_generated_file__1.ts_failed.ts b/tests/Actions/__snapshots__/files/FormatTypeScriptActionTest__it_can_format_an_generated_file__1.ts_failed.ts deleted file mode 100644 index 440cf18b..00000000 --- a/tests/Actions/__snapshots__/files/FormatTypeScriptActionTest__it_can_format_an_generated_file__1.ts_failed.ts +++ /dev/null @@ -1 +0,0 @@ -formatted \ No newline at end of file diff --git a/tests/Actions/__snapshots__/files/PersistTypesCollectionActionTest__it_can_persist_multiple_types_in_one_namespace__1.ts b/tests/Actions/__snapshots__/files/PersistTypesCollectionActionTest__it_can_persist_multiple_types_in_one_namespace__1.ts deleted file mode 100644 index 81568689..00000000 --- a/tests/Actions/__snapshots__/files/PersistTypesCollectionActionTest__it_can_persist_multiple_types_in_one_namespace__1.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare namespace test { -export type Enum = transformed test\Enum; -export type OtherEnum = transformed test\OtherEnum; -} -export type Enum = transformed Enum; -export type OtherEnum = transformed OtherEnum; \ No newline at end of file diff --git a/tests/Actions/__snapshots__/files/PersistTypesCollectionActionTest__it_can_re_save_the_file__1.ts b/tests/Actions/__snapshots__/files/PersistTypesCollectionActionTest__it_can_re_save_the_file__1.ts deleted file mode 100644 index 039e88c9..00000000 --- a/tests/Actions/__snapshots__/files/PersistTypesCollectionActionTest__it_can_re_save_the_file__1.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare namespace test { -export type Enum = fake-transformed; -} -export type Enum = fake-transformed; \ No newline at end of file diff --git a/tests/Actions/__snapshots__/files/PersistTypesCollectionActionTest__it_will_persist_the_types__1.ts b/tests/Actions/__snapshots__/files/PersistTypesCollectionActionTest__it_will_persist_the_types__1.ts deleted file mode 100644 index 8fe4cf80..00000000 --- a/tests/Actions/__snapshots__/files/PersistTypesCollectionActionTest__it_will_persist_the_types__1.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare namespace test { -export type Enum = fake-transformed; -} -declare namespace test.test { -export type Enum = fake-transformed; -} -export type Enum = fake-transformed; \ No newline at end of file diff --git a/tests/Attributes/TransformAsTypescriptTest.php b/tests/Attributes/TransformAsTypescriptTest.php deleted file mode 100644 index 3ff59192..00000000 --- a/tests/Attributes/TransformAsTypescriptTest.php +++ /dev/null @@ -1,30 +0,0 @@ -getType()); - assertEquals('string|int', (string) $attribute->getType()); -}); - -it('can create the attribute from an array', function () { - $attribute = new TypeScriptType([ - 'a_string' => 'string', - 'a_float' => 'float', - 'a_class' => RegularEnum::class, - 'an_array' => 'int[]', - 'an_object' => [ - 'a_bool' => 'bool', - 'an_int' => 'int', - ], - ]); - - assertInstanceOf(StructType::class, $attribute->getType()); -}); diff --git a/tests/ClassReaderTest.php b/tests/ClassReaderTest.php deleted file mode 100644 index 150d1174..00000000 --- a/tests/ClassReaderTest.php +++ /dev/null @@ -1,78 +0,0 @@ -reader = new ClassReader(); -}); - -it('non transformable case', function () { - $fake = new class { - }; - - ['transformable' => $transformable] = $this->reader->forClass( - new ReflectionClass($fake) - ); - - assertFalse($transformable); -}); - -it('default case', function () { - /** - * @typescript - */ - $fake = new class { - }; - - ['name' => $name] = $this->reader->forClass( - new ReflectionClass($fake) - ); - - assertStringContainsString('class@anonymous', $name); -}); - -it('default file case', function () { - /** - * @typescript OtherEnum - */ - $fake = new class { - }; - - ['name' => $name] = $this->reader->forClass( - new ReflectionClass($fake) - ); - - assertEquals('OtherEnum', $name); -}); - -it('will resolve the transformer', function () { - /** - * @typescript-transformer \Spatie\TypeScriptTransformer\Transformers\MyclabsEnumTransformer - */ - $fake = new class { - }; - - assertEquals('\\' . MyclabsEnumTransformer::class, $this->reader->forClass( - new ReflectionClass($fake) - )['transformer']); -}); - -it('inline case', function () { - /** - * @typescript - * @typescript-inline - */ - $fake = new class { - }; - - ['inline' => $inline] = $this->reader->forClass( - new ReflectionClass($fake) - ); - - assertTrue($inline); -}); diff --git a/tests/Collectors/DefaultCollectorTest.php b/tests/Collectors/DefaultCollectorTest.php deleted file mode 100644 index 50b4f445..00000000 --- a/tests/Collectors/DefaultCollectorTest.php +++ /dev/null @@ -1,204 +0,0 @@ -config = TypeScriptTransformerConfig::create()->transformers([ - MyclabsEnumTransformer::class, - ]); - - $this->collector = new DefaultCollector($this->config); -}); - -it('will not collect non annotated classes', function () { - $class = new class('a') extends Enum { - const A = 'a'; - }; - - $reflection = new ReflectionClass( - $class - ); - - assertNull($this->collector->getTransformedType($reflection)); -}); - -it('will collect annotated classes', function () { - /** @typescript */ - $class = new class('a') extends Enum { - const A = 'a'; - }; - - $reflection = new ReflectionClass( - $class - ); - - $transformedType = $this->collector->getTransformedType($reflection); - - assertNotNull($transformedType); - assertEquals( - "'a' | 'yes' | 'no'", - $transformedType->transformed, - ); -}); - -it('will collect annotated classes and use the given name', function () { - /** @typescript EnumTransformed */ - $class = new class('a') extends Enum { - const A = 'a'; - }; - - $reflection = new ReflectionClass( - $class - ); - - $transformedType = $this->collector->getTransformedType($reflection); - - assertNotNull($transformedType); - assertEquals('EnumTransformed', $transformedType->name); - assertEquals( - "'a' | 'yes' | 'no'", - $transformedType->transformed, - ); -}); - -it('will read overwritten transformers', function () { - /** - * @typescript DtoTransformed - * @typescript-transformer \Spatie\TypeScriptTransformer\Transformers\DtoTransformer - */ - $class = new class('a') extends Enum { - const A = 'a'; - - public int $an_integer; - }; - - $reflection = new ReflectionClass( - $class - ); - - $transformedType = $this->collector->getTransformedType($reflection); - - assertNotNull($transformedType); - assertEquals('DtoTransformed', $transformedType->name); - assertEquals( - '{'.PHP_EOL.'an_integer: number;'.PHP_EOL.'}', - $transformedType->transformed, - ); -}); - -it('will throw an exception if a transformer is not found', function () { - /** @typescript */ - $class = new class { - }; - - $reflection = new ReflectionClass( - $class - ); - - $this->collector->getTransformedType($reflection); -})->throws(TransformerNotFound::class); - -it('will collect classes with attributes', function () { - $reflection = new ReflectionClass(WithTypeScriptAttribute::class); - - $transformedType = $this->collector->getTransformedType($reflection); - - assertNotNull($transformedType); - assertEquals('WithTypeScriptAttribute', $transformedType->name); - assertEquals( - "'a' | 'b'", - $transformedType->transformed, - ); -}); - -it('will collect attribute overwritten transformers', function () { - $reflection = new ReflectionClass(WithTypeScriptTransformerAttribute::class); - - $transformedType = $this->collector->getTransformedType($reflection); - - assertNotNull($transformedType); - assertEquals('WithTypeScriptTransformerAttribute', $transformedType->name); - assertEquals( - '{'.PHP_EOL.'an_int: number;'.PHP_EOL.'}', - $transformedType->transformed, - ); -}); - -it('will collect classes with already transformed attributes', function () { - $reflection = new ReflectionClass(WithAlreadyTransformedAttributeAttribute::class); - - $transformedType = $this->collector->getTransformedType($reflection); - - assertNotNull($transformedType); - assertEquals( - '{an_int:number;a_bool:boolean;}', - $transformedType->transformed, - ); -}); - -it('can inline collected classes with annotations', function () { - $reflection = new ReflectionClass(WithTypeScriptInlineAttribute::class); - - $transformedType = $this->collector->getTransformedType($reflection); - - assertNotNull($transformedType); - assertTrue($transformedType->isInline); -}); - -it('can inline collected classes with attributes', function () { - /** - * @typescript - * @typescript-inline - */ - $class = new class('a') extends Enum { - const A = 'a'; - }; - - $transformedType = $this->collector->getTransformedType(new ReflectionClass($class)); - - assertNotNull($transformedType); - assertTrue($transformedType->isInline); -}); - -it('will will throw an exception with non existing transformers', function () { - $this->expectException(InvalidTransformerGiven::class); - $this->expectExceptionMessage("does not exist!"); - - /** - * @typescript DtoTransformed - * @typescript-transformer FAKE - */ - $class = new class('a') extends Enum { - const A = 'a'; - - public int $an_integer; - }; - - $this->collector->getTransformedType(new ReflectionClass($class)); -}); - -it('will will throw an exception with class that does not implement transformer', function () { - $this->expectException(InvalidTransformerGiven::class); - $this->expectExceptionMessage("does not implement the Transformer interface!"); - - /** - * @typescript-transformer \Spatie\TypeScriptTransformer\Structures\TransformedType - */ - $class = new class { - }; - - $this->collector->getTransformedType(new ReflectionClass($class)); -}); diff --git a/tests/Collectors/EnumCollectorTest.php b/tests/Collectors/EnumCollectorTest.php deleted file mode 100644 index 223245b7..00000000 --- a/tests/Collectors/EnumCollectorTest.php +++ /dev/null @@ -1,26 +0,0 @@ -transformers([ - EnumTransformer::class, - ]) - ); - - $reflection = new ReflectionClass(BackedEnumWithoutAnnotation::class); - $transformedType = $collector->getTransformedType($reflection); - - assertNotNull($transformedType); - assertEquals( - "'foo' | 'bar'", - $transformedType->transformed, - ); -})->skip(version_compare(PHP_VERSION, '8.1', '<'), 'Enums are a PHP 8.1+ feature.'); diff --git a/tests/Datasets/ReflectionClasses.php b/tests/Datasets/ReflectionClasses.php deleted file mode 100644 index c272a5a0..00000000 --- a/tests/Datasets/ReflectionClasses.php +++ /dev/null @@ -1,102 +0,0 @@ - [ - 'reflection' => $reflection = FakeReflectionClass::create(), - 'transformable' => false, - 'inline' => false, - 'name' => $reflection->getName(), - 'transformer' => null, - 'type' => null, - ]; - - yield '@typescript annotation' => [ - 'reflection' => $reflection = FakeReflectionClass::create()->withDocComment('/** @typescript */'), - 'transformable' => true, - 'inline' => false, - 'name' => $reflection->getName(), - 'transformer' => null, - 'type' => null, - ]; - - yield '@typescript annotation with name' => [ - 'reflection' => $reflection = FakeReflectionClass::create()->withDocComment('/** @typescript YoloClass */'), - 'transformable' => true, - 'inline' => false, - 'name' => 'YoloClass', - 'transformer' => null, - 'type' => null, - ]; - - yield '@typescript annotation with transformer' => [ - 'reflection' => $reflection = FakeReflectionClass::create()->withDocComment('/** @typescript @typescript-transformer FakeTransformer */'), - 'transformable' => true, - 'inline' => false, - 'name' => $reflection->getName(), - 'transformer' => 'FakeTransformer', - 'type' => null, - ]; - - yield '@typescript annotation with inline' => [ - 'reflection' => $reflection = FakeReflectionClass::create()->withDocComment('/** @typescript @typescript-inline */'), - 'transformable' => true, - 'inline' => true, - 'name' => $reflection->getName(), - 'transformer' => null, - 'type' => null, - ]; - - yield 'TypeScript attribute' => [ - 'reflection' => new ReflectionClass(WithTypeScriptAttribute::class), - 'transformable' => true, - 'inline' => false, - 'name' => 'WithTypeScriptAttribute', - 'transformer' => null, - 'type' => null, - ]; - - yield 'TypeScript attribute with name' => [ - 'reflection' => new ReflectionClass(WithTypeScriptNamedAttribute::class), - 'transformable' => true, - 'inline' => false, - 'name' => 'YoloClass', - 'transformer' => null, - 'type' => null, - ]; - - yield 'TypeScript inline attribute' => [ - 'reflection' => new ReflectionClass(WithTypeScriptInlineAttribute::class), - 'transformable' => true, - 'inline' => true, - 'name' => 'WithTypeScriptInlineAttribute', - 'transformer' => null, - 'type' => null, - ]; - - yield 'TypeScript transformer attribute' => [ - 'reflection' => new ReflectionClass(WithTypeScriptTransformerAttribute::class), - 'transformable' => true, - 'inline' => false, - 'name' => 'WithTypeScriptTransformerAttribute', - 'transformer' => DtoTransformer::class, - 'type' => null, - ]; - - yield 'TypeScript already transformed attribute' => [ - 'reflection' => new ReflectionClass(WithAlreadyTransformedAttributeAttribute::class), - 'transformable' => true, - 'inline' => false, - 'name' => 'WithAlreadyTransformedAttributeAttribute', - 'transformer' => null, - 'type' => StructType::fromArray(['an_int' => 'int', 'a_bool' => 'bool']), - ]; -}); diff --git a/tests/Datasets/Types.php b/tests/Datasets/Types.php deleted file mode 100644 index b87f898c..00000000 --- a/tests/Datasets/Types.php +++ /dev/null @@ -1,94 +0,0 @@ -'], - - // Arrays - ['string[]', 'Array'], - ['string[]|Array', 'Array'], - ['(string|integer)[]', 'Array'], - ['Array', 'Array'], - - // Objects - ['Array', '{ [key: number]: string }'], - ['Array', '{ [key: string]: number }'], - ['Array', '{ [key: string]: number | boolean }'], - - // Null - ['?string', 'string | null'], - ['?string[]', 'Array | null'], - - // Objects - [Enum::class, '{%' . Enum::class . '%}'], - [Enum::class . '[]', 'Array<{%' . Enum::class . '%}>'], - - // Simple - ['string', 'string'], - ['boolean', 'boolean'], - ['integer', 'number'], - ['double', 'number'], - ['float', 'number'], - ['class-string<' . Enum::class . '>', 'string'], - ['null', 'null'], - ['object', 'object'], - ['array', 'Array'], - - // references - ['self', '{%fake_class%}'], - ['static', '{%fake_class%}'], - ['$this', '{%fake_class%}'], - - // Scalar - ['scalar', 'string|number|boolean'], - - // Mixed - ['mixed', 'any'], - - // Collections - ['Collection', 'Array'], -]); - -dataset('docblock_types', [ - ['int', 'int'], - ['bool', 'bool'], - ['string', 'string'], - ['float', 'float'], - ['mixed', 'mixed'], - ['array', 'array'], - - ['bool|int', 'bool|int'], - ['?int', '?int'], - ['int[]', 'int[]'], -]); - -dataset('reflection_types', [ - ['int', true, 'int'], - ['bool', true, 'bool'], - ['mixed', true, 'mixed'], - ['string', true, 'string'], - ['float', true, 'float'], - ['array', true, 'array'], - - [Enum::class, false, '\\' . Enum::class], -]); - -dataset('ignored_types', [ - ['int', 'int', 'int'], - ['int|array', 'array', 'int|array'], - ['int[]', 'array', 'int[]'], - ['?int[]', 'array', '?int[]'], -]); - -dataset('nullified_types', [ - ['', '?int'], - ['?int', '?int'], - ['int', '?int'], - ['array|int', 'array|int|null'], - ['array|int|null', 'array|int|null'], - ['mixed', 'mixed'], -]); diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php new file mode 100644 index 00000000..5d363218 --- /dev/null +++ b/tests/ExampleTest.php @@ -0,0 +1,5 @@ +toBeTrue(); +}); diff --git a/tests/FakeClasses/Annotations/FakeAnnotationsClass.php b/tests/FakeClasses/Annotations/FakeAnnotationsClass.php deleted file mode 100644 index 97b8657f..00000000 --- a/tests/FakeClasses/Annotations/FakeAnnotationsClass.php +++ /dev/null @@ -1,17 +0,0 @@ - 'int', 'a_bool' => 'bool'])] -class WithAlreadyTransformedAttributeAttribute -{ -} diff --git a/tests/FakeClasses/Attributes/WithTypeScriptAttribute.php b/tests/FakeClasses/Attributes/WithTypeScriptAttribute.php deleted file mode 100644 index 6becfeac..00000000 --- a/tests/FakeClasses/Attributes/WithTypeScriptAttribute.php +++ /dev/null @@ -1,13 +0,0 @@ - */ - public $mixed_with_array; - - /** @var array */ - public $array_with_null; - - public Enum $enum; - - public RegularEnum $non_typescripted_type; - - public OtherDto $other_dto; - - /** @var \Spatie\TypeScriptTransformer\Tests\FakeClasses\Integration\OtherDto[] */ - public array $other_dto_array; - - public OtherDtoCollection $other_dto_collection; - - public DtoWithChildren $dto_with_children; - - public YetAnotherDto $another_namespace_dto; - - /** @var string|int */ - public ?string $nullable_string; - - public DateTime $reflection_replaced_default_type; - - /** @var \DateTime */ - public $docblock_replaced_default_type; - - /** @var \DateTime[] */ - public array $array_replaced_default_type; - - /** @var array */ - public array $array_as_object; -} diff --git a/tests/FakeClasses/Integration/DtoWithChildren.php b/tests/FakeClasses/Integration/DtoWithChildren.php deleted file mode 100644 index 072b67e4..00000000 --- a/tests/FakeClasses/Integration/DtoWithChildren.php +++ /dev/null @@ -1,16 +0,0 @@ - 'Draft', - 'published' => 'Published', - 'archived' => 'Archived', - ]; - } -} diff --git a/tests/FakeClasses/StringBackedEnum.php b/tests/FakeClasses/StringBackedEnum.php deleted file mode 100644 index 865c2b22..00000000 --- a/tests/FakeClasses/StringBackedEnum.php +++ /dev/null @@ -1,12 +0,0 @@ -withNamespace = $namespace; - - return $this; - } - - public function withoutNamespace(): self - { - $this->withNamespace = ''; - - return $this; - } - - public function getNamespaceName(): string - { - return $this->withNamespace ?: parent::getNamespaceName(); - } - - public function getName(): string - { - $name = $this->entityName ?? parent::getShortName(); - - return empty($this->getNamespaceName()) - ? $name - : "{$this->getNamespaceName()}\\{$name}"; - } -} diff --git a/tests/Fakes/FakeReflectionMethod.php b/tests/Fakes/FakeReflectionMethod.php deleted file mode 100644 index aa4369b0..00000000 --- a/tests/Fakes/FakeReflectionMethod.php +++ /dev/null @@ -1,10 +0,0 @@ -type = $type; - - return $this; - } - - public function withIsBuiltIn(bool $isBuiltIn = true): self - { - $this->isBuiltIn = $isBuiltIn; - - return $this; - } - - public function withAllowsNull(bool $allowsNull = true): self - { - $this->allowsNull = $allowsNull; - - return $this; - } - - public function getName(): string - { - return $this->type; - } - - public function isBuiltin(): bool - { - return $this->isBuiltIn; - } - - public function allowsNull(): bool - { - return $this->allowsNull; - } - - public function __toString() - { - return $this->getName(); - } -} diff --git a/tests/Fakes/FakeReflectionUnionType.php b/tests/Fakes/FakeReflectionUnionType.php deleted file mode 100644 index 0db43f0b..00000000 --- a/tests/Fakes/FakeReflectionUnionType.php +++ /dev/null @@ -1,43 +0,0 @@ -types = array_merge($this->types, $type); - - return $this; - } - - public function getTypes(): array - { - return $this->types; - } - - public function allowsNull(): bool - { - foreach ($this->types as $type) { - if ($type->allowsNull()) { - return true; - } - } - - return false; - } -} diff --git a/tests/Fakes/FakeTransformedType.php b/tests/Fakes/FakeTransformedType.php deleted file mode 100644 index 80fc4f41..00000000 --- a/tests/Fakes/FakeTransformedType.php +++ /dev/null @@ -1,78 +0,0 @@ -withName($name), - $name, - 'fake-transformed', - new MissingSymbolsCollection(), - false - ); - } - - public function withReflection(ReflectionClass $reflection): self - { - $this->reflection = $reflection; - - return $this; - } - - public function withNamespace(string $namespace): self - { - $this->reflection->withNamespace($namespace); - - return $this; - } - - public function withoutNamespace(): self - { - $this->reflection->withoutNamespace(); - - return $this; - } - - public function withTransformed(string $transformed): self - { - $this->transformed = $transformed; - - return $this; - } - - public function withMissingSymbols(array $missingSymbols): self - { - foreach ($missingSymbols as $missingSymbol) { - $this->missingSymbols->add($missingSymbol); - } - - return $this; - } - - public function isInline(bool $isInline = true): self - { - $this->isInline = $isInline; - - return $this; - } -} diff --git a/tests/Fakes/FakeTypeScriptCollector.php b/tests/Fakes/FakeTypeScriptCollector.php deleted file mode 100644 index 1e646870..00000000 --- a/tests/Fakes/FakeTypeScriptCollector.php +++ /dev/null @@ -1,28 +0,0 @@ -getName(), Enum::class); - } - - public function getTransformedType(ReflectionClass $class): TransformedType - { - return new TransformedType( - $class, - $class->getShortName(), - 'fake-collected-class', - new MissingSymbolsCollection(), - false - ); - } -} diff --git a/tests/Fakes/FakeTypeScriptTransformer.php b/tests/Fakes/FakeTypeScriptTransformer.php deleted file mode 100644 index 38093ba5..00000000 --- a/tests/Fakes/FakeTypeScriptTransformer.php +++ /dev/null @@ -1,30 +0,0 @@ -isSubclassOf(Enum::class); - } - - public function transform(ReflectionClass $class, string $name): TransformedType - { - return FakeTransformedType::fake($name) - ->withReflection($class) - ->withTransformed($this->transformed); - } -} diff --git a/tests/Fakes/FakedReflection.php b/tests/Fakes/FakedReflection.php deleted file mode 100644 index d0eedf80..00000000 --- a/tests/Fakes/FakedReflection.php +++ /dev/null @@ -1,77 +0,0 @@ -docComment = $docComment; - - return $this; - } - - public function withName(string $name): self - { - $this->entityName = $name; - - return $this; - } - - public function withType(FakeReflectionType | ReflectionType $type): self - { - $this->type = $type; - - return $this; - } - - public function getDocComment(): string|false - { - if ($this->docComment === '') { - return false; - } - - return $this->docComment; - } - - public function getName(): string - { - if ($this->entityName === null) { - return ''; - } - - return $this->entityName; - } - - public function getType(): null | ReflectionType | ReflectionUnionType | FakeReflectionType - { - if ($this->type === null) { - return null; - } - - return $this->type; - } - - public function getAttributes(?string $name = null, int $flags = 0): array - { - return []; - } -} diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php deleted file mode 100644 index 74bbd345..00000000 --- a/tests/IntegrationTest.php +++ /dev/null @@ -1,57 +0,0 @@ -autoDiscoverTypes(__DIR__ . '/FakeClasses/Integration') - ->defaultTypeReplacements([ - DateTime::class => 'string', - ]) - ->transformers([ - MyclabsEnumTransformer::class, - DtoTransformer::class, - ]) - ->collectors([ - DefaultCollector::class, - ]); -} - -it('works', function () { - $temporaryDirectory = (new TemporaryDirectory())->create(); - - $transformer = new TypeScriptTransformer( - getTransformerConfig() - ->outputFile($temporaryDirectory->path('types.d.ts')) - ); - - $transformer->transform(); - - $transformed = file_get_contents($temporaryDirectory->path('types.d.ts')); - - assertMatchesSnapshot($transformed); -}); - -it('can transform to es modules', function () { - $temporaryDirectory = (new TemporaryDirectory())->create(); - - $transformer = new TypeScriptTransformer( - getTransformerConfig() - ->writer(ModuleWriter::class) - ->outputFile($temporaryDirectory->path('types.ts')) - ); - - $transformer->transform(); - - $transformed = file_get_contents($temporaryDirectory->path('types.ts')); - - assertMatchesSnapshot($transformed); -}); diff --git a/tests/Pest.php b/tests/Pest.php index 93c205cb..b3d9bbc7 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,19 +1 @@ withoutNamespace(); - - assertCount(1, $structure); - assertEquals([ - 'Enum' => $fake, - ], iterator_to_array($structure)); -}); - -it('can add types in a multi layered namespaces', function () { - $structure = TypesCollection::create(); - - $structure[] = $fakeC = FakeTransformedType::fake('Enum')->withNamespace('a\b\c'); - $structure[] = $fakeB = FakeTransformedType::fake('Enum')->withNamespace('a\b'); - $structure[] = $fakeA = FakeTransformedType::fake('Enum')->withNamespace('a'); - $structure[] = $fake = FakeTransformedType::fake('Enum')->withoutNamespace(); - - assertCount(4, $structure); - assertEquals([ - 'Enum' => $fake, - 'a\Enum' => $fakeA, - 'a\b\Enum' => $fakeB, - 'a\b\c\Enum' => $fakeC, - ], iterator_to_array($structure)); -}); - -it('can add multiple types to one namespace', function () { - $structure = TypesCollection::create(); - - $structure[] = $fakeA = FakeTransformedType::fake('EnumA')->withNamespace('test'); - $structure[] = $fakeB = FakeTransformedType::fake('EnumB')->withNamespace('test'); - - assertCount(2, $structure); - assertEquals([ - 'test\EnumA' => $fakeA, - 'test\EnumB' => $fakeB, - ], iterator_to_array($structure)); -}); - -it('can add a real type', function () { - $reflection = new ReflectionClass(TypeScriptEnum::class); - - $structure = TypesCollection::create(); - - $structure[] = $fake = FakeTransformedType::fake('TypeScriptEnum')->withReflection($reflection); - - assertCount(1, $structure); - assertEquals([ - TypeScriptEnum::class => $fake, - ], iterator_to_array($structure)); -}); - -it('cannot have a namespace and type with the same name', function () { - $collection = TypesCollection::create(); - - $collection[] = $fakeA = FakeTransformedType::fake('Enum')->withNamespace('Enum'); - $collection[] = $fakeB = FakeTransformedType::fake('Enum')->withoutNamespace(); -})->throws(SymbolAlreadyExists::class); - -it('cannot have a namespace and type with the same name reversed', function () { - $collection = TypesCollection::create(); - - $collection[] = $fakeB = FakeTransformedType::fake('Enum')->withoutNamespace(); - $collection[] = $fakeA = FakeTransformedType::fake('Enum')->withNamespace('Enum'); -})->throws(SymbolAlreadyExists::class); - -it('can get a type', function () { - $collection = TypesCollection::create(); - - $collection[] = $fake = FakeTransformedType::fake('Enum')->withNamespace('a\b\c'); - - assertEquals($fake, $collection['a\b\c\Enum']); -}); - -it('can get a type in the root namespace', function () { - $collection = TypesCollection::create(); - - $collection[] = $fake = FakeTransformedType::fake('Enum')->withoutNamespace(); - - assertEquals($fake, $collection['Enum']); -}); - -it('when searching a non existing type null is returned', function () { - $collection = TypesCollection::create(); - - assertNull($collection['Enum']); - assertNull($collection['a\b\Enum']); - assertNull($collection['a\b\Enum']); -}); - -it('can add inline types without structure checking', function () { - $collection = TypesCollection::create(); - - $collection[] = $fakeA = FakeTransformedType::fake('Enum')->withoutNamespace()->isInline(); - $collection[] = $fakeB = FakeTransformedType::fake('Enum')->withNamespace('Enum'); - - assertEquals($fakeA, $collection['Enum']); - assertEquals($fakeB, $collection['Enum\Enum']); -}); diff --git a/tests/Stubs/PhpDocTypesStub.php b/tests/Stubs/PhpDocTypesStub.php new file mode 100644 index 00000000..89d3c296 --- /dev/null +++ b/tests/Stubs/PhpDocTypesStub.php @@ -0,0 +1,108 @@ + */ + public $arrayGeneric; + + /** @var array */ + public $arrayGenericWithKey; + + /** @var string[] */ + public $typeArray; + + /** @var array> */ + public $nestedArray; + + /** @var array{a: int, 'b': int, "c": int, d?: int} */ + public $arrayShape; + + /** @var class-string */ + public $classString; + + /** @var class-string */ + public $classStringGeneric; + + /** @var \Illuminate\Support\Collection */ + public $reference; + + /** @var Collection */ + public $referenceWithImport; + + /** @var Collection */ + public $generic; +} diff --git a/tests/Stubs/PhpTypesStub.php b/tests/Stubs/PhpTypesStub.php new file mode 100644 index 00000000..8c0f611b --- /dev/null +++ b/tests/Stubs/PhpTypesStub.php @@ -0,0 +1,46 @@ +defaultTypeReplacements([ - DateTime::class => 'string', - ]); - - $this->transformer = new DtoTransformer($config); -}); - -it('will replace types', function () { - $type = $this->transformer->transform( - new ReflectionClass(Dto::class), - 'Typed' - ); - - assertMatchesTextSnapshot($type->transformed); - assertEquals([ - Enum::class, - RegularEnum::class, - OtherDto::class, - DtoWithChildren::class, - YetAnotherDto::class, - ], $type->missingSymbols->all()); -}); - -it('a type processor can remove properties', function () { - $config = TypeScriptTransformerConfig::create(); - - $transformer = new class($config) extends DtoTransformer { - protected function typeProcessors(): array - { - $onlyStringPropertiesProcessor = new class implements TypeProcessor { - public function process( - Type $type, - ReflectionProperty | ReflectionParameter | ReflectionMethod $reflection, - MissingSymbolsCollection $missingSymbolsCollection - ): ?Type { - return $type instanceof String_ ? $type : null; - } - }; - - return [$onlyStringPropertiesProcessor]; - } - }; - - $type = $transformer->transform( - new ReflectionClass(Dto::class), - 'Typed' - ); - - assertMatchesTextSnapshot($type->transformed); -}); - -it('will take transform as typescript attributes into account', function () { - $class = new class { - #[TypeScriptType('int')] - public $int; - - #[TypeScriptType('int|bool')] - public int $overwritable; - - #[TypeScriptType(['an_int' => 'int', 'a_bool' => 'bool'])] - public $object; - - #[LiteralTypeScriptType('never')] - public $pure_typescript; - - #[LiteralTypeScriptType(['an_any' => 'any', 'a_never' => 'never'])] - public $pure_typescript_object; - - public int $regular_type; - }; - - $type = $this->transformer->transform( - new ReflectionClass($class), - 'Typed' - ); - - assertMatchesSnapshot($type->transformed); -}); - -it('transforms properties to optional ones when using optional attribute', function () { - $class = new class { - #[Optional] - public string $string; - }; - - $type = $this->transformer->transform( - new ReflectionClass($class), - 'Typed' - ); - - assertMatchesSnapshot($type->transformed); -}); - -it('transforms all properties of a class with optional attribute to optional', function () { - #[Optional] - class DummyOptionalDto - { - public string $string; - public int $int; - } - - $type = $this->transformer->transform( - new ReflectionClass(DummyOptionalDto::class), - 'Typed' - ); - - assertMatchesSnapshot($type->transformed); -}); - - -it('transforms properties to hidden ones when using hidden attribute', function () { - $class = new class() { - public string $visible; - #[Hidden] - public string $hidden; - }; - - $type = $this->transformer->transform( - new ReflectionClass($class), - 'Typed' - ); - - assertMatchesSnapshot($type->transformed); -}); diff --git a/tests/Transformers/EnumTransformerTest.php b/tests/Transformers/EnumTransformerTest.php deleted file mode 100644 index 6c0c4501..00000000 --- a/tests/Transformers/EnumTransformerTest.php +++ /dev/null @@ -1,115 +0,0 @@ -markTestSkipped('Native enums not supported before PHP 8.1'); - } -}); - -it('will only convert enums', function () { - $transformer = new EnumTransformer( - TypeScriptTransformerConfig::create()->transformToNativeEnums(false) - ); - - assertNotNull($transformer->transform( - new ReflectionClass(StringBackedEnum::class), - 'Enum', - )); - - assertNull($transformer->transform( - new ReflectionClass(DateTime::class), - 'Enum', - )); -}); - -it('does not transform a unit enum', function () { - $transformer = new EnumTransformer( - TypeScriptTransformerConfig::create()->transformToNativeEnums(false) - ); - - $type = $transformer->transform( - new ReflectionClass(UnitEnum::class), - 'Enum' - ); - - assertNull($type); -}); - -it('can transform a backed enum into enum', function () { - $transformer = new EnumTransformer( - TypeScriptTransformerConfig::create()->transformToNativeEnums(true) - ); - - $type = $transformer->transform( - new ReflectionClass(StringBackedEnum::class), - 'Enum' - ); - - assertEquals("'JS' = 'js', 'PHP' = 'php'", $type->transformed); - assertTrue($type->missingSymbols->isEmpty()); - assertFalse($type->isInline); - assertEquals('enum', $type->keyword); -}); - -it('can transform a backed enum into a union', function () { - $transformer = new EnumTransformer( - TypeScriptTransformerConfig::create()->transformToNativeEnums(false) - ); - - $type = $transformer->transform( - new ReflectionClass(StringBackedEnum::class), - 'Enum' - ); - - assertEquals("'js' | 'php'", $type->transformed); - assertTrue($type->missingSymbols->isEmpty()); - assertFalse($type->isInline); - assertEquals('type', $type->keyword); -}); - -it('can transform a backed enum with integers into an enm', function () { - $transformer = new EnumTransformer( - TypeScriptTransformerConfig::create()->transformToNativeEnums(true) - ); - - $type = $transformer->transform( - new ReflectionClass(IntBackedEnum::class), - 'Enum' - ); - - assertEquals("'JS' = 1, 'PHP' = 2", $type->transformed); - assertTrue($type->missingSymbols->isEmpty()); - assertFalse($type->isInline); - assertEquals('enum', $type->keyword); -}); - -it('can transform a backed enum with integers into a union', function () { - $transformer = new EnumTransformer( - TypeScriptTransformerConfig::create()->transformToNativeEnums(false) - ); - - $type = $transformer->transform( - new ReflectionClass(IntBackedEnum::class), - 'Enum' - ); - - assertEquals("1 | 2", $type->transformed); - assertTrue($type->missingSymbols->isEmpty()); - assertFalse($type->isInline); - assertEquals('type', $type->keyword); -}); diff --git a/tests/Transformers/InterfaceTransformerTest.php b/tests/Transformers/InterfaceTransformerTest.php deleted file mode 100644 index 784599dc..00000000 --- a/tests/Transformers/InterfaceTransformerTest.php +++ /dev/null @@ -1,37 +0,0 @@ -transform( - new ReflectionClass(DateTimeInterface::class), - 'State', - )); - - assertNull($transformer->transform( - new ReflectionClass(DateTime::class), - 'State', - )); -}); - -it('will replace methods', function () { - $transformer = new InterfaceTransformer( - TypeScriptTransformerConfig::create() - ); - - $type = $transformer->transform( - new ReflectionClass(FakeInterface::class), - 'State', - ); - - assertMatchesTextSnapshot($type->transformed); -}); diff --git a/tests/Transformers/MyclabsEnumTransformerTest.php b/tests/Transformers/MyclabsEnumTransformerTest.php deleted file mode 100644 index d4e05003..00000000 --- a/tests/Transformers/MyclabsEnumTransformerTest.php +++ /dev/null @@ -1,60 +0,0 @@ -transformToNativeEnums(false) - ); - - $enum = new class('view') extends Enum { - private const VIEW = 'view'; - private const EDIT = 'edit'; - }; - - $noEnum = new class { - }; - - assertNotNull($transformer->transform(new ReflectionClass($enum), 'Enum')); - assertNull($transformer->transform(new ReflectionClass($noEnum), 'Enum')); -}); - -it('can transform an enum into a type', function () { - $transformer = new MyclabsEnumTransformer( - TypeScriptTransformerConfig::create()->transformToNativeEnums(false) - ); - - $enum = new class('view') extends Enum { - private const VIEW = 'view'; - private const EDIT = 'edit'; - }; - - $type = $transformer->transform(new ReflectionClass($enum), 'Enum'); - - assertEquals("'view' | 'edit'", $type->transformed); - assertTrue($type->missingSymbols->isEmpty()); - assertEquals('type', $type->keyword); -}); - -it('can transform an enum into an enum', function () { - $transformer = new MyclabsEnumTransformer( - TypeScriptTransformerConfig::create()->transformToNativeEnums(true) - ); - - $enum = new class('view') extends Enum { - private const VIEW = 'view'; - private const EDIT = 'edit'; - }; - - $type = $transformer->transform(new ReflectionClass($enum), 'Enum'); - - assertEquals("'VIEW' = 'view', 'EDIT' = 'edit'", $type->transformed); - assertTrue($type->missingSymbols->isEmpty()); - assertEquals('enum', $type->keyword); -}); diff --git a/tests/Transformers/SpatieEnumTransformerTest.php b/tests/Transformers/SpatieEnumTransformerTest.php deleted file mode 100644 index ed8d0519..00000000 --- a/tests/Transformers/SpatieEnumTransformerTest.php +++ /dev/null @@ -1,58 +0,0 @@ -transformToNativeEnums(false) - ); - - assertNotNull($transformer->transform( - new ReflectionClass(SpatieEnum::class), - 'State', - )); - - assertNull($transformer->transform( - new ReflectionClass(DateTime::class), - 'State', - )); -}); - -it('can transform an enum into a type', function () { - $transformer = new SpatieEnumTransformer( - TypeScriptTransformerConfig::create()->transformToNativeEnums(false) - ); - - $type = $transformer->transform( - new ReflectionClass(SpatieEnum::class), - 'FakeEnum' - ); - - assertEquals("'draft' | 'published' | 'archived'", $type->transformed); - assertTrue($type->missingSymbols->isEmpty()); - assertFalse($type->isInline); - assertEquals('type', $type->keyword); -}); - -it('can transform an enum into an enum', function () { - $transformer = new SpatieEnumTransformer( - TypeScriptTransformerConfig::create()->transformToNativeEnums(true) - ); - - $type = $transformer->transform( - new ReflectionClass(SpatieEnum::class), - 'FakeEnum' - ); - - assertEquals("'draft' = 'Draft', 'published' = 'Published', 'archived' = 'Archived'", $type->transformed); - assertTrue($type->missingSymbols->isEmpty()); - assertFalse($type->isInline); - assertEquals('enum', $type->keyword); -}); diff --git a/tests/Transformers/__snapshots__/DtoTransformerTest__a_property_processor_can_remove_properties__1.txt b/tests/Transformers/__snapshots__/DtoTransformerTest__a_property_processor_can_remove_properties__1.txt deleted file mode 100644 index d0fe530f..00000000 --- a/tests/Transformers/__snapshots__/DtoTransformerTest__a_property_processor_can_remove_properties__1.txt +++ /dev/null @@ -1 +0,0 @@ -{string: string;default: string;documented_string: string;} diff --git a/tests/Transformers/__snapshots__/DtoTransformerTest__a_type_processor_can_remove_properties__1.txt b/tests/Transformers/__snapshots__/DtoTransformerTest__a_type_processor_can_remove_properties__1.txt deleted file mode 100644 index 1cc389ad..00000000 --- a/tests/Transformers/__snapshots__/DtoTransformerTest__a_type_processor_can_remove_properties__1.txt +++ /dev/null @@ -1,5 +0,0 @@ -{ -string: string; -default: string; -documented_string: string; -} \ No newline at end of file diff --git a/tests/Transformers/__snapshots__/DtoTransformerTest__it_transforms_nullable_properties_to_optional_ones_according_to_config__1.txt b/tests/Transformers/__snapshots__/DtoTransformerTest__it_transforms_nullable_properties_to_optional_ones_according_to_config__1.txt deleted file mode 100644 index dbf55ed0..00000000 --- a/tests/Transformers/__snapshots__/DtoTransformerTest__it_transforms_nullable_properties_to_optional_ones_according_to_config__1.txt +++ /dev/null @@ -1,3 +0,0 @@ -{ -string?: string; -} \ No newline at end of file diff --git a/tests/Transformers/__snapshots__/DtoTransformerTest__it_transforms_nullable_properties_to_optional_ones_when_using_optional_attribute__1.txt b/tests/Transformers/__snapshots__/DtoTransformerTest__it_transforms_nullable_properties_to_optional_ones_when_using_optional_attribute__1.txt deleted file mode 100644 index dbf55ed0..00000000 --- a/tests/Transformers/__snapshots__/DtoTransformerTest__it_transforms_nullable_properties_to_optional_ones_when_using_optional_attribute__1.txt +++ /dev/null @@ -1,3 +0,0 @@ -{ -string?: string; -} \ No newline at end of file diff --git a/tests/Transformers/__snapshots__/DtoTransformerTest__it_will_replace_types__1.txt b/tests/Transformers/__snapshots__/DtoTransformerTest__it_will_replace_types__1.txt deleted file mode 100644 index 2c588550..00000000 --- a/tests/Transformers/__snapshots__/DtoTransformerTest__it_will_replace_types__1.txt +++ /dev/null @@ -1,28 +0,0 @@ -{ -string: string; -nullbable: string | null; -default: string; -int: number; -boolean: boolean; -float: number; -object: object; -array: Array; -none: any; -documented_string: string; -mixed: number | string; -documented_array: Array; -mixed_with_array: number | string | Array; -array_with_null: Array; -enum: {%Spatie\TypeScriptTransformer\Tests\FakeClasses\Integration\Enum%}; -non_typescripted_type: {%Spatie\TypeScriptTransformer\Tests\FakeClasses\Enum\RegularEnum%}; -other_dto: {%Spatie\TypeScriptTransformer\Tests\FakeClasses\Integration\OtherDto%}; -other_dto_array: Array<{%Spatie\TypeScriptTransformer\Tests\FakeClasses\Integration\OtherDto%}>; -other_dto_collection: Array<{%Spatie\TypeScriptTransformer\Tests\FakeClasses\Integration\OtherDto%}>; -dto_with_children: {%Spatie\TypeScriptTransformer\Tests\FakeClasses\Integration\DtoWithChildren%}; -another_namespace_dto: {%Spatie\TypeScriptTransformer\Tests\FakeClasses\Integration\LevelUp\YetAnotherDto%}; -nullable_string: string | number | null; -reflection_replaced_default_type: string; -docblock_replaced_default_type: string; -array_replaced_default_type: Array; -array_as_object: { [key: string]: any }; -} \ No newline at end of file diff --git a/tests/Transformers/__snapshots__/DtoTransformerTest__it_will_take_transform_as_typescript_attributes_into_account__1.txt b/tests/Transformers/__snapshots__/DtoTransformerTest__it_will_take_transform_as_typescript_attributes_into_account__1.txt deleted file mode 100644 index a2a03224..00000000 --- a/tests/Transformers/__snapshots__/DtoTransformerTest__it_will_take_transform_as_typescript_attributes_into_account__1.txt +++ /dev/null @@ -1,8 +0,0 @@ -{ -int: number; -overwritable: number | boolean; -object: {an_int:number;a_bool:boolean;}; -pure_typescript: never; -pure_typescript_object: {an_any:any;a_never:never;}; -regular_type: number; -} \ No newline at end of file diff --git a/tests/Transformers/__snapshots__/InterfaceTransformerTest__it_will_replace_methods__1.txt b/tests/Transformers/__snapshots__/InterfaceTransformerTest__it_will_replace_methods__1.txt deleted file mode 100644 index f7072e84..00000000 --- a/tests/Transformers/__snapshots__/InterfaceTransformerTest__it_will_replace_methods__1.txt +++ /dev/null @@ -1,4 +0,0 @@ -{ -testFunction(input: string, output: Array): number; -anotherTestFunction(): boolean; -} \ No newline at end of file diff --git a/tests/TypeProcessors/DtoCollectionTypeProcessorTest.php b/tests/TypeProcessors/DtoCollectionTypeProcessorTest.php deleted file mode 100644 index 3704fb12..00000000 --- a/tests/TypeProcessors/DtoCollectionTypeProcessorTest.php +++ /dev/null @@ -1,75 +0,0 @@ -typeResolver = new TypeResolver(); - - $this->processor = new DtoCollectionTypeProcessor(); -}); - -it('will process a dto collection', function () { - $type = $this->processor->process( - $this->typeResolver->resolve(DtoCollection::class), - FakeReflectionProperty::create(), - new MissingSymbolsCollection() - ); - - assertEquals( - '\Spatie\TypeScriptTransformer\Tests\FakeClasses\Integration\Dto[]', - (string) $type - ); -}); - -it('will process a nullable dto collection', function () { - $type = $this->processor->process( - $this->typeResolver->resolve(NullableDtoCollection::class), - FakeReflectionProperty::create(), - new MissingSymbolsCollection() - ); - - assertEquals( - '?\Spatie\TypeScriptTransformer\Tests\FakeClasses\Integration\Dto[]', - (string) $type - ); -}); - -it('will process a dto collection with built in type', function () { - $type = $this->processor->process( - $this->typeResolver->resolve(StringDtoCollection::class), - FakeReflectionProperty::create(), - new MissingSymbolsCollection() - ); - - assertEquals('string[]', (string) $type); -}); - -it('will process a dto collection without type', function () { - $type = $this->processor->process( - $this->typeResolver->resolve(UntypedDtoCollection::class), - FakeReflectionProperty::create(), - new MissingSymbolsCollection() - ); - - assertEquals(new Array_(new TypeScriptType('any')), $type); -}); - -it('will pass non dto collections', function () { - $type = $this->processor->process( - $this->typeResolver->resolve('string'), - FakeReflectionProperty::create(), - new MissingSymbolsCollection() - ); - - assertEquals('string', (string) $type); -}); diff --git a/tests/TypeProcessors/ProcessesTypesTest.php b/tests/TypeProcessors/ProcessesTypesTest.php deleted file mode 100644 index c0eb5695..00000000 --- a/tests/TypeProcessors/ProcessesTypesTest.php +++ /dev/null @@ -1,72 +0,0 @@ -walk($type, $closure); - } - }; - - $initialType = is_string($initialType) - ? (new TypeResolver())->resolve($initialType) - : $initialType; - - $expectedType = is_string($expectedType) - ? (new TypeResolver())->resolve($expectedType) - : $expectedType; - - $found = $processor->run($initialType, $closure); - - assertEquals($expectedType, $found); -} - -it('supports types', function () { - assertProcessed( - 'string', - 'string', - fn (Type $type) => $type, - ); - - assertProcessed( - null, - 'string', - fn (Type $type) => null, - ); - - assertProcessed( - 'Array', - 'Array', - fn (Type $type) => $type, - ); - - assertProcessed( - 'string', - 'string|int', - fn (Type $type) => $type instanceof Integer ? null : $type, - ); - - assertProcessed( - 'int[]', - 'int[]', - fn (Type $type) => $type, - ); - - assertProcessed( - 'Collection', - 'Collection', - fn (Type $type) => $type, - ); -}); diff --git a/tests/TypeProcessors/ReplaceDefaultsTypeProcessorTest.php b/tests/TypeProcessors/ReplaceDefaultsTypeProcessorTest.php deleted file mode 100644 index e21ae2e5..00000000 --- a/tests/TypeProcessors/ReplaceDefaultsTypeProcessorTest.php +++ /dev/null @@ -1,51 +0,0 @@ -typeResolver = new TypeResolver(); - - $this->processor = new ReplaceDefaultsTypeProcessor([ - DateTime::class => new String_(), - Dto::class => new TypeScriptType('array'), - ]); -}); - -it('can replace types', function () { - $type = $this->processor->process( - $this->typeResolver->resolve(Dto::class), - FakeReflectionProperty::create(), - new MissingSymbolsCollection() - ); - - assertEquals(new TypeScriptType('array'), $type); -}); - -it('can replace types as nullable', function () { - $type = $this->processor->process( - $this->typeResolver->resolve('?' . DateTime::class), - FakeReflectionProperty::create(), - new MissingSymbolsCollection() - ); - - assertEquals(new Nullable(new String_()), $type); -}); - -it('can replace types in arrays', function () { - $type = $this->processor->process( - $this->typeResolver->resolve(DateTime::class . '[]'), - FakeReflectionProperty::create(), - new MissingSymbolsCollection() - ); - - assertEquals(new Array_(new String_()), $type); -}); diff --git a/tests/TypeReflectors/ClassTypeReflectorTest.php b/tests/TypeReflectors/ClassTypeReflectorTest.php deleted file mode 100644 index 37800439..00000000 --- a/tests/TypeReflectors/ClassTypeReflectorTest.php +++ /dev/null @@ -1,23 +0,0 @@ -isTransformable()); - assertEquals($inline, $reflected->isInline()); - assertEquals($name, $reflected->getName()); - assertEquals($transformer, $reflected->getTransformerClass()); - assertEquals($type, $reflected->getType()); -})->with('reflection_classes'); diff --git a/tests/TypeReflectors/MethodParameterTypeReflectorTest.php b/tests/TypeReflectors/MethodParameterTypeReflectorTest.php deleted file mode 100644 index 5d8c41a7..00000000 --- a/tests/TypeReflectors/MethodParameterTypeReflectorTest.php +++ /dev/null @@ -1,116 +0,0 @@ -getParameters(); - - assertEquals( - 'int', - (string) MethodParameterTypeReflector::create($parameters[0])->reflect() - ); - - assertEquals( - '?int', - (string) MethodParameterTypeReflector::create($parameters[1])->reflect() - ); - - assertEquals( - 'int|float', - (string) MethodParameterTypeReflector::create($parameters[2])->reflect() - ); - - assertEquals( - 'int|float|null', - (string) MethodParameterTypeReflector::create($parameters[3])->reflect() - ); - - assertEquals( - 'any', - (string) MethodParameterTypeReflector::create($parameters[4])->reflect() - ); -}); - -it('can reflect from docblock', function () { - $class = new class { - /** - * @param int $int - * @param ?int $nullable_int - * @param int|float $union - * @param int|float|null $nullable_union - * @param array $array - * @param $without_type - */ - public function method( - $int, - $nullable_int, - $union, - $nullable_union, - $array, - $without_type - ) { - } - }; - - $parameters = (new ReflectionMethod($class, 'method'))->getParameters(); - - assertEquals( - 'int', - (string) MethodParameterTypeReflector::create($parameters[0])->reflect() - ); - - assertEquals( - '?int', - (string) MethodParameterTypeReflector::create($parameters[1])->reflect() - ); - - assertEquals( - 'int|float', - (string) MethodParameterTypeReflector::create($parameters[2])->reflect() - ); - - assertEquals( - 'int|float|null', - (string) MethodParameterTypeReflector::create($parameters[3])->reflect() - ); - - assertEquals( - 'array', - (string) MethodParameterTypeReflector::create($parameters[4])->reflect() - ); - - assertEquals( - 'any', - (string) MethodParameterTypeReflector::create($parameters[5])->reflect() - ); -}); - -it('cannot reflect from attribute', function () { - $class = new class { - #[LiteralTypeScriptType('int')] - public function method( - $int, - ) { - } - }; - - $parameters = (new ReflectionMethod($class, 'method'))->getParameters(); - - assertEquals( - 'any', - (string) MethodParameterTypeReflector::create($parameters[0])->reflect() - ); -}); diff --git a/tests/TypeReflectors/MethodReturnTypeReflectorTest.php b/tests/TypeReflectors/MethodReturnTypeReflectorTest.php deleted file mode 100644 index 532d6093..00000000 --- a/tests/TypeReflectors/MethodReturnTypeReflectorTest.php +++ /dev/null @@ -1,143 +0,0 @@ -reflect() - ); - - assertEquals( - '?int', - (string) MethodReturnTypeReflector::create(new ReflectionMethod($class, 'm2'))->reflect() - ); - - assertEquals( - 'int|float', - (string) MethodReturnTypeReflector::create(new ReflectionMethod($class, 'm3'))->reflect() - ); - - assertEquals( - 'int|float|null', - (string) MethodReturnTypeReflector::create(new ReflectionMethod($class, 'm4'))->reflect() - ); - - assertEquals( - 'any', - (string) MethodReturnTypeReflector::create(new ReflectionMethod($class, 'm5'))->reflect() - ); -}); - -it('can reflect from docblock', function () { - $class = new class { - /** @return int */ - public function m1() - { - return 42; - } - - /** @return ?int */ - public function m2() - { - return 42; - } - - /** @return int|float */ - public function m3() - { - return 42; - } - - /** @return int|float|null */ - public function m4() - { - return 42; - } - - public function m5() - { - return 42; - } - - /** @return array */ - public function m6() - { - return []; - } - }; - - assertEquals( - 'int', - (string) MethodReturnTypeReflector::create(new ReflectionMethod($class, 'm1'))->reflect() - ); - - assertEquals( - '?int', - (string) MethodReturnTypeReflector::create(new ReflectionMethod($class, 'm2'))->reflect() - ); - - assertEquals( - 'int|float', - (string) MethodReturnTypeReflector::create(new ReflectionMethod($class, 'm3'))->reflect() - ); - - assertEquals( - 'int|float|null', - (string) MethodReturnTypeReflector::create(new ReflectionMethod($class, 'm4'))->reflect() - ); - - assertEquals( - 'any', - (string) MethodReturnTypeReflector::create(new ReflectionMethod($class, 'm5'))->reflect() - ); - - assertEquals( - 'array', - (string) MethodReturnTypeReflector::create(new ReflectionMethod($class, 'm6'))->reflect() - ); -}); - -it('can reflect from attribute', function () { - $class = new class { - #[LiteralTypeScriptType('Integer')] - public function m1() - { - return 42; - } - }; - - assertEquals( - 'Integer', - (string) MethodReturnTypeReflector::create(new ReflectionMethod($class, 'm1'))->reflect() - ); -}); diff --git a/tests/TypeReflectors/PropertyTypeReflectorTest.php b/tests/TypeReflectors/PropertyTypeReflectorTest.php deleted file mode 100644 index dddaae76..00000000 --- a/tests/TypeReflectors/PropertyTypeReflectorTest.php +++ /dev/null @@ -1,103 +0,0 @@ -reflect() - ); - - assertEquals( - '?int', - (string) PropertyTypeReflector::create(new ReflectionProperty($class, 'p2'))->reflect() - ); - - assertEquals( - 'int|float', - (string) PropertyTypeReflector::create(new ReflectionProperty($class, 'p3'))->reflect() - ); - - assertEquals( - 'int|float|null', - (string) PropertyTypeReflector::create(new ReflectionProperty($class, 'p4'))->reflect() - ); - - assertEquals( - 'any', - (string) PropertyTypeReflector::create(new ReflectionProperty($class, 'p5'))->reflect() - ); -}); - -it('can reflect from docblock', function () { - $class = new class { - /** @var int */ - public $p1; - - /** @var ?int */ - public $p2; - - /** @var int|float */ - public $p3; - - /** @var int|float|null */ - public $p4; - - public $p5; - - /** @var array */ - public $p6; - }; - - assertEquals( - 'int', - (string) PropertyTypeReflector::create(new ReflectionProperty($class, 'p1'))->reflect() - ); - - assertEquals( - '?int', - (string) PropertyTypeReflector::create(new ReflectionProperty($class, 'p2'))->reflect() - ); - - assertEquals( - 'int|float', - (string) PropertyTypeReflector::create(new ReflectionProperty($class, 'p3'))->reflect() - ); - - assertEquals( - 'int|float|null', - (string) PropertyTypeReflector::create(new ReflectionProperty($class, 'p4'))->reflect() - ); - - assertEquals( - 'any', - (string) PropertyTypeReflector::create(new ReflectionProperty($class, 'p5'))->reflect() - ); - - assertEquals( - 'array', - (string) PropertyTypeReflector::create(new ReflectionProperty($class, 'p6'))->reflect() - ); -}); - -it('can reflect from attribute', function () { - $class = new class { - #[LiteralTypeScriptType('Integer')] - public $p1; - }; - - assertEquals( - 'Integer', - (string) PropertyTypeReflector::create(new ReflectionProperty($class, 'p1'))->reflect() - ); -}); diff --git a/tests/TypeReflectors/TypeReflectorTest.php b/tests/TypeReflectors/TypeReflectorTest.php deleted file mode 100644 index 8c65c4b0..00000000 --- a/tests/TypeReflectors/TypeReflectorTest.php +++ /dev/null @@ -1,125 +0,0 @@ -withDocComment("@var {$input}") - ); - - assertEquals($outputType, (string) $reflector->reflect()); -})->with('docblock_types'); - -it('will handle no docblock', function () { - $reflector = new PropertyTypeReflector( - FakeReflectionProperty::create() - ); - - assertEquals('any', (string) $reflector->reflect()); -}); - -it('can handle another non var docblock', function () { - $reflector = new PropertyTypeReflector( - FakeReflectionProperty::create()->withDocComment('@method bla') - ); - - assertEquals('any', (string) $reflector->reflect()); -}); - -it('can handle an incorrect docblock', function () { - $reflector = new PropertyTypeReflector( - FakeReflectionProperty::create()->withDocComment('@var int bool') - ); - - assertEquals('int', (string) $reflector->reflect()); -}); - -it('can resolve reflection types', function (string $input, bool $isBuiltIn, string $outputType) { - $reflection = FakeReflectionProperty::create()->withType( - FakeReflectionType::create()->withIsBuiltIn($isBuiltIn)->withType($input) - ); - - $reflector = new PropertyTypeReflector($reflection); - - assertEquals($outputType, (string) $reflector->reflect()); -})->with('reflection_types'); - -it('will ignore a reflected type if it is already in the docblock', function (string $reflection, string $docbloc, string $outputType) { - $reflection = FakeReflectionProperty::create() - ->withType(FakeReflectionType::create()->withType($reflection)) - ->withDocComment($docbloc); - - $reflector = new PropertyTypeReflector($reflection); - - assertEquals($outputType, (string) $reflector->reflect()); -})->with('ignored_types'); - -it('can only use reflection property for typing', function () { - $reflection = FakeReflectionProperty::create()->withType( - FakeReflectionType::create()->withIsBuiltIn(true)->withType('string') - ); - - $reflector = new PropertyTypeReflector($reflection); - - assertEquals('string', (string) $reflector->reflect()); -}); - -it('can nullify types based upon reflection', function (string $docbloc, string $outputType) { - $reflection = FakeReflectionProperty::create()->withType( - FakeReflectionType::create()->withType('int')->withAllowsNull() - )->withDocComment("@var {$docbloc}"); - - $reflector = new PropertyTypeReflector($reflection); - - assertEquals($outputType, (string) $reflector->reflect()); -})->with('nullified_types'); - -it('can use an union type with reflection', function () { - $reflection = FakeReflectionProperty::create()->withType( - FakeReflectionUnionType::create()->withType( - FakeReflectionType::create()->withType('int')->withAllowsNull(), - FakeReflectionType::create()->withType('float'), - ) - ); - - $reflector = new PropertyTypeReflector($reflection); - - assertEquals('int|float|null', (string) $reflector->reflect()); -}); - -it('can use a transformable attribute as type', function () { - $class = new class() { - #[LiteralTypeScriptType('EnumType[]')] - public $literal; - }; - - $reflection = new ReflectionProperty($class, 'literal'); - - $reflector = new PropertyTypeReflector($reflection); - - assertEquals('EnumType[]', (string) $reflector->reflect()); -}); - -it('can reflect docblocks without a complete fsqen', function () { - assertEquals( - '\\' . Dto::class, - (string) PropertyTypeReflector::create(new ReflectionProperty(FakeAnnotationsClass::class, 'property'))->reflect() - ); - - assertEquals( - '\\' . Dto::class, - (string) PropertyTypeReflector::create(new ReflectionProperty(FakeAnnotationsClass::class, 'fsqnProperty'))->reflect() - ); - - assertEquals( - '\\' . Dto::class . '[]', - (string) PropertyTypeReflector::create(new ReflectionProperty(FakeAnnotationsClass::class, 'arrayProperty'))->reflect() - ); -}); diff --git a/tests/TypeScriptTransformerConfigTest.php b/tests/TypeScriptTransformerConfigTest.php deleted file mode 100644 index 03a0cddd..00000000 --- a/tests/TypeScriptTransformerConfigTest.php +++ /dev/null @@ -1,70 +0,0 @@ -transformers([ - MyclabsEnumTransformer::class, - ]); - - assertEquals([new MyclabsEnumTransformer($config)], $config->getTransformers()); -}); - -it('can create transformers with constructor', function () { - $config = TypeScriptTransformerConfig::create()->transformers([ - DtoTransformer::class, - ]); - - assertEquals([new DtoTransformer($config)], $config->getTransformers()); -}); - -it('will check if a class property replacement class exists', function () { - $this->expectException(InvalidDefaultTypeReplacer::class); - - $config = TypeScriptTransformerConfig::create()->defaultTypeReplacements([ - 'fake-class' => 'string', - ]); - - $config->getDefaultTypeReplacements(); -}); - -it('can use a php type in a class property replacer', function () { - $config = TypeScriptTransformerConfig::create()->defaultTypeReplacements([ - DateTime::class => 'array', - ]); - - assertEquals( - [DateTime::class => new Array_(new String_(), new String_())], - $config->getDefaultTypeReplacements() - ); -}); - -it('can use a typescript type in a class property replacer', function () { - $config = TypeScriptTransformerConfig::create()->defaultTypeReplacements([ - Dto::class => new TypeScriptType('any'), - ]); - - assertEquals( - [Dto::class => new TypeScriptType('any')], - $config->getDefaultTypeReplacements() - ); -}); - -it('can use a php dodumenter type in a class property replacer', function () { - $config = TypeScriptTransformerConfig::create()->defaultTypeReplacements([ - Dto::class => new String_(), - ]); - - assertEquals( - [Dto::class => new String_()], - $config->getDefaultTypeReplacements() - ); -}); diff --git a/tests/Types/RecordTypeTest.php b/tests/Types/RecordTypeTest.php deleted file mode 100644 index 776a856b..00000000 --- a/tests/Types/RecordTypeTest.php +++ /dev/null @@ -1,46 +0,0 @@ -getKeyType()); - assertEquals(new Object_(new Fqsen('\\'.RegularEnum::class)), $record->getValueType()); -}); - -it('creates a scalar key and an struct value', function () { - $record = new RecordType('string', [ - 'enum' => RegularEnum::class, - 'array' => 'int[]', - ]); - - assertInstanceOf(RecordType::class, $record); - assertEquals(new String_(), $record->getKeyType()); - - assertInstanceOf(StructType::class, $record->getValueType()); - assertEquals([ - 'enum' => new Object_(new Fqsen('\\'.RegularEnum::class)), - 'array' => new Array_(new Integer()), - ], $record->getValueType()->getTypes()); -}); - -it('creates a scalar key and an array value', function () { - $record = new RecordType(RegularEnum::class, BackedEnumWithoutAnnotation::class, array: true); - - assertInstanceOf(RecordType::class, $record); - assertEquals(new Object_(new Fqsen('\\'.RegularEnum::class)), $record->getKeyType()); - assertEquals(new Array_(new Object_(new Fqsen('\\'.BackedEnumWithoutAnnotation::class))), $record->getValueType()); -}); diff --git a/tests/Types/StructTypeTest.php b/tests/Types/StructTypeTest.php deleted file mode 100644 index fcd96845..00000000 --- a/tests/Types/StructTypeTest.php +++ /dev/null @@ -1,38 +0,0 @@ - 'string', - 'a_float' => 'float', - 'a_class' => RegularEnum::class, - 'an_array' => 'int[]', - 'an_object' => [ - 'a_bool' => 'bool', - 'an_int' => 'int', - ], - ]); - - assertInstanceOf(StructType::class, $struct); - assertEquals([ - 'a_string' => new String_(), - 'a_float' => new Float_(), - 'a_class' => new Object_(new Fqsen('\\'.RegularEnum::class)), - 'an_array' => new Array_(new Integer()), - 'an_object' => new StructType([ - 'a_bool' => new Boolean(), - 'an_int' => new Integer(), - ]), - ], $struct->getTypes()); -}); diff --git a/tests/__snapshots__/DtoTransformerTest__a_type_processor_can_remove_properties__1.txt b/tests/__snapshots__/DtoTransformerTest__a_type_processor_can_remove_properties__1.txt deleted file mode 100644 index 1cc389ad..00000000 --- a/tests/__snapshots__/DtoTransformerTest__a_type_processor_can_remove_properties__1.txt +++ /dev/null @@ -1,5 +0,0 @@ -{ -string: string; -default: string; -documented_string: string; -} \ No newline at end of file diff --git a/tests/__snapshots__/DtoTransformerTest__it_a_type_processor_can_remove_properties__1.txt b/tests/__snapshots__/DtoTransformerTest__it_a_type_processor_can_remove_properties__1.txt deleted file mode 100644 index 1cc389ad..00000000 --- a/tests/__snapshots__/DtoTransformerTest__it_a_type_processor_can_remove_properties__1.txt +++ /dev/null @@ -1,5 +0,0 @@ -{ -string: string; -default: string; -documented_string: string; -} \ No newline at end of file diff --git a/tests/__snapshots__/DtoTransformerTest__it_transforms_all_properties_of_a_class_with_optional_attribute_to_optional__1.txt b/tests/__snapshots__/DtoTransformerTest__it_transforms_all_properties_of_a_class_with_optional_attribute_to_optional__1.txt deleted file mode 100644 index 99404a4d..00000000 --- a/tests/__snapshots__/DtoTransformerTest__it_transforms_all_properties_of_a_class_with_optional_attribute_to_optional__1.txt +++ /dev/null @@ -1,4 +0,0 @@ -{ -string?: string; -int?: number; -} \ No newline at end of file diff --git a/tests/__snapshots__/DtoTransformerTest__it_transforms_nullable_properties_to_optional_ones_when_using_optional_attribute__1.txt b/tests/__snapshots__/DtoTransformerTest__it_transforms_nullable_properties_to_optional_ones_when_using_optional_attribute__1.txt deleted file mode 100644 index dbf55ed0..00000000 --- a/tests/__snapshots__/DtoTransformerTest__it_transforms_nullable_properties_to_optional_ones_when_using_optional_attribute__1.txt +++ /dev/null @@ -1,3 +0,0 @@ -{ -string?: string; -} \ No newline at end of file diff --git a/tests/__snapshots__/DtoTransformerTest__it_transforms_properties_to_hidden_ones_when_using_hidden_attribute__1.txt b/tests/__snapshots__/DtoTransformerTest__it_transforms_properties_to_hidden_ones_when_using_hidden_attribute__1.txt deleted file mode 100644 index 80dc4741..00000000 --- a/tests/__snapshots__/DtoTransformerTest__it_transforms_properties_to_hidden_ones_when_using_hidden_attribute__1.txt +++ /dev/null @@ -1,3 +0,0 @@ -{ -visible: string; -} \ No newline at end of file diff --git a/tests/__snapshots__/DtoTransformerTest__it_transforms_properties_to_optional_ones_when_using_optional_attribute__1.txt b/tests/__snapshots__/DtoTransformerTest__it_transforms_properties_to_optional_ones_when_using_optional_attribute__1.txt deleted file mode 100644 index dbf55ed0..00000000 --- a/tests/__snapshots__/DtoTransformerTest__it_transforms_properties_to_optional_ones_when_using_optional_attribute__1.txt +++ /dev/null @@ -1,3 +0,0 @@ -{ -string?: string; -} \ No newline at end of file diff --git a/tests/__snapshots__/DtoTransformerTest__it_will_replace_types__1.txt b/tests/__snapshots__/DtoTransformerTest__it_will_replace_types__1.txt deleted file mode 100644 index 7528c1f2..00000000 --- a/tests/__snapshots__/DtoTransformerTest__it_will_replace_types__1.txt +++ /dev/null @@ -1,29 +0,0 @@ -{ -string: string; -nullbable: string | null; -default: string; -int: number; -boolean: boolean; -float: number; -object: object; -array: Array; -none: any; -documented_string: string; -mixed: number | string; -number: number; -documented_array: Array; -mixed_with_array: number | string | Array; -array_with_null: Array; -enum: {%Spatie\TypeScriptTransformer\Tests\FakeClasses\Integration\Enum%}; -non_typescripted_type: {%Spatie\TypeScriptTransformer\Tests\FakeClasses\Enum\RegularEnum%}; -other_dto: {%Spatie\TypeScriptTransformer\Tests\FakeClasses\Integration\OtherDto%}; -other_dto_array: Array<{%Spatie\TypeScriptTransformer\Tests\FakeClasses\Integration\OtherDto%}>; -other_dto_collection: Array<{%Spatie\TypeScriptTransformer\Tests\FakeClasses\Integration\OtherDto%}>; -dto_with_children: {%Spatie\TypeScriptTransformer\Tests\FakeClasses\Integration\DtoWithChildren%}; -another_namespace_dto: {%Spatie\TypeScriptTransformer\Tests\FakeClasses\Integration\LevelUp\YetAnotherDto%}; -nullable_string: string | number | null; -reflection_replaced_default_type: string; -docblock_replaced_default_type: string; -array_replaced_default_type: Array; -array_as_object: { [key: string]: any }; -} \ No newline at end of file diff --git a/tests/__snapshots__/DtoTransformerTest__it_will_take_transform_as_typescript_attributes_into_account__1.txt b/tests/__snapshots__/DtoTransformerTest__it_will_take_transform_as_typescript_attributes_into_account__1.txt deleted file mode 100644 index a2a03224..00000000 --- a/tests/__snapshots__/DtoTransformerTest__it_will_take_transform_as_typescript_attributes_into_account__1.txt +++ /dev/null @@ -1,8 +0,0 @@ -{ -int: number; -overwritable: number | boolean; -object: {an_int:number;a_bool:boolean;}; -pure_typescript: never; -pure_typescript_object: {an_any:any;a_never:never;}; -regular_type: number; -} \ No newline at end of file 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 deleted file mode 100644 index 1488b8ea..00000000 --- a/tests/__snapshots__/IntegrationTest__it_can_transform_to_es_modules__1.txt +++ /dev/null @@ -1,43 +0,0 @@ -export type Dto = { -string: string; -nullbable: string | null; -default: string; -int: number; -boolean: boolean; -float: number; -object: object; -array: Array; -none: any; -documented_string: string; -mixed: number | string; -number: number; -documented_array: Array; -mixed_with_array: number | string | Array; -array_with_null: Array; -enum: Enum; -non_typescripted_type: any; -other_dto: OtherDto; -other_dto_array: Array; -other_dto_collection: Array; -dto_with_children: DtoWithChildren; -another_namespace_dto: YetAnotherDto; -nullable_string: string | number | null; -reflection_replaced_default_type: string; -docblock_replaced_default_type: string; -array_replaced_default_type: Array; -array_as_object: { [key: string]: any }; -}; -export type DtoWithChildren = { -name: string; -other_dto: OtherDto; -other_dto_array: Array; -}; -export type Enum = 'yes' | 'no'; -export type OtherDto = { -name: string; -}; -export type OtherDtoCollection = { -}; -export type YetAnotherDto = { -name: string; -}; diff --git a/tests/__snapshots__/IntegrationTest__it_works__1.txt b/tests/__snapshots__/IntegrationTest__it_works__1.txt deleted file mode 100644 index f6f06697..00000000 --- a/tests/__snapshots__/IntegrationTest__it_works__1.txt +++ /dev/null @@ -1,47 +0,0 @@ -declare namespace Spatie.TypeScriptTransformer.Tests.FakeClasses.Integration { -export type Dto = { -string: string; -nullbable: string | null; -default: string; -int: number; -boolean: boolean; -float: number; -object: object; -array: Array; -none: any; -documented_string: string; -mixed: number | string; -number: number; -documented_array: Array; -mixed_with_array: number | string | Array; -array_with_null: Array; -enum: Spatie.TypeScriptTransformer.Tests.FakeClasses.Integration.Enum; -non_typescripted_type: any; -other_dto: Spatie.TypeScriptTransformer.Tests.FakeClasses.Integration.OtherDto; -other_dto_array: Array; -other_dto_collection: Array; -dto_with_children: Spatie.TypeScriptTransformer.Tests.FakeClasses.Integration.DtoWithChildren; -another_namespace_dto: Spatie.TypeScriptTransformer.Tests.FakeClasses.Integration.LevelUp.YetAnotherDto; -nullable_string: string | number | null; -reflection_replaced_default_type: string; -docblock_replaced_default_type: string; -array_replaced_default_type: Array; -array_as_object: { [key: string]: any }; -}; -export type DtoWithChildren = { -name: string; -other_dto: Spatie.TypeScriptTransformer.Tests.FakeClasses.Integration.OtherDto; -other_dto_array: Array; -}; -export type Enum = 'yes' | 'no'; -export type OtherDto = { -name: string; -}; -export type OtherDtoCollection = { -}; -} -declare namespace Spatie.TypeScriptTransformer.Tests.FakeClasses.Integration.LevelUp { -export type YetAnotherDto = { -name: string; -}; -} diff --git a/tests/__snapshots__/InterfaceTransformerTest__it_will_replace_methods__1.txt b/tests/__snapshots__/InterfaceTransformerTest__it_will_replace_methods__1.txt deleted file mode 100644 index f7072e84..00000000 --- a/tests/__snapshots__/InterfaceTransformerTest__it_will_replace_methods__1.txt +++ /dev/null @@ -1,4 +0,0 @@ -{ -testFunction(input: string, output: Array): number; -anotherTestFunction(): boolean; -} \ No newline at end of file diff --git a/tests/__snapshots__/TranspileTypeToTypeScriptActionTest__it_can_resolve_a_struct_type__1.txt b/tests/__snapshots__/TranspileTypeToTypeScriptActionTest__it_can_resolve_a_struct_type__1.txt deleted file mode 100644 index d15eb552..00000000 --- a/tests/__snapshots__/TranspileTypeToTypeScriptActionTest__it_can_resolve_a_struct_type__1.txt +++ /dev/null @@ -1 +0,0 @@ -{a_string:string;a_float:number;a_class:{%Spatie\TypeScriptTransformer\Tests\FakeClasses\Enum\RegularEnum%};an_array:Array;a_self_reference:{%fake_class%};an_object:{a_bool:boolean;an_int:number;};} \ No newline at end of file diff --git a/tests/__snapshots__/files/FormatTypeScriptActionTest__it_can_disable_formatting__1.ts b/tests/__snapshots__/files/FormatTypeScriptActionTest__it_can_disable_formatting__1.ts deleted file mode 100644 index 0144c8c2..00000000 --- a/tests/__snapshots__/files/FormatTypeScriptActionTest__it_can_disable_formatting__1.ts +++ /dev/null @@ -1 +0,0 @@ -export type Enum='yes'|'no';export type OtherDto={name:string} \ No newline at end of file diff --git a/tests/__snapshots__/files/PersistTypesCollectionActionTest__it_can_persist_multiple_types_in_one_namespace__1.ts b/tests/__snapshots__/files/PersistTypesCollectionActionTest__it_can_persist_multiple_types_in_one_namespace__1.ts deleted file mode 100644 index 81568689..00000000 --- a/tests/__snapshots__/files/PersistTypesCollectionActionTest__it_can_persist_multiple_types_in_one_namespace__1.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare namespace test { -export type Enum = transformed test\Enum; -export type OtherEnum = transformed test\OtherEnum; -} -export type Enum = transformed Enum; -export type OtherEnum = transformed OtherEnum; \ No newline at end of file diff --git a/tests/__snapshots__/files/PersistTypesCollectionActionTest__it_can_re_save_the_file__1.ts b/tests/__snapshots__/files/PersistTypesCollectionActionTest__it_can_re_save_the_file__1.ts deleted file mode 100644 index 039e88c9..00000000 --- a/tests/__snapshots__/files/PersistTypesCollectionActionTest__it_can_re_save_the_file__1.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare namespace test { -export type Enum = fake-transformed; -} -export type Enum = fake-transformed; \ No newline at end of file diff --git a/tests/__snapshots__/files/PersistTypesCollectionActionTest__it_will_persist_the_types__1.ts b/tests/__snapshots__/files/PersistTypesCollectionActionTest__it_will_persist_the_types__1.ts deleted file mode 100644 index 8fe4cf80..00000000 --- a/tests/__snapshots__/files/PersistTypesCollectionActionTest__it_will_persist_the_types__1.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare namespace test { -export type Enum = fake-transformed; -} -declare namespace test.test { -export type Enum = fake-transformed; -} -export type Enum = fake-transformed; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000..39e776f5 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +typescript@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" + integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== From 809cfac910540602e045a2e1b1c68137f66b0c56 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Tue, 4 Jul 2023 09:26:38 +0000 Subject: [PATCH 02/51] Fix styling --- src/Actions/AppendDefaultTypesAction.php | 3 +-- src/Actions/ConnectReferencesAction.php | 2 +- src/Actions/DiscoverTypesAction.php | 3 +-- src/Actions/FindClassNameFqcnAction.php | 5 ++--- src/Actions/FormatFilesAction.php | 3 +-- src/Actions/ParseUseDefinitionsAction.php | 1 - src/Actions/SplitTransformedPerLocationAction.php | 3 +-- src/Actions/TransformTypesAction.php | 1 - src/Actions/WriteTypesAction.php | 3 +-- src/Attributes/LiteralTypeScriptType.php | 4 ++-- src/Attributes/TypeScriptType.php | 4 ++-- src/Laravel/SpatieLaravelDefaultTypesProvider.php | 14 +++++++------- src/References/FunctionReference.php | 3 +-- src/Support/WritingContext.php | 2 +- src/Transformed/Transformed.php | 5 ++--- src/Transformers/ClassTransformer.php | 1 - src/Transformers/DataClassTransformer.php | 2 -- src/Transformers/EnumTransformer.php | 4 ++-- src/TypeScript/TypeScriptGeneric.php | 2 +- src/TypeScript/TypeScriptIdentifier.php | 3 +-- src/TypeScript/TypeScriptImport.php | 5 ++--- src/TypeScript/TypeScriptInterface.php | 5 ++--- src/TypeScript/TypeScriptMethod.php | 2 +- src/TypeScript/TypeScriptObject.php | 2 +- src/TypeScript/TypeScriptUnion.php | 2 +- src/TypeScriptTransformer.php | 1 - src/TypeScriptTransformerConfig.php | 2 +- src/Writers/ModuleWriter.php | 2 +- src/Writers/Writer.php | 1 - tests/Actions/ResolveRelativePathActionTest.php | 1 - ...ileReflectionTypeToTypeScriptTypeActionTest.php | 8 ++++---- tests/Stubs/PhpDocTypesStub.php | 6 +++--- tests/Stubs/PhpTypesStub.php | 4 +++- 33 files changed, 46 insertions(+), 63 deletions(-) diff --git a/src/Actions/AppendDefaultTypesAction.php b/src/Actions/AppendDefaultTypesAction.php index 02b54251..bde4ffde 100644 --- a/src/Actions/AppendDefaultTypesAction.php +++ b/src/Actions/AppendDefaultTypesAction.php @@ -16,8 +16,7 @@ public function __construct( } /** - * @param array $transformed - * + * @param array $transformed * @return array */ public function execute(array $transformed): array diff --git a/src/Actions/ConnectReferencesAction.php b/src/Actions/ConnectReferencesAction.php index ac8bf73d..5bcdf908 100644 --- a/src/Actions/ConnectReferencesAction.php +++ b/src/Actions/ConnectReferencesAction.php @@ -18,7 +18,7 @@ public function __construct( } /** - * @param array $transformed + * @param array $transformed */ public function execute(array $transformed): ReferenceMap { diff --git a/src/Actions/DiscoverTypesAction.php b/src/Actions/DiscoverTypesAction.php index 1d1c21ea..c866813e 100644 --- a/src/Actions/DiscoverTypesAction.php +++ b/src/Actions/DiscoverTypesAction.php @@ -12,8 +12,7 @@ class DiscoverTypesAction public function __construct( public TypeScriptTransformerConfig $config, public TypeScriptTransformerLog $log, - ) - { + ) { } /** @return array */ diff --git a/src/Actions/FindClassNameFqcnAction.php b/src/Actions/FindClassNameFqcnAction.php index a2224326..7d2a4c37 100644 --- a/src/Actions/FindClassNameFqcnAction.php +++ b/src/Actions/FindClassNameFqcnAction.php @@ -26,7 +26,7 @@ public function execute(ReflectionClass $reflectionClass, string $className): ?s $guessedFqcn = "{$reflectionClass->getNamespaceName()}\\{$className}"; - if(class_exists($guessedFqcn)){ + if (class_exists($guessedFqcn)) { return $this->cleanupClassname($guessedFqcn); } @@ -46,8 +46,7 @@ protected function loadUsages(ReflectionClass $reflectionClass): UsageCollection protected function cleanupClassname( string $classname - ):string - { + ): string { return ltrim($classname, '\\'); } } diff --git a/src/Actions/FormatFilesAction.php b/src/Actions/FormatFilesAction.php index 31e696e0..79fcd23c 100644 --- a/src/Actions/FormatFilesAction.php +++ b/src/Actions/FormatFilesAction.php @@ -11,8 +11,7 @@ class FormatFilesAction public function __construct( public TypeScriptTransformerConfig $config, public TypeScriptTransformerLog $log, - ) - { + ) { } /** diff --git a/src/Actions/ParseUseDefinitionsAction.php b/src/Actions/ParseUseDefinitionsAction.php index 0405805b..8ec6a17a 100644 --- a/src/Actions/ParseUseDefinitionsAction.php +++ b/src/Actions/ParseUseDefinitionsAction.php @@ -20,7 +20,6 @@ public function execute( string $filename, ): UsageCollection { /** @todo refactor this to the structure discoverer package, it is copied from there */ - try { $contents = file_get_contents($filename); diff --git a/src/Actions/SplitTransformedPerLocationAction.php b/src/Actions/SplitTransformedPerLocationAction.php index a5aa75f5..51de1a63 100644 --- a/src/Actions/SplitTransformedPerLocationAction.php +++ b/src/Actions/SplitTransformedPerLocationAction.php @@ -8,8 +8,7 @@ class SplitTransformedPerLocationAction { /** - * @param array $transformedTypes - * + * @param array $transformedTypes * @return array */ public function execute(array $transformedTypes): array diff --git a/src/Actions/TransformTypesAction.php b/src/Actions/TransformTypesAction.php index 7530327b..13df7749 100644 --- a/src/Actions/TransformTypesAction.php +++ b/src/Actions/TransformTypesAction.php @@ -19,7 +19,6 @@ public function __construct( /** * @param array $types - * * @return array */ public function execute(array $types): array diff --git a/src/Actions/WriteTypesAction.php b/src/Actions/WriteTypesAction.php index 5b74feea..c4671b02 100644 --- a/src/Actions/WriteTypesAction.php +++ b/src/Actions/WriteTypesAction.php @@ -19,8 +19,7 @@ public function __construct( public function execute( array $transformed, ReferenceMap $referenceMap - ): array - { + ): array { return $this->config->writer->output($transformed, $referenceMap); } } diff --git a/src/Attributes/LiteralTypeScriptType.php b/src/Attributes/LiteralTypeScriptType.php index 01222894..6465f88d 100644 --- a/src/Attributes/LiteralTypeScriptType.php +++ b/src/Attributes/LiteralTypeScriptType.php @@ -10,9 +10,9 @@ #[Attribute] class LiteralTypeScriptType implements TypeScriptTransformableAttribute { - private string | array $typeScript; + private string|array $typeScript; - public function __construct(string | array $typeScript) + public function __construct(string|array $typeScript) { $this->typeScript = $typeScript; } diff --git a/src/Attributes/TypeScriptType.php b/src/Attributes/TypeScriptType.php index 75b2782f..282368c2 100644 --- a/src/Attributes/TypeScriptType.php +++ b/src/Attributes/TypeScriptType.php @@ -11,9 +11,9 @@ #[Attribute] class TypeScriptType implements TypeScriptTransformableAttribute { - private array | string $type; + private array|string $type; - public function __construct(string | array $type) + public function __construct(string|array $type) { $this->type = $type; } diff --git a/src/Laravel/SpatieLaravelDefaultTypesProvider.php b/src/Laravel/SpatieLaravelDefaultTypesProvider.php index 5a5ba56a..ef84e412 100644 --- a/src/Laravel/SpatieLaravelDefaultTypesProvider.php +++ b/src/Laravel/SpatieLaravelDefaultTypesProvider.php @@ -22,14 +22,14 @@ public function provide(): array if (class_exists(\Spatie\LaravelOptions\Options::class)) { $types[] = new Transformed( new TypeScriptExport(new TypeScriptAlias( - new TypeScriptIdentifier('Options'), - new TypeScriptArray( - new TypeScriptObject([ - new TypeScriptProperty('label', new TypeScriptString()), - new TypeScriptProperty('value', new TypeScriptString()), - ]), - ) + new TypeScriptIdentifier('Options'), + new TypeScriptArray( + new TypeScriptObject([ + new TypeScriptProperty('label', new TypeScriptString()), + new TypeScriptProperty('value', new TypeScriptString()), + ]), ) + ) ), new ClassStringReference(\Spatie\LaravelOptions\Options::class), 'Options', diff --git a/src/References/FunctionReference.php b/src/References/FunctionReference.php index 19365fd2..6d74335a 100644 --- a/src/References/FunctionReference.php +++ b/src/References/FunctionReference.php @@ -6,8 +6,7 @@ class FunctionReference implements Reference { public function __construct( public string $name, - ) - { + ) { } public function getKey(): string diff --git a/src/Support/WritingContext.php b/src/Support/WritingContext.php index 0dd312e0..9bdfee48 100644 --- a/src/Support/WritingContext.php +++ b/src/Support/WritingContext.php @@ -8,7 +8,7 @@ class WritingContext { /** - * @param callable(Reference):string $referenceWriter + * @param callable(Reference):string $referenceWriter */ public function __construct( public Closure $referenceWriter, diff --git a/src/Transformed/Transformed.php b/src/Transformed/Transformed.php index 89f65fa6..0e931f09 100644 --- a/src/Transformed/Transformed.php +++ b/src/Transformed/Transformed.php @@ -4,14 +4,13 @@ use ReflectionClass; use Spatie\TypeScriptTransformer\References\Reference; -use Spatie\TypeScriptTransformer\Support\Import; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; class Transformed { /** - * @param array $location - * @param array $references + * @param array $location + * @param array $references */ public function __construct( public TypeScriptNode $typeScriptNode, diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index 61e3ddb8..8e820e3b 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -4,7 +4,6 @@ use ReflectionClass; use ReflectionProperty; -use Spatie\TypeScriptTransformer\Actions\FindClassNameFqcnAction; use Spatie\TypeScriptTransformer\Actions\ParseUseDefinitionsAction; use Spatie\TypeScriptTransformer\Actions\TranspilePhpStanTypeToTypeScriptTypeAction; use Spatie\TypeScriptTransformer\Actions\TranspileReflectionTypeToTypeScriptTypeAction; diff --git a/src/Transformers/DataClassTransformer.php b/src/Transformers/DataClassTransformer.php index 0ef2e46d..a3387713 100644 --- a/src/Transformers/DataClassTransformer.php +++ b/src/Transformers/DataClassTransformer.php @@ -4,8 +4,6 @@ use ReflectionClass; use ReflectionProperty; -use Spatie\StructureDiscoverer\Collections\UsageCollection; -use Spatie\TypeScriptTransformer\Actions\FindClassNameFqcnAction; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; diff --git a/src/Transformers/EnumTransformer.php b/src/Transformers/EnumTransformer.php index 236ea6b6..afc50db4 100644 --- a/src/Transformers/EnumTransformer.php +++ b/src/Transformers/EnumTransformer.php @@ -9,11 +9,11 @@ use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\Transformed\Untransformable; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptExport; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptEnum; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptExport; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptLiteral; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; use UnitEnum; diff --git a/src/TypeScript/TypeScriptGeneric.php b/src/TypeScript/TypeScriptGeneric.php index 16fd322d..ea1f1034 100644 --- a/src/TypeScript/TypeScriptGeneric.php +++ b/src/TypeScript/TypeScriptGeneric.php @@ -7,7 +7,7 @@ class TypeScriptGeneric implements TypeScriptNode, TypeScriptNodeWithChildren { /** - * @param array $genericTypes + * @param array $genericTypes */ public function __construct( public TypeScriptNode $type, diff --git a/src/TypeScript/TypeScriptIdentifier.php b/src/TypeScript/TypeScriptIdentifier.php index a490eb7e..23c5dfe8 100644 --- a/src/TypeScript/TypeScriptIdentifier.php +++ b/src/TypeScript/TypeScriptIdentifier.php @@ -8,8 +8,7 @@ class TypeScriptIdentifier implements TypeScriptNode { public function __construct( public string $name, - ) - { + ) { } public function write(WritingContext $context): string diff --git a/src/TypeScript/TypeScriptImport.php b/src/TypeScript/TypeScriptImport.php index f1691345..28fe0639 100644 --- a/src/TypeScript/TypeScriptImport.php +++ b/src/TypeScript/TypeScriptImport.php @@ -9,14 +9,13 @@ class TypeScriptImport implements TypeScriptNode public function __construct( public string $path, public array $names, - ) - { + ) { } public function write(WritingContext $context): string { $names = implode(', ', $this->names); - return "import { {$names} } from '{$this->path}';" . PHP_EOL; + return "import { {$names} } from '{$this->path}';".PHP_EOL; } } diff --git a/src/TypeScript/TypeScriptInterface.php b/src/TypeScript/TypeScriptInterface.php index afa3862f..4e72c7fc 100644 --- a/src/TypeScript/TypeScriptInterface.php +++ b/src/TypeScript/TypeScriptInterface.php @@ -7,9 +7,8 @@ class TypeScriptInterface implements TypeScriptNode, TypeScriptNodeWithChildren { /** - * @param string $name - * @param array $properties - * @param array $methods + * @param array $properties + * @param array $methods */ public function __construct( public string $name, diff --git a/src/TypeScript/TypeScriptMethod.php b/src/TypeScript/TypeScriptMethod.php index 725ec4a0..5bd9ccb7 100644 --- a/src/TypeScript/TypeScriptMethod.php +++ b/src/TypeScript/TypeScriptMethod.php @@ -7,7 +7,7 @@ class TypeScriptMethod implements TypeScriptNode, TypeScriptNodeWithChildren { /** - * @param array $parameters + * @param array $parameters */ public function __construct( public string $name, diff --git a/src/TypeScript/TypeScriptObject.php b/src/TypeScript/TypeScriptObject.php index bf43e1a8..525ef950 100644 --- a/src/TypeScript/TypeScriptObject.php +++ b/src/TypeScript/TypeScriptObject.php @@ -7,7 +7,7 @@ class TypeScriptObject implements TypeScriptNode, TypeScriptNodeWithChildren { /** - * @param array $properties + * @param array $properties */ public function __construct( public array $properties diff --git a/src/TypeScript/TypeScriptUnion.php b/src/TypeScript/TypeScriptUnion.php index 22f95401..0f97c817 100644 --- a/src/TypeScript/TypeScriptUnion.php +++ b/src/TypeScript/TypeScriptUnion.php @@ -8,7 +8,7 @@ class TypeScriptUnion implements TypeScriptNode, TypeScriptNodeWithChildren { /** - * @param array $types + * @param array $types */ public function __construct( public array $types, diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index f3ba1910..6f4dec28 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -6,7 +6,6 @@ use Spatie\TypeScriptTransformer\Actions\ConnectReferencesAction; use Spatie\TypeScriptTransformer\Actions\DiscoverTypesAction; use Spatie\TypeScriptTransformer\Actions\FormatFilesAction; -use Spatie\TypeScriptTransformer\Actions\ReplaceTypeReferencesAction; use Spatie\TypeScriptTransformer\Actions\TransformTypesAction; use Spatie\TypeScriptTransformer\Actions\WriteTypesAction; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; diff --git a/src/TypeScriptTransformerConfig.php b/src/TypeScriptTransformerConfig.php index 92de833d..49915cd5 100755 --- a/src/TypeScriptTransformerConfig.php +++ b/src/TypeScriptTransformerConfig.php @@ -11,7 +11,7 @@ /** * @param array $directories * @param array $transformers - * @param array> $defaultTypeProviders + * @param array> $defaultTypeProviders */ public function __construct( public array $directories, diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index ae2d5cf0..555b8568 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -50,7 +50,7 @@ protected function writeLocation( ): WrittenFile { $imports = $this->resolveImports($location, $referenceMap); - $path = "{$this->path}/".implode('/', $location->segments)."/"; + $path = "{$this->path}/".implode('/', $location->segments).'/'; $output = ''; diff --git a/src/Writers/Writer.php b/src/Writers/Writer.php index dea785dc..412590b0 100644 --- a/src/Writers/Writer.php +++ b/src/Writers/Writer.php @@ -4,7 +4,6 @@ use Spatie\TypeScriptTransformer\Collections\ReferenceMap; use Spatie\TypeScriptTransformer\Support\WrittenFile; -use Spatie\TypeScriptTransformer\Transformed\Transformed; interface Writer { diff --git a/tests/Actions/ResolveRelativePathActionTest.php b/tests/Actions/ResolveRelativePathActionTest.php index b5786a5d..0f4474cc 100644 --- a/tests/Actions/ResolveRelativePathActionTest.php +++ b/tests/Actions/ResolveRelativePathActionTest.php @@ -1,6 +1,5 @@ Date: Tue, 4 Jul 2023 12:56:27 +0200 Subject: [PATCH 03/51] wip --- composer.json | 3 +- package.json | 3 + src/Actions/AppendDefaultTypesAction.php | 14 +-- src/Actions/ConnectReferencesAction.php | 23 ++-- .../SplitTransformedPerLocationAction.php | 14 +-- src/Actions/TransformTypeAction.php | 59 ++++++++++ src/Actions/TransformTypesAction.php | 53 +++------ src/Actions/WatchFileSystemAction.php | 25 +++++ src/Actions/WriteTypesAction.php | 5 +- .../Watch/DirectoryCreatedWatchEvent.php | 8 ++ .../Watch/DirectoryDeletedWatchEvent.php | 8 ++ src/Events/Watch/FileCreatedWatchEvent.php | 8 ++ src/Events/Watch/FileDeletedWatchEvent.php | 8 ++ src/Events/Watch/FileUpdatedWatchEvent.php | 8 ++ src/Events/Watch/WatchEvent.php | 11 ++ .../Commands/WatchTypeScriptCommand.php | 24 +++++ .../TypeScriptTransformerServiceProvider.php | 2 + src/Support/TransformedCollection.php | 34 ++++++ src/TypeScriptTransformer.php | 11 +- src/Writers/ModuleWriter.php | 5 +- src/Writers/NamespaceWriter.php | 7 +- src/Writers/Writer.php | 3 +- yarn.lock | 102 ++++++++++++++++++ 23 files changed, 352 insertions(+), 86 deletions(-) create mode 100644 src/Actions/TransformTypeAction.php create mode 100644 src/Actions/WatchFileSystemAction.php create mode 100644 src/Events/Watch/DirectoryCreatedWatchEvent.php create mode 100644 src/Events/Watch/DirectoryDeletedWatchEvent.php create mode 100644 src/Events/Watch/FileCreatedWatchEvent.php create mode 100644 src/Events/Watch/FileDeletedWatchEvent.php create mode 100644 src/Events/Watch/FileUpdatedWatchEvent.php create mode 100644 src/Events/Watch/WatchEvent.php create mode 100644 src/Laravel/Commands/WatchTypeScriptCommand.php create mode 100644 src/Support/TransformedCollection.php diff --git a/composer.json b/composer.json index 5ce3ca69..6abfa0fa 100644 --- a/composer.json +++ b/composer.json @@ -17,8 +17,9 @@ "require": { "php": "^8.2", "illuminate/contracts": "^10.0", - "spatie/laravel-package-tools": "^1.14.0", "phpstan/phpdoc-parser": "^1.13", + "spatie/file-system-watcher": "^1.1", + "spatie/laravel-package-tools": "^1.14.0", "spatie/php-structure-discoverer": "^1.1" }, "require-dev": { diff --git a/package.json b/package.json index 3f60d288..d89dad0a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,8 @@ { "devDependencies": { "typescript": "^5.1.6" + }, + "dependencies": { + "chokidar": "^3.5.3" } } diff --git a/src/Actions/AppendDefaultTypesAction.php b/src/Actions/AppendDefaultTypesAction.php index bde4ffde..aec32ee5 100644 --- a/src/Actions/AppendDefaultTypesAction.php +++ b/src/Actions/AppendDefaultTypesAction.php @@ -3,8 +3,8 @@ namespace Spatie\TypeScriptTransformer\Actions; use Spatie\TypeScriptTransformer\DefaultTypeProviders\DefaultTypesProvider; +use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; -use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class AppendDefaultTypesAction @@ -15,21 +15,13 @@ public function __construct( ) { } - /** - * @param array $transformed - * @return array - */ - public function execute(array $transformed): array + public function execute(TransformedCollection $collection): void { - $defaults = []; - foreach ($this->config->defaultTypeProviders as $defaultTypeProviderClass) { /** @var DefaultTypesProvider $defaultTypeProvider */ $defaultTypeProvider = new $defaultTypeProviderClass; - array_push($defaults, ...$defaultTypeProvider->provide()); + $collection->add(...$defaultTypeProvider->provide()); } - - return array_merge($transformed, $defaults); } } diff --git a/src/Actions/ConnectReferencesAction.php b/src/Actions/ConnectReferencesAction.php index 5bcdf908..efd53f9c 100644 --- a/src/Actions/ConnectReferencesAction.php +++ b/src/Actions/ConnectReferencesAction.php @@ -3,8 +3,8 @@ namespace Spatie\TypeScriptTransformer\Actions; use Spatie\TypeScriptTransformer\Collections\ReferenceMap; +use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; -use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; @@ -17,29 +17,26 @@ public function __construct( ) { } - /** - * @param array $transformed - */ - public function execute(array $transformed): ReferenceMap + public function execute(TransformedCollection $collection): ReferenceMap { $referenceMap = new ReferenceMap(); - foreach ($transformed as $transformedItem) { - if ($transformedItem->reference) { - $referenceMap->add($transformedItem); + foreach ($collection as $transformed) { + if ($transformed->reference) { + $referenceMap->add($transformed); } } - foreach ($transformed as $transformedItem) { + foreach ($collection as $transformed) { $references = []; $this->visitTypeScriptTreeAction->execute( - $transformedItem->typeScriptNode, - function (TypeReference $typeReference) use ($referenceMap, &$references, $transformedItem) { + $transformed->typeScriptNode, + function (TypeReference $typeReference) use ($referenceMap, &$references, $transformed) { $reference = $typeReference->reference; if (! $referenceMap->has($reference)) { - $this->log->warning("Tried replacing reference to `{$reference->humanFriendlyName()}` in `{$transformedItem->reference->humanFriendlyName()}` but it was not found in the transformed types"); + $this->log->warning("Tried replacing reference to `{$reference->humanFriendlyName()}` in `{$transformed->reference->humanFriendlyName()}` but it was not found in the transformed types"); return; } @@ -50,7 +47,7 @@ function (TypeReference $typeReference) use ($referenceMap, &$references, $trans [TypeReference::class] ); - $transformedItem->references = $references; + $transformed->references = $references; } return $referenceMap; diff --git a/src/Actions/SplitTransformedPerLocationAction.php b/src/Actions/SplitTransformedPerLocationAction.php index 51de1a63..893720a3 100644 --- a/src/Actions/SplitTransformedPerLocationAction.php +++ b/src/Actions/SplitTransformedPerLocationAction.php @@ -3,28 +3,28 @@ namespace Spatie\TypeScriptTransformer\Actions; use Spatie\TypeScriptTransformer\Support\Location; +use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Transformed\Transformed; class SplitTransformedPerLocationAction { /** - * @param array $transformedTypes * @return array */ - public function execute(array $transformedTypes): array + public function execute(TransformedCollection $collection): array { $split = []; - foreach ($transformedTypes as $transformedType) { - $splitKey = count($transformedType->location) > 0 - ? implode('.', $transformedType->location) + foreach ($collection as $transformed) { + $splitKey = count($transformed->location) > 0 + ? implode('.', $transformed->location) : ''; if (! array_key_exists($splitKey, $split)) { - $split[$splitKey] = new Location($transformedType->location, []); + $split[$splitKey] = new Location($transformed->location, []); } - $split[$splitKey]->transformed[] = $transformedType; + $split[$splitKey]->transformed[] = $transformed; } ksort($split); diff --git a/src/Actions/TransformTypeAction.php b/src/Actions/TransformTypeAction.php new file mode 100644 index 00000000..95ccaa89 --- /dev/null +++ b/src/Actions/TransformTypeAction.php @@ -0,0 +1,59 @@ +config->transformers as $transformer) { + $transformed = $transformer->transform( + $reflection, + $this->createTransformationContext($reflection), + ); + + if ($transformed instanceof Transformed) { + return $transformed; + } + } + + return null; + } + + protected function createTransformationContext( + ReflectionClass $reflection + ): TransformationContext { + $name = $reflection->getShortName(); + + $nameSpaceSegments = explode('\\', $reflection->getNamespaceName()); + + return new TransformationContext( + $name, + $nameSpaceSegments, + ); + } +} diff --git a/src/Actions/TransformTypesAction.php b/src/Actions/TransformTypesAction.php index 13df7749..66a85c76 100644 --- a/src/Actions/TransformTypesAction.php +++ b/src/Actions/TransformTypesAction.php @@ -2,65 +2,36 @@ namespace Spatie\TypeScriptTransformer\Actions; -use ReflectionClass; -use ReflectionException; -use Spatie\TypeScriptTransformer\Support\TransformationContext; +use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; -use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class TransformTypesAction { + protected TransformTypeAction $transformTypeAction; + public function __construct( protected TypeScriptTransformerConfig $config, - public TypeScriptTransformerLog $log, + protected TypeScriptTransformerLog $log, ) { + $this->transformTypeAction = new TransformTypeAction($config, $log); } /** - * @param array $types - * @return array + * @param array $types */ - public function execute(array $types): array + public function execute(array $types): TransformedCollection { - $transformedTypes = []; + $collection = new TransformedCollection(); foreach ($types as $type) { - try { - $reflection = new ReflectionClass($type); - } catch (ReflectionException) { - // TODO: maybe add some kind of log? - - continue; - } - - foreach ($this->config->transformers as $transformer) { - $transformed = $transformer->transform( - $reflection, - $this->createTransformationContext($reflection), - ); - - if ($transformed instanceof Transformed) { - $transformedTypes[] = $transformed; + $transformed = $this->transformTypeAction->execute($type); - break; - } + if ($transformed) { + $collection->add($transformed); } } - return $transformedTypes; - } - - protected function createTransformationContext( - ReflectionClass $reflection - ): TransformationContext { - $name = $reflection->getShortName(); - - $nameSpaceSegments = explode('\\', $reflection->getNamespaceName()); - - return new TransformationContext( - $name, - $nameSpaceSegments, - ); + return $collection; } } diff --git a/src/Actions/WatchFileSystemAction.php b/src/Actions/WatchFileSystemAction.php new file mode 100644 index 00000000..f9442840 --- /dev/null +++ b/src/Actions/WatchFileSystemAction.php @@ -0,0 +1,25 @@ +config->directories) + ->onAnyChange(function (string $type, string $path) { + echo $type.'|'.$path; + }) + ->start(); + } +} diff --git a/src/Actions/WriteTypesAction.php b/src/Actions/WriteTypesAction.php index c4671b02..fea3e865 100644 --- a/src/Actions/WriteTypesAction.php +++ b/src/Actions/WriteTypesAction.php @@ -3,6 +3,7 @@ namespace Spatie\TypeScriptTransformer\Actions; use Spatie\TypeScriptTransformer\Collections\ReferenceMap; +use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Support\WrittenFile; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; @@ -17,9 +18,9 @@ public function __construct( /** @return array */ public function execute( - array $transformed, + TransformedCollection $collection, ReferenceMap $referenceMap ): array { - return $this->config->writer->output($transformed, $referenceMap); + return $this->config->writer->output($collection, $referenceMap); } } diff --git a/src/Events/Watch/DirectoryCreatedWatchEvent.php b/src/Events/Watch/DirectoryCreatedWatchEvent.php new file mode 100644 index 00000000..6ced5860 --- /dev/null +++ b/src/Events/Watch/DirectoryCreatedWatchEvent.php @@ -0,0 +1,8 @@ +comment('Watching for changes...'); + + return self::SUCCESS; + } +} diff --git a/src/Laravel/TypeScriptTransformerServiceProvider.php b/src/Laravel/TypeScriptTransformerServiceProvider.php index ae694559..a1f5be80 100644 --- a/src/Laravel/TypeScriptTransformerServiceProvider.php +++ b/src/Laravel/TypeScriptTransformerServiceProvider.php @@ -5,6 +5,7 @@ use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; use Spatie\TypeScriptTransformer\Laravel\Commands\TransformTypeScriptCommand; +use Spatie\TypeScriptTransformer\Laravel\Commands\WatchTypeScriptCommand; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; @@ -14,6 +15,7 @@ public function configurePackage(Package $package): void { $package ->name('typescript-transformer') + ->hasCommand(WatchTypeScriptCommand::class) ->hasCommand(TransformTypeScriptCommand::class); } diff --git a/src/Support/TransformedCollection.php b/src/Support/TransformedCollection.php new file mode 100644 index 00000000..2b7c8a21 --- /dev/null +++ b/src/Support/TransformedCollection.php @@ -0,0 +1,34 @@ + + */ +class TransformedCollection implements IteratorAggregate +{ + /** + * @param array $items + */ + public function __construct( + protected array $items = [], + ) { + } + + public function add(Transformed ...$transformed): self + { + array_push($this->items, ...$transformed); + + return $this; + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->items); + } +} diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index 6f4dec28..f4f7009f 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -25,7 +25,7 @@ public function __construct( } - public function execute(): TypeScriptTransformerLog + public function execute(bool $watch = false): TypeScriptTransformerLog { // Parallelize // - discovering types @@ -34,15 +34,16 @@ public function execute(): TypeScriptTransformerLog // Cant't do parallel // - replace type references + // watch -> only reload when the config changes (difficult, maybe skip for now) $discovered = $this->discoverTypesAction->execute(); - $transformed = $this->transformTypesAction->execute($discovered); + $transformedCollection = $this->transformTypesAction->execute($discovered); - $transformed = $this->appendDefaultTypesAction->execute($transformed); + $this->appendDefaultTypesAction->execute($transformedCollection); - $referenceMap = $this->connectReferencesAction->execute($transformed); + $referenceMap = $this->connectReferencesAction->execute($transformedCollection); - $writtenFiles = $this->writeTypesAction->execute($transformed, $referenceMap); + $writtenFiles = $this->writeTypesAction->execute($transformedCollection, $referenceMap); $this->formatFilesAction->execute($writtenFiles); diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index 555b8568..47c13b00 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -7,6 +7,7 @@ use Spatie\TypeScriptTransformer\Collections\ReferenceMap; use Spatie\TypeScriptTransformer\References\Reference; use Spatie\TypeScriptTransformer\Support\Location; +use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\WritingContext; use Spatie\TypeScriptTransformer\Support\WrittenFile; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptImport; @@ -29,10 +30,10 @@ public function __construct( $this->resolveRelativePathAction = new ResolveRelativePathAction(); } - public function output(array $transformedTypes, ReferenceMap $referenceMap): array + public function output(TransformedCollection $collection, ReferenceMap $referenceMap): array { $locations = $this->transformedPerLocationAction->execute( - $transformedTypes + $collection ); $writtenFiles = []; diff --git a/src/Writers/NamespaceWriter.php b/src/Writers/NamespaceWriter.php index 49e263e2..59abd49f 100644 --- a/src/Writers/NamespaceWriter.php +++ b/src/Writers/NamespaceWriter.php @@ -5,6 +5,7 @@ use Spatie\TypeScriptTransformer\Actions\SplitTransformedPerLocationAction; use Spatie\TypeScriptTransformer\Collections\ReferenceMap; use Spatie\TypeScriptTransformer\References\Reference; +use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\WritingContext; use Spatie\TypeScriptTransformer\Support\WrittenFile; use Spatie\TypeScriptTransformer\Transformed\Transformed; @@ -20,10 +21,10 @@ public function __construct( $this->splitTransformedPerLocationAction = new SplitTransformedPerLocationAction(); } - public function output(array $transformedTypes, ReferenceMap $referenceMap): array + public function output(TransformedCollection $collection, ReferenceMap $referenceMap): array { $split = $this->splitTransformedPerLocationAction->execute( - $transformedTypes + $collection ); $output = ''; @@ -62,7 +63,7 @@ public function output(array $transformedTypes, ReferenceMap $referenceMap): arr return [ new WrittenFile( $this->filename, - $transformedTypes, + [], ), ]; } diff --git a/src/Writers/Writer.php b/src/Writers/Writer.php index 412590b0..523b41b4 100644 --- a/src/Writers/Writer.php +++ b/src/Writers/Writer.php @@ -3,13 +3,14 @@ namespace Spatie\TypeScriptTransformer\Writers; use Spatie\TypeScriptTransformer\Collections\ReferenceMap; +use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\WrittenFile; interface Writer { /** @return array */ public function output( - array $transformedTypes, + TransformedCollection $collection, ReferenceMap $referenceMap, ): array; } diff --git a/yarn.lock b/yarn.lock index 39e776f5..b28679fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,108 @@ # yarn lockfile v1 +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + typescript@^5.1.6: version "5.1.6" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" From 67445b07652f8091942733c4b3831b54918c1643 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Tue, 4 Jul 2023 10:57:08 +0000 Subject: [PATCH 04/51] Fix styling --- src/Actions/TransformTypeAction.php | 2 +- src/Actions/TransformTypesAction.php | 2 +- src/Events/Watch/DirectoryCreatedWatchEvent.php | 1 - src/Events/Watch/DirectoryDeletedWatchEvent.php | 1 - src/Events/Watch/FileCreatedWatchEvent.php | 1 - src/Events/Watch/FileDeletedWatchEvent.php | 1 - src/Events/Watch/FileUpdatedWatchEvent.php | 1 - src/Laravel/Commands/WatchTypeScriptCommand.php | 2 -- src/Support/TransformedCollection.php | 2 +- 9 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Actions/TransformTypeAction.php b/src/Actions/TransformTypeAction.php index 95ccaa89..0b0b281e 100644 --- a/src/Actions/TransformTypeAction.php +++ b/src/Actions/TransformTypeAction.php @@ -18,7 +18,7 @@ public function __construct( } /** - * @param class-string $type + * @param class-string $type */ public function execute(string $type): ?Transformed { diff --git a/src/Actions/TransformTypesAction.php b/src/Actions/TransformTypesAction.php index 66a85c76..b6d8a485 100644 --- a/src/Actions/TransformTypesAction.php +++ b/src/Actions/TransformTypesAction.php @@ -18,7 +18,7 @@ public function __construct( } /** - * @param array $types + * @param array $types */ public function execute(array $types): TransformedCollection { diff --git a/src/Events/Watch/DirectoryCreatedWatchEvent.php b/src/Events/Watch/DirectoryCreatedWatchEvent.php index 6ced5860..9f0eb364 100644 --- a/src/Events/Watch/DirectoryCreatedWatchEvent.php +++ b/src/Events/Watch/DirectoryCreatedWatchEvent.php @@ -4,5 +4,4 @@ class DirectoryCreatedWatchEvent extends WatchEvent { - } diff --git a/src/Events/Watch/DirectoryDeletedWatchEvent.php b/src/Events/Watch/DirectoryDeletedWatchEvent.php index d8142008..58bad040 100644 --- a/src/Events/Watch/DirectoryDeletedWatchEvent.php +++ b/src/Events/Watch/DirectoryDeletedWatchEvent.php @@ -4,5 +4,4 @@ class DirectoryDeletedWatchEvent extends WatchEvent { - } diff --git a/src/Events/Watch/FileCreatedWatchEvent.php b/src/Events/Watch/FileCreatedWatchEvent.php index 5d5ddb1c..4bb04667 100644 --- a/src/Events/Watch/FileCreatedWatchEvent.php +++ b/src/Events/Watch/FileCreatedWatchEvent.php @@ -4,5 +4,4 @@ class FileCreatedWatchEvent extends WatchEvent { - } diff --git a/src/Events/Watch/FileDeletedWatchEvent.php b/src/Events/Watch/FileDeletedWatchEvent.php index eacdcdb3..103d53fb 100644 --- a/src/Events/Watch/FileDeletedWatchEvent.php +++ b/src/Events/Watch/FileDeletedWatchEvent.php @@ -4,5 +4,4 @@ class FileDeletedWatchEvent extends WatchEvent { - } diff --git a/src/Events/Watch/FileUpdatedWatchEvent.php b/src/Events/Watch/FileUpdatedWatchEvent.php index 0f2c974d..31dbc734 100644 --- a/src/Events/Watch/FileUpdatedWatchEvent.php +++ b/src/Events/Watch/FileUpdatedWatchEvent.php @@ -4,5 +4,4 @@ class FileUpdatedWatchEvent extends WatchEvent { - } diff --git a/src/Laravel/Commands/WatchTypeScriptCommand.php b/src/Laravel/Commands/WatchTypeScriptCommand.php index abc26235..832a3c61 100644 --- a/src/Laravel/Commands/WatchTypeScriptCommand.php +++ b/src/Laravel/Commands/WatchTypeScriptCommand.php @@ -4,7 +4,6 @@ use Illuminate\Console\Command; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; -use Spatie\Watcher\Watch; class WatchTypeScriptCommand extends Command { @@ -16,7 +15,6 @@ public function handle( TypeScriptTransformerConfig $config, ): int { - $this->comment('Watching for changes...'); return self::SUCCESS; diff --git a/src/Support/TransformedCollection.php b/src/Support/TransformedCollection.php index 2b7c8a21..656fc99b 100644 --- a/src/Support/TransformedCollection.php +++ b/src/Support/TransformedCollection.php @@ -13,7 +13,7 @@ class TransformedCollection implements IteratorAggregate { /** - * @param array $items + * @param array $items */ public function __construct( protected array $items = [], From f917eb66618cba16361b9267bb7c4aa17ccde33f Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 5 Jul 2023 14:31:30 +0200 Subject: [PATCH 05/51] wip --- src/Attributes/LiteralTypeScriptType.php | 24 +++++--- src/Attributes/TypeScript.php | 12 ++-- .../TypeScriptTransformableAttribute.php | 10 ---- src/Attributes/TypeScriptTransformer.php | 16 ------ src/Attributes/TypeScriptType.php | 29 ++++++---- .../TypeScriptTypeAttributeContract.php | 11 ++++ src/Transformers/AttributeTransformer.php | 39 +++++++++++++ src/Transformers/ClassTransformer.php | 55 ++++++++++++++++--- 8 files changed, 138 insertions(+), 58 deletions(-) delete mode 100644 src/Attributes/TypeScriptTransformableAttribute.php delete mode 100644 src/Attributes/TypeScriptTransformer.php create mode 100644 src/Attributes/TypeScriptTypeAttributeContract.php create mode 100644 src/Transformers/AttributeTransformer.php diff --git a/src/Attributes/LiteralTypeScriptType.php b/src/Attributes/LiteralTypeScriptType.php index 6465f88d..9ed47c68 100644 --- a/src/Attributes/LiteralTypeScriptType.php +++ b/src/Attributes/LiteralTypeScriptType.php @@ -3,12 +3,16 @@ namespace Spatie\TypeScriptTransformer\Attributes; use Attribute; -use phpDocumentor\Reflection\Type; +use ReflectionClass; use Spatie\TypeScriptTransformer\Types\StructType; use Spatie\TypeScriptTransformer\Types\TypeScriptType; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptRaw; #[Attribute] -class LiteralTypeScriptType implements TypeScriptTransformableAttribute +class LiteralTypeScriptType implements TypeScriptTypeAttributeContract { private string|array $typeScript; @@ -17,17 +21,19 @@ public function __construct(string|array $typeScript) $this->typeScript = $typeScript; } - public function getType(): Type + public function getType(ReflectionClass $class): TypeScriptNode { if (is_string($this->typeScript)) { - return new TypeScriptType($this->typeScript); + return new TypeScriptRaw($this->typeScript); } - $types = array_map( - fn (string $type) => new TypeScriptType($type), - $this->typeScript - ); + $properties = collect($this->typeScript) + ->map(fn (string $type, string $name) => new TypeScriptProperty( + $name, + new TypeScriptRaw($type) + )) + ->all(); - return new StructType($types); + return new TypeScriptObject($properties); } } diff --git a/src/Attributes/TypeScript.php b/src/Attributes/TypeScript.php index b6365d17..03f93931 100644 --- a/src/Attributes/TypeScript.php +++ b/src/Attributes/TypeScript.php @@ -7,10 +7,12 @@ #[Attribute] class TypeScript { - public ?string $name; - - public function __construct(?string $name = null) - { - $this->name = $name; + /** + * @param array|null $location + */ + public function __construct( + public ?string $name = null, + public ?array $location = null, + ) { } } diff --git a/src/Attributes/TypeScriptTransformableAttribute.php b/src/Attributes/TypeScriptTransformableAttribute.php deleted file mode 100644 index 80f0ba53..00000000 --- a/src/Attributes/TypeScriptTransformableAttribute.php +++ /dev/null @@ -1,10 +0,0 @@ -transformer = $transformer; - } -} diff --git a/src/Attributes/TypeScriptType.php b/src/Attributes/TypeScriptType.php index 282368c2..d862f6c7 100644 --- a/src/Attributes/TypeScriptType.php +++ b/src/Attributes/TypeScriptType.php @@ -3,13 +3,17 @@ namespace Spatie\TypeScriptTransformer\Attributes; use Attribute; -use phpDocumentor\Reflection\Type; -use phpDocumentor\Reflection\TypeResolver; +use ReflectionClass; +use Spatie\TypeScriptTransformer\Actions\TranspilePhpStanTypeToTypeScriptTypeAction; use Spatie\TypeScriptTransformer\Exceptions\UnableToTransformUsingAttribute; +use Spatie\TypeScriptTransformer\TypeResolvers\DocTypeResolver; use Spatie\TypeScriptTransformer\Types\StructType; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; #[Attribute] -class TypeScriptType implements TypeScriptTransformableAttribute +class TypeScriptType implements TypeScriptTypeAttributeContract { private array|string $type; @@ -18,17 +22,22 @@ public function __construct(string|array $type) $this->type = $type; } - public function getType(): Type + public function getType(ReflectionClass $class): TypeScriptNode { + $docResolver = new DocTypeResolver(); + $transpiler = new TranspilePhpStanTypeToTypeScriptTypeAction(); + if (is_string($this->type)) { - return (new TypeResolver())->resolve($this->type); + return $transpiler->execute($docResolver->type($this->type), $class); } - /** @psalm-suppress RedundantCondition */ - if (is_array($this->type)) { - return StructType::fromArray($this->type); - } + $properties = collect($this->type) + ->map(fn (string $type, string $name) => new TypeScriptProperty( + $name, + $transpiler->execute($docResolver->type($type), $class) + )) + ->all(); - throw UnableToTransformUsingAttribute::create($this->type); + return new TypeScriptObject($properties); } } diff --git a/src/Attributes/TypeScriptTypeAttributeContract.php b/src/Attributes/TypeScriptTypeAttributeContract.php new file mode 100644 index 00000000..098d55ba --- /dev/null +++ b/src/Attributes/TypeScriptTypeAttributeContract.php @@ -0,0 +1,11 @@ +getAttributes(TypeScript::class)) > 0; + } + + public function transform(ReflectionClass $reflectionClass, TransformationContext $context): Transformed|Untransformable + { + $transformed = parent::transform($reflectionClass, $context); + + if ($transformed instanceof Untransformable) { + return $transformed; + } + + /** @var TypeScript $attribute */ + $attribute = $reflectionClass->getAttributes(TypeScript::class)[0]->newInstance(); + + if ($attribute->name !== null) { + $transformed->name = $attribute->name; + } + + if ($attribute->location !== null) { + $transformed->location = $attribute->location; + } + + return $transformed; + } +} diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index 8e820e3b..6abd0d4f 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -8,6 +8,7 @@ use Spatie\TypeScriptTransformer\Actions\TranspilePhpStanTypeToTypeScriptTypeAction; use Spatie\TypeScriptTransformer\Actions\TranspileReflectionTypeToTypeScriptTypeAction; use Spatie\TypeScriptTransformer\Attributes\Optional; +use Spatie\TypeScriptTransformer\Attributes\TypeScriptTypeAttributeContract; use Spatie\TypeScriptTransformer\References\ReflectionClassReference; use Spatie\TypeScriptTransformer\Support\TransformationContext; use Spatie\TypeScriptTransformer\Transformed\Transformed; @@ -38,6 +39,30 @@ public function transform(ReflectionClass $reflectionClass, TransformationContex return Untransformable::create(); } + + return new Transformed( + new TypeScriptExport( + new TypeScriptAlias( + new TypeScriptIdentifier($context->name), + $this->getTypeScriptNode($reflectionClass) + ) + ), + new ReflectionClassReference($reflectionClass), + $context->name, + true, + $context->nameSpaceSegments, + ); + } + + abstract public function shouldTransform(ReflectionClass $reflection): bool; + + protected function getTypeScriptNode( + ReflectionClass $reflectionClass + ): TypeScriptNode { + if ($resolvedAttributeType = $this->resolveTypeByAttribute($reflectionClass)) { + return $resolvedAttributeType; + } + $classAnnotations = $this->docTypeResolver->class($reflectionClass)?->properties ?? []; $constructorAnnotations = $reflectionClass->hasMethod('__construct') @@ -55,16 +80,26 @@ public function transform(ReflectionClass $reflectionClass, TransformationContex ); } - return new Transformed( - new TypeScriptExport(new TypeScriptAlias(new TypeScriptIdentifier($context->name), new TypeScriptObject($properties))), - new ReflectionClassReference($reflectionClass), - $context->name, - true, - $context->nameSpaceSegments, - ); + return new TypeScriptObject($properties); } - abstract public function shouldTransform(ReflectionClass $reflection): bool; + protected function resolveTypeByAttribute( + ReflectionClass $reflectionClass, + ?ReflectionProperty $property = null, + ): ?TypeScriptNode { + $subject = $property ?? $reflectionClass; + + foreach ($subject->getAttributes() as $attribute) { + if (is_a($attribute->getName(), TypeScriptTypeAttributeContract::class, true)) { + /** @var TypeScriptTypeAttributeContract $attributeInstance */ + $attributeInstance = $attribute->newInstance(); + + return $attributeInstance->getType($reflectionClass); + } + } + + return null; + } protected function getProperties(ReflectionClass $reflection): array { @@ -103,6 +138,10 @@ protected function resolveTypeForProperty( ReflectionProperty $reflectionProperty, ?ParsedNameAndType $annotation, ): TypeScriptNode { + if ($resolvedAttributeType = $this->resolveTypeByAttribute($reflectionClass, $reflectionProperty)) { + return $resolvedAttributeType; + } + if ($annotation) { return $this->transpilePhpStanTypeToTypeScriptTypeAction->execute( $annotation->type, From fcc502b153bc83e382665cd7cc8bdc7abc9d2906 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Wed, 5 Jul 2023 12:32:07 +0000 Subject: [PATCH 06/51] Fix styling --- src/Attributes/LiteralTypeScriptType.php | 2 -- src/Attributes/TypeScript.php | 2 +- src/Attributes/TypeScriptType.php | 2 -- src/Transformers/ClassTransformer.php | 1 - 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Attributes/LiteralTypeScriptType.php b/src/Attributes/LiteralTypeScriptType.php index 9ed47c68..c217977a 100644 --- a/src/Attributes/LiteralTypeScriptType.php +++ b/src/Attributes/LiteralTypeScriptType.php @@ -4,8 +4,6 @@ use Attribute; use ReflectionClass; -use Spatie\TypeScriptTransformer\Types\StructType; -use Spatie\TypeScriptTransformer\Types\TypeScriptType; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; diff --git a/src/Attributes/TypeScript.php b/src/Attributes/TypeScript.php index 03f93931..fd6b85a9 100644 --- a/src/Attributes/TypeScript.php +++ b/src/Attributes/TypeScript.php @@ -8,7 +8,7 @@ class TypeScript { /** - * @param array|null $location + * @param array|null $location */ public function __construct( public ?string $name = null, diff --git a/src/Attributes/TypeScriptType.php b/src/Attributes/TypeScriptType.php index d862f6c7..e3b034b6 100644 --- a/src/Attributes/TypeScriptType.php +++ b/src/Attributes/TypeScriptType.php @@ -5,9 +5,7 @@ use Attribute; use ReflectionClass; use Spatie\TypeScriptTransformer\Actions\TranspilePhpStanTypeToTypeScriptTypeAction; -use Spatie\TypeScriptTransformer\Exceptions\UnableToTransformUsingAttribute; use Spatie\TypeScriptTransformer\TypeResolvers\DocTypeResolver; -use Spatie\TypeScriptTransformer\Types\StructType; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index 6abd0d4f..fba484fc 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -39,7 +39,6 @@ public function transform(ReflectionClass $reflectionClass, TransformationContex return Untransformable::create(); } - return new Transformed( new TypeScriptExport( new TypeScriptAlias( From 84bcb180e0bc82a6763565ecc685da50a8da8437 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Mon, 17 Jul 2023 16:34:12 -0400 Subject: [PATCH 07/51] wip --- ...spilePhpStanTypeToTypeScriptTypeAction.php | 28 +-- ...leReflectionTypeToTypeScriptTypeAction.php | 2 +- src/Laravel/LaravelDefaultTypesProvider.php | 16 +- src/Laravel/RouterGenerator.php | 191 ++++++++++++++++++ .../Routes/InvokableRouteController.php | 39 ++++ src/Laravel/Routes/RouteController.php | 41 ++++ src/Laravel/Routes/RouteControllerAction.php | 38 ++++ .../Routes/RouteControllerCollection.php | 35 ++++ src/Laravel/Routes/RouteParameter.php | 32 +++ .../Routes/RouteParameterCollection.php | 29 +++ src/Laravel/Routes/RouterStructure.php | 12 ++ .../SpatieLaravelDefaultTypesProvider.php | 33 ++- src/TypeScript/TypeScriptArray.php | 16 +- src/TypeScript/TypeScriptConditional.php | 25 +++ .../TypeScriptFunctionDefinition.php | 35 ++++ .../TypeScriptGenericTypeVariable.php | 31 +++ src/TypeScript/TypeScriptIdentifier.php | 2 +- src/TypeScript/TypeScriptIndexedAccess.php | 34 ++++ src/TypeScript/TypeScriptInterface.php | 4 +- src/TypeScript/TypeScriptOperator.php | 62 ++++++ src/TypeScript/TypeScriptParameter.php | 2 +- ...ePhpStanTypeToTypeScriptTypeActionTest.php | 11 +- ...flectionTypeToTypeScriptTypeActionTest.php | 2 +- 23 files changed, 677 insertions(+), 43 deletions(-) create mode 100644 src/Laravel/RouterGenerator.php create mode 100644 src/Laravel/Routes/InvokableRouteController.php create mode 100644 src/Laravel/Routes/RouteController.php create mode 100644 src/Laravel/Routes/RouteControllerAction.php create mode 100644 src/Laravel/Routes/RouteControllerCollection.php create mode 100644 src/Laravel/Routes/RouteParameter.php create mode 100644 src/Laravel/Routes/RouteParameterCollection.php create mode 100644 src/Laravel/Routes/RouterStructure.php create mode 100644 src/TypeScript/TypeScriptConditional.php create mode 100644 src/TypeScript/TypeScriptFunctionDefinition.php create mode 100644 src/TypeScript/TypeScriptGenericTypeVariable.php create mode 100644 src/TypeScript/TypeScriptIndexedAccess.php create mode 100644 src/TypeScript/TypeScriptOperator.php diff --git a/src/Actions/TranspilePhpStanTypeToTypeScriptTypeAction.php b/src/Actions/TranspilePhpStanTypeToTypeScriptTypeAction.php index a79a770d..da674e67 100644 --- a/src/Actions/TranspilePhpStanTypeToTypeScriptTypeAction.php +++ b/src/Actions/TranspilePhpStanTypeToTypeScriptTypeAction.php @@ -82,7 +82,7 @@ protected function identifierNode( } if ($node->name === 'array') { - return new TypeScriptArray(null); + return new TypeScriptArray([]); } if ($node->name === 'callable') { @@ -121,8 +121,9 @@ protected function arrayTypeNode( ArrayTypeNode $node, ?ReflectionClass $reflectionClass ): TypeScriptNode { - return new TypeScriptArray( - $this->execute($node->type, $reflectionClass) + return new TypeScriptGeneric( + new TypeScriptIdentifier('Array'), + [$this->execute($node->type, $reflectionClass)] ); } @@ -183,17 +184,12 @@ protected function genericNode( GenericTypeNode $node, ?ReflectionClass $reflectionClass ): TypeScriptNode { - $type = $this->execute($node->type, $reflectionClass); - - if ($type instanceof TypeScriptString) { - return $type; // class-string case - } - - if ($type instanceof TypeScriptArray) { + if ($node->type->name === 'array') { return match (count($node->genericTypes)) { - 0 => $type, - 1 => new TypeScriptArray( - $this->execute($node->genericTypes[0], $reflectionClass) + 0 => new TypeScriptArray([]), + 1 => new TypeScriptGeneric( + new TypeScriptIdentifier('Array'), + [$this->execute($node->genericTypes[0], $reflectionClass)] ), 2 => new TypeScriptGeneric( new TypeScriptIdentifier('Record'), @@ -206,6 +202,12 @@ protected function genericNode( }; } + $type = $this->execute($node->type, $reflectionClass); + + if ($type instanceof TypeScriptString) { + return $type; // class-string case + } + return new TypeScriptGeneric( $type, array_map( diff --git a/src/Actions/TranspileReflectionTypeToTypeScriptTypeAction.php b/src/Actions/TranspileReflectionTypeToTypeScriptTypeAction.php index 3c3a1d5c..a8883bfe 100644 --- a/src/Actions/TranspileReflectionTypeToTypeScriptTypeAction.php +++ b/src/Actions/TranspileReflectionTypeToTypeScriptTypeAction.php @@ -79,7 +79,7 @@ protected function reflectionNamedType( } if ($type->getName() === 'array') { - return new TypeScriptArray(null); + return new TypeScriptArray([]); } if ($type->getName() === 'null') { diff --git a/src/Laravel/LaravelDefaultTypesProvider.php b/src/Laravel/LaravelDefaultTypesProvider.php index 210fe29f..76ccd34e 100644 --- a/src/Laravel/LaravelDefaultTypesProvider.php +++ b/src/Laravel/LaravelDefaultTypesProvider.php @@ -11,7 +11,6 @@ use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptArray; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptBoolean; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptExport; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGeneric; @@ -47,8 +46,9 @@ protected function collection(): Transformed new TypeScriptIdentifier('Collection'), [new TypeScriptIdentifier('T')], ), - new TypeScriptArray( - new TypeScriptIdentifier('T'), + new TypeScriptGeneric( + new TypeScriptIdentifier('Array'), + [new TypeScriptIdentifier('T')], ), ) ), @@ -68,8 +68,9 @@ protected function eloquentCollection(): Transformed new TypeScriptIdentifier('Collection'), [new TypeScriptIdentifier('T')], ), - new TypeScriptArray( - new TypeScriptIdentifier('T'), + new TypeScriptGeneric( + new TypeScriptIdentifier('Array'), + [new TypeScriptIdentifier('T')], ), ) ), @@ -90,7 +91,10 @@ protected function lengthAwarePaginator(): Transformed [new TypeScriptIdentifier('T')], ), new TypeScriptObject([ - new TypeScriptProperty('data', new TypeScriptArray(new TypeScriptIdentifier('T'))), + new TypeScriptProperty('data', new TypeScriptGeneric( + new TypeScriptIdentifier('Array'), + [new TypeScriptIdentifier('T')], + ),), new TypeScriptProperty('links', new TypeScriptObject([ new TypeScriptProperty('url', new TypeScriptUnion([ new TypeScriptIdentifier('string'), diff --git a/src/Laravel/RouterGenerator.php b/src/Laravel/RouterGenerator.php new file mode 100644 index 00000000..fb16bbe1 --- /dev/null +++ b/src/Laravel/RouterGenerator.php @@ -0,0 +1,191 @@ +(action: [TController, TAction] | TController, params?: TParams): string { + * + * } + */ +class RouterGenerator implements DefaultTypesProvider +{ + public function provide(): array + { + $controllers = $this->resolveRoutes(); + + $transformedRoutes = new Transformed( + new TypeScriptAlias( + new TypeScriptIdentifier('Routes'), + $controllers->toTypeScriptNode(), + ), + null, + 'Routes', + true, + [], + ); + + $actionParam = new Transformed( + new TypeScriptAlias( + new TypeScriptGeneric( + new TypeScriptIdentifier('ActionParam'), + [ + new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TController')), + new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TAction')), + ] + ), + new TypeScriptConditional( + TypeScriptOperator::extends( + new TypeScriptIndexedAccess( + new TypeScriptIdentifier('Routes'), + [new TypeScriptIdentifier('TController')], + ), + new TypeScriptObject([ + new TypeScriptProperty('invokable', new TypeScriptRaw('true')), + ]) + ), + new TypeScriptIdentifier('TController'), + new TypeScriptArray([ + new TypeScriptIdentifier('TController'), + new TypeScriptIdentifier('TAction'), + ]) + ) + ), + null, + 'ActionParam', + true, + [], + ); + + $transformedAction = new Transformed( + new TypeScriptFunctionDefinition( + new TypeScriptGeneric( + new TypeScriptIdentifier('route'), + [ + new TypeScriptGenericTypeVariable( + new TypeScriptIdentifier('TController'), + extends: new TypeScriptIdentifier('keyof Routes'), + ), + new TypeScriptGenericTypeVariable( + new TypeScriptIdentifier('TAction'), + extends: new TypeScriptIdentifier('keyof Routes[TController]["actions"]'), + ), + new TypeScriptGenericTypeVariable( + new TypeScriptIdentifier('TParams'), + extends: new TypeScriptIdentifier('Routes[TController]["actions"][TAction]["parameters"]'), + ), + ] + ), + [ + new TypeScriptParameter('action', new TypeScriptGeneric( + new TypeScriptIdentifier('ActionParam'), + [ + new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TController')), + new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TAction')), + ] + )), + new TypeScriptParameter('params', new TypeScriptIdentifier('TParams'), isOptional: true), + ], + new TypeScriptString(), + new TypeScriptRaw("let routes = JSON.parse('".json_encode($controllers->toJsObject(), flags: JSON_UNESCAPED_SLASHES). "')") + ), + null, + 'route', + true, + [], + ); + + return [$transformedRoutes, $actionParam, $transformedAction]; + } + + private function resolveRoutes(): RouteControllerCollection + { + /** @var array $controllers */ + $controllers = []; + + foreach (app(Router::class)->getRoutes()->getRoutes() as $route) { + $controllerClass = $route->getControllerClass(); + + if ($controllerClass === null) { + continue; + } + + $controllerClass = str_replace('\\', '.', $controllerClass); + + if ($route->getActionMethod() === $route->getControllerClass()) { + $controllers[$controllerClass] = new InvokableRouteController( + $this->resolveRouteParameters($route), + $route->methods, + $this->resolveUrl($route), + ); + + continue; + } + + if (! array_key_exists($controllerClass, $controllers)) { + $controllers[$controllerClass] = new RouteController([]); + } + + $controllers[$controllerClass]->actions[$route->getActionMethod()] = new RouteControllerAction( + $route->getActionMethod(), + $this->resolveRouteParameters($route), + $route->methods, + $this->resolveUrl($route), + ); + } + + return new RouteControllerCollection($controllers); + } + + protected function resolveRouteParameters( + Route $route + ): RouteParameterCollection { + preg_match_all('/\{(.*?)\}/', $route->getDomain().$route->uri, $matches); + + $parameters = array_map(fn (string $match) => new RouteParameter( + trim($match, '?'), + str_ends_with($match, '?') + ), $matches[1]); + + return new RouteParameterCollection($parameters); + } + + protected function resolveUrl(Route $route): string + { + return str_replace('?}', '}', $route->getDomain().$route->uri); + } +} diff --git a/src/Laravel/Routes/InvokableRouteController.php b/src/Laravel/Routes/InvokableRouteController.php new file mode 100644 index 00000000..902cba0f --- /dev/null +++ b/src/Laravel/Routes/InvokableRouteController.php @@ -0,0 +1,39 @@ + $methods + */ + public function __construct( + public RouteParameterCollection $parameters, + public array $methods, + public string $url, + ) + { + } + + public function toTypeScriptNode(): TypeScriptNode + { + return new TypeScriptObject([ + new TypeScriptProperty('invokable', new TypeScriptRaw('true')), + new TypeScriptProperty('parameters', $this->parameters->toTypeScriptNode()), + ]); + } + + public function toJsObject(): array + { + return [ + 'url' => $this->url, + 'methods' => array_values($this->methods) + ]; + } +} diff --git a/src/Laravel/Routes/RouteController.php b/src/Laravel/Routes/RouteController.php new file mode 100644 index 00000000..e425d8b6 --- /dev/null +++ b/src/Laravel/Routes/RouteController.php @@ -0,0 +1,41 @@ + $actions + */ + public function __construct( + public array $actions, + ) { + } + + public function toTypeScriptNode(): TypeScriptNode + { + return new TypeScriptObject([ + new TypeScriptProperty('actions', new TypeScriptObject(collect($this->actions)->map(function (RouteControllerAction $controller, string $name) { + return new TypeScriptProperty( + $name, + $controller->toTypeScriptNode(), + ); + })->all())), + ]); + } + + public function toJsObject(): array + { + return [ + 'actions' => collect($this->actions)->map(function (RouteControllerAction $action, string $name) { + return [ + $name => $action->toJsObject(), + ]; + })->all(), + ]; + } +} diff --git a/src/Laravel/Routes/RouteControllerAction.php b/src/Laravel/Routes/RouteControllerAction.php new file mode 100644 index 00000000..b557cc7a --- /dev/null +++ b/src/Laravel/Routes/RouteControllerAction.php @@ -0,0 +1,38 @@ + $methods + */ + public function __construct( + public string $name, + public RouteParameterCollection $parameters, + public array $methods, + public string $url, + ) { + } + + public function toTypeScriptNode(): TypeScriptNode + { + return new TypeScriptObject([ + new TypeScriptProperty('name', new TypeScriptLiteral($this->name)), + new TypeScriptProperty('parameters', $this->parameters->toTypeScriptNode()), + ]); + } + + public function toJsObject(): array + { + return [ + 'url' => $this->url, + 'methods' => array_values($this->methods), + ]; + } +} diff --git a/src/Laravel/Routes/RouteControllerCollection.php b/src/Laravel/Routes/RouteControllerCollection.php new file mode 100644 index 00000000..201f50e7 --- /dev/null +++ b/src/Laravel/Routes/RouteControllerCollection.php @@ -0,0 +1,35 @@ + $controllers + */ + public function __construct( + public array $controllers + ) { + } + + public function toTypeScriptNode(): TypeScriptNode + { + return new TypeScriptObject(collect($this->controllers)->map(function (RouteController|InvokableRouteController $controller, string $name) { + return new TypeScriptProperty( + $name, + $controller->toTypeScriptNode(), + ); + })->all()); + } + + public function toJsObject(): array + { + return collect($this->controllers)->map(function (RouteController|InvokableRouteController $controller) { + return $controller->toJsObject(); + })->all(); + } +} diff --git a/src/Laravel/Routes/RouteParameter.php b/src/Laravel/Routes/RouteParameter.php new file mode 100644 index 00000000..364ec89b --- /dev/null +++ b/src/Laravel/Routes/RouteParameter.php @@ -0,0 +1,32 @@ +name, + new TypeScriptUnion([new TypeScriptString(), new TypeScriptNumber()]), + isOptional: $this->optional, + ); + } + + public function toJsObject(): array + { + return []; + } +} diff --git a/src/Laravel/Routes/RouteParameterCollection.php b/src/Laravel/Routes/RouteParameterCollection.php new file mode 100644 index 00000000..f96d1ab9 --- /dev/null +++ b/src/Laravel/Routes/RouteParameterCollection.php @@ -0,0 +1,29 @@ + $parameters + */ + public function __construct( + public array $parameters, + ) { + } + + public function toTypeScriptNode(): TypeScriptNode + { + return new TypeScriptObject(array_map(function (RouteParameter $parameter) { + return $parameter->toTypeScriptNode(); + }, $this->parameters)); + } + + public function toJsObject(): array + { + return []; + } +} diff --git a/src/Laravel/Routes/RouterStructure.php b/src/Laravel/Routes/RouterStructure.php new file mode 100644 index 00000000..6cf366a0 --- /dev/null +++ b/src/Laravel/Routes/RouterStructure.php @@ -0,0 +1,12 @@ +type - ? "Array<{$this->type->write($context)}>" - : 'Array'; + $types = implode(', ', array_map( + fn(TypeScriptNode $type) => $type->write($context), + $this->types + )); + + return "[$types]"; } public function children(): array { - return $this->type ? [$this->type] : []; + return $this->types; } } diff --git a/src/TypeScript/TypeScriptConditional.php b/src/TypeScript/TypeScriptConditional.php new file mode 100644 index 00000000..fb4211a1 --- /dev/null +++ b/src/TypeScript/TypeScriptConditional.php @@ -0,0 +1,25 @@ +condition->write($context)} ? {$this->ifTrue->write($context)} : {$this->ifFalse->write($context)}"; + } + + public function children(): array + { + return [$this->condition, $this->ifTrue, $this->ifFalse]; + } +} diff --git a/src/TypeScript/TypeScriptFunctionDefinition.php b/src/TypeScript/TypeScriptFunctionDefinition.php new file mode 100644 index 00000000..353f8602 --- /dev/null +++ b/src/TypeScript/TypeScriptFunctionDefinition.php @@ -0,0 +1,35 @@ + $parameter->write($context), $this->parameters)); + + return "function {$this->identifier->write($context)}({$parameters}): {$this->returnType->write($context)} { + {$this->body->write($context)} + }"; + } + + public function children(): array + { + return [ + $this->identifier, + ...$this->parameters ?? [], + $this->returnType, + $this->body, + ]; + } +} diff --git a/src/TypeScript/TypeScriptGenericTypeVariable.php b/src/TypeScript/TypeScriptGenericTypeVariable.php new file mode 100644 index 00000000..bea8d1b6 --- /dev/null +++ b/src/TypeScript/TypeScriptGenericTypeVariable.php @@ -0,0 +1,31 @@ +identifier->write($context)}". + ($this->extends ? " extends {$this->extends?->write($context)}" : ''). + ($this->default ? " = {$this->default?->write($context)}" : ''); + } + + public function children(): array + { + return array_filter([ + $this->identifier, + $this->extends, + $this->default, + ]); + } +} diff --git a/src/TypeScript/TypeScriptIdentifier.php b/src/TypeScript/TypeScriptIdentifier.php index 23c5dfe8..8ec07129 100644 --- a/src/TypeScript/TypeScriptIdentifier.php +++ b/src/TypeScript/TypeScriptIdentifier.php @@ -13,6 +13,6 @@ public function __construct( public function write(WritingContext $context): string { - return $this->name; + return (str_contains($this->name, '.') || str_contains($this->name, '\\')) ? "'{$this->name}'" : $this->name; } } diff --git a/src/TypeScript/TypeScriptIndexedAccess.php b/src/TypeScript/TypeScriptIndexedAccess.php new file mode 100644 index 00000000..535c6544 --- /dev/null +++ b/src/TypeScript/TypeScriptIndexedAccess.php @@ -0,0 +1,34 @@ + $segments + */ + public function __construct( + public TypeScriptIdentifier $node, + public array $segments, + ) { + } + + public function write(WritingContext $context): string + { + $segments = array_map( + fn(TypeScriptNode $segment) => "[{$segment->write($context)}]", + $this->segments + ); + + return "{$this->node->write($context)}" . implode('', $segments); + } + + public function children(): array + { + return [$this->node, ...$this->segments]; + } +} diff --git a/src/TypeScript/TypeScriptInterface.php b/src/TypeScript/TypeScriptInterface.php index 4e72c7fc..4a253b08 100644 --- a/src/TypeScript/TypeScriptInterface.php +++ b/src/TypeScript/TypeScriptInterface.php @@ -11,7 +11,7 @@ class TypeScriptInterface implements TypeScriptNode, TypeScriptNodeWithChildren * @param array $methods */ public function __construct( - public string $name, + public TypeScriptIdentifier $name, public array $properties, public array $methods, ) { @@ -27,7 +27,7 @@ public function write(WritingContext $context): string empty($combined) ? '' : PHP_EOL ); - return "interface {$this->name} {{$items}}"; + return "interface {$this->name->write($context)} {{$items}}"; } public function children(): array diff --git a/src/TypeScript/TypeScriptOperator.php b/src/TypeScript/TypeScriptOperator.php new file mode 100644 index 00000000..80f76c5a --- /dev/null +++ b/src/TypeScript/TypeScriptOperator.php @@ -0,0 +1,62 @@ +left === null) { + return "{$this->operator}{$this->right->write($context)}"; + } + + return "{$this->left->write($context)} {$this->operator} {$this->right->write($context)}"; + } + + public function children(): array + { + return [$this->left, $this->right]; + } +} diff --git a/src/TypeScript/TypeScriptParameter.php b/src/TypeScript/TypeScriptParameter.php index 456ad0cc..3ac40871 100644 --- a/src/TypeScript/TypeScriptParameter.php +++ b/src/TypeScript/TypeScriptParameter.php @@ -9,7 +9,7 @@ class TypeScriptParameter implements TypeScriptNode, TypeScriptNodeWithChildren public function __construct( public string $name, public TypeScriptNode $type, - public bool $isOptional, + public bool $isOptional = false, ) { } diff --git a/tests/Actions/TranspilePhpStanTypeToTypeScriptTypeActionTest.php b/tests/Actions/TranspilePhpStanTypeToTypeScriptTypeActionTest.php index 7f32795d..9136edbe 100644 --- a/tests/Actions/TranspilePhpStanTypeToTypeScriptTypeActionTest.php +++ b/tests/Actions/TranspilePhpStanTypeToTypeScriptTypeActionTest.php @@ -170,12 +170,15 @@ yield [ 'array', - new TypeScriptArray(null), + new TypeScriptArray([]), ]; yield [ 'arrayGeneric', - new TypeScriptArray(new TypeScriptString()), + new TypeScriptGeneric( + new TypeScriptIdentifier('Array'), + [new TypeScriptString()] + ), ]; yield [ @@ -191,7 +194,7 @@ yield [ 'typeArray', - new TypeScriptArray(new TypeScriptString()), + new TypeScriptGeneric(new TypeScriptIdentifier('Array'), [new TypeScriptString()]), ]; yield [ @@ -200,7 +203,7 @@ new TypeScriptIdentifier('Record'), [ new TypeScriptNumber(), - new TypeScriptArray(new TypeScriptString()), + new TypeScriptGeneric(new TypeScriptIdentifier('Array'), [new TypeScriptString()]), ] ), ]; diff --git a/tests/Actions/TranspileReflectionTypeToTypeScriptTypeActionTest.php b/tests/Actions/TranspileReflectionTypeToTypeScriptTypeActionTest.php index 7e50eacd..80953c7f 100644 --- a/tests/Actions/TranspileReflectionTypeToTypeScriptTypeActionTest.php +++ b/tests/Actions/TranspileReflectionTypeToTypeScriptTypeActionTest.php @@ -130,7 +130,7 @@ yield [ 'array', - new TypeScriptArray(null), + new TypeScriptArray([]), ]; yield [ From 1c46069a4abf1e6425db2bdbd7f28faa7205f0ca Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Mon, 17 Jul 2023 20:34:50 +0000 Subject: [PATCH 08/51] Fix styling --- src/Actions/VisitTypeScriptTreeAction.php | 2 +- src/Laravel/LaravelDefaultTypesProvider.php | 2 +- src/Laravel/RouterGenerator.php | 3 +- .../Routes/InvokableRouteController.php | 8 ++-- src/Laravel/Routes/RouteController.php | 2 +- src/Laravel/Routes/RouteControllerAction.php | 2 +- .../Routes/RouteControllerCollection.php | 2 +- .../Routes/RouteParameterCollection.php | 2 +- .../SpatieLaravelDefaultTypesProvider.php | 44 +++++++++---------- src/Transformers/ClassTransformer.php | 2 +- src/TypeScript/TypeScriptArray.php | 4 +- src/TypeScript/TypeScriptIndexedAccess.php | 8 ++-- 12 files changed, 38 insertions(+), 43 deletions(-) diff --git a/src/Actions/VisitTypeScriptTreeAction.php b/src/Actions/VisitTypeScriptTreeAction.php index 701408ce..1831b525 100644 --- a/src/Actions/VisitTypeScriptTreeAction.php +++ b/src/Actions/VisitTypeScriptTreeAction.php @@ -11,7 +11,7 @@ class VisitTypeScriptTreeAction public function execute( TypeScriptNode $typeScriptNode, Closure $walker, - ?array $allowedNodes = null + array $allowedNodes = null ): void { // TODO: would be cool to replace nodes, remove them etc // Problem: nodes are sometimes structured in different properties which makes this complicated diff --git a/src/Laravel/LaravelDefaultTypesProvider.php b/src/Laravel/LaravelDefaultTypesProvider.php index 76ccd34e..6b5e04ea 100644 --- a/src/Laravel/LaravelDefaultTypesProvider.php +++ b/src/Laravel/LaravelDefaultTypesProvider.php @@ -94,7 +94,7 @@ protected function lengthAwarePaginator(): Transformed new TypeScriptProperty('data', new TypeScriptGeneric( new TypeScriptIdentifier('Array'), [new TypeScriptIdentifier('T')], - ),), + ), ), new TypeScriptProperty('links', new TypeScriptObject([ new TypeScriptProperty('url', new TypeScriptUnion([ new TypeScriptIdentifier('string'), diff --git a/src/Laravel/RouterGenerator.php b/src/Laravel/RouterGenerator.php index fb16bbe1..e7757f2b 100644 --- a/src/Laravel/RouterGenerator.php +++ b/src/Laravel/RouterGenerator.php @@ -20,7 +20,6 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGenericTypeVariable; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIndexedAccess; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptInterface; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptOperator; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptParameter; @@ -121,7 +120,7 @@ public function provide(): array new TypeScriptParameter('params', new TypeScriptIdentifier('TParams'), isOptional: true), ], new TypeScriptString(), - new TypeScriptRaw("let routes = JSON.parse('".json_encode($controllers->toJsObject(), flags: JSON_UNESCAPED_SLASHES). "')") + new TypeScriptRaw("let routes = JSON.parse('".json_encode($controllers->toJsObject(), flags: JSON_UNESCAPED_SLASHES)."')") ), null, 'route', diff --git a/src/Laravel/Routes/InvokableRouteController.php b/src/Laravel/Routes/InvokableRouteController.php index 902cba0f..9e2ada1f 100644 --- a/src/Laravel/Routes/InvokableRouteController.php +++ b/src/Laravel/Routes/InvokableRouteController.php @@ -2,7 +2,6 @@ namespace Spatie\TypeScriptTransformer\Laravel\Routes; -use PHPUnit\Runner\Extension\ParameterCollection; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; @@ -11,14 +10,13 @@ class InvokableRouteController implements RouterStructure { /** - * @param array $methods + * @param array $methods */ public function __construct( public RouteParameterCollection $parameters, public array $methods, public string $url, - ) - { + ) { } public function toTypeScriptNode(): TypeScriptNode @@ -33,7 +31,7 @@ public function toJsObject(): array { return [ 'url' => $this->url, - 'methods' => array_values($this->methods) + 'methods' => array_values($this->methods), ]; } } diff --git a/src/Laravel/Routes/RouteController.php b/src/Laravel/Routes/RouteController.php index e425d8b6..0cc6e74a 100644 --- a/src/Laravel/Routes/RouteController.php +++ b/src/Laravel/Routes/RouteController.php @@ -9,7 +9,7 @@ class RouteController implements RouterStructure { /** - * @param array $actions + * @param array $actions */ public function __construct( public array $actions, diff --git a/src/Laravel/Routes/RouteControllerAction.php b/src/Laravel/Routes/RouteControllerAction.php index b557cc7a..6e38e652 100644 --- a/src/Laravel/Routes/RouteControllerAction.php +++ b/src/Laravel/Routes/RouteControllerAction.php @@ -10,7 +10,7 @@ class RouteControllerAction implements RouterStructure { /** - * @param array $methods + * @param array $methods */ public function __construct( public string $name, diff --git a/src/Laravel/Routes/RouteControllerCollection.php b/src/Laravel/Routes/RouteControllerCollection.php index 201f50e7..1bd48a3f 100644 --- a/src/Laravel/Routes/RouteControllerCollection.php +++ b/src/Laravel/Routes/RouteControllerCollection.php @@ -9,7 +9,7 @@ class RouteControllerCollection implements RouterStructure { /** - * @param array $controllers + * @param array $controllers */ public function __construct( public array $controllers diff --git a/src/Laravel/Routes/RouteParameterCollection.php b/src/Laravel/Routes/RouteParameterCollection.php index f96d1ab9..aefcc7e1 100644 --- a/src/Laravel/Routes/RouteParameterCollection.php +++ b/src/Laravel/Routes/RouteParameterCollection.php @@ -8,7 +8,7 @@ class RouteParameterCollection implements RouterStructure { /** - * @param array $parameters + * @param array $parameters */ public function __construct( public array $parameters, diff --git a/src/Laravel/SpatieLaravelDefaultTypesProvider.php b/src/Laravel/SpatieLaravelDefaultTypesProvider.php index bcc160a3..6a78e434 100644 --- a/src/Laravel/SpatieLaravelDefaultTypesProvider.php +++ b/src/Laravel/SpatieLaravelDefaultTypesProvider.php @@ -22,29 +22,29 @@ public function provide(): array if (class_exists(\Spatie\LaravelOptions\Options::class)) { $types[] = new Transformed( new TypeScriptExport(new TypeScriptAlias( - new TypeScriptGeneric( - new TypeScriptIdentifier('Options'), - [ - new TypeScriptGenericTypeVariable( - new TypeScriptIdentifier('TValue'), - default: new TypeScriptIdentifier('string'), - ), - new TypeScriptGenericTypeVariable( - new TypeScriptIdentifier('TLabel'), - default: new TypeScriptIdentifier('string'), - ), - ] - ), - new TypeScriptGeneric( - new TypeScriptIdentifier('Array'), - [ - new TypeScriptObject([ - new TypeScriptProperty('value', new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TValue'))), - new TypeScriptProperty('label', new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TLabel'))), - ]), - ], - ) + new TypeScriptGeneric( + new TypeScriptIdentifier('Options'), + [ + new TypeScriptGenericTypeVariable( + new TypeScriptIdentifier('TValue'), + default: new TypeScriptIdentifier('string'), + ), + new TypeScriptGenericTypeVariable( + new TypeScriptIdentifier('TLabel'), + default: new TypeScriptIdentifier('string'), + ), + ] + ), + new TypeScriptGeneric( + new TypeScriptIdentifier('Array'), + [ + new TypeScriptObject([ + new TypeScriptProperty('value', new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TValue'))), + new TypeScriptProperty('label', new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TLabel'))), + ]), + ], ) + ) ), new ClassStringReference(\Spatie\LaravelOptions\Options::class), 'Options', diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index fba484fc..5094a21f 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -84,7 +84,7 @@ protected function getTypeScriptNode( protected function resolveTypeByAttribute( ReflectionClass $reflectionClass, - ?ReflectionProperty $property = null, + ReflectionProperty $property = null, ): ?TypeScriptNode { $subject = $property ?? $reflectionClass; diff --git a/src/TypeScript/TypeScriptArray.php b/src/TypeScript/TypeScriptArray.php index 46cf3617..4d9198a0 100644 --- a/src/TypeScript/TypeScriptArray.php +++ b/src/TypeScript/TypeScriptArray.php @@ -7,7 +7,7 @@ class TypeScriptArray implements TypeScriptNode, TypeScriptNodeWithChildren { /** - * @param TypeScriptNode[] $types + * @param TypeScriptNode[] $types */ public function __construct( public array $types @@ -17,7 +17,7 @@ public function __construct( public function write(WritingContext $context): string { $types = implode(', ', array_map( - fn(TypeScriptNode $type) => $type->write($context), + fn (TypeScriptNode $type) => $type->write($context), $this->types )); diff --git a/src/TypeScript/TypeScriptIndexedAccess.php b/src/TypeScript/TypeScriptIndexedAccess.php index 535c6544..9e739440 100644 --- a/src/TypeScript/TypeScriptIndexedAccess.php +++ b/src/TypeScript/TypeScriptIndexedAccess.php @@ -2,14 +2,12 @@ namespace Spatie\TypeScriptTransformer\TypeScript; -use PHPStan\PhpDocParser\Ast\Type\TypeNode; use Spatie\TypeScriptTransformer\Support\WritingContext; class TypeScriptIndexedAccess implements TypeScriptNode, TypeScriptNodeWithChildren { /** - * @param TypeScriptIdentifier $node - * @param array $segments + * @param array $segments */ public function __construct( public TypeScriptIdentifier $node, @@ -20,11 +18,11 @@ public function __construct( public function write(WritingContext $context): string { $segments = array_map( - fn(TypeScriptNode $segment) => "[{$segment->write($context)}]", + fn (TypeScriptNode $segment) => "[{$segment->write($context)}]", $this->segments ); - return "{$this->node->write($context)}" . implode('', $segments); + return "{$this->node->write($context)}".implode('', $segments); } public function children(): array From 08b571a665ee4915ef6ec3c563f05d906c1dc4f5 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Mon, 17 Jul 2023 16:48:37 -0400 Subject: [PATCH 09/51] wip --- src/Laravel/RouterGenerator.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Laravel/RouterGenerator.php b/src/Laravel/RouterGenerator.php index e7757f2b..a97b6247 100644 --- a/src/Laravel/RouterGenerator.php +++ b/src/Laravel/RouterGenerator.php @@ -93,7 +93,7 @@ public function provide(): array $transformedAction = new Transformed( new TypeScriptFunctionDefinition( new TypeScriptGeneric( - new TypeScriptIdentifier('route'), + new TypeScriptIdentifier('action'), [ new TypeScriptGenericTypeVariable( new TypeScriptIdentifier('TController'), @@ -123,7 +123,7 @@ public function provide(): array new TypeScriptRaw("let routes = JSON.parse('".json_encode($controllers->toJsObject(), flags: JSON_UNESCAPED_SLASHES)."')") ), null, - 'route', + 'action', true, [], ); From 35024f053b3eb043380d6b6b67bb435bc2c521f2 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 27 Jul 2023 13:59:50 +0200 Subject: [PATCH 10/51] wip --- src/Actions/FormatFilesAction.php | 8 +- src/Formatters/EslintFormatter.php | 19 +++ src/Formatters/Formatter.php | 8 ++ src/Formatters/PrettierFormatter.php | 19 +++ ...LaravelRoutControllerCollectionsAction.php | 72 ++++++++++++ ... => LaravelActionDefaultTypesProvider.php} | 110 +++++++++--------- src/Laravel/Routes/RouteController.php | 14 +-- src/Laravel/Routes/RouteControllerAction.php | 17 +-- .../Routes/RouteControllerCollection.php | 16 +-- ...oller.php => RouteInvokableController.php} | 10 +- src/Laravel/Routes/RouteParameter.php | 11 +- .../Routes/RouteParameterCollection.php | 14 +-- src/Laravel/Routes/RouterStructure.php | 2 - src/Support/WrittenFile.php | 1 - src/TypeScriptTransformerConfig.php | 2 + src/Writers/ModuleWriter.php | 2 +- src/Writers/NamespaceWriter.php | 5 +- 17 files changed, 197 insertions(+), 133 deletions(-) create mode 100644 src/Formatters/EslintFormatter.php create mode 100644 src/Formatters/Formatter.php create mode 100644 src/Formatters/PrettierFormatter.php create mode 100644 src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php rename src/Laravel/{RouterGenerator.php => LaravelActionDefaultTypesProvider.php} (64%) rename src/Laravel/Routes/{InvokableRouteController.php => RouteInvokableController.php} (67%) diff --git a/src/Actions/FormatFilesAction.php b/src/Actions/FormatFilesAction.php index 79fcd23c..8387d60b 100644 --- a/src/Actions/FormatFilesAction.php +++ b/src/Actions/FormatFilesAction.php @@ -15,10 +15,16 @@ public function __construct( } /** - * @param array $writtenFiles + * @param array $writtenFiles */ public function execute(array $writtenFiles): void { + if ($this->config->formatter === null) { + return; + } + foreach ($writtenFiles as $writtenFile) { + $this->config->formatter->format($writtenFile->path); + } } } diff --git a/src/Formatters/EslintFormatter.php b/src/Formatters/EslintFormatter.php new file mode 100644 index 00000000..ba16062c --- /dev/null +++ b/src/Formatters/EslintFormatter.php @@ -0,0 +1,19 @@ +run(); + + if (! $process->isSuccessful()) { + throw new ProcessFailedException($process); + } + } +} diff --git a/src/Formatters/Formatter.php b/src/Formatters/Formatter.php new file mode 100644 index 00000000..34cc6a13 --- /dev/null +++ b/src/Formatters/Formatter.php @@ -0,0 +1,8 @@ +run(); + + if (! $process->isSuccessful()) { + throw new ProcessFailedException($process); + } + } +} diff --git a/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php b/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php new file mode 100644 index 00000000..f12b2c69 --- /dev/null +++ b/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php @@ -0,0 +1,72 @@ + $controllers */ + $controllers = []; + + foreach (app(Router::class)->getRoutes()->getRoutes() as $route) { + $controllerClass = $route->getControllerClass(); + + if ($controllerClass === null) { + continue; + } + + $controllerClass = str_replace('\\', '.', $controllerClass); + + if ($route->getActionMethod() === $route->getControllerClass()) { + $controllers[$controllerClass] = new RouteInvokableController( + $this->resolveRouteParameters($route), + $route->methods, + $this->resolveUrl($route), + ); + + continue; + } + + if (! array_key_exists($controllerClass, $controllers)) { + $controllers[$controllerClass] = new RouteController([]); + } + + $controllers[$controllerClass]->actions[$route->getActionMethod()] = new RouteControllerAction( + $route->getActionMethod(), + $this->resolveRouteParameters($route), + $route->methods, + $this->resolveUrl($route), + ); + } + + return new RouteControllerCollection($controllers); + } + + protected function resolveRouteParameters( + Route $route + ): RouteParameterCollection { + preg_match_all('/\{(.*?)\}/', $route->getDomain().$route->uri, $matches); + + $parameters = array_map(fn (string $match) => new RouteParameter( + trim($match, '?'), + str_ends_with($match, '?') + ), $matches[1]); + + return new RouteParameterCollection($parameters); + } + + protected function resolveUrl(Route $route): string + { + return str_replace('?}', '}', $route->getDomain().$route->uri); + } +} diff --git a/src/Laravel/RouterGenerator.php b/src/Laravel/LaravelActionDefaultTypesProvider.php similarity index 64% rename from src/Laravel/RouterGenerator.php rename to src/Laravel/LaravelActionDefaultTypesProvider.php index a97b6247..e67d77cb 100644 --- a/src/Laravel/RouterGenerator.php +++ b/src/Laravel/LaravelActionDefaultTypesProvider.php @@ -2,13 +2,12 @@ namespace Spatie\TypeScriptTransformer\Laravel; -use Illuminate\Routing\Route; -use Illuminate\Routing\Router; use Spatie\TypeScriptTransformer\DefaultTypeProviders\DefaultTypesProvider; -use Spatie\TypeScriptTransformer\Laravel\Routes\InvokableRouteController; +use Spatie\TypeScriptTransformer\Laravel\Actions\ResolveLaravelRoutControllerCollectionsAction; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteController; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteControllerAction; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteControllerCollection; +use Spatie\TypeScriptTransformer\Laravel\Routes\RouteInvokableController; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameter; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameterCollection; use Spatie\TypeScriptTransformer\Transformed\Transformed; @@ -20,17 +19,20 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGenericTypeVariable; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIndexedAccess; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptLiteral; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNumber; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptOperator; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptParameter; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptRaw; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; // @todo implement the method, probably using a RawTypeScriptNode, creating individual notes for each JS construct is probably a bit far fetched // @todo make sure we support __invoke routes without action // @todo add support for nullable parameters, these should be inferred -// @todo a syntax like route(['Controller', 'action'], {params}), route(['Controller', 'action'], param), route(InvokeableController) would be even cooler but maybe too complicated at the moment /** * function route< @@ -41,16 +43,21 @@ * * } */ -class RouterGenerator implements DefaultTypesProvider +class LaravelActionDefaultTypesProvider implements DefaultTypesProvider { + public function __construct( + protected ResolveLaravelRoutControllerCollectionsAction $resolveLaravelRoutControllerCollectionsAction = new ResolveLaravelRoutControllerCollectionsAction() + ) { + } + public function provide(): array { - $controllers = $this->resolveRoutes(); + $controllers = $this->resolveLaravelRoutControllerCollectionsAction->execute(); $transformedRoutes = new Transformed( new TypeScriptAlias( new TypeScriptIdentifier('Routes'), - $controllers->toTypeScriptNode(), + $this->parseRouteControllerCollection($controllers), ), null, 'Routes', @@ -131,60 +138,59 @@ public function provide(): array return [$transformedRoutes, $actionParam, $transformedAction]; } - private function resolveRoutes(): RouteControllerCollection + protected function parseRouteControllerCollection(RouteControllerCollection $collection): TypeScriptNode { - /** @var array $controllers */ - $controllers = []; - - foreach (app(Router::class)->getRoutes()->getRoutes() as $route) { - $controllerClass = $route->getControllerClass(); - - if ($controllerClass === null) { - continue; - } - - $controllerClass = str_replace('\\', '.', $controllerClass); - - if ($route->getActionMethod() === $route->getControllerClass()) { - $controllers[$controllerClass] = new InvokableRouteController( - $this->resolveRouteParameters($route), - $route->methods, - $this->resolveUrl($route), - ); - - continue; - } - - if (! array_key_exists($controllerClass, $controllers)) { - $controllers[$controllerClass] = new RouteController([]); - } - - $controllers[$controllerClass]->actions[$route->getActionMethod()] = new RouteControllerAction( - $route->getActionMethod(), - $this->resolveRouteParameters($route), - $route->methods, - $this->resolveUrl($route), + return new TypeScriptObject(collect($collection->controllers)->map(function (RouteController|RouteInvokableController $controller, string $name) { + return new TypeScriptProperty( + $name, + $controller instanceof RouteInvokableController + ? $this->parseInvokableController($controller) + : $this->parseController($controller), ); - } + })->all()); + } - return new RouteControllerCollection($controllers); + protected function parseController(RouteController $controller): TypeScriptNode + { + return new TypeScriptObject([ + new TypeScriptProperty('actions', new TypeScriptObject(collect($controller->actions)->map(function (RouteControllerAction $action, string $name) { + return new TypeScriptProperty( + $name, + $this->parseControllerAction($action) + ); + })->all())), + ]); } - protected function resolveRouteParameters( - Route $route - ): RouteParameterCollection { - preg_match_all('/\{(.*?)\}/', $route->getDomain().$route->uri, $matches); + protected function parseControllerAction(RouteControllerAction $action): TypeScriptNode + { + return new TypeScriptObject([ + new TypeScriptProperty('name', new TypeScriptLiteral($action->name)), + new TypeScriptProperty('parameters', $this->parseRouteParameterCollection($action->parameters)), + ]); + } - $parameters = array_map(fn (string $match) => new RouteParameter( - trim($match, '?'), - str_ends_with($match, '?') - ), $matches[1]); + protected function parseInvokableController(RouteInvokableController $controller): TypeScriptNode + { + return new TypeScriptObject([ + new TypeScriptProperty('invokable', new TypeScriptRaw('true')), + new TypeScriptProperty('parameters', $this->parseRouteParameterCollection($controller->parameters)), + ]); + } - return new RouteParameterCollection($parameters); + protected function parseRouteParameterCollection(RouteParameterCollection $collection): TypeScriptNode + { + return new TypeScriptObject(array_map(function (RouteParameter $parameter) { + return $this->parseRouteParameter($parameter); + }, $collection->parameters)); } - protected function resolveUrl(Route $route): string + protected function parseRouteParameter(RouteParameter $parameter): TypeScriptNode { - return str_replace('?}', '}', $route->getDomain().$route->uri); + return new TypeScriptProperty( + $parameter->name, + new TypeScriptUnion([new TypeScriptString(), new TypeScriptNumber()]), + isOptional: $parameter->optional, + ); } } diff --git a/src/Laravel/Routes/RouteController.php b/src/Laravel/Routes/RouteController.php index 0cc6e74a..171cdbb0 100644 --- a/src/Laravel/Routes/RouteController.php +++ b/src/Laravel/Routes/RouteController.php @@ -6,7 +6,7 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -class RouteController implements RouterStructure +readonly class RouteController implements RouterStructure { /** * @param array $actions @@ -16,18 +16,6 @@ public function __construct( ) { } - public function toTypeScriptNode(): TypeScriptNode - { - return new TypeScriptObject([ - new TypeScriptProperty('actions', new TypeScriptObject(collect($this->actions)->map(function (RouteControllerAction $controller, string $name) { - return new TypeScriptProperty( - $name, - $controller->toTypeScriptNode(), - ); - })->all())), - ]); - } - public function toJsObject(): array { return [ diff --git a/src/Laravel/Routes/RouteControllerAction.php b/src/Laravel/Routes/RouteControllerAction.php index 6e38e652..ec94adc5 100644 --- a/src/Laravel/Routes/RouteControllerAction.php +++ b/src/Laravel/Routes/RouteControllerAction.php @@ -2,15 +2,10 @@ namespace Spatie\TypeScriptTransformer\Laravel\Routes; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptLiteral; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; - -class RouteControllerAction implements RouterStructure +readonly class RouteControllerAction implements RouterStructure { /** - * @param array $methods + * @param array $methods */ public function __construct( public string $name, @@ -20,14 +15,6 @@ public function __construct( ) { } - public function toTypeScriptNode(): TypeScriptNode - { - return new TypeScriptObject([ - new TypeScriptProperty('name', new TypeScriptLiteral($this->name)), - new TypeScriptProperty('parameters', $this->parameters->toTypeScriptNode()), - ]); - } - public function toJsObject(): array { return [ diff --git a/src/Laravel/Routes/RouteControllerCollection.php b/src/Laravel/Routes/RouteControllerCollection.php index 1bd48a3f..d64bd17b 100644 --- a/src/Laravel/Routes/RouteControllerCollection.php +++ b/src/Laravel/Routes/RouteControllerCollection.php @@ -6,29 +6,19 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -class RouteControllerCollection implements RouterStructure +readonly class RouteControllerCollection implements RouterStructure { /** - * @param array $controllers + * @param array $controllers */ public function __construct( public array $controllers ) { } - public function toTypeScriptNode(): TypeScriptNode - { - return new TypeScriptObject(collect($this->controllers)->map(function (RouteController|InvokableRouteController $controller, string $name) { - return new TypeScriptProperty( - $name, - $controller->toTypeScriptNode(), - ); - })->all()); - } - public function toJsObject(): array { - return collect($this->controllers)->map(function (RouteController|InvokableRouteController $controller) { + return collect($this->controllers)->map(function (RouteController|RouteInvokableController $controller) { return $controller->toJsObject(); })->all(); } diff --git a/src/Laravel/Routes/InvokableRouteController.php b/src/Laravel/Routes/RouteInvokableController.php similarity index 67% rename from src/Laravel/Routes/InvokableRouteController.php rename to src/Laravel/Routes/RouteInvokableController.php index 9e2ada1f..8d2febce 100644 --- a/src/Laravel/Routes/InvokableRouteController.php +++ b/src/Laravel/Routes/RouteInvokableController.php @@ -7,7 +7,7 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptRaw; -class InvokableRouteController implements RouterStructure +readonly class RouteInvokableController implements RouterStructure { /** * @param array $methods @@ -19,14 +19,6 @@ public function __construct( ) { } - public function toTypeScriptNode(): TypeScriptNode - { - return new TypeScriptObject([ - new TypeScriptProperty('invokable', new TypeScriptRaw('true')), - new TypeScriptProperty('parameters', $this->parameters->toTypeScriptNode()), - ]); - } - public function toJsObject(): array { return [ diff --git a/src/Laravel/Routes/RouteParameter.php b/src/Laravel/Routes/RouteParameter.php index 364ec89b..4871c815 100644 --- a/src/Laravel/Routes/RouteParameter.php +++ b/src/Laravel/Routes/RouteParameter.php @@ -8,7 +8,7 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; -class RouteParameter implements RouterStructure +readonly class RouteParameter implements RouterStructure { public function __construct( public string $name, @@ -16,15 +16,6 @@ public function __construct( ) { } - public function toTypeScriptNode(): TypeScriptNode - { - return new TypeScriptProperty( - $this->name, - new TypeScriptUnion([new TypeScriptString(), new TypeScriptNumber()]), - isOptional: $this->optional, - ); - } - public function toJsObject(): array { return []; diff --git a/src/Laravel/Routes/RouteParameterCollection.php b/src/Laravel/Routes/RouteParameterCollection.php index aefcc7e1..6457bfcf 100644 --- a/src/Laravel/Routes/RouteParameterCollection.php +++ b/src/Laravel/Routes/RouteParameterCollection.php @@ -2,26 +2,16 @@ namespace Spatie\TypeScriptTransformer\Laravel\Routes; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; - -class RouteParameterCollection implements RouterStructure +readonly class RouteParameterCollection implements RouterStructure { /** - * @param array $parameters + * @param array $parameters */ public function __construct( public array $parameters, ) { } - public function toTypeScriptNode(): TypeScriptNode - { - return new TypeScriptObject(array_map(function (RouteParameter $parameter) { - return $parameter->toTypeScriptNode(); - }, $this->parameters)); - } - public function toJsObject(): array { return []; diff --git a/src/Laravel/Routes/RouterStructure.php b/src/Laravel/Routes/RouterStructure.php index 6cf366a0..5e5d94c3 100644 --- a/src/Laravel/Routes/RouterStructure.php +++ b/src/Laravel/Routes/RouterStructure.php @@ -6,7 +6,5 @@ interface RouterStructure { - public function toTypeScriptNode(): TypeScriptNode; - public function toJsObject(): array; } diff --git a/src/Support/WrittenFile.php b/src/Support/WrittenFile.php index a1ba68f0..2e2397b5 100644 --- a/src/Support/WrittenFile.php +++ b/src/Support/WrittenFile.php @@ -6,7 +6,6 @@ class WrittenFile { public function __construct( public string $path, - public array $types, ) { } } diff --git a/src/TypeScriptTransformerConfig.php b/src/TypeScriptTransformerConfig.php index 49915cd5..4d4ae71c 100755 --- a/src/TypeScriptTransformerConfig.php +++ b/src/TypeScriptTransformerConfig.php @@ -3,6 +3,7 @@ namespace Spatie\TypeScriptTransformer; use Spatie\TypeScriptTransformer\DefaultTypeProviders\DefaultTypesProvider; +use Spatie\TypeScriptTransformer\Formatters\Formatter; use Spatie\TypeScriptTransformer\Transformers\Transformer; use Spatie\TypeScriptTransformer\Writers\Writer; @@ -18,6 +19,7 @@ public function __construct( public array $transformers, public array $defaultTypeProviders, public Writer $writer, + public ?Formatter $formatter ) { } } diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index 47c13b00..feefbe52 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -75,7 +75,7 @@ protected function writeLocation( file_put_contents("{$path}/{$this->filename}.{$this->extension}", $output); - return new WrittenFile($path, $location->transformed); + return new WrittenFile($path); } /** diff --git a/src/Writers/NamespaceWriter.php b/src/Writers/NamespaceWriter.php index 59abd49f..72b99636 100644 --- a/src/Writers/NamespaceWriter.php +++ b/src/Writers/NamespaceWriter.php @@ -61,10 +61,7 @@ public function output(TransformedCollection $collection, ReferenceMap $referenc ); return [ - new WrittenFile( - $this->filename, - [], - ), + new WrittenFile($this->filename), ]; } From 5df83a39454e07aa2cc2e05994944daec39301ad Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 27 Jul 2023 17:19:00 +0200 Subject: [PATCH 11/51] Fix routes --- src/Actions/AppendDefaultTypesAction.php | 7 +- src/Actions/VisitTypeScriptTreeAction.php | 4 +- ...LaravelRoutControllerCollectionsAction.php | 36 +++- .../LaravelActionDefaultTypesProvider.php | 149 +++++++++----- src/Laravel/Routes/RouteClosure.php | 24 +++ src/Laravel/Routes/RouteCollection.php | 24 +++ src/Laravel/Routes/RouteController.php | 14 +- src/Laravel/Routes/RouteControllerAction.php | 2 +- .../Routes/RouteControllerCollection.php | 25 --- .../Routes/RouteInvokableController.php | 2 +- src/Laravel/Routes/RouteParameter.php | 2 +- .../Routes/RouteParameterCollection.php | 2 +- src/References/CustomReference.php | 22 +++ src/TypeScript/TypeScriptIndexedAccess.php | 2 +- src/TypeScript/TypeScriptOperator.php | 8 +- src/TypeScriptTransformerConfig.php | 2 +- src/Writers/NamespaceWriter.php | 4 + ...velRoutControllerCollectionsActionTest.php | 184 ++++++++++++++++++ .../FakeClasses/InvokableController.php | 11 ++ .../FakeClasses/ResourceController.php | 64 ++++++ tests/Laravel/LaravelTestCase.php | 16 ++ tests/Stubs/PhpTypesStub.php | 4 +- 22 files changed, 509 insertions(+), 99 deletions(-) create mode 100644 src/Laravel/Routes/RouteClosure.php create mode 100644 src/Laravel/Routes/RouteCollection.php delete mode 100644 src/Laravel/Routes/RouteControllerCollection.php create mode 100644 src/References/CustomReference.php create mode 100644 tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php create mode 100644 tests/Laravel/FakeClasses/InvokableController.php create mode 100644 tests/Laravel/FakeClasses/ResourceController.php create mode 100644 tests/Laravel/LaravelTestCase.php diff --git a/src/Actions/AppendDefaultTypesAction.php b/src/Actions/AppendDefaultTypesAction.php index aec32ee5..16c4816a 100644 --- a/src/Actions/AppendDefaultTypesAction.php +++ b/src/Actions/AppendDefaultTypesAction.php @@ -17,9 +17,10 @@ public function __construct( public function execute(TransformedCollection $collection): void { - foreach ($this->config->defaultTypeProviders as $defaultTypeProviderClass) { - /** @var DefaultTypesProvider $defaultTypeProvider */ - $defaultTypeProvider = new $defaultTypeProviderClass; + foreach ($this->config->defaultTypeProviders as $defaultTypeProvider) { + $defaultTypeProvider = $defaultTypeProvider instanceof DefaultTypesProvider + ? $defaultTypeProvider + : new $defaultTypeProvider; $collection->add(...$defaultTypeProvider->provide()); } diff --git a/src/Actions/VisitTypeScriptTreeAction.php b/src/Actions/VisitTypeScriptTreeAction.php index 1831b525..a763a265 100644 --- a/src/Actions/VisitTypeScriptTreeAction.php +++ b/src/Actions/VisitTypeScriptTreeAction.php @@ -21,7 +21,9 @@ public function execute( } if ($typeScriptNode instanceof TypeScriptNodeWithChildren) { - foreach ($typeScriptNode->children() as $child) { + $children = array_values(array_filter($typeScriptNode->children())); + + foreach ($children as $child) { $this->execute($child, $walker, $allowedNodes); } } diff --git a/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php b/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php index f12b2c69..a700ab7a 100644 --- a/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php +++ b/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php @@ -4,27 +4,53 @@ use Illuminate\Routing\Route; use Illuminate\Routing\Router; -use Spatie\TypeScriptTransformer\Laravel\Routes\RouteInvokableController; +use Illuminate\Support\Str; +use Spatie\TypeScriptTransformer\Laravel\Routes\RouteClosure; +use Spatie\TypeScriptTransformer\Laravel\Routes\RouteCollection; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteController; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteControllerAction; -use Spatie\TypeScriptTransformer\Laravel\Routes\RouteControllerCollection; +use Spatie\TypeScriptTransformer\Laravel\Routes\RouteInvokableController; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameter; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameterCollection; class ResolveLaravelRoutControllerCollectionsAction { - public function execute(): RouteControllerCollection - { + public function execute( + ?string $defaultNamespace, + bool $includeRouteClosures, + ): RouteCollection { /** @var array $controllers */ $controllers = []; + /** @var array $closures */ + $closures = []; foreach (app(Router::class)->getRoutes()->getRoutes() as $route) { $controllerClass = $route->getControllerClass(); + if ($controllerClass === null && ! $includeRouteClosures) { + continue; + } + if ($controllerClass === null) { + $name = "Closure({$route->uri})"; + + $closures[$name] = new RouteClosure( + $this->resolveRouteParameters($route), + $route->methods, + $this->resolveUrl($route), + ); + continue; } + if ($defaultNamespace !== null) { + $controllerClass = Str::of($controllerClass) + ->trim('\\') + ->replace($defaultNamespace, '') + ->trim('\\') + ->toString(); + } + $controllerClass = str_replace('\\', '.', $controllerClass); if ($route->getActionMethod() === $route->getControllerClass()) { @@ -49,7 +75,7 @@ public function execute(): RouteControllerCollection ); } - return new RouteControllerCollection($controllers); + return new RouteCollection($controllers, $closures); } protected function resolveRouteParameters( diff --git a/src/Laravel/LaravelActionDefaultTypesProvider.php b/src/Laravel/LaravelActionDefaultTypesProvider.php index e67d77cb..32819dff 100644 --- a/src/Laravel/LaravelActionDefaultTypesProvider.php +++ b/src/Laravel/LaravelActionDefaultTypesProvider.php @@ -4,13 +4,15 @@ use Spatie\TypeScriptTransformer\DefaultTypeProviders\DefaultTypesProvider; use Spatie\TypeScriptTransformer\Laravel\Actions\ResolveLaravelRoutControllerCollectionsAction; +use Spatie\TypeScriptTransformer\Laravel\Routes\RouteCollection; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteController; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteControllerAction; -use Spatie\TypeScriptTransformer\Laravel\Routes\RouteControllerCollection; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteInvokableController; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameter; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameterCollection; +use Spatie\TypeScriptTransformer\References\CustomReference; use Spatie\TypeScriptTransformer\Transformed\Transformed; +use Spatie\TypeScriptTransformer\TypeScript\TypeReference; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptArray; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptConditional; @@ -30,60 +32,54 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; -// @todo implement the method, probably using a RawTypeScriptNode, creating individual notes for each JS construct is probably a bit far fetched -// @todo make sure we support __invoke routes without action -// @todo add support for nullable parameters, these should be inferred - -/** - * function route< - * TController extends keyof Routes, - * TAction extends keyof Routes[TController], - * TParams extends Routes[TController][TAction]["parameters"] - * >(action: [TController, TAction] | TController, params?: TParams): string { - * - * } - */ class LaravelActionDefaultTypesProvider implements DefaultTypesProvider { public function __construct( - protected ResolveLaravelRoutControllerCollectionsAction $resolveLaravelRoutControllerCollectionsAction = new ResolveLaravelRoutControllerCollectionsAction() + protected ResolveLaravelRoutControllerCollectionsAction $resolveLaravelRoutControllerCollectionsAction = new ResolveLaravelRoutControllerCollectionsAction(), + protected ?string $defaultNamespace = null, + protected array $location = [], ) { } public function provide(): array { - $controllers = $this->resolveLaravelRoutControllerCollectionsAction->execute(); + $controllers = $this->resolveLaravelRoutControllerCollectionsAction->execute( + $this->defaultNamespace, + includeRouteClosures: false, + ); $transformedRoutes = new Transformed( new TypeScriptAlias( - new TypeScriptIdentifier('Routes'), + new TypeScriptIdentifier('RoutesList'), $this->parseRouteControllerCollection($controllers), ), - null, - 'Routes', + $routesListReference = new CustomReference('laravel_route_actions', 'routes_list'), + 'RoutesList', true, - [], + $this->location, ); - $actionParam = new Transformed( + $isInvokableControllerCondition = TypeScriptOperator::extends( + new TypeScriptIndexedAccess( + new TypeReference($routesListReference), + [new TypeScriptIdentifier('TController')], + ), + new TypeScriptObject([ + new TypeScriptProperty('invokable', new TypeScriptRaw('true')), + ]) + ); + + $actionController = new Transformed( new TypeScriptAlias( new TypeScriptGeneric( - new TypeScriptIdentifier('ActionParam'), + new TypeScriptIdentifier('ActionController'), [ new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TController')), new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TAction')), ] ), new TypeScriptConditional( - TypeScriptOperator::extends( - new TypeScriptIndexedAccess( - new TypeScriptIdentifier('Routes'), - [new TypeScriptIdentifier('TController')], - ), - new TypeScriptObject([ - new TypeScriptProperty('invokable', new TypeScriptRaw('true')), - ]) - ), + $isInvokableControllerCondition, new TypeScriptIdentifier('TController'), new TypeScriptArray([ new TypeScriptIdentifier('TController'), @@ -91,12 +87,44 @@ public function provide(): array ]) ) ), - null, - 'ActionParam', + $actionControllerReference = new CustomReference('laravel_route_actions', 'action_controller'), + 'ActionController', true, - [], + $this->location, ); + $actionParameters = new Transformed( + new TypeScriptAlias( + new TypeScriptGeneric( + new TypeScriptIdentifier('ActionParameters'), + [ + new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TController')), + new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TAction')), + ] + ), + new TypeScriptConditional( + $isInvokableControllerCondition, + new TypeScriptIndexedAccess(new TypeReference($routesListReference), [ + new TypeScriptIdentifier('TController'), + new TypeScriptIdentifier('"parameters"'), + ]), + new TypeScriptIndexedAccess(new TypeReference($routesListReference), [ + new TypeScriptIdentifier('TController'), + new TypeScriptIdentifier('"actions"'), + new TypeScriptIdentifier('TAction'), + new TypeScriptIdentifier('"parameters"'), + ]) + ) + ), + $actionParametersReference = new CustomReference('laravel_route_actions', 'action_parameters'), + 'ActionParameters', + true, + $this->location, + ); + + $jsonEncodedRoutes = json_encode($controllers->toJsObject(), flags: JSON_UNESCAPED_SLASHES); + $baseUrl = url('/'); + $transformedAction = new Transformed( new TypeScriptFunctionDefinition( new TypeScriptGeneric( @@ -104,41 +132,74 @@ public function provide(): array [ new TypeScriptGenericTypeVariable( new TypeScriptIdentifier('TController'), - extends: new TypeScriptIdentifier('keyof Routes'), + extends: TypeScriptOperator::keyof(new TypeReference($routesListReference)) ), new TypeScriptGenericTypeVariable( new TypeScriptIdentifier('TAction'), - extends: new TypeScriptIdentifier('keyof Routes[TController]["actions"]'), + extends: TypeScriptOperator::keyof(new TypeScriptIndexedAccess(new TypeReference($routesListReference), [ + new TypeScriptIdentifier('TController'), + new TypeScriptIdentifier('"actions"'), + ])) ), new TypeScriptGenericTypeVariable( new TypeScriptIdentifier('TParams'), - extends: new TypeScriptIdentifier('Routes[TController]["actions"][TAction]["parameters"]'), + extends: new TypeScriptIndexedAccess(new TypeReference($routesListReference), [ + new TypeScriptIdentifier('TController'), + new TypeScriptIdentifier('"actions"'), + new TypeScriptIdentifier('TAction'), + new TypeScriptIdentifier('"parameters"'), + ]) ), ] ), [ new TypeScriptParameter('action', new TypeScriptGeneric( - new TypeScriptIdentifier('ActionParam'), + new TypeReference($actionControllerReference), [ new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TController')), new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TAction')), ] )), - new TypeScriptParameter('params', new TypeScriptIdentifier('TParams'), isOptional: true), + new TypeScriptParameter('parameters', new TypeScriptGeneric( + new TypeReference($actionParametersReference), + [ + new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TController')), + new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TAction')), + ] + ), isOptional: true), ], new TypeScriptString(), - new TypeScriptRaw("let routes = JSON.parse('".json_encode($controllers->toJsObject(), flags: JSON_UNESCAPED_SLASHES)."')") + new TypeScriptRaw(<<toJsObject(), flags: JSON_UNESCAPED_SLASHES)."')") ), - null, + new CustomReference('laravel_route_actions', 'action_function'), 'action', true, - [], + $this->location, ); - return [$transformedRoutes, $actionParam, $transformedAction]; + return [$transformedRoutes, $actionController, $actionParameters, $transformedAction]; } - protected function parseRouteControllerCollection(RouteControllerCollection $collection): TypeScriptNode + protected function parseRouteControllerCollection(RouteCollection $collection): TypeScriptNode { return new TypeScriptObject(collect($collection->controllers)->map(function (RouteController|RouteInvokableController $controller, string $name) { return new TypeScriptProperty( diff --git a/src/Laravel/Routes/RouteClosure.php b/src/Laravel/Routes/RouteClosure.php new file mode 100644 index 00000000..221146ef --- /dev/null +++ b/src/Laravel/Routes/RouteClosure.php @@ -0,0 +1,24 @@ + $methods + */ + public function __construct( + public RouteParameterCollection $parameters, + public array $methods, + public string $url, + ) { + } + + public function toJsObject(): array + { + return [ + 'url' => $this->url, + 'methods' => array_values($this->methods), + ]; + } +} diff --git a/src/Laravel/Routes/RouteCollection.php b/src/Laravel/Routes/RouteCollection.php new file mode 100644 index 00000000..eb9d6b7f --- /dev/null +++ b/src/Laravel/Routes/RouteCollection.php @@ -0,0 +1,24 @@ + $controllers + * @param array $closures + */ + public function __construct( + public array $controllers, + public array $closures, + ) { + } + + public function toJsObject(): array + { + return [ + 'controllers' => collect($this->controllers)->map(fn (RouteController|RouteInvokableController $controller) => $controller->toJsObject())->all(), + 'closures' => collect($this->closures)->map(fn (RouteClosure $closure) => $closure->toJsObject())->all(), + ]; + } +} diff --git a/src/Laravel/Routes/RouteController.php b/src/Laravel/Routes/RouteController.php index 171cdbb0..50417a48 100644 --- a/src/Laravel/Routes/RouteController.php +++ b/src/Laravel/Routes/RouteController.php @@ -2,14 +2,10 @@ namespace Spatie\TypeScriptTransformer\Laravel\Routes; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; - -readonly class RouteController implements RouterStructure +class RouteController implements RouterStructure { /** - * @param array $actions + * @param array $actions */ public function __construct( public array $actions, @@ -19,11 +15,7 @@ public function __construct( public function toJsObject(): array { return [ - 'actions' => collect($this->actions)->map(function (RouteControllerAction $action, string $name) { - return [ - $name => $action->toJsObject(), - ]; - })->all(), + 'actions' => collect($this->actions)->map(fn (RouteControllerAction $action, string $name) => $action->toJsObject())->all(), ]; } } diff --git a/src/Laravel/Routes/RouteControllerAction.php b/src/Laravel/Routes/RouteControllerAction.php index ec94adc5..27d26c03 100644 --- a/src/Laravel/Routes/RouteControllerAction.php +++ b/src/Laravel/Routes/RouteControllerAction.php @@ -2,7 +2,7 @@ namespace Spatie\TypeScriptTransformer\Laravel\Routes; -readonly class RouteControllerAction implements RouterStructure +class RouteControllerAction implements RouterStructure { /** * @param array $methods diff --git a/src/Laravel/Routes/RouteControllerCollection.php b/src/Laravel/Routes/RouteControllerCollection.php deleted file mode 100644 index d64bd17b..00000000 --- a/src/Laravel/Routes/RouteControllerCollection.php +++ /dev/null @@ -1,25 +0,0 @@ - $controllers - */ - public function __construct( - public array $controllers - ) { - } - - public function toJsObject(): array - { - return collect($this->controllers)->map(function (RouteController|RouteInvokableController $controller) { - return $controller->toJsObject(); - })->all(); - } -} diff --git a/src/Laravel/Routes/RouteInvokableController.php b/src/Laravel/Routes/RouteInvokableController.php index 8d2febce..03d10616 100644 --- a/src/Laravel/Routes/RouteInvokableController.php +++ b/src/Laravel/Routes/RouteInvokableController.php @@ -7,7 +7,7 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptRaw; -readonly class RouteInvokableController implements RouterStructure +class RouteInvokableController implements RouterStructure { /** * @param array $methods diff --git a/src/Laravel/Routes/RouteParameter.php b/src/Laravel/Routes/RouteParameter.php index 4871c815..3a5e6db3 100644 --- a/src/Laravel/Routes/RouteParameter.php +++ b/src/Laravel/Routes/RouteParameter.php @@ -8,7 +8,7 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; -readonly class RouteParameter implements RouterStructure +class RouteParameter implements RouterStructure { public function __construct( public string $name, diff --git a/src/Laravel/Routes/RouteParameterCollection.php b/src/Laravel/Routes/RouteParameterCollection.php index 6457bfcf..ab4a4806 100644 --- a/src/Laravel/Routes/RouteParameterCollection.php +++ b/src/Laravel/Routes/RouteParameterCollection.php @@ -2,7 +2,7 @@ namespace Spatie\TypeScriptTransformer\Laravel\Routes; -readonly class RouteParameterCollection implements RouterStructure +class RouteParameterCollection implements RouterStructure { /** * @param array $parameters diff --git a/src/References/CustomReference.php b/src/References/CustomReference.php new file mode 100644 index 00000000..b2cbedca --- /dev/null +++ b/src/References/CustomReference.php @@ -0,0 +1,22 @@ +group}_{$this->name}"; + } + + public function humanFriendlyName(): string + { + return "custom {$this->group}::{$this->name}"; + } +} diff --git a/src/TypeScript/TypeScriptIndexedAccess.php b/src/TypeScript/TypeScriptIndexedAccess.php index 9e739440..f16773a1 100644 --- a/src/TypeScript/TypeScriptIndexedAccess.php +++ b/src/TypeScript/TypeScriptIndexedAccess.php @@ -10,7 +10,7 @@ class TypeScriptIndexedAccess implements TypeScriptNode, TypeScriptNodeWithChild * @param array $segments */ public function __construct( - public TypeScriptIdentifier $node, + public TypeScriptIdentifier|TypeReference $node, public array $segments, ) { } diff --git a/src/TypeScript/TypeScriptOperator.php b/src/TypeScript/TypeScriptOperator.php index 80f76c5a..ad29569d 100644 --- a/src/TypeScript/TypeScriptOperator.php +++ b/src/TypeScript/TypeScriptOperator.php @@ -26,6 +26,12 @@ public static function typeof( return new self('typeof', $type); } + public static function keyof( + TypeScriptNode $type, + ): self { + return new self('keyof', $type); + } + public static function instanceof( TypeScriptNode $instance, TypeScriptNode $class, @@ -49,7 +55,7 @@ public static function extends( public function write(WritingContext $context): string { if ($this->left === null) { - return "{$this->operator}{$this->right->write($context)}"; + return "{$this->operator} {$this->right->write($context)}"; } return "{$this->left->write($context)} {$this->operator} {$this->right->write($context)}"; diff --git a/src/TypeScriptTransformerConfig.php b/src/TypeScriptTransformerConfig.php index 4d4ae71c..c0cc6dd0 100755 --- a/src/TypeScriptTransformerConfig.php +++ b/src/TypeScriptTransformerConfig.php @@ -12,7 +12,7 @@ /** * @param array $directories * @param array $transformers - * @param array> $defaultTypeProviders + * @param array|DefaultTypesProvider> $defaultTypeProviders */ public function __construct( public array $directories, diff --git a/src/Writers/NamespaceWriter.php b/src/Writers/NamespaceWriter.php index 72b99636..d72f6da6 100644 --- a/src/Writers/NamespaceWriter.php +++ b/src/Writers/NamespaceWriter.php @@ -32,6 +32,10 @@ public function output(TransformedCollection $collection, ReferenceMap $referenc $writingContext = new WritingContext(function (Reference $reference) use ($referenceMap) { $transformable = $referenceMap->get($reference); + if(empty($transformable->location)){ + return $transformable->name; + } + return implode('.', $transformable->location).'.'.$transformable->name; }); diff --git a/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php b/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php new file mode 100644 index 00000000..d3c99c40 --- /dev/null +++ b/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php @@ -0,0 +1,184 @@ +in(__DIR__.'/../'); + + +it('can resolve all possible routes', function (Closure $route, Closure $expectations) { + $route(app(Router::class)); + + $routes = app(ResolveLaravelRoutControllerCollectionsAction::class)->execute(null, true); + + $expectations($routes); +})->with(function () { + yield 'simple closure' => [ + fn (Router $router) => $router->get('simple', fn () => 'simple'), + function (RouteCollection $routes) { + expect($routes->controllers)->toBeEmpty(); + expect($routes->closures)->toHaveCount(1); + + expect($routes->closures['Closure(simple)']->url)->toBe('simple'); + expect($routes->closures['Closure(simple)']->methods)->toBe(['GET', 'HEAD']); + }, + ]; + yield 'controller action' => [ + fn (Router $router) => $router->get('action', [ResourceController::class, 'update']), + function (RouteCollection $routes) { + expect($routes->controllers)->toHaveCount(1); + expect($routes->closures)->toBeEmpty(); + + $actions = $routes->controllers['Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController']->actions; + + expect($actions)->toHaveCount(1); + expect($actions['update'])->toBeInstanceOf(RouteControllerAction::class); + expect($actions['update']->url)->toBe('action'); + expect($actions['update']->methods)->toBe(['GET', 'HEAD']); + + expect($actions['update']->parameters)->toBeInstanceOf(RouteParameterCollection::class); + expect($actions['update']->parameters->parameters)->toBeEmpty(); + }, + ]; + yield 'invokable controller' => [ + fn (Router $router) => $router->get('invokable', InvokableController::class), + function (RouteCollection $routes) { + expect($routes->controllers)->toHaveCount(1); + expect($routes->closures)->toBeEmpty(); + + $controller = $routes->controllers['Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController']; + + expect($controller)->toBeInstanceOf(RouteInvokableController::class); + expect($controller->url)->toBe('invokable'); + expect($controller->methods)->toBe(['GET', 'HEAD']); + + expect($controller->parameters)->toBeInstanceOf(RouteParameterCollection::class); + expect($controller->parameters->parameters)->toBeEmpty(); + }, + ]; + yield 'resource controller' => [ + fn (Router $router) => $router->resource('resource', ResourceController::class), + function (RouteCollection $routes) { + expect($routes->controllers)->toHaveCount(1); + expect($routes->closures)->toBeEmpty(); + + $controller = $routes->controllers['Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController']; + + expect($controller)->toBeInstanceOf(RouteController::class); + expect($controller->actions)->toHaveCount(7); + + expect($controller->actions['index'])->toBeInstanceOf(RouteControllerAction::class); + expect($controller->actions['index']->url)->toBe('resource'); + expect($controller->actions['index']->methods)->toBe(['GET', 'HEAD']); + expect($controller->actions['index']->parameters->parameters)->toBeEmpty(); + + expect($controller->actions['create'])->toBeInstanceOf(RouteControllerAction::class); + expect($controller->actions['create']->url)->toBe('resource/create'); + expect($controller->actions['create']->methods)->toBe(['GET', 'HEAD']); + expect($controller->actions['create']->parameters->parameters)->toBeEmpty(); + + expect($controller->actions['store'])->toBeInstanceOf(RouteControllerAction::class); + expect($controller->actions['store']->url)->toBe('resource'); + expect($controller->actions['store']->methods)->toBe(['POST']); + expect($controller->actions['store']->parameters->parameters)->toBeEmpty(); + + expect($controller->actions['show'])->toBeInstanceOf(RouteControllerAction::class); + expect($controller->actions['show']->url)->toBe('resource/{resource}'); + expect($controller->actions['show']->methods)->toBe(['GET', 'HEAD']); + expect($controller->actions['show']->parameters->parameters)->toHaveCount(1); + + expect($controller->actions['edit'])->toBeInstanceOf(RouteControllerAction::class); + expect($controller->actions['edit']->url)->toBe('resource/{resource}/edit'); + expect($controller->actions['edit']->methods)->toBe(['GET', 'HEAD']); + expect($controller->actions['edit']->parameters->parameters)->toHaveCount(1); + + expect($controller->actions['update'])->toBeInstanceOf(RouteControllerAction::class); + expect($controller->actions['update']->url)->toBe('resource/{resource}'); + expect($controller->actions['update']->methods)->toBe(['PUT', 'PATCH']); + expect($controller->actions['update']->parameters->parameters)->toHaveCount(1); + + expect($controller->actions['destroy'])->toBeInstanceOf(RouteControllerAction::class); + expect($controller->actions['destroy']->url)->toBe('resource/{resource}'); + expect($controller->actions['destroy']->methods)->toBe(['DELETE']); + expect($controller->actions['destroy']->parameters->parameters)->toHaveCount(1); + }, + ]; + yield 'nested' => [ + fn (Router $router) => $router->group(['prefix' => 'nested'], fn (Router $router) => $router->get('simple', fn () => 'simple')), + function (RouteCollection $routes) { + expect($routes->controllers)->toBeEmpty(); + expect($routes->closures)->toHaveCount(1); + + expect($routes->closures['Closure(nested/simple)']->url)->toBe('nested/simple'); + expect($routes->closures['Closure(nested/simple)']->methods)->toBe(['GET', 'HEAD']); + }, + ]; + yield 'methods' => [ + function (Router $router) { + $router->get('get', fn () => 'get'); + $router->post('post', fn () => 'post'); + $router->put('put', fn () => 'put'); + $router->patch('patch', fn () => 'patch'); + $router->delete('delete', fn () => 'delete'); + $router->options('options', fn () => 'options'); + }, + function (RouteCollection $routes) { + expect($routes->controllers)->toBeEmpty(); + expect($routes->closures)->toHaveCount(6); + + expect($routes->closures['Closure(get)']->methods)->toBe(['GET', 'HEAD']); + expect($routes->closures['Closure(post)']->methods)->toBe(['POST']); + expect($routes->closures['Closure(put)']->methods)->toBe(['PUT']); + expect($routes->closures['Closure(patch)']->methods)->toBe(['PATCH']); + expect($routes->closures['Closure(delete)']->methods)->toBe(['DELETE']); + expect($routes->closures['Closure(options)']->methods)->toBe(['OPTIONS']); + }, + ]; + yield 'parameter' => [ + fn (Router $router) => $router->get('simple/{id}', fn () => 'simple'), + function (RouteCollection $routes) { + expect($routes->controllers)->toBeEmpty(); + expect($routes->closures)->toHaveCount(1); + + expect($routes->closures['Closure(simple/{id})']->url)->toBe('simple/{id}'); + expect($routes->closures['Closure(simple/{id})']->methods)->toBe(['GET', 'HEAD']); + expect($routes->closures['Closure(simple/{id})']->parameters->parameters)->toHaveCount(1); + expect($routes->closures['Closure(simple/{id})']->parameters->parameters[0]->name)->toBe('id'); + expect($routes->closures['Closure(simple/{id})']->parameters->parameters[0]->optional)->toBeFalse(); + }, + ]; + yield 'nullable parameter' => [ + fn (Router $router) => $router->get('simple/{id?}', fn () => 'simple'), + function (RouteCollection $routes) { + expect($routes->controllers)->toBeEmpty(); + expect($routes->closures)->toHaveCount(1); + + expect($routes->closures['Closure(simple/{id?})']->url)->toBe('simple/{id}'); + expect($routes->closures['Closure(simple/{id?})']->methods)->toBe(['GET', 'HEAD']); + expect($routes->closures['Closure(simple/{id?})']->parameters->parameters)->toHaveCount(1); + expect($routes->closures['Closure(simple/{id?})']->parameters->parameters[0]->name)->toBe('id'); + expect($routes->closures['Closure(simple/{id?})']->parameters->parameters[0]->optional)->toBeTrue(); + }, + ]; +}); + +it('can omit certain parts of a specified namespace', function (){ + app(Router::class)->get('error', ErrorController::class); + app(Router::class)->get('invokable', InvokableController::class); + + $routes = app(ResolveLaravelRoutControllerCollectionsAction::class)->execute('Spatie\TypeScriptTransformer\Tests\Laravel\FakeClasses', true); + + expect($routes->controllers)->toHaveCount(2)->toHaveKeys([ + 'Symfony.Component.HttpKernel.Controller.ErrorController', + 'InvokableController' + ]); +}); diff --git a/tests/Laravel/FakeClasses/InvokableController.php b/tests/Laravel/FakeClasses/InvokableController.php new file mode 100644 index 00000000..bf62c29b --- /dev/null +++ b/tests/Laravel/FakeClasses/InvokableController.php @@ -0,0 +1,11 @@ + Date: Thu, 27 Jul 2023 15:19:35 +0000 Subject: [PATCH 12/51] Fix styling --- src/Actions/FormatFilesAction.php | 2 +- src/Laravel/LaravelActionDefaultTypesProvider.php | 4 ++-- src/Laravel/Routes/RouteClosure.php | 2 +- src/Laravel/Routes/RouteCollection.php | 4 ++-- src/Laravel/Routes/RouteController.php | 2 +- src/Laravel/Routes/RouteControllerAction.php | 2 +- src/Laravel/Routes/RouteInvokableController.php | 5 ----- src/Laravel/Routes/RouteParameter.php | 6 ------ src/Laravel/Routes/RouteParameterCollection.php | 2 +- src/Laravel/Routes/RouterStructure.php | 2 -- src/Writers/NamespaceWriter.php | 2 +- .../ResolveLaravelRoutControllerCollectionsActionTest.php | 5 ++--- tests/Stubs/PhpTypesStub.php | 4 +++- 13 files changed, 15 insertions(+), 27 deletions(-) diff --git a/src/Actions/FormatFilesAction.php b/src/Actions/FormatFilesAction.php index 8387d60b..a4a0c3eb 100644 --- a/src/Actions/FormatFilesAction.php +++ b/src/Actions/FormatFilesAction.php @@ -15,7 +15,7 @@ public function __construct( } /** - * @param array $writtenFiles + * @param array $writtenFiles */ public function execute(array $writtenFiles): void { diff --git a/src/Laravel/LaravelActionDefaultTypesProvider.php b/src/Laravel/LaravelActionDefaultTypesProvider.php index 32819dff..77586d14 100644 --- a/src/Laravel/LaravelActionDefaultTypesProvider.php +++ b/src/Laravel/LaravelActionDefaultTypesProvider.php @@ -187,8 +187,8 @@ public function provide(): array return url; TS -) -// new TypeScriptRaw("let routes = JSON.parse('".json_encode($controllers->toJsObject(), flags: JSON_UNESCAPED_SLASHES)."')") + ) + // new TypeScriptRaw("let routes = JSON.parse('".json_encode($controllers->toJsObject(), flags: JSON_UNESCAPED_SLASHES)."')") ), new CustomReference('laravel_route_actions', 'action_function'), 'action', diff --git a/src/Laravel/Routes/RouteClosure.php b/src/Laravel/Routes/RouteClosure.php index 221146ef..adf4732e 100644 --- a/src/Laravel/Routes/RouteClosure.php +++ b/src/Laravel/Routes/RouteClosure.php @@ -5,7 +5,7 @@ class RouteClosure implements RouterStructure { /** - * @param array $methods + * @param array $methods */ public function __construct( public RouteParameterCollection $parameters, diff --git a/src/Laravel/Routes/RouteCollection.php b/src/Laravel/Routes/RouteCollection.php index eb9d6b7f..a0cec5ad 100644 --- a/src/Laravel/Routes/RouteCollection.php +++ b/src/Laravel/Routes/RouteCollection.php @@ -5,8 +5,8 @@ class RouteCollection implements RouterStructure { /** - * @param array $controllers - * @param array $closures + * @param array $controllers + * @param array $closures */ public function __construct( public array $controllers, diff --git a/src/Laravel/Routes/RouteController.php b/src/Laravel/Routes/RouteController.php index 50417a48..c129e4f1 100644 --- a/src/Laravel/Routes/RouteController.php +++ b/src/Laravel/Routes/RouteController.php @@ -5,7 +5,7 @@ class RouteController implements RouterStructure { /** - * @param array $actions + * @param array $actions */ public function __construct( public array $actions, diff --git a/src/Laravel/Routes/RouteControllerAction.php b/src/Laravel/Routes/RouteControllerAction.php index 27d26c03..b6855d73 100644 --- a/src/Laravel/Routes/RouteControllerAction.php +++ b/src/Laravel/Routes/RouteControllerAction.php @@ -5,7 +5,7 @@ class RouteControllerAction implements RouterStructure { /** - * @param array $methods + * @param array $methods */ public function __construct( public string $name, diff --git a/src/Laravel/Routes/RouteInvokableController.php b/src/Laravel/Routes/RouteInvokableController.php index 03d10616..175e9f41 100644 --- a/src/Laravel/Routes/RouteInvokableController.php +++ b/src/Laravel/Routes/RouteInvokableController.php @@ -2,11 +2,6 @@ namespace Spatie\TypeScriptTransformer\Laravel\Routes; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptRaw; - class RouteInvokableController implements RouterStructure { /** diff --git a/src/Laravel/Routes/RouteParameter.php b/src/Laravel/Routes/RouteParameter.php index 3a5e6db3..d70f6f1e 100644 --- a/src/Laravel/Routes/RouteParameter.php +++ b/src/Laravel/Routes/RouteParameter.php @@ -2,12 +2,6 @@ namespace Spatie\TypeScriptTransformer\Laravel\Routes; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNumber; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; - class RouteParameter implements RouterStructure { public function __construct( diff --git a/src/Laravel/Routes/RouteParameterCollection.php b/src/Laravel/Routes/RouteParameterCollection.php index ab4a4806..b35fa2b3 100644 --- a/src/Laravel/Routes/RouteParameterCollection.php +++ b/src/Laravel/Routes/RouteParameterCollection.php @@ -5,7 +5,7 @@ class RouteParameterCollection implements RouterStructure { /** - * @param array $parameters + * @param array $parameters */ public function __construct( public array $parameters, diff --git a/src/Laravel/Routes/RouterStructure.php b/src/Laravel/Routes/RouterStructure.php index 5e5d94c3..ce23f26e 100644 --- a/src/Laravel/Routes/RouterStructure.php +++ b/src/Laravel/Routes/RouterStructure.php @@ -2,8 +2,6 @@ namespace Spatie\TypeScriptTransformer\Laravel\Routes; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; - interface RouterStructure { public function toJsObject(): array; diff --git a/src/Writers/NamespaceWriter.php b/src/Writers/NamespaceWriter.php index d72f6da6..6998a093 100644 --- a/src/Writers/NamespaceWriter.php +++ b/src/Writers/NamespaceWriter.php @@ -32,7 +32,7 @@ public function output(TransformedCollection $collection, ReferenceMap $referenc $writingContext = new WritingContext(function (Reference $reference) use ($referenceMap) { $transformable = $referenceMap->get($reference); - if(empty($transformable->location)){ + if (empty($transformable->location)) { return $transformable->name; } diff --git a/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php b/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php index d3c99c40..ba98b0ec 100644 --- a/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php +++ b/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php @@ -14,7 +14,6 @@ uses(LaravelTestCase::class)->in(__DIR__.'/../'); - it('can resolve all possible routes', function (Closure $route, Closure $expectations) { $route(app(Router::class)); @@ -171,7 +170,7 @@ function (RouteCollection $routes) { ]; }); -it('can omit certain parts of a specified namespace', function (){ +it('can omit certain parts of a specified namespace', function () { app(Router::class)->get('error', ErrorController::class); app(Router::class)->get('invokable', InvokableController::class); @@ -179,6 +178,6 @@ function (RouteCollection $routes) { expect($routes->controllers)->toHaveCount(2)->toHaveKeys([ 'Symfony.Component.HttpKernel.Controller.ErrorController', - 'InvokableController' + 'InvokableController', ]); }); diff --git a/tests/Stubs/PhpTypesStub.php b/tests/Stubs/PhpTypesStub.php index 8c0f611b..86a8973f 100644 --- a/tests/Stubs/PhpTypesStub.php +++ b/tests/Stubs/PhpTypesStub.php @@ -30,7 +30,9 @@ class PhpTypesStub extends stdClass public Collection&Arrayable $intersection; - public (Collection&Arrayable)|null $bnf; + public (Collection&Arrayable) + +|null $bnf; public self $self; From 3d5ff27c7ea3b250dc37d2729518e7c678ef55cd Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 27 Jul 2023 18:17:18 +0200 Subject: [PATCH 13/51] Updated routing --- ...LaravelRoutControllerCollectionsAction.php | 4 +- .../LaravelNamedRouteDefaultTypesProvider.php | 194 ++++++++++++++++++ ...aravelRouteActionDefaultTypesProvider.php} | 38 +++- src/Laravel/Routes/RouteClosure.php | 9 +- src/Laravel/Routes/RouteCollection.php | 8 - src/Laravel/Routes/RouteController.php | 7 - src/Laravel/Routes/RouteControllerAction.php | 10 +- .../Routes/RouteInvokableController.php | 9 +- src/Laravel/Routes/RouteParameter.php | 5 - .../Routes/RouteParameterCollection.php | 5 - src/Laravel/Routes/RouterStructure.php | 1 - ...velRoutControllerCollectionsActionTest.php | 26 +++ 12 files changed, 253 insertions(+), 63 deletions(-) create mode 100644 src/Laravel/LaravelNamedRouteDefaultTypesProvider.php rename src/Laravel/{LaravelActionDefaultTypesProvider.php => LaravelRouteActionDefaultTypesProvider.php} (89%) diff --git a/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php b/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php index a700ab7a..1f9a789c 100644 --- a/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php +++ b/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php @@ -38,6 +38,7 @@ public function execute( $this->resolveRouteParameters($route), $route->methods, $this->resolveUrl($route), + $route->getName(), ); continue; @@ -58,6 +59,7 @@ public function execute( $this->resolveRouteParameters($route), $route->methods, $this->resolveUrl($route), + $route->getName(), ); continue; @@ -68,10 +70,10 @@ public function execute( } $controllers[$controllerClass]->actions[$route->getActionMethod()] = new RouteControllerAction( - $route->getActionMethod(), $this->resolveRouteParameters($route), $route->methods, $this->resolveUrl($route), + $route->getName(), ); } diff --git a/src/Laravel/LaravelNamedRouteDefaultTypesProvider.php b/src/Laravel/LaravelNamedRouteDefaultTypesProvider.php new file mode 100644 index 00000000..3b360cca --- /dev/null +++ b/src/Laravel/LaravelNamedRouteDefaultTypesProvider.php @@ -0,0 +1,194 @@ +resolveLaravelRoutControllerCollectionsAction->execute( + null, + includeRouteClosures: true, + ); + + $transformedRoutes = new Transformed( + new TypeScriptAlias( + new TypeScriptIdentifier('NamedRouteList'), + $this->parseRouteCollection($routeCollection), + ), + $routesListReference = new CustomReference('laravel_named_routes', 'routes_list'), + 'NamedRouteList', + true, + $this->location, + ); + + $jsonEncodedRoutes = $this->routeCollectionToJson($routeCollection); + $baseUrl = url('/'); + + $transformedRoute = new Transformed( + new TypeScriptFunctionDefinition( + new TypeScriptGeneric( + new TypeScriptIdentifier('route'), + [ + new TypeScriptGenericTypeVariable( + new TypeScriptIdentifier('TRoute'), + extends: TypeScriptOperator::keyof(new TypeReference($routesListReference)) + ), + ] + ), + [ + new TypeScriptParameter('route', new TypeScriptIdentifier('TRoute')), + new TypeScriptParameter( + 'parameters', + new TypeScriptIndexedAccess(new TypeReference($routesListReference), [ + new TypeScriptIdentifier('TRoute'), + new TypeScriptIdentifier('"parameters"'), + ]), + isOptional: true), + ], + new TypeScriptString(), + new TypeScriptRaw(<<location, + ); + + return [$transformedRoutes, $transformedRoute]; + } + + protected function parseRouteCollection(RouteCollection $collection): TypeScriptNode + { + $mappingFunction = fn (RouteControllerAction|RouteInvokableController|RouteClosure $entity) => new TypeScriptProperty( + $entity->name, + new TypeScriptObject([ + new TypeScriptProperty( + 'parameters', + $this->parseRouteParameterCollection($entity->parameters), + ), + ]) + ); + + $properties = collect(array_merge($collection->controllers, $collection->closures)) + ->flatMap(function (RouteController|RouteInvokableController|RouteClosure $entity) use ($mappingFunction) { + $singleRoute = $entity instanceof RouteInvokableController || $entity instanceof RouteClosure; + + if ($singleRoute && $entity->name) { + return [$mappingFunction($entity)]; + } + + if ($entity instanceof RouteController) { + return collect($entity->actions) + ->filter(fn (RouteControllerAction $action) => $action->name) + ->values() + ->map($mappingFunction); + } + + return []; + }) + ->all(); + + return new TypeScriptObject($properties); + } + + protected function parseRouteParameterCollection(RouteParameterCollection $collection): TypeScriptNode + { + return new TypeScriptObject(array_map(function (RouteParameter $parameter) { + return $this->parseRouteParameter($parameter); + }, $collection->parameters)); + } + + protected function parseRouteParameter(RouteParameter $parameter): TypeScriptNode + { + return new TypeScriptProperty( + $parameter->name, + new TypeScriptUnion([new TypeScriptString(), new TypeScriptNumber()]), + isOptional: $parameter->optional, + ); + } + + protected function routeCollectionToJson(RouteCollection $collection): string + { + $mappingFunction = fn (RouteInvokableController|RouteControllerAction|RouteClosure $entity) => [ + $entity->name => [ + 'url' => $entity->url, + 'methods' => array_values($entity->methods), + ], + ]; + + $controllers = collect($collection->controllers)->mapWithKeys(function (RouteController|RouteInvokableController $controller) use ($mappingFunction) { + if ($controller instanceof RouteInvokableController && $controller->name) { + return $mappingFunction($controller); + } + + if ($controller instanceof RouteController) { + return collect($controller->actions) + ->filter(fn (RouteControllerAction $action) => $action->name) + ->values() + ->mapWithKeys($mappingFunction); + } + + return []; + }); + + $closures = collect($collection->closures) + ->filter(fn (RouteClosure $closure) => $closure->name) + ->values() + ->mapWithKeys(function (RouteClosure $closure) use ($mappingFunction) { + return $mappingFunction($closure); + }); + + return $controllers->merge($closures)->toJson(JSON_UNESCAPED_SLASHES); + } +} diff --git a/src/Laravel/LaravelActionDefaultTypesProvider.php b/src/Laravel/LaravelRouteActionDefaultTypesProvider.php similarity index 89% rename from src/Laravel/LaravelActionDefaultTypesProvider.php rename to src/Laravel/LaravelRouteActionDefaultTypesProvider.php index 77586d14..ffaecccb 100644 --- a/src/Laravel/LaravelActionDefaultTypesProvider.php +++ b/src/Laravel/LaravelRouteActionDefaultTypesProvider.php @@ -32,7 +32,7 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; -class LaravelActionDefaultTypesProvider implements DefaultTypesProvider +class LaravelRouteActionDefaultTypesProvider implements DefaultTypesProvider { public function __construct( protected ResolveLaravelRoutControllerCollectionsAction $resolveLaravelRoutControllerCollectionsAction = new ResolveLaravelRoutControllerCollectionsAction(), @@ -43,18 +43,18 @@ public function __construct( public function provide(): array { - $controllers = $this->resolveLaravelRoutControllerCollectionsAction->execute( + $routeCollection = $this->resolveLaravelRoutControllerCollectionsAction->execute( $this->defaultNamespace, includeRouteClosures: false, ); $transformedRoutes = new Transformed( new TypeScriptAlias( - new TypeScriptIdentifier('RoutesList'), - $this->parseRouteControllerCollection($controllers), + new TypeScriptIdentifier('ActionRoutesList'), + $this->parseRouteCollection($routeCollection), ), $routesListReference = new CustomReference('laravel_route_actions', 'routes_list'), - 'RoutesList', + 'ActionRoutesList', true, $this->location, ); @@ -122,7 +122,7 @@ public function provide(): array $this->location, ); - $jsonEncodedRoutes = json_encode($controllers->toJsObject(), flags: JSON_UNESCAPED_SLASHES); + $jsonEncodedRoutes = $this->routeCollectionToJson($routeCollection); $baseUrl = url('/'); $transformedAction = new Transformed( @@ -174,8 +174,8 @@ public function provide(): array let baseUrl = '$baseUrl'; let found = typeof action === 'string' - ? routes.controllers[action] - : routes.controllers[action[0]]['actions'][action[1]]; + ? routes[action] + : routes[action[0]]['actions'][action[1]]; let url = baseUrl + '/' + found.url; @@ -188,7 +188,6 @@ public function provide(): array return url; TS ) - // new TypeScriptRaw("let routes = JSON.parse('".json_encode($controllers->toJsObject(), flags: JSON_UNESCAPED_SLASHES)."')") ), new CustomReference('laravel_route_actions', 'action_function'), 'action', @@ -199,7 +198,7 @@ public function provide(): array return [$transformedRoutes, $actionController, $actionParameters, $transformedAction]; } - protected function parseRouteControllerCollection(RouteCollection $collection): TypeScriptNode + protected function parseRouteCollection(RouteCollection $collection): TypeScriptNode { return new TypeScriptObject(collect($collection->controllers)->map(function (RouteController|RouteInvokableController $controller, string $name) { return new TypeScriptProperty( @@ -226,7 +225,6 @@ protected function parseController(RouteController $controller): TypeScriptNode protected function parseControllerAction(RouteControllerAction $action): TypeScriptNode { return new TypeScriptObject([ - new TypeScriptProperty('name', new TypeScriptLiteral($action->name)), new TypeScriptProperty('parameters', $this->parseRouteParameterCollection($action->parameters)), ]); } @@ -254,4 +252,22 @@ protected function parseRouteParameter(RouteParameter $parameter): TypeScriptNod isOptional: $parameter->optional, ); } + + protected function routeCollectionToJson(RouteCollection $collection): string + { + return collect($collection->controllers) + ->map(fn (RouteController|RouteInvokableController $controller) => $controller instanceof RouteInvokableController + ? [ + 'url' => $controller->url, + 'methods' => array_values($controller->methods), + ] + : [ + 'actions' => collect($controller->actions)->map(fn (RouteControllerAction $action) => [ + 'url' => $action->url, + 'methods' => array_values($action->methods), + ]), + ] + ) + ->toJson(JSON_UNESCAPED_SLASHES); + } } diff --git a/src/Laravel/Routes/RouteClosure.php b/src/Laravel/Routes/RouteClosure.php index adf4732e..812b5ba4 100644 --- a/src/Laravel/Routes/RouteClosure.php +++ b/src/Laravel/Routes/RouteClosure.php @@ -11,14 +11,7 @@ public function __construct( public RouteParameterCollection $parameters, public array $methods, public string $url, + public ?string $name, ) { } - - public function toJsObject(): array - { - return [ - 'url' => $this->url, - 'methods' => array_values($this->methods), - ]; - } } diff --git a/src/Laravel/Routes/RouteCollection.php b/src/Laravel/Routes/RouteCollection.php index a0cec5ad..4a5bf5fa 100644 --- a/src/Laravel/Routes/RouteCollection.php +++ b/src/Laravel/Routes/RouteCollection.php @@ -13,12 +13,4 @@ public function __construct( public array $closures, ) { } - - public function toJsObject(): array - { - return [ - 'controllers' => collect($this->controllers)->map(fn (RouteController|RouteInvokableController $controller) => $controller->toJsObject())->all(), - 'closures' => collect($this->closures)->map(fn (RouteClosure $closure) => $closure->toJsObject())->all(), - ]; - } } diff --git a/src/Laravel/Routes/RouteController.php b/src/Laravel/Routes/RouteController.php index c129e4f1..d082305d 100644 --- a/src/Laravel/Routes/RouteController.php +++ b/src/Laravel/Routes/RouteController.php @@ -11,11 +11,4 @@ public function __construct( public array $actions, ) { } - - public function toJsObject(): array - { - return [ - 'actions' => collect($this->actions)->map(fn (RouteControllerAction $action, string $name) => $action->toJsObject())->all(), - ]; - } } diff --git a/src/Laravel/Routes/RouteControllerAction.php b/src/Laravel/Routes/RouteControllerAction.php index b6855d73..2d1d355a 100644 --- a/src/Laravel/Routes/RouteControllerAction.php +++ b/src/Laravel/Routes/RouteControllerAction.php @@ -8,18 +8,10 @@ class RouteControllerAction implements RouterStructure * @param array $methods */ public function __construct( - public string $name, public RouteParameterCollection $parameters, public array $methods, public string $url, + public ?string $name, ) { } - - public function toJsObject(): array - { - return [ - 'url' => $this->url, - 'methods' => array_values($this->methods), - ]; - } } diff --git a/src/Laravel/Routes/RouteInvokableController.php b/src/Laravel/Routes/RouteInvokableController.php index 175e9f41..2edbaa57 100644 --- a/src/Laravel/Routes/RouteInvokableController.php +++ b/src/Laravel/Routes/RouteInvokableController.php @@ -11,14 +11,7 @@ public function __construct( public RouteParameterCollection $parameters, public array $methods, public string $url, + public ?string $name, ) { } - - public function toJsObject(): array - { - return [ - 'url' => $this->url, - 'methods' => array_values($this->methods), - ]; - } } diff --git a/src/Laravel/Routes/RouteParameter.php b/src/Laravel/Routes/RouteParameter.php index d70f6f1e..adc3511a 100644 --- a/src/Laravel/Routes/RouteParameter.php +++ b/src/Laravel/Routes/RouteParameter.php @@ -9,9 +9,4 @@ public function __construct( public bool $optional, ) { } - - public function toJsObject(): array - { - return []; - } } diff --git a/src/Laravel/Routes/RouteParameterCollection.php b/src/Laravel/Routes/RouteParameterCollection.php index b35fa2b3..38364ad3 100644 --- a/src/Laravel/Routes/RouteParameterCollection.php +++ b/src/Laravel/Routes/RouteParameterCollection.php @@ -11,9 +11,4 @@ public function __construct( public array $parameters, ) { } - - public function toJsObject(): array - { - return []; - } } diff --git a/src/Laravel/Routes/RouterStructure.php b/src/Laravel/Routes/RouterStructure.php index ce23f26e..c9036f18 100644 --- a/src/Laravel/Routes/RouterStructure.php +++ b/src/Laravel/Routes/RouterStructure.php @@ -4,5 +4,4 @@ interface RouterStructure { - public function toJsObject(): array; } diff --git a/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php b/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php index ba98b0ec..b6dd0cf1 100644 --- a/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php +++ b/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php @@ -168,6 +168,32 @@ function (RouteCollection $routes) { expect($routes->closures['Closure(simple/{id?})']->parameters->parameters[0]->optional)->toBeTrue(); }, ]; + yield 'named routes' => [ + function (Router $router) { + $router->get('simple', fn () => 'simple')->name('simple'); + $router->get('invokable', InvokableController::class)->name('invokable'); + $router->resource('resource', ResourceController::class); + + }, + function (RouteCollection $routes) { + expect($routes->controllers)->toHaveCount(2); + expect($routes->closures)->toHaveCount(1); + + expect($routes->closures['Closure(simple)']->name)->toBe('simple'); + + expect($routes->controllers['Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController']->name)->toBe('invokable'); + + $resourceController = $routes->controllers['Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController']; + + expect($resourceController->actions['index']->name)->toBe('resource.index'); + expect($resourceController->actions['show']->name)->toBe('resource.show'); + expect($resourceController->actions['create']->name)->toBe('resource.create'); + expect($resourceController->actions['update']->name)->toBe('resource.update'); + expect($resourceController->actions['store']->name)->toBe('resource.store'); + expect($resourceController->actions['edit']->name)->toBe('resource.edit'); + expect($resourceController->actions['destroy']->name)->toBe('resource.destroy'); + }, + ]; }); it('can omit certain parts of a specified namespace', function () { From 3581d52df25c4703952a453a4cadd18f29968569 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Thu, 27 Jul 2023 16:17:53 +0000 Subject: [PATCH 14/51] Fix styling --- src/Laravel/LaravelRouteActionDefaultTypesProvider.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Laravel/LaravelRouteActionDefaultTypesProvider.php b/src/Laravel/LaravelRouteActionDefaultTypesProvider.php index ffaecccb..b428f289 100644 --- a/src/Laravel/LaravelRouteActionDefaultTypesProvider.php +++ b/src/Laravel/LaravelRouteActionDefaultTypesProvider.php @@ -21,7 +21,6 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGenericTypeVariable; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIndexedAccess; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptLiteral; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNumber; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; From 4cfe74098a47d92735bd988632673e823cb45530 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 1 Aug 2023 14:55:16 +0200 Subject: [PATCH 15/51] Add visitor support --- src/Actions/ConnectReferencesAction.php | 38 ++++--- src/Actions/DiscoverTypesAction.php | 18 ++-- ...TypesAction.php => ProvideTypesAction.php} | 14 ++- src/Actions/TransformTypeAction.php | 59 ---------- src/Actions/TransformTypesAction.php | 37 ------- src/Actions/VisitTypeScriptTreeAction.php | 31 ------ src/Actions/WatchFileSystemAction.php | 2 +- .../DefaultTypesProvider.php | 11 -- ...moveDataLazyTypeClassPropertyProcessor.php | 34 ++++++ ...ollectionByArrayClassPropertyProcessor.php | 54 ++++++++++ ...php => LaravelNamedRouteTypesProvider.php} | 13 +-- ...hp => LaravelRouteActionTypesProvider.php} | 15 ++- ...sProvider.php => LaravelTypesProvider.php} | 19 ++-- .../SpatieLaravelDefaultTypesProvider.php | 58 ---------- src/Laravel/SpatieLaravelTypesProvider.php | 58 ++++++++++ .../Transformers/DataClassTransformer.php | 21 ++++ .../Transformers/LaravelClassTransformer.php | 16 +++ src/Support/VisitorProfile.php | 31 ++++++ src/Transformed/Transformed.php | 9 +- src/Transformers/AttributeTransformer.php | 2 +- .../ClassPropertyProcessor.php | 16 +++ src/Transformers/ClassTransformer.php | 63 ++++++++--- src/Transformers/DataClassTransformer.php | 55 ---------- src/Transformers/EnumTransformer.php | 1 - .../TransformerTypesProvider.php | 93 ++++++++++++++++ src/TypeProviders/TypesProvider.php | 17 +++ src/TypeScript/TypeScriptAlias.php | 7 +- src/TypeScript/TypeScriptArray.php | 7 +- src/TypeScript/TypeScriptConditional.php | 7 +- src/TypeScript/TypeScriptExport.php | 7 +- .../TypeScriptFunctionDefinition.php | 12 +-- src/TypeScript/TypeScriptGeneric.php | 10 +- .../TypeScriptGenericTypeVariable.php | 8 +- src/TypeScript/TypeScriptIndexSignature.php | 7 +- src/TypeScript/TypeScriptIndexedAccess.php | 8 +- src/TypeScript/TypeScriptInterface.php | 10 +- src/TypeScript/TypeScriptIntersection.php | 7 +- src/TypeScript/TypeScriptMethod.php | 7 +- src/TypeScript/TypeScriptNodeWithChildren.php | 9 -- src/TypeScript/TypeScriptObject.php | 7 +- src/TypeScript/TypeScriptOperator.php | 7 +- src/TypeScript/TypeScriptParameter.php | 7 +- src/TypeScript/TypeScriptProperty.php | 8 +- src/TypeScript/TypeScriptUnion.php | 7 +- src/TypeScript/TypeScriptVisitableNode.php | 10 ++ src/TypeScriptTransformer.php | 17 ++- src/TypeScriptTransformerConfig.php | 18 ++-- src/Visitor/Visitor.php | 102 ++++++++++++++++++ src/Visitor/VisitorClosure.php | 44 ++++++++ src/Visitor/VisitorOperation.php | 30 ++++++ src/Visitor/VisitorOperationType.php | 10 ++ .../Actions/VisitTypeScriptTreeActionTest.php | 89 +++++++++++++++ 52 files changed, 838 insertions(+), 409 deletions(-) rename src/Actions/{AppendDefaultTypesAction.php => ProvideTypesAction.php} (66%) delete mode 100644 src/Actions/TransformTypeAction.php delete mode 100644 src/Actions/TransformTypesAction.php delete mode 100644 src/Actions/VisitTypeScriptTreeAction.php delete mode 100644 src/DefaultTypeProviders/DefaultTypesProvider.php create mode 100644 src/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessor.php create mode 100644 src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php rename src/Laravel/{LaravelNamedRouteDefaultTypesProvider.php => LaravelNamedRouteTypesProvider.php} (93%) rename src/Laravel/{LaravelRouteActionDefaultTypesProvider.php => LaravelRouteActionTypesProvider.php} (95%) rename src/Laravel/{LaravelDefaultTypesProvider.php => LaravelTypesProvider.php} (93%) delete mode 100644 src/Laravel/SpatieLaravelDefaultTypesProvider.php create mode 100644 src/Laravel/SpatieLaravelTypesProvider.php create mode 100644 src/Laravel/Transformers/DataClassTransformer.php create mode 100644 src/Laravel/Transformers/LaravelClassTransformer.php create mode 100644 src/Support/VisitorProfile.php create mode 100644 src/Transformers/ClassPropertyProcessors/ClassPropertyProcessor.php delete mode 100644 src/Transformers/DataClassTransformer.php create mode 100644 src/TypeProviders/TransformerTypesProvider.php create mode 100644 src/TypeProviders/TypesProvider.php delete mode 100644 src/TypeScript/TypeScriptNodeWithChildren.php create mode 100644 src/TypeScript/TypeScriptVisitableNode.php create mode 100644 src/Visitor/Visitor.php create mode 100644 src/Visitor/VisitorClosure.php create mode 100644 src/Visitor/VisitorOperation.php create mode 100644 src/Visitor/VisitorOperationType.php create mode 100644 tests/Actions/VisitTypeScriptTreeActionTest.php diff --git a/src/Actions/ConnectReferencesAction.php b/src/Actions/ConnectReferencesAction.php index efd53f9c..5379ae3c 100644 --- a/src/Actions/ConnectReferencesAction.php +++ b/src/Actions/ConnectReferencesAction.php @@ -5,8 +5,11 @@ use Spatie\TypeScriptTransformer\Collections\ReferenceMap; use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; +use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; +use Spatie\TypeScriptTransformer\Visitor\Visitor; +use Spatie\TypeScriptTransformer\Visitor\VisitTypeScriptTreeAction; class ConnectReferencesAction { @@ -27,25 +30,30 @@ public function execute(TransformedCollection $collection): ReferenceMap } } - foreach ($collection as $transformed) { - $references = []; + $visitor = Visitor::create()->before(function (TypeReference $typeReference, array $metadata) use ($referenceMap, &$references) { + $reference = $typeReference->reference; + + if (! $referenceMap->has($reference)) { + /** @var Transformed $transformed */ + $transformed = $metadata['transformed']; + + $this->log->warning("Tried replacing reference to `{$reference->humanFriendlyName()}` in `{$transformed->reference->humanFriendlyName()}` but it was not found in the transformed types"); - $this->visitTypeScriptTreeAction->execute( - $transformed->typeScriptNode, - function (TypeReference $typeReference) use ($referenceMap, &$references, $transformed) { - $reference = $typeReference->reference; + return; + } + + $references[] = $reference; + $typeReference->connect($referenceMap->get($reference)); + }, [TypeReference::class]); - if (! $referenceMap->has($reference)) { - $this->log->warning("Tried replacing reference to `{$reference->humanFriendlyName()}` in `{$transformed->reference->humanFriendlyName()}` but it was not found in the transformed types"); + foreach ($collection as $transformed) { + $references = []; - return; - } + $metadata = [ + 'transformed' => $transformed, + ]; - $references[] = $reference; - $typeReference->connect($referenceMap->get($reference)); - }, - [TypeReference::class] - ); + $visitor->execute($transformed->typeScriptNode, $metadata); $transformed->references = $references; } diff --git a/src/Actions/DiscoverTypesAction.php b/src/Actions/DiscoverTypesAction.php index c866813e..d58355c3 100644 --- a/src/Actions/DiscoverTypesAction.php +++ b/src/Actions/DiscoverTypesAction.php @@ -9,18 +9,16 @@ class DiscoverTypesAction { - public function __construct( - public TypeScriptTransformerConfig $config, - public TypeScriptTransformerLog $log, - ) { - } - - /** @return array */ - public function execute(): array - { + /** + * @param array $directories + * @return array + */ + public function execute( + array $directories = [], + ): array { // Idea / TODO : make it possible for other packages to hook in to find types in other directories, like their vendor dir - return Discover::in(...$this->config->directories) + return Discover::in(...$directories) ->types( DiscoveredStructureType::ClassDefinition, DiscoveredStructureType::Enum, diff --git a/src/Actions/AppendDefaultTypesAction.php b/src/Actions/ProvideTypesAction.php similarity index 66% rename from src/Actions/AppendDefaultTypesAction.php rename to src/Actions/ProvideTypesAction.php index 16c4816a..cade2057 100644 --- a/src/Actions/AppendDefaultTypesAction.php +++ b/src/Actions/ProvideTypesAction.php @@ -2,12 +2,12 @@ namespace Spatie\TypeScriptTransformer\Actions; -use Spatie\TypeScriptTransformer\DefaultTypeProviders\DefaultTypesProvider; +use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; -class AppendDefaultTypesAction +class ProvideTypesAction { public function __construct( protected TypeScriptTransformerConfig $config, @@ -17,12 +17,16 @@ public function __construct( public function execute(TransformedCollection $collection): void { - foreach ($this->config->defaultTypeProviders as $defaultTypeProvider) { - $defaultTypeProvider = $defaultTypeProvider instanceof DefaultTypesProvider + foreach ($this->config->typeProviders as $defaultTypeProvider) { + $defaultTypeProvider = $defaultTypeProvider instanceof TypesProvider ? $defaultTypeProvider : new $defaultTypeProvider; - $collection->add(...$defaultTypeProvider->provide()); + $defaultTypeProvider->provide( + $this->config, + $this->log, + $collection + ); } } } diff --git a/src/Actions/TransformTypeAction.php b/src/Actions/TransformTypeAction.php deleted file mode 100644 index 0b0b281e..00000000 --- a/src/Actions/TransformTypeAction.php +++ /dev/null @@ -1,59 +0,0 @@ -config->transformers as $transformer) { - $transformed = $transformer->transform( - $reflection, - $this->createTransformationContext($reflection), - ); - - if ($transformed instanceof Transformed) { - return $transformed; - } - } - - return null; - } - - protected function createTransformationContext( - ReflectionClass $reflection - ): TransformationContext { - $name = $reflection->getShortName(); - - $nameSpaceSegments = explode('\\', $reflection->getNamespaceName()); - - return new TransformationContext( - $name, - $nameSpaceSegments, - ); - } -} diff --git a/src/Actions/TransformTypesAction.php b/src/Actions/TransformTypesAction.php deleted file mode 100644 index b6d8a485..00000000 --- a/src/Actions/TransformTypesAction.php +++ /dev/null @@ -1,37 +0,0 @@ -transformTypeAction = new TransformTypeAction($config, $log); - } - - /** - * @param array $types - */ - public function execute(array $types): TransformedCollection - { - $collection = new TransformedCollection(); - - foreach ($types as $type) { - $transformed = $this->transformTypeAction->execute($type); - - if ($transformed) { - $collection->add($transformed); - } - } - - return $collection; - } -} diff --git a/src/Actions/VisitTypeScriptTreeAction.php b/src/Actions/VisitTypeScriptTreeAction.php deleted file mode 100644 index a763a265..00000000 --- a/src/Actions/VisitTypeScriptTreeAction.php +++ /dev/null @@ -1,31 +0,0 @@ -children())); - - foreach ($children as $child) { - $this->execute($child, $walker, $allowedNodes); - } - } - } -} diff --git a/src/Actions/WatchFileSystemAction.php b/src/Actions/WatchFileSystemAction.php index f9442840..38243428 100644 --- a/src/Actions/WatchFileSystemAction.php +++ b/src/Actions/WatchFileSystemAction.php @@ -16,7 +16,7 @@ public function __construct( public function execute( TransformedCollection $transformedCollection, ) { - Watch::paths($this->config->directories) + Watch::paths($this->config->directoriesToWatch) ->onAnyChange(function (string $type, string $path) { echo $type.'|'.$path; }) diff --git a/src/DefaultTypeProviders/DefaultTypesProvider.php b/src/DefaultTypeProviders/DefaultTypesProvider.php deleted file mode 100644 index b939c4f9..00000000 --- a/src/DefaultTypeProviders/DefaultTypesProvider.php +++ /dev/null @@ -1,11 +0,0 @@ - */ - public function provide(): array; -} diff --git a/src/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessor.php b/src/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessor.php new file mode 100644 index 00000000..17d6cdfc --- /dev/null +++ b/src/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessor.php @@ -0,0 +1,34 @@ +type instanceof TypeScriptUnion) { + return $property; + } + + for ($i = 0; $i < count($property->type->types); $i++) { + $subType = $property->type->types[$i]; + + if ($subType instanceof TypeReference && is_a($subType->reference, \Spatie\LaravelData\Lazy::class, true)) { + $property->isOptional = true; + + unset($property->type->types[$i]); + } + } + + $property->type->types = array_values($property->type->types); + + return $property; + } +} diff --git a/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php b/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php new file mode 100644 index 00000000..b56fa380 --- /dev/null +++ b/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php @@ -0,0 +1,54 @@ +visitTypeScriptTreeAction->execute($property->type, function (TypeScriptGeneric $generic) { + $isCollection = $generic->type instanceof TypeReference + && $generic->type->reference instanceof ClassStringReference + && is_a($generic->type->reference->classString, Collection::class, true); + + if (! $isCollection) { + return; + } + + if (count($generic->genericTypes) !== 2) { + // Someone messed with the type, let's skip it + return; + } + + $isRecord = $generic->genericTypes[0] instanceof TypeScriptUnion || $generic->genericTypes[0] instanceof TypeScriptString; + +// $generic->type = new + + if ($isCollection) { +// $generic->type = new TypeReference(new TypeScriptArray()); + } + }, [TypeScriptGeneric::class]); + + return $property; + } +} diff --git a/src/Laravel/LaravelNamedRouteDefaultTypesProvider.php b/src/Laravel/LaravelNamedRouteTypesProvider.php similarity index 93% rename from src/Laravel/LaravelNamedRouteDefaultTypesProvider.php rename to src/Laravel/LaravelNamedRouteTypesProvider.php index 3b360cca..65fa1ef9 100644 --- a/src/Laravel/LaravelNamedRouteDefaultTypesProvider.php +++ b/src/Laravel/LaravelNamedRouteTypesProvider.php @@ -2,7 +2,7 @@ namespace Spatie\TypeScriptTransformer\Laravel; -use Spatie\TypeScriptTransformer\DefaultTypeProviders\DefaultTypesProvider; +use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\Laravel\Actions\ResolveLaravelRoutControllerCollectionsAction; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteClosure; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteCollection; @@ -12,6 +12,8 @@ use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameter; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameterCollection; use Spatie\TypeScriptTransformer\References\CustomReference; +use Spatie\TypeScriptTransformer\Support\TransformedCollection; +use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; @@ -29,8 +31,9 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptRaw; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; +use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; -class LaravelNamedRouteDefaultTypesProvider implements DefaultTypesProvider +class LaravelNamedRouteTypesProvider implements TypesProvider { public function __construct( protected ResolveLaravelRoutControllerCollectionsAction $resolveLaravelRoutControllerCollectionsAction = new ResolveLaravelRoutControllerCollectionsAction(), @@ -38,7 +41,7 @@ public function __construct( ) { } - public function provide(): array + public function provide(TypeScriptTransformerConfig $config, TypeScriptTransformerLog $log, TransformedCollection $types): void { $routeCollection = $this->resolveLaravelRoutControllerCollectionsAction->execute( null, @@ -52,7 +55,6 @@ public function provide(): array ), $routesListReference = new CustomReference('laravel_named_routes', 'routes_list'), 'NamedRouteList', - true, $this->location, ); @@ -101,11 +103,10 @@ public function provide(): array ), new CustomReference('laravel_named_routes', 'route_function'), 'route', - true, $this->location, ); - return [$transformedRoutes, $transformedRoute]; + $types->add($transformedRoutes, $transformedRoute); } protected function parseRouteCollection(RouteCollection $collection): TypeScriptNode diff --git a/src/Laravel/LaravelRouteActionDefaultTypesProvider.php b/src/Laravel/LaravelRouteActionTypesProvider.php similarity index 95% rename from src/Laravel/LaravelRouteActionDefaultTypesProvider.php rename to src/Laravel/LaravelRouteActionTypesProvider.php index b428f289..d6442753 100644 --- a/src/Laravel/LaravelRouteActionDefaultTypesProvider.php +++ b/src/Laravel/LaravelRouteActionTypesProvider.php @@ -2,7 +2,7 @@ namespace Spatie\TypeScriptTransformer\Laravel; -use Spatie\TypeScriptTransformer\DefaultTypeProviders\DefaultTypesProvider; +use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\Laravel\Actions\ResolveLaravelRoutControllerCollectionsAction; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteCollection; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteController; @@ -11,6 +11,8 @@ use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameter; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameterCollection; use Spatie\TypeScriptTransformer\References\CustomReference; +use Spatie\TypeScriptTransformer\Support\TransformedCollection; +use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; @@ -30,8 +32,9 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptRaw; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; +use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; -class LaravelRouteActionDefaultTypesProvider implements DefaultTypesProvider +class LaravelRouteActionTypesProvider implements TypesProvider { public function __construct( protected ResolveLaravelRoutControllerCollectionsAction $resolveLaravelRoutControllerCollectionsAction = new ResolveLaravelRoutControllerCollectionsAction(), @@ -40,7 +43,7 @@ public function __construct( ) { } - public function provide(): array + public function provide(TypeScriptTransformerConfig $config, TypeScriptTransformerLog $log, TransformedCollection $types): void { $routeCollection = $this->resolveLaravelRoutControllerCollectionsAction->execute( $this->defaultNamespace, @@ -54,7 +57,6 @@ public function provide(): array ), $routesListReference = new CustomReference('laravel_route_actions', 'routes_list'), 'ActionRoutesList', - true, $this->location, ); @@ -88,7 +90,6 @@ public function provide(): array ), $actionControllerReference = new CustomReference('laravel_route_actions', 'action_controller'), 'ActionController', - true, $this->location, ); @@ -117,7 +118,6 @@ public function provide(): array ), $actionParametersReference = new CustomReference('laravel_route_actions', 'action_parameters'), 'ActionParameters', - true, $this->location, ); @@ -190,11 +190,10 @@ public function provide(): array ), new CustomReference('laravel_route_actions', 'action_function'), 'action', - true, $this->location, ); - return [$transformedRoutes, $actionController, $actionParameters, $transformedAction]; + $types->add($transformedRoutes, $actionController, $actionParameters, $transformedAction); } protected function parseRouteCollection(RouteCollection $collection): TypeScriptNode diff --git a/src/Laravel/LaravelDefaultTypesProvider.php b/src/Laravel/LaravelTypesProvider.php similarity index 93% rename from src/Laravel/LaravelDefaultTypesProvider.php rename to src/Laravel/LaravelTypesProvider.php index 6b5e04ea..d551be71 100644 --- a/src/Laravel/LaravelDefaultTypesProvider.php +++ b/src/Laravel/LaravelTypesProvider.php @@ -6,8 +6,10 @@ use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; -use Spatie\TypeScriptTransformer\DefaultTypeProviders\DefaultTypesProvider; +use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\References\ClassStringReference; +use Spatie\TypeScriptTransformer\Support\TransformedCollection; +use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; @@ -21,20 +23,21 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; +use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; -class LaravelDefaultTypesProvider implements DefaultTypesProvider +class LaravelTypesProvider implements TypesProvider { - public function provide(): array + public function provide(TypeScriptTransformerConfig $config, TypeScriptTransformerLog $log, TransformedCollection $types): void { /** @todo We should only keep these types if they are referenced otherwise they arent't required to be transformed */ /** @todo writing types in phpdoc syntax would be a lot easier here */ - return [ + $types->add( $this->collection(), $this->eloquentCollection(), $this->lengthAwarePaginator(), $this->lengthAwarePaginatorInterface(), - ]; + ); } protected function collection(): Transformed @@ -54,7 +57,6 @@ protected function collection(): Transformed ), new ClassStringReference(Collection::class), 'Collection', - true, ['Illuminate', 'Support'], ); } @@ -76,7 +78,6 @@ protected function eloquentCollection(): Transformed ), new ClassStringReference(EloquentCollection::class), 'Collection', - true, ['Illuminate', 'Database', 'Eloquent', 'Collection'], ); } @@ -94,7 +95,7 @@ protected function lengthAwarePaginator(): Transformed new TypeScriptProperty('data', new TypeScriptGeneric( new TypeScriptIdentifier('Array'), [new TypeScriptIdentifier('T')], - ), ), + ),), new TypeScriptProperty('links', new TypeScriptObject([ new TypeScriptProperty('url', new TypeScriptUnion([ new TypeScriptIdentifier('string'), @@ -133,7 +134,6 @@ protected function lengthAwarePaginator(): Transformed ), new ClassStringReference(LengthAwarePaginator::class), 'LengthAwarePaginator', - true, ['Illuminate', 'Pagination'], ); } @@ -155,7 +155,6 @@ protected function lengthAwarePaginatorInterface(): Transformed ), new ClassStringReference(LengthAwarePaginatorInterface::class), 'LengthAwarePaginator', - true, ['Illuminate', 'Contracts', 'Pagination'], ); } diff --git a/src/Laravel/SpatieLaravelDefaultTypesProvider.php b/src/Laravel/SpatieLaravelDefaultTypesProvider.php deleted file mode 100644 index 6a78e434..00000000 --- a/src/Laravel/SpatieLaravelDefaultTypesProvider.php +++ /dev/null @@ -1,58 +0,0 @@ -add($optionsType); + } + } +} diff --git a/src/Laravel/Transformers/DataClassTransformer.php b/src/Laravel/Transformers/DataClassTransformer.php new file mode 100644 index 00000000..e60926ed --- /dev/null +++ b/src/Laravel/Transformers/DataClassTransformer.php @@ -0,0 +1,21 @@ +implementsInterface(\Spatie\LaravelData\Contracts\BaseData::class); + } + + protected function classPropertyProcessors(): array + { + return array_merge(parent::classPropertyProcessors(), [ + new RemoveDataLazyTypeClassPropertyProcessor(), + ]); + } +} diff --git a/src/Laravel/Transformers/LaravelClassTransformer.php b/src/Laravel/Transformers/LaravelClassTransformer.php new file mode 100644 index 00000000..8991d7a5 --- /dev/null +++ b/src/Laravel/Transformers/LaravelClassTransformer.php @@ -0,0 +1,16 @@ +singleNodes, ...$nodes); + + return $this; + } + + public function iterable(string ...$nodes): self + { + array_push($this->iterableNodes, ...$nodes); + + return $this; + } +} diff --git a/src/Transformed/Transformed.php b/src/Transformed/Transformed.php index 0e931f09..0cc31ff1 100644 --- a/src/Transformed/Transformed.php +++ b/src/Transformed/Transformed.php @@ -14,15 +14,10 @@ class Transformed */ public function __construct( public TypeScriptNode $typeScriptNode, - public ?Reference $reference, // Not always referenceable - public ?string $name, // Not always needs a name - public bool $export, // Not always exportable + public Reference $reference, + public string $name, public array $location, public array $references = [], ) { } } - -// Niet per se tied aan een ReflectionClass -// Heeft niet per se een naam -> enkel indien exportable en dus referencable -// Location duid een structuur aan, maar is niet per se een namespace, kan evengoed een collectie aan files zijn diff --git a/src/Transformers/AttributeTransformer.php b/src/Transformers/AttributeTransformer.php index b3fb5abb..4bf05125 100644 --- a/src/Transformers/AttributeTransformer.php +++ b/src/Transformers/AttributeTransformer.php @@ -10,7 +10,7 @@ class AttributeTransformer extends ClassTransformer { - public function shouldTransform(ReflectionClass $reflection): bool + protected function shouldTransform(ReflectionClass $reflection): bool { return count($reflection->getAttributes(TypeScript::class)) > 0; } diff --git a/src/Transformers/ClassPropertyProcessors/ClassPropertyProcessor.php b/src/Transformers/ClassPropertyProcessors/ClassPropertyProcessor.php new file mode 100644 index 00000000..729e3a16 --- /dev/null +++ b/src/Transformers/ClassPropertyProcessors/ClassPropertyProcessor.php @@ -0,0 +1,16 @@ +name, - true, $context->nameSpaceSegments, ); } - abstract public function shouldTransform(ReflectionClass $reflection): bool; + abstract protected function shouldTransform(ReflectionClass $reflection): bool; + + /** @return array */ + protected function classPropertyProcessors(): array + { + return []; + } protected function getTypeScriptNode( ReflectionClass $reflectionClass @@ -71,12 +77,26 @@ protected function getTypeScriptNode( $properties = []; foreach ($this->getProperties($reflectionClass) as $reflectionProperty) { - $properties[] = $this->createProperty( + $annotation = $classAnnotations[$reflectionProperty->getName()] + ?? $constructorAnnotations[$reflectionProperty->getName()] + ?? $this->docTypeResolver->property($reflectionProperty) + ?? null; + + $property = $this->createProperty( $reflectionClass, $reflectionProperty, - $classAnnotations, - $constructorAnnotations, + $annotation?->type ); + + $property = $this->runClassPropertyProcessors( + $reflectionProperty, + $annotation?->type, + $property + ); + + if ($property !== null) { + $properties[] = $property; + } } return new TypeScriptObject($properties); @@ -111,17 +131,12 @@ protected function getProperties(ReflectionClass $reflection): array protected function createProperty( ReflectionClass $reflectionClass, ReflectionProperty $reflectionProperty, - array $classAnnotations, - array $constructorAnnotations, + ?TypeNode $annotation, ): TypeScriptProperty { - $propertyAnnotation = $this->docTypeResolver->property($reflectionProperty); - $type = $this->resolveTypeForProperty( $reflectionClass, $reflectionProperty, - $classAnnotations[$reflectionProperty->getName()] - ?? $constructorAnnotations[$reflectionProperty->getName()] - ?? $propertyAnnotation, + $annotation ); return new TypeScriptProperty( @@ -135,7 +150,7 @@ protected function createProperty( protected function resolveTypeForProperty( ReflectionClass $reflectionClass, ReflectionProperty $reflectionProperty, - ?ParsedNameAndType $annotation, + ?TypeNode $annotation, ): TypeScriptNode { if ($resolvedAttributeType = $this->resolveTypeByAttribute($reflectionClass, $reflectionProperty)) { return $resolvedAttributeType; @@ -143,7 +158,7 @@ protected function resolveTypeForProperty( if ($annotation) { return $this->transpilePhpStanTypeToTypeScriptTypeAction->execute( - $annotation->type, + $annotation, $reflectionClass, ); } @@ -173,4 +188,22 @@ protected function isPropertyReadonly( ): bool { return $reflectionProperty->isReadOnly() || $reflectionClass->isReadOnly(); } + + protected function runClassPropertyProcessors( + ReflectionProperty $reflectionProperty, + ?TypeNode $annotation, + TypeScriptProperty $property, + ): ?TypeScriptProperty { + $processors = $this->classPropertyProcessors(); + + foreach ($processors as $processor) { + $property = $processor->execute($reflectionProperty, $annotation, $property); + + if ($property === null) { + return null; + } + } + + return $property; + } } diff --git a/src/Transformers/DataClassTransformer.php b/src/Transformers/DataClassTransformer.php deleted file mode 100644 index a3387713..00000000 --- a/src/Transformers/DataClassTransformer.php +++ /dev/null @@ -1,55 +0,0 @@ -implementsInterface(\Spatie\LaravelData\Contracts\BaseData::class); - } - - protected function createProperty( - ReflectionClass $reflectionClass, - ReflectionProperty $reflectionProperty, - array $classAnnotations, - array $constructorAnnotations, - ): TypeScriptProperty { - $property = parent::createProperty( - $reflectionClass, - $reflectionProperty, - $classAnnotations, - $constructorAnnotations, - ); - - $this->replaceLazy($property); - - return $property; - } - - protected function replaceLazy( - TypeScriptProperty $property, - ): void { - if (! $property->type instanceof TypeScriptUnion) { - return; - } - - for ($i = 0; $i < count($property->type->types); $i++) { - $subType = $property->type->types[$i]; - - if ($subType instanceof TypeReference && is_a($subType->reference, \Spatie\LaravelData\Lazy::class, true)) { - $property->isOptional = true; - - unset($property->type->types[$i]); - } - } - - $property->type->types = array_values($property->type->types); - } -} diff --git a/src/Transformers/EnumTransformer.php b/src/Transformers/EnumTransformer.php index afc50db4..1cbb2d25 100644 --- a/src/Transformers/EnumTransformer.php +++ b/src/Transformers/EnumTransformer.php @@ -40,7 +40,6 @@ public function transform( : $this->transformAsUnion($context->name, $cases), new ReflectionClassReference($reflectionClass), $context->name, - true, $context->nameSpaceSegments, ); } diff --git a/src/TypeProviders/TransformerTypesProvider.php b/src/TypeProviders/TransformerTypesProvider.php new file mode 100644 index 00000000..6d80ab01 --- /dev/null +++ b/src/TypeProviders/TransformerTypesProvider.php @@ -0,0 +1,93 @@ + */ + protected array $transformers; + + /** + * @param array|Transformer> $transformers + * @param array $directories + */ + public function __construct( + array $transformers, + protected array $directories, + ) { + foreach ($transformers as $transformer) { + $this->transformers[] = $transformer instanceof Transformer + ? $transformer + : new $transformer; + } + } + + public function provide( + TypeScriptTransformerConfig $config, + TypeScriptTransformerLog $log, + TransformedCollection $types + ): void { + $discoveredClasses = (new DiscoverTypesAction())->execute($this->directories); + + array_push($config->directoriesToWatch, ...$this->directories); + + foreach ($discoveredClasses as $discoveredClass) { + $transformed = $this->transformType($discoveredClass); + + + if ($transformed) { + $types->add($transformed); + } + } + } + + /** + * @param class-string $type + */ + protected function transformType(string $type): ?Transformed + { + try { + $reflection = new ReflectionClass($type); + } catch (ReflectionException) { + // TODO: maybe add some kind of log? + + return null; + } + + foreach ($this->transformers as $transformer) { + $transformed = $transformer->transform( + $reflection, + $this->createTransformationContext($reflection), + ); + + if ($transformed instanceof Transformed) { + return $transformed; + } + } + + return null; + } + + protected function createTransformationContext( + ReflectionClass $reflection + ): TransformationContext { + $name = $reflection->getShortName(); + + $nameSpaceSegments = explode('\\', $reflection->getNamespaceName()); + + return new TransformationContext( + $name, + $nameSpaceSegments, + ); + } +} diff --git a/src/TypeProviders/TypesProvider.php b/src/TypeProviders/TypesProvider.php new file mode 100644 index 00000000..d9641e16 --- /dev/null +++ b/src/TypeProviders/TypesProvider.php @@ -0,0 +1,17 @@ +identifier->write($context)} = {$this->type->write($context)};"; } - public function children(): array + public function visitorProfile(): VisitorProfile { - return [$this->identifier, $this->type]; + return VisitorProfile::create()->single('identifier', 'type'); } } diff --git a/src/TypeScript/TypeScriptArray.php b/src/TypeScript/TypeScriptArray.php index 4d9198a0..fef3ebea 100644 --- a/src/TypeScript/TypeScriptArray.php +++ b/src/TypeScript/TypeScriptArray.php @@ -2,9 +2,10 @@ namespace Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptArray implements TypeScriptNode, TypeScriptNodeWithChildren +class TypeScriptArray implements TypeScriptNode, TypeScriptVisitableNode { /** * @param TypeScriptNode[] $types @@ -24,8 +25,8 @@ public function write(WritingContext $context): string return "[$types]"; } - public function children(): array + public function visitorProfile(): VisitorProfile { - return $this->types; + return VisitorProfile::create()->iterable('types'); } } diff --git a/src/TypeScript/TypeScriptConditional.php b/src/TypeScript/TypeScriptConditional.php index fb4211a1..c2a004d0 100644 --- a/src/TypeScript/TypeScriptConditional.php +++ b/src/TypeScript/TypeScriptConditional.php @@ -2,9 +2,10 @@ namespace Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptConditional implements TypeScriptNode, TypeScriptNodeWithChildren +class TypeScriptConditional implements TypeScriptNode, TypeScriptVisitableNode { public function __construct( public TypeScriptNode $condition, @@ -18,8 +19,8 @@ public function write(WritingContext $context): string return "{$this->condition->write($context)} ? {$this->ifTrue->write($context)} : {$this->ifFalse->write($context)}"; } - public function children(): array + public function visitorProfile(): VisitorProfile { - return [$this->condition, $this->ifTrue, $this->ifFalse]; + return VisitorProfile::create()->single('condition', 'ifTrue', 'ifFalse'); } } diff --git a/src/TypeScript/TypeScriptExport.php b/src/TypeScript/TypeScriptExport.php index a0884210..05fa9a2e 100644 --- a/src/TypeScript/TypeScriptExport.php +++ b/src/TypeScript/TypeScriptExport.php @@ -2,9 +2,10 @@ namespace Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptExport implements TypeScriptNode, TypeScriptNodeWithChildren +class TypeScriptExport implements TypeScriptNode, TypeScriptVisitableNode { public function __construct( public TypeScriptNode $node, @@ -16,8 +17,8 @@ public function write(WritingContext $context): string return "export {$this->node->write($context)}".PHP_EOL; } - public function children(): array + public function visitorProfile(): VisitorProfile { - return [$this->node]; + return VisitorProfile::create()->single('node'); } } diff --git a/src/TypeScript/TypeScriptFunctionDefinition.php b/src/TypeScript/TypeScriptFunctionDefinition.php index 353f8602..e8474876 100644 --- a/src/TypeScript/TypeScriptFunctionDefinition.php +++ b/src/TypeScript/TypeScriptFunctionDefinition.php @@ -2,9 +2,10 @@ namespace Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptFunctionDefinition implements TypeScriptNode, TypeScriptNodeWithChildren +class TypeScriptFunctionDefinition implements TypeScriptNode, TypeScriptVisitableNode { public function __construct( public TypeScriptNode $identifier, @@ -23,13 +24,8 @@ public function write(WritingContext $context): string }"; } - public function children(): array + public function visitorProfile(): VisitorProfile { - return [ - $this->identifier, - ...$this->parameters ?? [], - $this->returnType, - $this->body, - ]; + return VisitorProfile::create()->single('identifier', 'returnType', 'body')->iterable('parameters'); } } diff --git a/src/TypeScript/TypeScriptGeneric.php b/src/TypeScript/TypeScriptGeneric.php index ea1f1034..d96fe05a 100644 --- a/src/TypeScript/TypeScriptGeneric.php +++ b/src/TypeScript/TypeScriptGeneric.php @@ -2,9 +2,10 @@ namespace Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptGeneric implements TypeScriptNode, TypeScriptNodeWithChildren +class TypeScriptGeneric implements TypeScriptNode, TypeScriptVisitableNode { /** * @param array $genericTypes @@ -25,11 +26,8 @@ public function write(WritingContext $context): string return "{$this->type->write($context)}<{$generics}>"; } - public function children(): array + public function visitorProfile(): VisitorProfile { - return [ - $this->type, - ...$this->genericTypes, - ]; + return VisitorProfile::create()->single('type')->iterable('genericTypes'); } } diff --git a/src/TypeScript/TypeScriptGenericTypeVariable.php b/src/TypeScript/TypeScriptGenericTypeVariable.php index bea8d1b6..c2f7d5c5 100644 --- a/src/TypeScript/TypeScriptGenericTypeVariable.php +++ b/src/TypeScript/TypeScriptGenericTypeVariable.php @@ -2,9 +2,10 @@ namespace Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptGenericTypeVariable implements TypeScriptNode, TypeScriptNodeWithChildren +class TypeScriptGenericTypeVariable implements TypeScriptNode, TypeScriptVisitableNode { public function __construct( public TypeScriptNode $identifier, @@ -28,4 +29,9 @@ public function children(): array $this->default, ]); } + + public function visitorProfile(): VisitorProfile + { + return VisitorProfile::create()->single('identifier', 'extends', 'default'); + } } diff --git a/src/TypeScript/TypeScriptIndexSignature.php b/src/TypeScript/TypeScriptIndexSignature.php index f8fd55ec..ada1b2eb 100644 --- a/src/TypeScript/TypeScriptIndexSignature.php +++ b/src/TypeScript/TypeScriptIndexSignature.php @@ -2,9 +2,10 @@ namespace Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptIndexSignature implements TypeScriptNode, TypeScriptNodeWithChildren +class TypeScriptIndexSignature implements TypeScriptNode, TypeScriptVisitableNode { public function __construct( public TypeScriptNode $type, @@ -17,8 +18,8 @@ public function write(WritingContext $context): string return "[{$this->name}: {$this->type->write($context)}]]"; } - public function children(): array + public function visitorProfile(): VisitorProfile { - return [$this->type]; + return VisitorProfile::create()->single('type'); } } diff --git a/src/TypeScript/TypeScriptIndexedAccess.php b/src/TypeScript/TypeScriptIndexedAccess.php index f16773a1..333e4b87 100644 --- a/src/TypeScript/TypeScriptIndexedAccess.php +++ b/src/TypeScript/TypeScriptIndexedAccess.php @@ -2,9 +2,10 @@ namespace Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptIndexedAccess implements TypeScriptNode, TypeScriptNodeWithChildren +class TypeScriptIndexedAccess implements TypeScriptNode, TypeScriptVisitableNode { /** * @param array $segments @@ -25,8 +26,9 @@ public function write(WritingContext $context): string return "{$this->node->write($context)}".implode('', $segments); } - public function children(): array + + public function visitorProfile(): VisitorProfile { - return [$this->node, ...$this->segments]; + return VisitorProfile::create()->single('node')->iterable('segments'); } } diff --git a/src/TypeScript/TypeScriptInterface.php b/src/TypeScript/TypeScriptInterface.php index 4a253b08..7694ef32 100644 --- a/src/TypeScript/TypeScriptInterface.php +++ b/src/TypeScript/TypeScriptInterface.php @@ -2,9 +2,10 @@ namespace Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptInterface implements TypeScriptNode, TypeScriptNodeWithChildren +class TypeScriptInterface implements TypeScriptNode, TypeScriptVisitableNode { /** * @param array $properties @@ -30,8 +31,11 @@ public function write(WritingContext $context): string return "interface {$this->name->write($context)} {{$items}}"; } - public function children(): array + public function visitorProfile(): VisitorProfile { - return [...$this->properties, ...$this->methods]; + return VisitorProfile::create() + ->single('name') + ->iterable('properties') + ->iterable('methods'); } } diff --git a/src/TypeScript/TypeScriptIntersection.php b/src/TypeScript/TypeScriptIntersection.php index cd9fdeee..1ab1837f 100644 --- a/src/TypeScript/TypeScriptIntersection.php +++ b/src/TypeScript/TypeScriptIntersection.php @@ -2,9 +2,10 @@ namespace Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptIntersection implements TypeScriptNode, TypeScriptNodeWithChildren +class TypeScriptIntersection implements TypeScriptNode, TypeScriptVisitableNode { /** * @param array $types @@ -22,8 +23,8 @@ public function write(WritingContext $context): string )); } - public function children(): array + public function visitorProfile(): VisitorProfile { - return $this->types; + return VisitorProfile::create()->iterable('types'); } } diff --git a/src/TypeScript/TypeScriptMethod.php b/src/TypeScript/TypeScriptMethod.php index 5bd9ccb7..ea946382 100644 --- a/src/TypeScript/TypeScriptMethod.php +++ b/src/TypeScript/TypeScriptMethod.php @@ -2,9 +2,10 @@ namespace Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptMethod implements TypeScriptNode, TypeScriptNodeWithChildren +class TypeScriptMethod implements TypeScriptNode, TypeScriptVisitableNode { /** * @param array $parameters @@ -26,8 +27,8 @@ public function write(WritingContext $context): string return "{$this->name}({$parameters}): {$this->returnType->write($context)};"; } - public function children(): array + public function visitorProfile(): VisitorProfile { - return [$this->returnType, ...$this->parameters]; + return VisitorProfile::create()->iterable('parameters')->single('returnType'); } } diff --git a/src/TypeScript/TypeScriptNodeWithChildren.php b/src/TypeScript/TypeScriptNodeWithChildren.php deleted file mode 100644 index 864e24bb..00000000 --- a/src/TypeScript/TypeScriptNodeWithChildren.php +++ /dev/null @@ -1,9 +0,0 @@ - */ - public function children(): array; -} diff --git a/src/TypeScript/TypeScriptObject.php b/src/TypeScript/TypeScriptObject.php index 525ef950..81b84bd5 100644 --- a/src/TypeScript/TypeScriptObject.php +++ b/src/TypeScript/TypeScriptObject.php @@ -2,9 +2,10 @@ namespace Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptObject implements TypeScriptNode, TypeScriptNodeWithChildren +class TypeScriptObject implements TypeScriptNode, TypeScriptVisitableNode { /** * @param array $properties @@ -29,8 +30,8 @@ public function write(WritingContext $context): string return $output.'}'; } - public function children(): array + public function visitorProfile(): VisitorProfile { - return $this->properties; + return VisitorProfile::create()->iterable('properties'); } } diff --git a/src/TypeScript/TypeScriptOperator.php b/src/TypeScript/TypeScriptOperator.php index ad29569d..2f468cf4 100644 --- a/src/TypeScript/TypeScriptOperator.php +++ b/src/TypeScript/TypeScriptOperator.php @@ -2,9 +2,10 @@ namespace Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptOperator implements TypeScriptNode, TypeScriptNodeWithChildren +class TypeScriptOperator implements TypeScriptNode, TypeScriptVisitableNode { public function __construct( public string $operator, @@ -61,8 +62,8 @@ public function write(WritingContext $context): string return "{$this->left->write($context)} {$this->operator} {$this->right->write($context)}"; } - public function children(): array + public function visitorProfile(): VisitorProfile { - return [$this->left, $this->right]; + return VisitorProfile::create()->single('left', 'right'); } } diff --git a/src/TypeScript/TypeScriptParameter.php b/src/TypeScript/TypeScriptParameter.php index 3ac40871..07be5122 100644 --- a/src/TypeScript/TypeScriptParameter.php +++ b/src/TypeScript/TypeScriptParameter.php @@ -2,9 +2,10 @@ namespace Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptParameter implements TypeScriptNode, TypeScriptNodeWithChildren +class TypeScriptParameter implements TypeScriptNode, TypeScriptVisitableNode { public function __construct( public string $name, @@ -24,8 +25,8 @@ public function write(WritingContext $context): string : "{$name}: {$this->type->write($context)}"; } - public function children(): array + public function visitorProfile(): VisitorProfile { - return [$this->type]; + return VisitorProfile::create()->single('type'); } } diff --git a/src/TypeScript/TypeScriptProperty.php b/src/TypeScript/TypeScriptProperty.php index 81aff03a..d7379cb1 100644 --- a/src/TypeScript/TypeScriptProperty.php +++ b/src/TypeScript/TypeScriptProperty.php @@ -2,9 +2,10 @@ namespace Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptProperty implements TypeScriptNode, TypeScriptNodeWithChildren +class TypeScriptProperty implements TypeScriptNode, TypeScriptVisitableNode { public TypeScriptIdentifier|TypeScriptIndexSignature $name; @@ -29,4 +30,9 @@ public function children(): array { return [$this->name, $this->type]; } + + public function visitorProfile(): VisitorProfile + { + return VisitorProfile::create()->single('name', 'type'); + } } diff --git a/src/TypeScript/TypeScriptUnion.php b/src/TypeScript/TypeScriptUnion.php index 0f97c817..30eda16b 100644 --- a/src/TypeScript/TypeScriptUnion.php +++ b/src/TypeScript/TypeScriptUnion.php @@ -3,9 +3,10 @@ namespace Spatie\TypeScriptTransformer\TypeScript; use Closure; +use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptUnion implements TypeScriptNode, TypeScriptNodeWithChildren +class TypeScriptUnion implements TypeScriptNode, TypeScriptVisitableNode { /** * @param array $types @@ -34,8 +35,8 @@ public function contains(Closure $closure): bool return false; } - public function children(): array + public function visitorProfile(): VisitorProfile { - return $this->types; + return VisitorProfile::create()->iterable('types'); } } diff --git a/src/TypeScript/TypeScriptVisitableNode.php b/src/TypeScript/TypeScriptVisitableNode.php new file mode 100644 index 00000000..cddf886e --- /dev/null +++ b/src/TypeScript/TypeScriptVisitableNode.php @@ -0,0 +1,10 @@ + only reload when the config changes (difficult, maybe skip for now) - $discovered = $this->discoverTypesAction->execute(); - $transformedCollection = $this->transformTypesAction->execute($discovered); + /** + * Notes after knowledge sharing + * - Split Laravel part again? + * - Make it possible to hijack the PHPStan types, or some way to rename a Laravel Collection to an array? Would be easier + * - When generating routes where we have the full namespace, prepend with ., check Laravel Echo for this + * - Prettier can run on complete directories, so formatting single files is maybe not required + + */ + $transformedCollection = new TransformedCollection(); $this->appendDefaultTypesAction->execute($transformedCollection); diff --git a/src/TypeScriptTransformerConfig.php b/src/TypeScriptTransformerConfig.php index c0cc6dd0..ad2e5c6c 100755 --- a/src/TypeScriptTransformerConfig.php +++ b/src/TypeScriptTransformerConfig.php @@ -2,24 +2,22 @@ namespace Spatie\TypeScriptTransformer; -use Spatie\TypeScriptTransformer\DefaultTypeProviders\DefaultTypesProvider; +use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\Formatters\Formatter; use Spatie\TypeScriptTransformer\Transformers\Transformer; use Spatie\TypeScriptTransformer\Writers\Writer; -readonly class TypeScriptTransformerConfig +class TypeScriptTransformerConfig { + public array $directoriesToWatch = []; + /** - * @param array $directories - * @param array $transformers - * @param array|DefaultTypesProvider> $defaultTypeProviders + * @param array|TypesProvider> $typeProviders */ public function __construct( - public array $directories, - public array $transformers, - public array $defaultTypeProviders, - public Writer $writer, - public ?Formatter $formatter + readonly public array $typeProviders, + readonly public Writer $writer, + readonly public ?Formatter $formatter ) { } } diff --git a/src/Visitor/Visitor.php b/src/Visitor/Visitor.php new file mode 100644 index 00000000..0b2f720f --- /dev/null +++ b/src/Visitor/Visitor.php @@ -0,0 +1,102 @@ + $beforeClosures + * @param array $afterClosures + */ + public function __construct( + protected array $beforeClosures = [], + protected array $afterClosures = [], + ) { + } + + public function before( + Closure $closure, + ?array $allowedNodes = null, + ): self { + $this->beforeClosures[] = new VisitorClosure($closure, $allowedNodes); + + return $this; + } + + public function after( + Closure $closure, + ?array $allowedNodes = null, + ): self { + $this->afterClosures[] = new VisitorClosure($closure, $allowedNodes); + + return $this; + } + + public function execute( + TypeScriptNode $node, + array &$metadata = [], + ): ?TypeScriptNode + { + foreach ($this->beforeClosures as $beforeClosure) { + if ($beforeClosure->shouldRun($node)) { + $operation = $beforeClosure->run($node, $metadata); + + if ($operation->type === VisitorOperationType::Remove) { + return null; + } + + if ($operation->type === VisitorOperationType::Replace) { + return $operation->node; + } + } + } + + if ($node instanceof TypeScriptVisitableNode) { + $profile = $node->visitorProfile(); + + foreach ($profile->singleNodes as $singleNodeName) { + $visited = $this->execute($node->$singleNodeName, $metadata); + + try { + $node->$singleNodeName = $visited; + } catch (Exception $e) { + throw new Exception("Tried setting $singleNodeName on ".get_class($node)." to ".get_class($visited)." but failed."); + } + } + + foreach ($profile->iterableNodes as $iterableNodeName) { + for ($i = 0; $i < count($node->$iterableNodeName); $i++) { + $node->$iterableNodeName[$i] = $this->execute($node->$iterableNodeName[$i], $metadata); + } + + $node->$iterableNodeName = array_values(array_filter($node->$iterableNodeName)); + } + } + + foreach ($this->afterClosures as $afterClosure) { + if ($afterClosure->shouldRun($node)) { + $operation = $afterClosure->run($node, $metadata); + + if ($operation->type === VisitorOperationType::Remove) { + return null; + } + + if ($operation->type === VisitorOperationType::Replace) { + return $operation->node; + } + } + } + + return $node; + } +} diff --git a/src/Visitor/VisitorClosure.php b/src/Visitor/VisitorClosure.php new file mode 100644 index 00000000..3d61f318 --- /dev/null +++ b/src/Visitor/VisitorClosure.php @@ -0,0 +1,44 @@ +requiresMetadata = (new ReflectionFunction($this->closure))->getNumberOfParameters() === 2; + } + + public function shouldRun( + TypeScriptNode $node + ): bool { + if ($this->allowedNodes === null) { + return true; + } + + return in_array(get_class($node), $this->allowedNodes); + } + + public function run( + TypeScriptNode $node, + array &$metadata, + ): VisitorOperation { + $output = $this->requiresMetadata + ? ($this->closure)($node, $metadata) + : ($this->closure)($node); + + if ($output instanceof VisitorOperation) { + return $output; + } + + return VisitorOperation::keep(); + } +} diff --git a/src/Visitor/VisitorOperation.php b/src/Visitor/VisitorOperation.php new file mode 100644 index 00000000..1d2f737b --- /dev/null +++ b/src/Visitor/VisitorOperation.php @@ -0,0 +1,30 @@ +before(function (TypeScriptNode $node) use (&$baseNode, &$subNodes) { + if ($node instanceof TypeScriptUnion) { + $baseNode = $node; + } else { + $subNodes[] = $node; + } + }) + ->execute($unionNode); + + expect($visited)->toBe($unionNode); + expect($baseNode)->toBe($unionNode); + expect($subNodes)->toEqual([$stringNode, $numberNode]); +}); + +it('can change a single node', function () { + $unionNode = new TypeScriptUnion([ + $stringNode = new TypeScriptString(), + new TypeScriptNumber(), + ]); + + $visited = Visitor::create() + ->before(function (TypeScriptNode $node) use (&$baseNode) { + if ($node instanceof TypeScriptUnion) { + unset($node->types[1]); + } + }) + ->execute($unionNode); + + expect($visited)->toBe($unionNode); + expect($unionNode->types)->toEqual([$stringNode]); +}); + +it('can remove a single node in an iterateable', function () { + $unionNode = new TypeScriptUnion([ + $stringNode = new TypeScriptString(), + new TypeScriptNumber(), + ]); + + $visited = Visitor::create() + ->before(function (TypeScriptNode $node) { + if ($node instanceof TypeScriptNumber) { + return VisitorOperation::remove(); + } + }) + ->execute($unionNode); + + expect($visited)->toBe($unionNode); + expect($unionNode->types)->toEqual([$stringNode]); +}); + +it('can replace a single node in an iterateable', function () { + $unionNode = new TypeScriptUnion([ + $stringNode = new TypeScriptString(), + new TypeScriptNumber(), + ]); + + $visited = Visitor::create() + ->before(function (TypeScriptNode $node) { + if ($node instanceof TypeScriptNumber) { + return VisitorOperation::replace( + new TypeScriptBoolean(), + ); + } + }) + ->execute($unionNode); + + expect($visited)->toBe($unionNode); + expect($unionNode->types)->toEqual([$stringNode, new TypeScriptBoolean()]); +}); From d9370404888350b44b57580e472bb1d1756bbd70 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 1 Aug 2023 15:09:50 +0200 Subject: [PATCH 16/51] Collections replacer --- src/Actions/ConnectReferencesAction.php | 2 - ...ollectionByArrayClassPropertyProcessor.php | 40 ++++++++++++------- src/TypeScript/TypeScriptUnion.php | 2 +- src/Visitor/Visitor.php | 15 ++++--- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/Actions/ConnectReferencesAction.php b/src/Actions/ConnectReferencesAction.php index 5379ae3c..bab26b09 100644 --- a/src/Actions/ConnectReferencesAction.php +++ b/src/Actions/ConnectReferencesAction.php @@ -9,14 +9,12 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeReference; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; use Spatie\TypeScriptTransformer\Visitor\Visitor; -use Spatie\TypeScriptTransformer\Visitor\VisitTypeScriptTreeAction; class ConnectReferencesAction { public function __construct( protected TypeScriptTransformerConfig $config, public TypeScriptTransformerLog $log, - protected VisitTypeScriptTreeAction $visitTypeScriptTreeAction = new VisitTypeScriptTreeAction(), ) { } diff --git a/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php b/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php index b56fa380..8317b3e1 100644 --- a/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php +++ b/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php @@ -8,25 +8,23 @@ use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\Transformers\ClassPropertyProcessors\ClassPropertyProcessor; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptArray; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGeneric; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; +use Spatie\TypeScriptTransformer\Visitor\Visitor; +use Spatie\TypeScriptTransformer\Visitor\VisitorOperation; use Spatie\TypeScriptTransformer\Visitor\VisitTypeScriptTreeAction; class ReplaceLaravelCollectionByArrayClassPropertyProcessor implements ClassPropertyProcessor { - public function __construct( - protected VisitTypeScriptTreeAction $visitTypeScriptTreeAction = new VisitTypeScriptTreeAction(), - ) { - } + protected Visitor $visitor; - public function execute( - ReflectionProperty $reflection, - ?TypeNode $annotation, - TypeScriptProperty $property - ): ?TypeScriptProperty { - $this->visitTypeScriptTreeAction->execute($property->type, function (TypeScriptGeneric $generic) { + public function __construct() + { + $this->visitor = Visitor::create()->before(function (TypeScriptGeneric $generic) { $isCollection = $generic->type instanceof TypeReference && $generic->type->reference instanceof ClassStringReference && is_a($generic->type->reference->classString, Collection::class, true); @@ -42,12 +40,26 @@ public function execute( $isRecord = $generic->genericTypes[0] instanceof TypeScriptUnion || $generic->genericTypes[0] instanceof TypeScriptString; -// $generic->type = new - - if ($isCollection) { -// $generic->type = new TypeReference(new TypeScriptArray()); + if($isRecord){ + return VisitorOperation::replace(new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [ + $generic->genericTypes[0], + $generic->genericTypes[1] + ] + )); } + + return VisitorOperation::replace(new TypeScriptArray([$generic->genericTypes[1]])); }, [TypeScriptGeneric::class]); + } + + public function execute( + ReflectionProperty $reflection, + ?TypeNode $annotation, + TypeScriptProperty $property + ): ?TypeScriptProperty { + $property->type = $this->visitor->execute($property->type); return $property; } diff --git a/src/TypeScript/TypeScriptUnion.php b/src/TypeScript/TypeScriptUnion.php index 30eda16b..8e903ead 100644 --- a/src/TypeScript/TypeScriptUnion.php +++ b/src/TypeScript/TypeScriptUnion.php @@ -9,7 +9,7 @@ class TypeScriptUnion implements TypeScriptNode, TypeScriptVisitableNode { /** - * @param array $types + * @param array $types */ public function __construct( public array $types, diff --git a/src/Visitor/Visitor.php b/src/Visitor/Visitor.php index 0b2f720f..2d1074f0 100644 --- a/src/Visitor/Visitor.php +++ b/src/Visitor/Visitor.php @@ -45,8 +45,7 @@ public function after( public function execute( TypeScriptNode $node, array &$metadata = [], - ): ?TypeScriptNode - { + ): ?TypeScriptNode { foreach ($this->beforeClosures as $beforeClosure) { if ($beforeClosure->shouldRun($node)) { $operation = $beforeClosure->run($node, $metadata); @@ -65,7 +64,13 @@ public function execute( $profile = $node->visitorProfile(); foreach ($profile->singleNodes as $singleNodeName) { - $visited = $this->execute($node->$singleNodeName, $metadata); + $subNode = $node->$singleNodeName; + + if ($subNode === null) { + continue; + } + + $visited = $this->execute($subNode, $metadata); try { $node->$singleNodeName = $visited; @@ -75,8 +80,8 @@ public function execute( } foreach ($profile->iterableNodes as $iterableNodeName) { - for ($i = 0; $i < count($node->$iterableNodeName); $i++) { - $node->$iterableNodeName[$i] = $this->execute($node->$iterableNodeName[$i], $metadata); + foreach ($node->$iterableNodeName as $key => $subNode) { + $node->$iterableNodeName[$key] = $this->execute($subNode, $metadata); } $node->$iterableNodeName = array_values(array_filter($node->$iterableNodeName)); From 40e9b98ff9765b4868dd2662b3102599f06426f2 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 1 Aug 2023 15:11:22 +0200 Subject: [PATCH 17/51] Further code cleanup --- ...eplaceLaravelCollectionByArrayClassPropertyProcessor.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php b/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php index 8317b3e1..40bedc58 100644 --- a/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php +++ b/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php @@ -2,6 +2,7 @@ namespace Spatie\TypeScriptTransformer\Laravel\ClassPropertyProcessors; +use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Support\Collection; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use ReflectionProperty; @@ -27,7 +28,10 @@ public function __construct() $this->visitor = Visitor::create()->before(function (TypeScriptGeneric $generic) { $isCollection = $generic->type instanceof TypeReference && $generic->type->reference instanceof ClassStringReference - && is_a($generic->type->reference->classString, Collection::class, true); + && in_array($generic->type->reference->classString, [ + Collection::class, + EloquentCollection::class, + ]); if (! $isCollection) { return; From 157584c10919c936f0c99829f8ce580cc189daa1 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Tue, 1 Aug 2023 13:12:04 +0000 Subject: [PATCH 18/51] Fix styling --- src/Actions/DiscoverTypesAction.php | 4 +- src/Actions/ProvideTypesAction.php | 2 +- ...ollectionByArrayClassPropertyProcessor.php | 5 +- .../LaravelNamedRouteTypesProvider.php | 2 +- .../LaravelRouteActionTypesProvider.php | 2 +- src/Laravel/LaravelTypesProvider.php | 5 +- src/Laravel/SpatieLaravelTypesProvider.php | 46 +++++++++---------- src/Support/VisitorProfile.php | 2 +- src/Transformed/Transformed.php | 1 - .../TransformerTypesProvider.php | 7 ++- src/TypeProviders/TypesProvider.php | 1 - src/TypeScript/TypeScriptIndexedAccess.php | 1 - src/TypeScript/TypeScriptUnion.php | 2 +- src/TypeScriptTransformer.php | 4 +- src/TypeScriptTransformerConfig.php | 3 +- src/Visitor/Visitor.php | 10 ++-- 16 files changed, 43 insertions(+), 54 deletions(-) diff --git a/src/Actions/DiscoverTypesAction.php b/src/Actions/DiscoverTypesAction.php index d58355c3..80f2310e 100644 --- a/src/Actions/DiscoverTypesAction.php +++ b/src/Actions/DiscoverTypesAction.php @@ -4,13 +4,11 @@ use Spatie\StructureDiscoverer\Discover; use Spatie\StructureDiscoverer\Enums\DiscoveredStructureType; -use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; -use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class DiscoverTypesAction { /** - * @param array $directories + * @param array $directories * @return array */ public function execute( diff --git a/src/Actions/ProvideTypesAction.php b/src/Actions/ProvideTypesAction.php index cade2057..5eabb785 100644 --- a/src/Actions/ProvideTypesAction.php +++ b/src/Actions/ProvideTypesAction.php @@ -2,9 +2,9 @@ namespace Spatie\TypeScriptTransformer\Actions; -use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; +use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class ProvideTypesAction diff --git a/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php b/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php index 40bedc58..b88d6e20 100644 --- a/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php +++ b/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php @@ -17,7 +17,6 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; use Spatie\TypeScriptTransformer\Visitor\Visitor; use Spatie\TypeScriptTransformer\Visitor\VisitorOperation; -use Spatie\TypeScriptTransformer\Visitor\VisitTypeScriptTreeAction; class ReplaceLaravelCollectionByArrayClassPropertyProcessor implements ClassPropertyProcessor { @@ -44,12 +43,12 @@ public function __construct() $isRecord = $generic->genericTypes[0] instanceof TypeScriptUnion || $generic->genericTypes[0] instanceof TypeScriptString; - if($isRecord){ + if ($isRecord) { return VisitorOperation::replace(new TypeScriptGeneric( new TypeScriptIdentifier('Record'), [ $generic->genericTypes[0], - $generic->genericTypes[1] + $generic->genericTypes[1], ] )); } diff --git a/src/Laravel/LaravelNamedRouteTypesProvider.php b/src/Laravel/LaravelNamedRouteTypesProvider.php index 65fa1ef9..e70a1020 100644 --- a/src/Laravel/LaravelNamedRouteTypesProvider.php +++ b/src/Laravel/LaravelNamedRouteTypesProvider.php @@ -2,7 +2,6 @@ namespace Spatie\TypeScriptTransformer\Laravel; -use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\Laravel\Actions\ResolveLaravelRoutControllerCollectionsAction; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteClosure; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteCollection; @@ -15,6 +14,7 @@ use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; +use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptFunctionDefinition; diff --git a/src/Laravel/LaravelRouteActionTypesProvider.php b/src/Laravel/LaravelRouteActionTypesProvider.php index d6442753..6886448e 100644 --- a/src/Laravel/LaravelRouteActionTypesProvider.php +++ b/src/Laravel/LaravelRouteActionTypesProvider.php @@ -2,7 +2,6 @@ namespace Spatie\TypeScriptTransformer\Laravel; -use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\Laravel\Actions\ResolveLaravelRoutControllerCollectionsAction; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteCollection; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteController; @@ -14,6 +13,7 @@ use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; +use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptArray; diff --git a/src/Laravel/LaravelTypesProvider.php b/src/Laravel/LaravelTypesProvider.php index d551be71..d41567c6 100644 --- a/src/Laravel/LaravelTypesProvider.php +++ b/src/Laravel/LaravelTypesProvider.php @@ -6,11 +6,11 @@ use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; -use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; +use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptBoolean; @@ -31,7 +31,6 @@ public function provide(TypeScriptTransformerConfig $config, TypeScriptTransform { /** @todo We should only keep these types if they are referenced otherwise they arent't required to be transformed */ /** @todo writing types in phpdoc syntax would be a lot easier here */ - $types->add( $this->collection(), $this->eloquentCollection(), @@ -95,7 +94,7 @@ protected function lengthAwarePaginator(): Transformed new TypeScriptProperty('data', new TypeScriptGeneric( new TypeScriptIdentifier('Array'), [new TypeScriptIdentifier('T')], - ),), + ), ), new TypeScriptProperty('links', new TypeScriptObject([ new TypeScriptProperty('url', new TypeScriptUnion([ new TypeScriptIdentifier('string'), diff --git a/src/Laravel/SpatieLaravelTypesProvider.php b/src/Laravel/SpatieLaravelTypesProvider.php index f80f4cde..772976c5 100644 --- a/src/Laravel/SpatieLaravelTypesProvider.php +++ b/src/Laravel/SpatieLaravelTypesProvider.php @@ -2,11 +2,11 @@ namespace Spatie\TypeScriptTransformer\Laravel; -use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; +use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptExport; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGeneric; @@ -23,29 +23,29 @@ public function provide(TypeScriptTransformerConfig $config, TypeScriptTransform if (class_exists(\Spatie\LaravelOptions\Options::class)) { $optionsType = new Transformed( new TypeScriptExport(new TypeScriptAlias( - new TypeScriptGeneric( - new TypeScriptIdentifier('Options'), - [ - new TypeScriptGenericTypeVariable( - new TypeScriptIdentifier('TValue'), - default: new TypeScriptIdentifier('string'), - ), - new TypeScriptGenericTypeVariable( - new TypeScriptIdentifier('TLabel'), - default: new TypeScriptIdentifier('string'), - ), - ] - ), - new TypeScriptGeneric( - new TypeScriptIdentifier('Array'), - [ - new TypeScriptObject([ - new TypeScriptProperty('value', new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TValue'))), - new TypeScriptProperty('label', new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TLabel'))), - ]), - ], - ) + new TypeScriptGeneric( + new TypeScriptIdentifier('Options'), + [ + new TypeScriptGenericTypeVariable( + new TypeScriptIdentifier('TValue'), + default: new TypeScriptIdentifier('string'), + ), + new TypeScriptGenericTypeVariable( + new TypeScriptIdentifier('TLabel'), + default: new TypeScriptIdentifier('string'), + ), + ] + ), + new TypeScriptGeneric( + new TypeScriptIdentifier('Array'), + [ + new TypeScriptObject([ + new TypeScriptProperty('value', new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TValue'))), + new TypeScriptProperty('label', new TypeScriptGenericTypeVariable(new TypeScriptIdentifier('TLabel'))), + ]), + ], ) + ) ), new ClassStringReference(\Spatie\LaravelOptions\Options::class), 'Options', diff --git a/src/Support/VisitorProfile.php b/src/Support/VisitorProfile.php index 02ede2f3..43d0ee0c 100644 --- a/src/Support/VisitorProfile.php +++ b/src/Support/VisitorProfile.php @@ -4,7 +4,7 @@ class VisitorProfile { - public static function create():self + public static function create(): self { return new self(); } diff --git a/src/Transformed/Transformed.php b/src/Transformed/Transformed.php index 0cc31ff1..eab60934 100644 --- a/src/Transformed/Transformed.php +++ b/src/Transformed/Transformed.php @@ -2,7 +2,6 @@ namespace Spatie\TypeScriptTransformer\Transformed; -use ReflectionClass; use Spatie\TypeScriptTransformer\References\Reference; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; diff --git a/src/TypeProviders/TransformerTypesProvider.php b/src/TypeProviders/TransformerTypesProvider.php index 6d80ab01..320c59fc 100644 --- a/src/TypeProviders/TransformerTypesProvider.php +++ b/src/TypeProviders/TransformerTypesProvider.php @@ -18,8 +18,8 @@ class TransformerTypesProvider implements TypesProvider protected array $transformers; /** - * @param array|Transformer> $transformers - * @param array $directories + * @param array|Transformer> $transformers + * @param array $directories */ public function __construct( array $transformers, @@ -44,7 +44,6 @@ public function provide( foreach ($discoveredClasses as $discoveredClass) { $transformed = $this->transformType($discoveredClass); - if ($transformed) { $types->add($transformed); } @@ -52,7 +51,7 @@ public function provide( } /** - * @param class-string $type + * @param class-string $type */ protected function transformType(string $type): ?Transformed { diff --git a/src/TypeProviders/TypesProvider.php b/src/TypeProviders/TypesProvider.php index d9641e16..674084db 100644 --- a/src/TypeProviders/TypesProvider.php +++ b/src/TypeProviders/TypesProvider.php @@ -4,7 +4,6 @@ use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; -use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; interface TypesProvider diff --git a/src/TypeScript/TypeScriptIndexedAccess.php b/src/TypeScript/TypeScriptIndexedAccess.php index 333e4b87..110c726d 100644 --- a/src/TypeScript/TypeScriptIndexedAccess.php +++ b/src/TypeScript/TypeScriptIndexedAccess.php @@ -26,7 +26,6 @@ public function write(WritingContext $context): string return "{$this->node->write($context)}".implode('', $segments); } - public function visitorProfile(): VisitorProfile { return VisitorProfile::create()->single('node')->iterable('segments'); diff --git a/src/TypeScript/TypeScriptUnion.php b/src/TypeScript/TypeScriptUnion.php index 8e903ead..30eda16b 100644 --- a/src/TypeScript/TypeScriptUnion.php +++ b/src/TypeScript/TypeScriptUnion.php @@ -9,7 +9,7 @@ class TypeScriptUnion implements TypeScriptNode, TypeScriptVisitableNode { /** - * @param array $types + * @param array $types */ public function __construct( public array $types, diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index a4e02a87..8109f18e 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -2,11 +2,10 @@ namespace Spatie\TypeScriptTransformer; -use Spatie\TypeScriptTransformer\Actions\ProvideTypesAction; use Spatie\TypeScriptTransformer\Actions\ConnectReferencesAction; use Spatie\TypeScriptTransformer\Actions\DiscoverTypesAction; use Spatie\TypeScriptTransformer\Actions\FormatFilesAction; -use Spatie\TypeScriptTransformer\Actions\TransformTypesAction; +use Spatie\TypeScriptTransformer\Actions\ProvideTypesAction; use Spatie\TypeScriptTransformer\Actions\WriteTypesAction; use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; @@ -42,7 +41,6 @@ public function execute(bool $watch = false): TypeScriptTransformerLog * - Make it possible to hijack the PHPStan types, or some way to rename a Laravel Collection to an array? Would be easier * - When generating routes where we have the full namespace, prepend with ., check Laravel Echo for this * - Prettier can run on complete directories, so formatting single files is maybe not required - */ $transformedCollection = new TransformedCollection(); diff --git a/src/TypeScriptTransformerConfig.php b/src/TypeScriptTransformerConfig.php index ad2e5c6c..c61a39ca 100755 --- a/src/TypeScriptTransformerConfig.php +++ b/src/TypeScriptTransformerConfig.php @@ -2,9 +2,8 @@ namespace Spatie\TypeScriptTransformer; -use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\Formatters\Formatter; -use Spatie\TypeScriptTransformer\Transformers\Transformer; +use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\Writers\Writer; class TypeScriptTransformerConfig diff --git a/src/Visitor/Visitor.php b/src/Visitor/Visitor.php index 2d1074f0..7ea9db18 100644 --- a/src/Visitor/Visitor.php +++ b/src/Visitor/Visitor.php @@ -15,8 +15,8 @@ public static function create(): self } /** - * @param array $beforeClosures - * @param array $afterClosures + * @param array $beforeClosures + * @param array $afterClosures */ public function __construct( protected array $beforeClosures = [], @@ -26,7 +26,7 @@ public function __construct( public function before( Closure $closure, - ?array $allowedNodes = null, + array $allowedNodes = null, ): self { $this->beforeClosures[] = new VisitorClosure($closure, $allowedNodes); @@ -35,7 +35,7 @@ public function before( public function after( Closure $closure, - ?array $allowedNodes = null, + array $allowedNodes = null, ): self { $this->afterClosures[] = new VisitorClosure($closure, $allowedNodes); @@ -75,7 +75,7 @@ public function execute( try { $node->$singleNodeName = $visited; } catch (Exception $e) { - throw new Exception("Tried setting $singleNodeName on ".get_class($node)." to ".get_class($visited)." but failed."); + throw new Exception("Tried setting $singleNodeName on ".get_class($node).' to '.get_class($visited).' but failed.'); } } From b89615c6bf6c001c3aae7387f04ad1cdff12b15c Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 3 Aug 2023 16:49:06 +0200 Subject: [PATCH 19/51] Config updates --- ...LaravelRoutControllerCollectionsAction.php | 37 +++++-- .../InstallTypeScriptTransformerCommand.php | 50 +++++++++ .../LaravelNamedRouteTypesProvider.php | 9 +- .../LaravelRouteActionTypesProvider.php | 9 +- src/Laravel/Support/WithoutRoutes.php | 65 +++++++++++ ...tTransformerApplicationServiceProvider.php | 23 ++++ .../TypeScriptTransformerServiceProvider.php | 16 +-- .../TransformerTypesProvider.php | 14 +-- src/TypeScriptTransformerConfig.php | 8 +- src/TypeScriptTransformerConfigBuilder.php | 104 ++++++++++++++++++ src/Writers/NamespaceWriter.php | 21 ++-- .../TypeScriptTransformerServiceProvider.stub | 23 ++++ ...velRoutControllerCollectionsActionTest.php | 95 +++++++++++++++- 13 files changed, 431 insertions(+), 43 deletions(-) create mode 100644 src/Laravel/Commands/InstallTypeScriptTransformerCommand.php create mode 100644 src/Laravel/Support/WithoutRoutes.php create mode 100644 src/Laravel/TypeScriptTransformerApplicationServiceProvider.php create mode 100644 src/TypeScriptTransformerConfigBuilder.php create mode 100644 stubs/TypeScriptTransformerServiceProvider.stub diff --git a/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php b/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php index 1f9a789c..dd1f2d65 100644 --- a/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php +++ b/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php @@ -5,6 +5,7 @@ use Illuminate\Routing\Route; use Illuminate\Routing\Router; use Illuminate\Support\Str; +use Illuminate\Support\Stringable; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteClosure; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteCollection; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteController; @@ -12,12 +13,17 @@ use Spatie\TypeScriptTransformer\Laravel\Routes\RouteInvokableController; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameter; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameterCollection; +use Spatie\TypeScriptTransformer\Laravel\Support\WithoutRoutes; class ResolveLaravelRoutControllerCollectionsAction { + /** + * @param array $filters + */ public function execute( ?string $defaultNamespace, bool $includeRouteClosures, + array $filters = [], ): RouteCollection { /** @var array $controllers */ $controllers = []; @@ -25,6 +31,12 @@ public function execute( $closures = []; foreach (app(Router::class)->getRoutes()->getRoutes() as $route) { + foreach ($filters as $filter) { + if ($filter->shouldHide($route)) { + continue 2; + } + } + $controllerClass = $route->getControllerClass(); if ($controllerClass === null && ! $includeRouteClosures) { @@ -44,15 +56,13 @@ public function execute( continue; } - if ($defaultNamespace !== null) { - $controllerClass = Str::of($controllerClass) - ->trim('\\') - ->replace($defaultNamespace, '') - ->trim('\\') - ->toString(); + $controllerClass = Str::of($controllerClass)->trim('\\'); + + if ($defaultNamespace) { + $controllerClass = $this->replaceDefaultNamespace($controllerClass, $defaultNamespace); } - $controllerClass = str_replace('\\', '.', $controllerClass); + $controllerClass = (string) $controllerClass->replace('\\', '.'); if ($route->getActionMethod() === $route->getControllerClass()) { $controllers[$controllerClass] = new RouteInvokableController( @@ -80,6 +90,19 @@ public function execute( return new RouteCollection($controllers, $closures); } + protected function replaceDefaultNamespace( + Stringable $controllerClass, + string $defaultNamespace + ): Stringable { + $defaultNamespace = Str::of($defaultNamespace)->trim('\\'); + + if (! $controllerClass->contains($defaultNamespace)) { + return $controllerClass; + } + + return $controllerClass->replace($defaultNamespace, '')->trim('\\')->prepend('.'); + } + protected function resolveRouteParameters( Route $route ): RouteParameterCollection { diff --git a/src/Laravel/Commands/InstallTypeScriptTransformerCommand.php b/src/Laravel/Commands/InstallTypeScriptTransformerCommand.php new file mode 100644 index 00000000..38f3e699 --- /dev/null +++ b/src/Laravel/Commands/InstallTypeScriptTransformerCommand.php @@ -0,0 +1,50 @@ +comment('Publishing TypeScript Transformer Service Provider...'); + $this->callSilent('vendor:publish', ['--tag' => 'typescript-transformer-provider']); + + $this->registerTypescriptTransformerServiceProvider(); + + $this->info('TypeScript Transformer scaffolding installed successfully.'); + } + + protected function registerTypescriptTransformerServiceProvider(): void + { + $namespace = Str::replaceLast('\\', '', $this->laravel->getNamespace()); + + $appConfig = file_get_contents(config_path('app.php')); + + if (Str::contains($appConfig, $namespace.'\\Providers\\TypeScriptTransformerServiceProvider::class')) { + return; + } + + file_put_contents(config_path('app.php'), str_replace( + "{$namespace}\\Providers\RouteServiceProvider::class,".PHP_EOL, + "{$namespace}\\Providers\RouteServiceProvider::class,".PHP_EOL.PHP_EOL."{$namespace}\Providers\TypeScriptTransformerServiceProvider::class,".PHP_EOL, + $appConfig + )); + + file_put_contents(app_path('Providers/TypeScriptTransformerServiceProvider.php'), str_replace( + "namespace App\Providers;", + "namespace {$namespace}\Providers;", + file_get_contents(app_path('Providers/TypeScriptTransformerServiceProvider.php')) + )); + } +} diff --git a/src/Laravel/LaravelNamedRouteTypesProvider.php b/src/Laravel/LaravelNamedRouteTypesProvider.php index e70a1020..ed04ef81 100644 --- a/src/Laravel/LaravelNamedRouteTypesProvider.php +++ b/src/Laravel/LaravelNamedRouteTypesProvider.php @@ -10,6 +10,7 @@ use Spatie\TypeScriptTransformer\Laravel\Routes\RouteInvokableController; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameter; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameterCollection; +use Spatie\TypeScriptTransformer\Laravel\Support\WithoutRoutes; use Spatie\TypeScriptTransformer\References\CustomReference; use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; @@ -35,17 +36,23 @@ class LaravelNamedRouteTypesProvider implements TypesProvider { + /** + * @param array $location + * @param array $filters + */ public function __construct( protected ResolveLaravelRoutControllerCollectionsAction $resolveLaravelRoutControllerCollectionsAction = new ResolveLaravelRoutControllerCollectionsAction(), protected array $location = [], + protected array $filters = [], ) { } public function provide(TypeScriptTransformerConfig $config, TypeScriptTransformerLog $log, TransformedCollection $types): void { $routeCollection = $this->resolveLaravelRoutControllerCollectionsAction->execute( - null, + defaultNamespace: null, includeRouteClosures: true, + filters: $this->filters, ); $transformedRoutes = new Transformed( diff --git a/src/Laravel/LaravelRouteActionTypesProvider.php b/src/Laravel/LaravelRouteActionTypesProvider.php index 6886448e..3a8e9a61 100644 --- a/src/Laravel/LaravelRouteActionTypesProvider.php +++ b/src/Laravel/LaravelRouteActionTypesProvider.php @@ -9,6 +9,7 @@ use Spatie\TypeScriptTransformer\Laravel\Routes\RouteInvokableController; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameter; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameterCollection; +use Spatie\TypeScriptTransformer\Laravel\Support\WithoutRoutes; use Spatie\TypeScriptTransformer\References\CustomReference; use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; @@ -36,18 +37,24 @@ class LaravelRouteActionTypesProvider implements TypesProvider { + /** + * @param array $location + * @param array $filters + */ public function __construct( protected ResolveLaravelRoutControllerCollectionsAction $resolveLaravelRoutControllerCollectionsAction = new ResolveLaravelRoutControllerCollectionsAction(), protected ?string $defaultNamespace = null, protected array $location = [], + protected array $filters = [], ) { } public function provide(TypeScriptTransformerConfig $config, TypeScriptTransformerLog $log, TransformedCollection $types): void { $routeCollection = $this->resolveLaravelRoutControllerCollectionsAction->execute( - $this->defaultNamespace, + defaultNamespace: $this->defaultNamespace, includeRouteClosures: false, + filters: $this->filters, ); $transformedRoutes = new Transformed( diff --git a/src/Laravel/Support/WithoutRoutes.php b/src/Laravel/Support/WithoutRoutes.php new file mode 100644 index 00000000..527f8c25 --- /dev/null +++ b/src/Laravel/Support/WithoutRoutes.php @@ -0,0 +1,65 @@ +closure)($route); + } + + public static function satisfying(Closure $closure): self + { + return new static($closure); + } + + public static function named(string ...$names): self + { + return new self(function (Route $route) use ($names): bool { + if ($route->getName() === null) { + return false; + } + + foreach ($names as $name) { + if (Str::is($name, $route->getName())) { + return true; + } + } + + return false; + }); + } + + public static function controller(string|array ...$controllers): self + { + return new self(function (Route $route) use ($controllers): bool { + if ($route->getControllerClass() === null) { + return false; + } + + foreach ($controllers as $controller) { + if (is_string($controller) && Str::is($controller, $route->getControllerClass())) { + return true; + } + + if (is_array($controller) + && Str::is($controller[0], $route->getControllerClass()) + && Str::is($controller[1], $route->getActionMethod()) + ) { + return true; + } + } + + return false; + }); + } +} diff --git a/src/Laravel/TypeScriptTransformerApplicationServiceProvider.php b/src/Laravel/TypeScriptTransformerApplicationServiceProvider.php new file mode 100644 index 00000000..f0fbdfa6 --- /dev/null +++ b/src/Laravel/TypeScriptTransformerApplicationServiceProvider.php @@ -0,0 +1,23 @@ +configure($builder); + + $config = $builder->get(); + + $this->app->singleton(TypeScriptTransformerConfig::class, fn () => $config); + } +} diff --git a/src/Laravel/TypeScriptTransformerServiceProvider.php b/src/Laravel/TypeScriptTransformerServiceProvider.php index a1f5be80..40d09fda 100644 --- a/src/Laravel/TypeScriptTransformerServiceProvider.php +++ b/src/Laravel/TypeScriptTransformerServiceProvider.php @@ -4,10 +4,10 @@ use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; +use Spatie\TypeScriptTransformer\Laravel\Commands\InstallTypeScriptTransformerCommand; use Spatie\TypeScriptTransformer\Laravel\Commands\TransformTypeScriptCommand; use Spatie\TypeScriptTransformer\Laravel\Commands\WatchTypeScriptCommand; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; -use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class TypeScriptTransformerServiceProvider extends PackageServiceProvider { @@ -16,17 +16,17 @@ public function configurePackage(Package $package): void $package ->name('typescript-transformer') ->hasCommand(WatchTypeScriptCommand::class) - ->hasCommand(TransformTypeScriptCommand::class); + ->hasCommand(TransformTypeScriptCommand::class) + ->hasCommand(InstallTypeScriptTransformerCommand::class); } public function bootingPackage(): void { - // TODO: use a laravel config file or something better here - - $this->app->singleton( - TypeScriptTransformerConfig::class, - fn () => LaravelTypeScriptTransformerConfig::$defined - ); + if ($this->app->runningInConsole()) { + $this->publishes([ + __DIR__.'/../../stubs/TypeScriptTransformerServiceProvider.stub' => app_path('Providers/TypeScriptTransformerServiceProvider.php'), + ], 'typescript-transformer-provider'); + } $this->app->singleton( TypeScriptTransformerLog::class, diff --git a/src/TypeProviders/TransformerTypesProvider.php b/src/TypeProviders/TransformerTypesProvider.php index 320c59fc..38a65a8f 100644 --- a/src/TypeProviders/TransformerTypesProvider.php +++ b/src/TypeProviders/TransformerTypesProvider.php @@ -14,22 +14,14 @@ class TransformerTypesProvider implements TypesProvider { - /** @var array */ - protected array $transformers; - /** - * @param array|Transformer> $transformers + * @param array $transformers * @param array $directories */ public function __construct( - array $transformers, + protected array $transformers, protected array $directories, ) { - foreach ($transformers as $transformer) { - $this->transformers[] = $transformer instanceof Transformer - ? $transformer - : new $transformer; - } } public function provide( @@ -39,8 +31,6 @@ public function provide( ): void { $discoveredClasses = (new DiscoverTypesAction())->execute($this->directories); - array_push($config->directoriesToWatch, ...$this->directories); - foreach ($discoveredClasses as $discoveredClass) { $transformed = $this->transformType($discoveredClass); diff --git a/src/TypeScriptTransformerConfig.php b/src/TypeScriptTransformerConfig.php index c61a39ca..f4564bbf 100755 --- a/src/TypeScriptTransformerConfig.php +++ b/src/TypeScriptTransformerConfig.php @@ -8,15 +8,15 @@ class TypeScriptTransformerConfig { - public array $directoriesToWatch = []; - /** - * @param array|TypesProvider> $typeProviders + * @param array $typeProviders + * @param array $directoriesToWatch */ public function __construct( readonly public array $typeProviders, readonly public Writer $writer, - readonly public ?Formatter $formatter + readonly public ?Formatter $formatter, + readonly public array $directoriesToWatch = [], ) { } } diff --git a/src/TypeScriptTransformerConfigBuilder.php b/src/TypeScriptTransformerConfigBuilder.php new file mode 100644 index 00000000..006658e5 --- /dev/null +++ b/src/TypeScriptTransformerConfigBuilder.php @@ -0,0 +1,104 @@ + $typeProviders + * @param array $transformers + */ + public function __construct( + protected array $typeProviders = [], + protected string|Writer|null $writer = null, + protected string|Formatter|null $formatter = null, + protected array $transformers = [], + protected array $directoriesToWatch = [], + ) { + } + + public function typesProvider(TypesProvider|string ...$typesProvider): self + { + array_push($this->typeProviders, ...$typesProvider); + + return $this; + } + + public function transformer(string|Transformer ...$transformer): self + { + array_push($this->transformers, ...$transformer); + + return $this; + } + + public function watchDirectories(string ...$directories): self + { + array_push($this->directoriesToWatch, ...$directories); + + return $this; + } + + public function writer(Writer $writer): self + { + $this->writer = $writer; + + return $this; + } + + public function formatter(Formatter|string $formatter): self + { + $this->formatter = $formatter; + + return $this; + } + + public function get(): TypeScriptTransformerConfig + { + $this->ensureConfigIsValid(); + + $typeProviders = array_map( + fn (TypesProvider|string $typeProvider) => is_string($typeProvider) ? new $typeProvider : $typeProvider, + $this->typeProviders + ); + + if (! empty($this->transformers)) { + $transformers = array_map( + fn (Transformer|string $transformer) => is_string($transformer) ? new $transformer : $transformer, + $this->transformers + ); + + $typeProviders[] = new TransformerTypesProvider($transformers, $this->directoriesToWatch); + } + + $writer = $this->writer ?? new NamespaceWriter( + resource_path('types/generated.d.ts') + ); + + if (is_string($writer)) { + $writer = new $writer; + } + + $formatter = is_string($this->formatter) ? new $this->formatter : $this->formatter; + + return new TypeScriptTransformerConfig( + $typeProviders, + $writer, + $formatter, + $this->directoriesToWatch + ); + } + + protected function ensureConfigIsValid(): void + { + if (! empty($this->transformers) && empty($this->directoriesToWatch)) { + throw new \Exception('When using transformers, you must specify which directories to watch'); + } + } +} diff --git a/src/Writers/NamespaceWriter.php b/src/Writers/NamespaceWriter.php index 6998a093..d043f313 100644 --- a/src/Writers/NamespaceWriter.php +++ b/src/Writers/NamespaceWriter.php @@ -59,19 +59,24 @@ public function output(TransformedCollection $collection, ReferenceMap $referenc $output .= $namespace->write($writingContext); } - file_put_contents( - $this->filename, - $output, - ); + $this->writeFile($output); return [ new WrittenFile($this->filename), ]; } - public function replaceReference( - Transformed $transformable - ): string { - return implode('.', $transformable->location).'.'.$transformable->name; + private function writeFile(string $output): void + { + $directory = dirname($this->filename); + + if(! file_exists($directory)){ + mkdir($directory, recursive: true); + } + + file_put_contents( + $this->filename, + $output, + ); } } diff --git a/stubs/TypeScriptTransformerServiceProvider.stub b/stubs/TypeScriptTransformerServiceProvider.stub new file mode 100644 index 00000000..a212c000 --- /dev/null +++ b/stubs/TypeScriptTransformerServiceProvider.stub @@ -0,0 +1,23 @@ +transformer(AttributeTransformer::class) + ->transformer(EnumTransformer::class) + ->watchDirectories(app_path()) + ->writer(new NamespaceWriter(resource_path('types/generated.d.ts'))) + ->formatter(PrettierFormatter::class); + } +} diff --git a/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php b/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php index b6dd0cf1..9ccda8f4 100644 --- a/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php +++ b/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php @@ -7,6 +7,7 @@ use Spatie\TypeScriptTransformer\Laravel\Routes\RouteControllerAction; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteInvokableController; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameterCollection; +use Spatie\TypeScriptTransformer\Laravel\Support\WithoutRoutes; use Spatie\TypeScriptTransformer\Tests\Laravel\FakeClasses\InvokableController; use Spatie\TypeScriptTransformer\Tests\Laravel\FakeClasses\ResourceController; use Spatie\TypeScriptTransformer\Tests\Laravel\LaravelTestCase; @@ -173,7 +174,6 @@ function (Router $router) { $router->get('simple', fn () => 'simple')->name('simple'); $router->get('invokable', InvokableController::class)->name('invokable'); $router->resource('resource', ResourceController::class); - }, function (RouteCollection $routes) { expect($routes->controllers)->toHaveCount(2); @@ -204,6 +204,97 @@ function (RouteCollection $routes) { expect($routes->controllers)->toHaveCount(2)->toHaveKeys([ 'Symfony.Component.HttpKernel.Controller.ErrorController', - 'InvokableController', + '.InvokableController', ]); }); + +it('can filter out certain routes', function ( + WithoutRoutes $withoutRoutes, + Closure $expectations +) { + app(Router::class)->get('simple', fn () => 'simple')->name('simple'); + app(Router::class)->get('invokable', InvokableController::class)->name('invokable'); + app(Router::class)->resource('resource', ResourceController::class); + + $routes = app(ResolveLaravelRoutControllerCollectionsAction::class)->execute(null, true, [$withoutRoutes]); + + $expectations($routes); +})->with(function () { + yield 'named' => [ + WithoutRoutes::named('simple'), + function(RouteCollection $routes){ + expect($routes->closures)->toBeEmpty(); + expect($routes->controllers)->toHaveCount(2); + } + ]; + yield 'multiple named' => [ + WithoutRoutes::named('simple', 'resource.index', 'resource.edit'), + function(RouteCollection $routes){ + expect($routes->closures)->toBeEmpty(); + expect($routes->controllers) + ->toHaveCount(2) + ->toHaveKeys([ + 'Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController', + 'Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController' + ]); + expect($routes->controllers['Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController']->actions) + ->toHaveCount(5) + ->toHaveKeys([ + 'show', + 'create', + 'update', + 'store', + 'destroy', + ]); + } + ]; + yield 'wildcard name' => [ + WithoutRoutes::named('invokable', 'resource.*'), + function(RouteCollection $routes){ + expect($routes->closures)->toHaveCount(1); + expect($routes->controllers)->toHaveCount(0); + } + ]; + yield 'controller' => [ + WithoutRoutes::controller(ResourceController::class), + function(RouteCollection $routes){ + expect($routes->closures)->toHaveCount(1); + expect($routes->controllers)->toHaveCount(1)->toHaveKey('Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController'); + } + ]; + yield 'multiple controllers' => [ + WithoutRoutes::controller(ResourceController::class), + function(RouteCollection $routes){ + expect($routes->closures)->toHaveCount(1); + expect($routes->controllers)->toHaveCount(1)->toHaveKey('Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController'); + } + ]; + yield 'controller wildcard' => [ + WithoutRoutes::controller('Spatie\TypeScriptTransformer\Tests\Laravel\FakeClasses\*'), + function(RouteCollection $routes){ + expect($routes->closures)->toHaveCount(1); + expect($routes->controllers)->toHaveCount(0); + } + ]; + yield 'controller action' => [ + WithoutRoutes::controller([ResourceController::class, 'index'], [ResourceController::class, 'edit']), + function(RouteCollection $routes){ + expect($routes->closures)->toHaveCount(1); + expect($routes->controllers) + ->toHaveCount(2) + ->toHaveKeys([ + 'Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController', + 'Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController' + ]); + expect($routes->controllers['Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController']->actions) + ->toHaveCount(5) + ->toHaveKeys([ + 'show', + 'create', + 'update', + 'store', + 'destroy', + ]); + } + ]; +}); From 23d37e7d44a0e8597f75213658c49cdf26977e22 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Thu, 3 Aug 2023 14:49:41 +0000 Subject: [PATCH 20/51] Fix styling --- ...LaravelRoutControllerCollectionsAction.php | 2 +- .../InstallTypeScriptTransformerCommand.php | 1 - .../LaravelNamedRouteTypesProvider.php | 4 +-- .../LaravelRouteActionTypesProvider.php | 4 +-- src/TypeScriptTransformerConfig.php | 2 +- src/TypeScriptTransformerConfigBuilder.php | 4 +-- src/Writers/NamespaceWriter.php | 2 +- ...velRoutControllerCollectionsActionTest.php | 32 +++++++++---------- 8 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php b/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php index dd1f2d65..0cdd8c12 100644 --- a/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php +++ b/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php @@ -18,7 +18,7 @@ class ResolveLaravelRoutControllerCollectionsAction { /** - * @param array $filters + * @param array $filters */ public function execute( ?string $defaultNamespace, diff --git a/src/Laravel/Commands/InstallTypeScriptTransformerCommand.php b/src/Laravel/Commands/InstallTypeScriptTransformerCommand.php index 38f3e699..4175be39 100644 --- a/src/Laravel/Commands/InstallTypeScriptTransformerCommand.php +++ b/src/Laravel/Commands/InstallTypeScriptTransformerCommand.php @@ -4,7 +4,6 @@ use Illuminate\Console\Command; use Illuminate\Support\Str; -use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; /** * @note Taken from the Laravel Horizon package diff --git a/src/Laravel/LaravelNamedRouteTypesProvider.php b/src/Laravel/LaravelNamedRouteTypesProvider.php index ed04ef81..b669cd25 100644 --- a/src/Laravel/LaravelNamedRouteTypesProvider.php +++ b/src/Laravel/LaravelNamedRouteTypesProvider.php @@ -37,8 +37,8 @@ class LaravelNamedRouteTypesProvider implements TypesProvider { /** - * @param array $location - * @param array $filters + * @param array $location + * @param array $filters */ public function __construct( protected ResolveLaravelRoutControllerCollectionsAction $resolveLaravelRoutControllerCollectionsAction = new ResolveLaravelRoutControllerCollectionsAction(), diff --git a/src/Laravel/LaravelRouteActionTypesProvider.php b/src/Laravel/LaravelRouteActionTypesProvider.php index 3a8e9a61..c71438da 100644 --- a/src/Laravel/LaravelRouteActionTypesProvider.php +++ b/src/Laravel/LaravelRouteActionTypesProvider.php @@ -38,8 +38,8 @@ class LaravelRouteActionTypesProvider implements TypesProvider { /** - * @param array $location - * @param array $filters + * @param array $location + * @param array $filters */ public function __construct( protected ResolveLaravelRoutControllerCollectionsAction $resolveLaravelRoutControllerCollectionsAction = new ResolveLaravelRoutControllerCollectionsAction(), diff --git a/src/TypeScriptTransformerConfig.php b/src/TypeScriptTransformerConfig.php index f4564bbf..5836dd58 100755 --- a/src/TypeScriptTransformerConfig.php +++ b/src/TypeScriptTransformerConfig.php @@ -10,7 +10,7 @@ class TypeScriptTransformerConfig { /** * @param array $typeProviders - * @param array $directoriesToWatch + * @param array $directoriesToWatch */ public function __construct( readonly public array $typeProviders, diff --git a/src/TypeScriptTransformerConfigBuilder.php b/src/TypeScriptTransformerConfigBuilder.php index 006658e5..8fd5ef78 100644 --- a/src/TypeScriptTransformerConfigBuilder.php +++ b/src/TypeScriptTransformerConfigBuilder.php @@ -12,8 +12,8 @@ class TypeScriptTransformerConfigBuilder { /** - * @param array $typeProviders - * @param array $transformers + * @param array $typeProviders + * @param array $transformers */ public function __construct( protected array $typeProviders = [], diff --git a/src/Writers/NamespaceWriter.php b/src/Writers/NamespaceWriter.php index d043f313..1339c896 100644 --- a/src/Writers/NamespaceWriter.php +++ b/src/Writers/NamespaceWriter.php @@ -70,7 +70,7 @@ private function writeFile(string $output): void { $directory = dirname($this->filename); - if(! file_exists($directory)){ + if (! file_exists($directory)) { mkdir($directory, recursive: true); } diff --git a/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php b/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php index 9ccda8f4..a50e5698 100644 --- a/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php +++ b/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php @@ -222,20 +222,20 @@ function (RouteCollection $routes) { })->with(function () { yield 'named' => [ WithoutRoutes::named('simple'), - function(RouteCollection $routes){ + function (RouteCollection $routes) { expect($routes->closures)->toBeEmpty(); expect($routes->controllers)->toHaveCount(2); - } + }, ]; yield 'multiple named' => [ WithoutRoutes::named('simple', 'resource.index', 'resource.edit'), - function(RouteCollection $routes){ + function (RouteCollection $routes) { expect($routes->closures)->toBeEmpty(); expect($routes->controllers) ->toHaveCount(2) ->toHaveKeys([ 'Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController', - 'Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController' + 'Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController', ]); expect($routes->controllers['Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController']->actions) ->toHaveCount(5) @@ -246,45 +246,45 @@ function(RouteCollection $routes){ 'store', 'destroy', ]); - } + }, ]; yield 'wildcard name' => [ WithoutRoutes::named('invokable', 'resource.*'), - function(RouteCollection $routes){ + function (RouteCollection $routes) { expect($routes->closures)->toHaveCount(1); expect($routes->controllers)->toHaveCount(0); - } + }, ]; yield 'controller' => [ WithoutRoutes::controller(ResourceController::class), - function(RouteCollection $routes){ + function (RouteCollection $routes) { expect($routes->closures)->toHaveCount(1); expect($routes->controllers)->toHaveCount(1)->toHaveKey('Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController'); - } + }, ]; yield 'multiple controllers' => [ WithoutRoutes::controller(ResourceController::class), - function(RouteCollection $routes){ + function (RouteCollection $routes) { expect($routes->closures)->toHaveCount(1); expect($routes->controllers)->toHaveCount(1)->toHaveKey('Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController'); - } + }, ]; yield 'controller wildcard' => [ WithoutRoutes::controller('Spatie\TypeScriptTransformer\Tests\Laravel\FakeClasses\*'), - function(RouteCollection $routes){ + function (RouteCollection $routes) { expect($routes->closures)->toHaveCount(1); expect($routes->controllers)->toHaveCount(0); - } + }, ]; yield 'controller action' => [ WithoutRoutes::controller([ResourceController::class, 'index'], [ResourceController::class, 'edit']), - function(RouteCollection $routes){ + function (RouteCollection $routes) { expect($routes->closures)->toHaveCount(1); expect($routes->controllers) ->toHaveCount(2) ->toHaveKeys([ 'Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController', - 'Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController' + 'Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController', ]); expect($routes->controllers['Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController']->actions) ->toHaveCount(5) @@ -295,6 +295,6 @@ function(RouteCollection $routes){ 'store', 'destroy', ]); - } + }, ]; }); From 0e6682bb0f27c98077bc697042cf36e190479a39 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Mon, 7 Aug 2023 12:17:12 +0200 Subject: [PATCH 21/51] Add some stuff --- src/Actions/ConnectReferencesAction.php | 12 +- src/Actions/FormatFilesAction.php | 9 +- src/Actions/ProvideTypesAction.php | 2 - .../SplitTransformedPerLocationAction.php | 2 +- src/Actions/WriteTypesAction.php | 1 - src/Formatters/EslintFormatter.php | 4 +- src/Formatters/Formatter.php | 2 +- src/Formatters/PrettierFormatter.php | 4 +- .../Commands/TransformTypeScriptCommand.php | 5 +- .../LaravelNamedRouteTypesProvider.php | 8 +- .../LaravelRouteActionTypesProvider.php | 14 +- .../LaravelTypeScriptTransformerConfig.php | 10 -- src/Laravel/LaravelTypesProvider.php | 159 ++++++++---------- src/Laravel/SpatieLaravelTypesProvider.php | 9 +- .../TypeScriptTransformerServiceProvider.php | 5 - src/Support/ImportName.php | 33 ++++ src/Support/TypeScriptTransformerLog.php | 11 ++ src/Transformed/Transformed.php | 58 ++++++- src/Transformers/AttributeTransformer.php | 2 +- src/Transformers/ClassTransformer.php | 12 +- src/Transformers/EnumTransformer.php | 10 +- .../TransformerTypesProvider.php | 1 - src/TypeProviders/TypesProvider.php | 1 - src/TypeScript/TypeReference.php | 7 +- src/TypeScript/TypeScriptAlias.php | 9 +- src/TypeScript/TypeScriptEnum.php | 7 +- src/TypeScript/TypeScriptExport.php | 2 +- src/TypeScript/TypeScriptExportableNode.php | 8 + .../TypeScriptForwardingExportableNode.php | 8 + .../TypeScriptFunctionDefinition.php | 9 +- src/TypeScript/TypeScriptGeneric.php | 9 +- src/TypeScript/TypeScriptIdentifier.php | 7 +- src/TypeScript/TypeScriptImport.php | 5 + src/TypeScript/TypeScriptInterface.php | 7 +- src/TypeScriptTransformer.php | 5 +- src/Writers/ModuleWriter.php | 53 +++++- src/Writers/NamespaceWriter.php | 12 +- src/Writers/Writer.php | 1 + 38 files changed, 335 insertions(+), 188 deletions(-) delete mode 100644 src/Laravel/LaravelTypeScriptTransformerConfig.php create mode 100644 src/Support/ImportName.php create mode 100644 src/TypeScript/TypeScriptExportableNode.php create mode 100644 src/TypeScript/TypeScriptForwardingExportableNode.php diff --git a/src/Actions/ConnectReferencesAction.php b/src/Actions/ConnectReferencesAction.php index bab26b09..1f489388 100644 --- a/src/Actions/ConnectReferencesAction.php +++ b/src/Actions/ConnectReferencesAction.php @@ -14,7 +14,6 @@ class ConnectReferencesAction { public function __construct( protected TypeScriptTransformerConfig $config, - public TypeScriptTransformerLog $log, ) { } @@ -28,32 +27,31 @@ public function execute(TransformedCollection $collection): ReferenceMap } } - $visitor = Visitor::create()->before(function (TypeReference $typeReference, array $metadata) use ($referenceMap, &$references) { + $visitor = Visitor::create()->before(function (TypeReference $typeReference, array &$metadata) use ($referenceMap) { $reference = $typeReference->reference; if (! $referenceMap->has($reference)) { /** @var Transformed $transformed */ $transformed = $metadata['transformed']; - $this->log->warning("Tried replacing reference to `{$reference->humanFriendlyName()}` in `{$transformed->reference->humanFriendlyName()}` but it was not found in the transformed types"); + TypeScriptTransformerLog::resolve()->warning("Tried replacing reference to `{$reference->humanFriendlyName()}` in `{$transformed->reference->humanFriendlyName()}` but it was not found in the transformed types"); return; } - $references[] = $reference; + $metadata['references'][] = $reference; $typeReference->connect($referenceMap->get($reference)); }, [TypeReference::class]); foreach ($collection as $transformed) { - $references = []; - $metadata = [ 'transformed' => $transformed, + 'references' => [], ]; $visitor->execute($transformed->typeScriptNode, $metadata); - $transformed->references = $references; + $transformed->references = $metadata['references']; } return $referenceMap; diff --git a/src/Actions/FormatFilesAction.php b/src/Actions/FormatFilesAction.php index a4a0c3eb..93ad693f 100644 --- a/src/Actions/FormatFilesAction.php +++ b/src/Actions/FormatFilesAction.php @@ -10,12 +10,11 @@ class FormatFilesAction { public function __construct( public TypeScriptTransformerConfig $config, - public TypeScriptTransformerLog $log, ) { } /** - * @param array $writtenFiles + * @param array $writtenFiles */ public function execute(array $writtenFiles): void { @@ -23,8 +22,8 @@ public function execute(array $writtenFiles): void return; } - foreach ($writtenFiles as $writtenFile) { - $this->config->formatter->format($writtenFile->path); - } + $this->config->formatter->format( + array_map(fn (WrittenFile $writtenFile) => $writtenFile->path, $writtenFiles) + ); } } diff --git a/src/Actions/ProvideTypesAction.php b/src/Actions/ProvideTypesAction.php index 5eabb785..38b9f48a 100644 --- a/src/Actions/ProvideTypesAction.php +++ b/src/Actions/ProvideTypesAction.php @@ -11,7 +11,6 @@ class ProvideTypesAction { public function __construct( protected TypeScriptTransformerConfig $config, - public TypeScriptTransformerLog $log, ) { } @@ -24,7 +23,6 @@ public function execute(TransformedCollection $collection): void $defaultTypeProvider->provide( $this->config, - $this->log, $collection ); } diff --git a/src/Actions/SplitTransformedPerLocationAction.php b/src/Actions/SplitTransformedPerLocationAction.php index 893720a3..158c39c6 100644 --- a/src/Actions/SplitTransformedPerLocationAction.php +++ b/src/Actions/SplitTransformedPerLocationAction.php @@ -30,7 +30,7 @@ public function execute(TransformedCollection $collection): array ksort($split); foreach ($split as $splitConstruct) { - usort($splitConstruct->transformed, fn (Transformed $a, Transformed $b) => $a->name <=> $b->name); + usort($splitConstruct->transformed, fn (Transformed $a, Transformed $b) => $a->getName() <=> $b->getName()); } return array_values($split); diff --git a/src/Actions/WriteTypesAction.php b/src/Actions/WriteTypesAction.php index fea3e865..bb4fd1b5 100644 --- a/src/Actions/WriteTypesAction.php +++ b/src/Actions/WriteTypesAction.php @@ -12,7 +12,6 @@ class WriteTypesAction { public function __construct( public TypeScriptTransformerConfig $config, - public TypeScriptTransformerLog $log, ) { } diff --git a/src/Formatters/EslintFormatter.php b/src/Formatters/EslintFormatter.php index ba16062c..db74eeda 100644 --- a/src/Formatters/EslintFormatter.php +++ b/src/Formatters/EslintFormatter.php @@ -7,9 +7,9 @@ class EslintFormatter implements Formatter { - public function format(string $file): void + public function format(array $files): void { - $process = new Process(['npx', '--yes', 'eslint', '--fix', '--no-ignore', $file]); + $process = new Process(['npx', '--yes', 'eslint', '--fix', '--no-ignore', ...$files]); $process->run(); if (! $process->isSuccessful()) { diff --git a/src/Formatters/Formatter.php b/src/Formatters/Formatter.php index 34cc6a13..bf93211d 100644 --- a/src/Formatters/Formatter.php +++ b/src/Formatters/Formatter.php @@ -4,5 +4,5 @@ interface Formatter { - public function format(string $file): void; + public function format(array $files): void; } diff --git a/src/Formatters/PrettierFormatter.php b/src/Formatters/PrettierFormatter.php index 34ba9015..ada4119b 100644 --- a/src/Formatters/PrettierFormatter.php +++ b/src/Formatters/PrettierFormatter.php @@ -7,9 +7,9 @@ class PrettierFormatter implements Formatter { - public function format(string $file): void + public function format(array $files): void { - $process = new Process(['npx', '--yes', 'prettier', '--write', $file]); + $process = new Process(['npx', '--yes', 'prettier', '--write', ...$files]); $process->run(); if (! $process->isSuccessful()) { diff --git a/src/Laravel/Commands/TransformTypeScriptCommand.php b/src/Laravel/Commands/TransformTypeScriptCommand.php index a83785e2..cc242edd 100644 --- a/src/Laravel/Commands/TransformTypeScriptCommand.php +++ b/src/Laravel/Commands/TransformTypeScriptCommand.php @@ -3,6 +3,7 @@ namespace Spatie\TypeScriptTransformer\Laravel\Commands; use Illuminate\Console\Command; +use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\TypeScriptTransformer; class TransformTypeScriptCommand extends Command @@ -14,7 +15,9 @@ class TransformTypeScriptCommand extends Command public function handle( TypeScriptTransformer $typeScriptTransformer ): int { - $log = $typeScriptTransformer->execute(); + $typeScriptTransformer->execute(); + + $log = TypeScriptTransformerLog::resolve(); if (! empty($log->infoMessages)) { foreach ($log->infoMessages as $infoMessage) { diff --git a/src/Laravel/LaravelNamedRouteTypesProvider.php b/src/Laravel/LaravelNamedRouteTypesProvider.php index b669cd25..258a0c89 100644 --- a/src/Laravel/LaravelNamedRouteTypesProvider.php +++ b/src/Laravel/LaravelNamedRouteTypesProvider.php @@ -47,7 +47,7 @@ public function __construct( ) { } - public function provide(TypeScriptTransformerConfig $config, TypeScriptTransformerLog $log, TransformedCollection $types): void + public function provide(TypeScriptTransformerConfig $config, TransformedCollection $types): void { $routeCollection = $this->resolveLaravelRoutControllerCollectionsAction->execute( defaultNamespace: null, @@ -61,8 +61,7 @@ public function provide(TypeScriptTransformerConfig $config, TypeScriptTransform $this->parseRouteCollection($routeCollection), ), $routesListReference = new CustomReference('laravel_named_routes', 'routes_list'), - 'NamedRouteList', - $this->location, + $this->location, true, ); $jsonEncodedRoutes = $this->routeCollectionToJson($routeCollection); @@ -109,8 +108,7 @@ public function provide(TypeScriptTransformerConfig $config, TypeScriptTransform ) ), new CustomReference('laravel_named_routes', 'route_function'), - 'route', - $this->location, + $this->location, true, ); $types->add($transformedRoutes, $transformedRoute); diff --git a/src/Laravel/LaravelRouteActionTypesProvider.php b/src/Laravel/LaravelRouteActionTypesProvider.php index c71438da..679c3fd9 100644 --- a/src/Laravel/LaravelRouteActionTypesProvider.php +++ b/src/Laravel/LaravelRouteActionTypesProvider.php @@ -49,7 +49,7 @@ public function __construct( ) { } - public function provide(TypeScriptTransformerConfig $config, TypeScriptTransformerLog $log, TransformedCollection $types): void + public function provide(TypeScriptTransformerConfig $config, TransformedCollection $types): void { $routeCollection = $this->resolveLaravelRoutControllerCollectionsAction->execute( defaultNamespace: $this->defaultNamespace, @@ -63,8 +63,7 @@ public function provide(TypeScriptTransformerConfig $config, TypeScriptTransform $this->parseRouteCollection($routeCollection), ), $routesListReference = new CustomReference('laravel_route_actions', 'routes_list'), - 'ActionRoutesList', - $this->location, + $this->location, true, ); $isInvokableControllerCondition = TypeScriptOperator::extends( @@ -96,8 +95,7 @@ public function provide(TypeScriptTransformerConfig $config, TypeScriptTransform ) ), $actionControllerReference = new CustomReference('laravel_route_actions', 'action_controller'), - 'ActionController', - $this->location, + $this->location, true, ); $actionParameters = new Transformed( @@ -124,8 +122,7 @@ public function provide(TypeScriptTransformerConfig $config, TypeScriptTransform ) ), $actionParametersReference = new CustomReference('laravel_route_actions', 'action_parameters'), - 'ActionParameters', - $this->location, + $this->location, true, ); $jsonEncodedRoutes = $this->routeCollectionToJson($routeCollection); @@ -196,8 +193,7 @@ public function provide(TypeScriptTransformerConfig $config, TypeScriptTransform ) ), new CustomReference('laravel_route_actions', 'action_function'), - 'action', - $this->location, + $this->location, true, ); $types->add($transformedRoutes, $actionController, $actionParameters, $transformedAction); diff --git a/src/Laravel/LaravelTypeScriptTransformerConfig.php b/src/Laravel/LaravelTypeScriptTransformerConfig.php deleted file mode 100644 index 6bdbf5a4..00000000 --- a/src/Laravel/LaravelTypeScriptTransformerConfig.php +++ /dev/null @@ -1,10 +0,0 @@ -add($optionsType); diff --git a/src/Laravel/TypeScriptTransformerServiceProvider.php b/src/Laravel/TypeScriptTransformerServiceProvider.php index 40d09fda..63b92b8b 100644 --- a/src/Laravel/TypeScriptTransformerServiceProvider.php +++ b/src/Laravel/TypeScriptTransformerServiceProvider.php @@ -27,10 +27,5 @@ public function bootingPackage(): void __DIR__.'/../../stubs/TypeScriptTransformerServiceProvider.stub' => app_path('Providers/TypeScriptTransformerServiceProvider.php'), ], 'typescript-transformer-provider'); } - - $this->app->singleton( - TypeScriptTransformerLog::class, - fn () => new TypeScriptTransformerLog(), - ); } } diff --git a/src/Support/ImportName.php b/src/Support/ImportName.php new file mode 100644 index 00000000..1483081b --- /dev/null +++ b/src/Support/ImportName.php @@ -0,0 +1,33 @@ +alias === null) { + return $this->name; + } + + return "{$this->name} as {$this->alias}"; + } + + public function isAliased(): bool + { + return $this->alias !== null; + } + + public static function fromNameAndImportedName( + string $name, + string $importedName, + ): self { + return new self($name, $name === $importedName ? null : $importedName); + } +} diff --git a/src/Support/TypeScriptTransformerLog.php b/src/Support/TypeScriptTransformerLog.php index 3cc067d6..23501dcf 100644 --- a/src/Support/TypeScriptTransformerLog.php +++ b/src/Support/TypeScriptTransformerLog.php @@ -8,6 +8,17 @@ class TypeScriptTransformerLog public array $warningMessages = []; + protected static self $instance; + + private function __construct() + { + } + + public static function resolve(): self + { + return self::$instance ??= new self(); + } + public function info(string $message): self { $this->infoMessages[] = $message; diff --git a/src/Transformed/Transformed.php b/src/Transformed/Transformed.php index eab60934..668b224e 100644 --- a/src/Transformed/Transformed.php +++ b/src/Transformed/Transformed.php @@ -3,20 +3,72 @@ namespace Spatie\TypeScriptTransformer\Transformed; use Spatie\TypeScriptTransformer\References\Reference; +use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptExport; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptExportableNode; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptForwardingExportableNode; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; class Transformed { + protected ?string $name; + /** - * @param array $location - * @param array $references + * @param array $location + * @param array $references */ public function __construct( public TypeScriptNode $typeScriptNode, public Reference $reference, - public string $name, public array $location, + public bool $export = true, public array $references = [], ) { } + + + public function getName(): ?string + { + if (isset($this->name)) { + return $this->name; + } + + if ($this->typeScriptNode instanceof TypeScriptExportableNode) { + return $this->name = $this->typeScriptNode->getExportedName(); + } + + if ($this->typeScriptNode instanceof TypeScriptForwardingExportableNode) { + $exportableNode = $this->typeScriptNode; + + while ($exportableNode instanceof TypeScriptForwardingExportableNode) { + $exportableNode = $exportableNode->getForwardedExportableNode(); + } + + return $this->name = $exportableNode->getExportedName(); + } + + return null; + } + + public function nameAs(string $name): self + { + $this->name = $name; + + return $this; + } + + public function prepareForWrite(): TypeScriptNode + { + if ($this->export === false) { + return $this->typeScriptNode; + } + + if (! $this->typeScriptNode instanceof TypeScriptExportableNode && ! $this->typeScriptNode instanceof TypeScriptForwardingExportableNode) { + TypeScriptTransformerLog::resolve()->warning("Could not export `{$this->reference->humanFriendlyName()}` because it is not exportable"); + + return $this->typeScriptNode; + } + + return new TypeScriptExport($this->typeScriptNode); + } } diff --git a/src/Transformers/AttributeTransformer.php b/src/Transformers/AttributeTransformer.php index 4bf05125..e9fe4c9c 100644 --- a/src/Transformers/AttributeTransformer.php +++ b/src/Transformers/AttributeTransformer.php @@ -27,7 +27,7 @@ public function transform(ReflectionClass $reflectionClass, TransformationContex $attribute = $reflectionClass->getAttributes(TypeScript::class)[0]->newInstance(); if ($attribute->name !== null) { - $transformed->name = $attribute->name; + $transformed->nameAs($attribute->name); } if ($attribute->location !== null) { diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index 375fba47..f6e48d74 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -17,7 +17,6 @@ use Spatie\TypeScriptTransformer\Transformers\ClassPropertyProcessors\ClassPropertyProcessor; use Spatie\TypeScriptTransformer\TypeResolvers\DocTypeResolver; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptExport; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; @@ -41,15 +40,12 @@ public function transform(ReflectionClass $reflectionClass, TransformationContex } return new Transformed( - new TypeScriptExport( - new TypeScriptAlias( - new TypeScriptIdentifier($context->name), - $this->getTypeScriptNode($reflectionClass) - ) + new TypeScriptAlias( + new TypeScriptIdentifier($context->name), + $this->getTypeScriptNode($reflectionClass) ), new ReflectionClassReference($reflectionClass), - $context->name, - $context->nameSpaceSegments, + $context->nameSpaceSegments, true, ); } diff --git a/src/Transformers/EnumTransformer.php b/src/Transformers/EnumTransformer.php index 1cbb2d25..98406926 100644 --- a/src/Transformers/EnumTransformer.php +++ b/src/Transformers/EnumTransformer.php @@ -10,7 +10,6 @@ use Spatie\TypeScriptTransformer\Transformed\Untransformable; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptEnum; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptExport; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptLiteral; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; @@ -39,8 +38,7 @@ public function transform( ? $this->transformAsNativeEnum($context->name, $cases) : $this->transformAsUnion($context->name, $cases), new ReflectionClassReference($reflectionClass), - $context->name, - $context->nameSpaceSegments, + $context->nameSpaceSegments, true, ); } @@ -69,14 +67,14 @@ protected function transformAsNativeEnum( string $name, array $cases ): TypeScriptNode { - return new TypeScriptExport(new TypeScriptEnum($name, $cases)); + return new TypeScriptEnum($name, $cases); } protected function transformAsUnion( string $name, array $cases ): TypeScriptNode { - return new TypeScriptExport(new TypeScriptAlias( + return new TypeScriptAlias( new TypeScriptIdentifier($name), new TypeScriptUnion( array_map( @@ -84,6 +82,6 @@ protected function transformAsUnion( $cases, ), ), - )); + ); } } diff --git a/src/TypeProviders/TransformerTypesProvider.php b/src/TypeProviders/TransformerTypesProvider.php index 38a65a8f..97de8517 100644 --- a/src/TypeProviders/TransformerTypesProvider.php +++ b/src/TypeProviders/TransformerTypesProvider.php @@ -26,7 +26,6 @@ public function __construct( public function provide( TypeScriptTransformerConfig $config, - TypeScriptTransformerLog $log, TransformedCollection $types ): void { $discoveredClasses = (new DiscoverTypesAction())->execute($this->directories); diff --git a/src/TypeProviders/TypesProvider.php b/src/TypeProviders/TypesProvider.php index 674084db..1616273a 100644 --- a/src/TypeProviders/TypesProvider.php +++ b/src/TypeProviders/TypesProvider.php @@ -10,7 +10,6 @@ interface TypesProvider { public function provide( TypeScriptTransformerConfig $config, - TypeScriptTransformerLog $log, TransformedCollection $types ): void; } diff --git a/src/TypeScript/TypeReference.php b/src/TypeScript/TypeReference.php index d11492a8..5bd735bc 100644 --- a/src/TypeScript/TypeReference.php +++ b/src/TypeScript/TypeReference.php @@ -6,7 +6,7 @@ use Spatie\TypeScriptTransformer\Support\WritingContext; use Spatie\TypeScriptTransformer\Transformed\Transformed; -class TypeReference implements TypeScriptNode +class TypeReference implements TypeScriptNode, TypeScriptExportableNode { public function __construct( public Reference $reference, @@ -23,4 +23,9 @@ public function write(WritingContext $context): string { return ($context->referenceWriter)($this->reference); } + + public function getExportedName(): string + { + return $this->referenced->getName(); + } } diff --git a/src/TypeScript/TypeScriptAlias.php b/src/TypeScript/TypeScriptAlias.php index 9ab3b258..43de622f 100644 --- a/src/TypeScript/TypeScriptAlias.php +++ b/src/TypeScript/TypeScriptAlias.php @@ -5,10 +5,10 @@ use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptAlias implements TypeScriptNode, TypeScriptVisitableNode +class TypeScriptAlias implements TypeScriptNode, TypeScriptVisitableNode, TypeScriptForwardingExportableNode { public function __construct( - public TypeScriptNode $identifier, + public TypeScriptIdentifier|TypeScriptGeneric $identifier, public TypeScriptNode $type, ) { } @@ -22,4 +22,9 @@ public function visitorProfile(): VisitorProfile { return VisitorProfile::create()->single('identifier', 'type'); } + + public function getForwardedExportableNode(): TypeScriptExportableNode|TypeScriptForwardingExportableNode + { + return $this->identifier; + } } diff --git a/src/TypeScript/TypeScriptEnum.php b/src/TypeScript/TypeScriptEnum.php index 676c55e5..9d5bcd43 100644 --- a/src/TypeScript/TypeScriptEnum.php +++ b/src/TypeScript/TypeScriptEnum.php @@ -4,7 +4,7 @@ use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptEnum implements TypeScriptNode +class TypeScriptEnum implements TypeScriptNode, TypeScriptExportableNode { public function __construct( public string $name, @@ -24,4 +24,9 @@ public function write(WritingContext $context): string return $output; } + + public function getExportedName(): string + { + return $this->name; + } } diff --git a/src/TypeScript/TypeScriptExport.php b/src/TypeScript/TypeScriptExport.php index 05fa9a2e..eb5aba7a 100644 --- a/src/TypeScript/TypeScriptExport.php +++ b/src/TypeScript/TypeScriptExport.php @@ -8,7 +8,7 @@ class TypeScriptExport implements TypeScriptNode, TypeScriptVisitableNode { public function __construct( - public TypeScriptNode $node, + public TypeScriptExportableNode|TypeScriptForwardingExportableNode $node, ) { } diff --git a/src/TypeScript/TypeScriptExportableNode.php b/src/TypeScript/TypeScriptExportableNode.php new file mode 100644 index 00000000..c8bcf88d --- /dev/null +++ b/src/TypeScript/TypeScriptExportableNode.php @@ -0,0 +1,8 @@ +single('identifier', 'returnType', 'body')->iterable('parameters'); } + + public function getForwardedExportableNode(): TypeScriptExportableNode|TypeScriptForwardingExportableNode + { + return $this->identifier; + } } diff --git a/src/TypeScript/TypeScriptGeneric.php b/src/TypeScript/TypeScriptGeneric.php index d96fe05a..7f2387d4 100644 --- a/src/TypeScript/TypeScriptGeneric.php +++ b/src/TypeScript/TypeScriptGeneric.php @@ -5,13 +5,13 @@ use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptGeneric implements TypeScriptNode, TypeScriptVisitableNode +class TypeScriptGeneric implements TypeScriptNode, TypeScriptVisitableNode, TypeScriptForwardingExportableNode { /** * @param array $genericTypes */ public function __construct( - public TypeScriptNode $type, + public TypeScriptIdentifier|TypeReference $type, public array $genericTypes, ) { } @@ -30,4 +30,9 @@ public function visitorProfile(): VisitorProfile { return VisitorProfile::create()->single('type')->iterable('genericTypes'); } + + public function getForwardedExportableNode(): TypeScriptExportableNode|TypeScriptForwardingExportableNode + { + return $this->type; + } } diff --git a/src/TypeScript/TypeScriptIdentifier.php b/src/TypeScript/TypeScriptIdentifier.php index 8ec07129..66dac38e 100644 --- a/src/TypeScript/TypeScriptIdentifier.php +++ b/src/TypeScript/TypeScriptIdentifier.php @@ -4,7 +4,7 @@ use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptIdentifier implements TypeScriptNode +class TypeScriptIdentifier implements TypeScriptNode, TypeScriptExportableNode { public function __construct( public string $name, @@ -15,4 +15,9 @@ public function write(WritingContext $context): string { return (str_contains($this->name, '.') || str_contains($this->name, '\\')) ? "'{$this->name}'" : $this->name; } + + public function getExportedName(): string + { + return $this->name; + } } diff --git a/src/TypeScript/TypeScriptImport.php b/src/TypeScript/TypeScriptImport.php index 28fe0639..9622b6d2 100644 --- a/src/TypeScript/TypeScriptImport.php +++ b/src/TypeScript/TypeScriptImport.php @@ -2,10 +2,15 @@ namespace Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\Support\ImportName; use Spatie\TypeScriptTransformer\Support\WritingContext; class TypeScriptImport implements TypeScriptNode { + /** + * @param string $path + * @param array $names + */ public function __construct( public string $path, public array $names, diff --git a/src/TypeScript/TypeScriptInterface.php b/src/TypeScript/TypeScriptInterface.php index 7694ef32..d0661eb3 100644 --- a/src/TypeScript/TypeScriptInterface.php +++ b/src/TypeScript/TypeScriptInterface.php @@ -5,7 +5,7 @@ use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptInterface implements TypeScriptNode, TypeScriptVisitableNode +class TypeScriptInterface implements TypeScriptNode, TypeScriptVisitableNode, TypeScriptForwardingExportableNode { /** * @param array $properties @@ -38,4 +38,9 @@ public function visitorProfile(): VisitorProfile ->iterable('properties') ->iterable('methods'); } + + public function getForwardedExportableNode(): TypeScriptExportableNode|TypeScriptForwardingExportableNode + { + return $this->name; + } } diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index 8109f18e..b1290179 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -14,7 +14,6 @@ class TypeScriptTransformer { public function __construct( protected TypeScriptTransformerConfig $config, - protected TypeScriptTransformerLog $log, protected DiscoverTypesAction $discoverTypesAction, protected ProvideTypesAction $appendDefaultTypesAction, protected ConnectReferencesAction $connectReferencesAction, @@ -24,7 +23,7 @@ public function __construct( } - public function execute(bool $watch = false): TypeScriptTransformerLog + public function execute(bool $watch = false): void { // Parallelize // - discovering types @@ -51,7 +50,5 @@ public function execute(bool $watch = false): TypeScriptTransformerLog $writtenFiles = $this->writeTypesAction->execute($transformedCollection, $referenceMap); $this->formatFilesAction->execute($writtenFiles); - - return $this->log; } } diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index feefbe52..de664541 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -6,10 +6,12 @@ use Spatie\TypeScriptTransformer\Actions\SplitTransformedPerLocationAction; use Spatie\TypeScriptTransformer\Collections\ReferenceMap; use Spatie\TypeScriptTransformer\References\Reference; +use Spatie\TypeScriptTransformer\Support\ImportName; use Spatie\TypeScriptTransformer\Support\Location; use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\WritingContext; use Spatie\TypeScriptTransformer\Support\WrittenFile; +use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptImport; class ModuleWriter implements Writer @@ -56,7 +58,7 @@ protected function writeLocation( $output = ''; $writingContext = new WritingContext(function (Reference $reference) use ($referenceMap) { - return $referenceMap->get($reference)->name; + return $referenceMap->get($reference)->getName(); }); foreach ($imports as $import) { @@ -66,7 +68,7 @@ protected function writeLocation( $output .= PHP_EOL; foreach ($location->transformed as $transformedItem) { - $output .= $transformedItem->typeScriptNode->write($writingContext); + $output .= $transformedItem->prepareForWrite()->write($writingContext); } if (is_dir($path) === false) { @@ -85,9 +87,17 @@ protected function resolveImports( Location $location, ReferenceMap $referenceMap, ): array { - /** @var array, names: array}> $imports */ + /** @var array, names: array}> $imports */ $imports = []; + // TODO: right now, we import directly the name + // Take a look at the LengthAwarePaginator interface, it imports LengthAwarePaginator which does not work + // + + $usedNamesInScope = array_values( + array_map(fn (Transformed $transformed) => $transformed->getName(), $location->transformed) + ); + foreach ($location->transformed as $transformedItem) { foreach ($transformedItem->references as $reference) { $transformedReference = $referenceMap->get($reference); @@ -99,12 +109,24 @@ protected function resolveImports( ]; } - $imports[implode($transformedReference->location)]['names'][] = $transformedReference->name; + $resolveImportedName = $this->resolveImportedName($usedNamesInScope, $transformedReference->getName()); + + $usedNamesInScope[] = $resolveImportedName; + + $imports[implode($transformedReference->location)]['names'][] = ImportName::fromNameAndImportedName( + $transformedReference->getName(), + $resolveImportedName, + ); + + // TODO: the reference should now point to the alias if used + // This is not possible at the moment because we don't have any idea about the node + // Ideally the references list is actually a list with pointers to the nodes } } return array_filter(array_map(function (array $import) use ($location) { - $names = array_values(array_unique($import['names'])); + $names = array_values($import['names']); + $path = $this->resolveRelativePathAction->execute( $location->segments, $import['location'], @@ -121,4 +143,25 @@ protected function resolveImports( ); }, $imports)); } + + protected function resolveImportedName( + array $usedNamesInScope, + string $name, + ): string { + if (! in_array($name, $usedNamesInScope)) { + return $name; + } + + if (! in_array("{$name}Alt", $usedNamesInScope)) { + return "{$name}Alt"; + } + + $counter = 2; + + while (in_array("{$name}Alt{$counter}", $usedNamesInScope)) { + $counter++; + } + + return "{$name}Alt{$counter}"; + } } diff --git a/src/Writers/NamespaceWriter.php b/src/Writers/NamespaceWriter.php index 1339c896..55100436 100644 --- a/src/Writers/NamespaceWriter.php +++ b/src/Writers/NamespaceWriter.php @@ -21,8 +21,10 @@ public function __construct( $this->splitTransformedPerLocationAction = new SplitTransformedPerLocationAction(); } - public function output(TransformedCollection $collection, ReferenceMap $referenceMap): array - { + public function output( + TransformedCollection $collection, + ReferenceMap $referenceMap + ): array { $split = $this->splitTransformedPerLocationAction->execute( $collection ); @@ -33,10 +35,10 @@ public function output(TransformedCollection $collection, ReferenceMap $referenc $transformable = $referenceMap->get($reference); if (empty($transformable->location)) { - return $transformable->name; + return $transformable->getName(); } - return implode('.', $transformable->location).'.'.$transformable->name; + return implode('.', $transformable->location).'.'.$transformable->getName(); }); foreach ($split as $splitConstruct) { @@ -51,7 +53,7 @@ public function output(TransformedCollection $collection, ReferenceMap $referenc $namespace = new TypeScriptNamespace( $splitConstruct->segments, array_map( - fn (Transformed $transformable) => $transformable->typeScriptNode, + fn (Transformed $transformable) => $transformable->prepareForWrite(), $splitConstruct->transformed, ), ); diff --git a/src/Writers/Writer.php b/src/Writers/Writer.php index 523b41b4..503a925d 100644 --- a/src/Writers/Writer.php +++ b/src/Writers/Writer.php @@ -4,6 +4,7 @@ use Spatie\TypeScriptTransformer\Collections\ReferenceMap; use Spatie\TypeScriptTransformer\Support\TransformedCollection; +use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Support\WrittenFile; interface Writer From 4bfb40e606f40bae3e9c2fd3d5fb726168294e1e Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 8 Aug 2023 14:34:15 +0200 Subject: [PATCH 22/51] Switch . in routes --- ...LaravelRoutControllerCollectionsAction.php | 10 +++---- ...velRoutControllerCollectionsActionTest.php | 30 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php b/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php index 0cdd8c12..c2a525b5 100644 --- a/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php +++ b/src/Laravel/Actions/ResolveLaravelRoutControllerCollectionsAction.php @@ -58,9 +58,9 @@ public function execute( $controllerClass = Str::of($controllerClass)->trim('\\'); - if ($defaultNamespace) { - $controllerClass = $this->replaceDefaultNamespace($controllerClass, $defaultNamespace); - } + $controllerClass = $defaultNamespace + ? $this->replaceDefaultNamespace($controllerClass, $defaultNamespace) + : $controllerClass->prepend('.'); $controllerClass = (string) $controllerClass->replace('\\', '.'); @@ -97,10 +97,10 @@ protected function replaceDefaultNamespace( $defaultNamespace = Str::of($defaultNamespace)->trim('\\'); if (! $controllerClass->contains($defaultNamespace)) { - return $controllerClass; + return $controllerClass->prepend('.'); } - return $controllerClass->replace($defaultNamespace, '')->trim('\\')->prepend('.'); + return $controllerClass->replace($defaultNamespace, '')->trim('\\'); } protected function resolveRouteParameters( diff --git a/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php b/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php index a50e5698..9ab981a1 100644 --- a/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php +++ b/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php @@ -38,7 +38,7 @@ function (RouteCollection $routes) { expect($routes->controllers)->toHaveCount(1); expect($routes->closures)->toBeEmpty(); - $actions = $routes->controllers['Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController']->actions; + $actions = $routes->controllers['.Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController']->actions; expect($actions)->toHaveCount(1); expect($actions['update'])->toBeInstanceOf(RouteControllerAction::class); @@ -55,7 +55,7 @@ function (RouteCollection $routes) { expect($routes->controllers)->toHaveCount(1); expect($routes->closures)->toBeEmpty(); - $controller = $routes->controllers['Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController']; + $controller = $routes->controllers['.Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController']; expect($controller)->toBeInstanceOf(RouteInvokableController::class); expect($controller->url)->toBe('invokable'); @@ -71,7 +71,7 @@ function (RouteCollection $routes) { expect($routes->controllers)->toHaveCount(1); expect($routes->closures)->toBeEmpty(); - $controller = $routes->controllers['Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController']; + $controller = $routes->controllers['.Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController']; expect($controller)->toBeInstanceOf(RouteController::class); expect($controller->actions)->toHaveCount(7); @@ -181,9 +181,9 @@ function (RouteCollection $routes) { expect($routes->closures['Closure(simple)']->name)->toBe('simple'); - expect($routes->controllers['Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController']->name)->toBe('invokable'); + expect($routes->controllers['.Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController']->name)->toBe('invokable'); - $resourceController = $routes->controllers['Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController']; + $resourceController = $routes->controllers['.Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController']; expect($resourceController->actions['index']->name)->toBe('resource.index'); expect($resourceController->actions['show']->name)->toBe('resource.show'); @@ -203,8 +203,8 @@ function (RouteCollection $routes) { $routes = app(ResolveLaravelRoutControllerCollectionsAction::class)->execute('Spatie\TypeScriptTransformer\Tests\Laravel\FakeClasses', true); expect($routes->controllers)->toHaveCount(2)->toHaveKeys([ - 'Symfony.Component.HttpKernel.Controller.ErrorController', - '.InvokableController', + '.Symfony.Component.HttpKernel.Controller.ErrorController', + 'InvokableController', ]); }); @@ -234,10 +234,10 @@ function (RouteCollection $routes) { expect($routes->controllers) ->toHaveCount(2) ->toHaveKeys([ - 'Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController', - 'Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController', + '.Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController', + '.Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController', ]); - expect($routes->controllers['Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController']->actions) + expect($routes->controllers['.Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController']->actions) ->toHaveCount(5) ->toHaveKeys([ 'show', @@ -259,14 +259,14 @@ function (RouteCollection $routes) { WithoutRoutes::controller(ResourceController::class), function (RouteCollection $routes) { expect($routes->closures)->toHaveCount(1); - expect($routes->controllers)->toHaveCount(1)->toHaveKey('Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController'); + expect($routes->controllers)->toHaveCount(1)->toHaveKey('.Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController'); }, ]; yield 'multiple controllers' => [ WithoutRoutes::controller(ResourceController::class), function (RouteCollection $routes) { expect($routes->closures)->toHaveCount(1); - expect($routes->controllers)->toHaveCount(1)->toHaveKey('Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController'); + expect($routes->controllers)->toHaveCount(1)->toHaveKey('.Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController'); }, ]; yield 'controller wildcard' => [ @@ -283,10 +283,10 @@ function (RouteCollection $routes) { expect($routes->controllers) ->toHaveCount(2) ->toHaveKeys([ - 'Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController', - 'Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController', + '.Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController', + '.Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.InvokableController', ]); - expect($routes->controllers['Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController']->actions) + expect($routes->controllers['.Spatie.TypeScriptTransformer.Tests.Laravel.FakeClasses.ResourceController']->actions) ->toHaveCount(5) ->toHaveKeys([ 'show', From 658940ec0a2af7d43e77fe797b1586c8b0b7e492 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 9 Aug 2023 14:40:47 +0200 Subject: [PATCH 23/51] wip --- composer.json | 3 +- src/Actions/ConnectReferencesAction.php | 1 - src/Actions/FormatFilesAction.php | 6 +- src/Actions/ResolveModuleImportsAction.php | 94 ++++++++ src/Actions/ResolveRelativePathAction.php | 49 ++--- src/Actions/WriteFilesAction.php | 34 +++ src/Actions/WriteTypesAction.php | 25 --- src/Collections/ImportsCollection.php | 67 ++++++ src/Collections/ReferenceMap.php | 11 + src/Laravel/LaravelTypesProvider.php | 2 +- src/Support/ImportLocation.php | 49 +++++ src/Support/ImportName.php | 11 +- .../{WrittenFile.php => WriteableFile.php} | 3 +- src/TypeScriptTransformer.php | 10 +- src/Writers/ModuleWriter.php | 131 +++-------- src/Writers/NamespaceWriter.php | 22 +- src/Writers/Writer.php | 4 +- .../Actions/ResolveRelativePathActionTest.php | 13 +- tests/Factories/TransformedFactory.php | 54 +++++ tests/Writers/ModuleWriterTest.php | 205 ++++++++++++++++++ 20 files changed, 590 insertions(+), 204 deletions(-) create mode 100644 src/Actions/ResolveModuleImportsAction.php create mode 100644 src/Actions/WriteFilesAction.php delete mode 100644 src/Actions/WriteTypesAction.php create mode 100644 src/Collections/ImportsCollection.php create mode 100644 src/Support/ImportLocation.php rename src/Support/{WrittenFile.php => WriteableFile.php} (71%) create mode 100644 tests/Factories/TransformedFactory.php create mode 100644 tests/Writers/ModuleWriterTest.php diff --git a/composer.json b/composer.json index 6abfa0fa..d0988ca9 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "phpstan/phpdoc-parser": "^1.13", "spatie/file-system-watcher": "^1.1", "spatie/laravel-package-tools": "^1.14.0", - "spatie/php-structure-discoverer": "^1.1" + "spatie/php-structure-discoverer": "^1.1", + "spatie/temporary-directory": "^2.1" }, "require-dev": { "laravel/pint": "^1.0", diff --git a/src/Actions/ConnectReferencesAction.php b/src/Actions/ConnectReferencesAction.php index 1f489388..097be241 100644 --- a/src/Actions/ConnectReferencesAction.php +++ b/src/Actions/ConnectReferencesAction.php @@ -13,7 +13,6 @@ class ConnectReferencesAction { public function __construct( - protected TypeScriptTransformerConfig $config, ) { } diff --git a/src/Actions/FormatFilesAction.php b/src/Actions/FormatFilesAction.php index 93ad693f..c941246b 100644 --- a/src/Actions/FormatFilesAction.php +++ b/src/Actions/FormatFilesAction.php @@ -3,7 +3,7 @@ namespace Spatie\TypeScriptTransformer\Actions; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; -use Spatie\TypeScriptTransformer\Support\WrittenFile; +use Spatie\TypeScriptTransformer\Support\WriteableFile; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class FormatFilesAction @@ -14,7 +14,7 @@ public function __construct( } /** - * @param array $writtenFiles + * @param array $writtenFiles */ public function execute(array $writtenFiles): void { @@ -23,7 +23,7 @@ public function execute(array $writtenFiles): void } $this->config->formatter->format( - array_map(fn (WrittenFile $writtenFile) => $writtenFile->path, $writtenFiles) + array_map(fn (WriteableFile $writtenFile) => $writtenFile->path, $writtenFiles) ); } } diff --git a/src/Actions/ResolveModuleImportsAction.php b/src/Actions/ResolveModuleImportsAction.php new file mode 100644 index 00000000..5e0dc9b1 --- /dev/null +++ b/src/Actions/ResolveModuleImportsAction.php @@ -0,0 +1,94 @@ +,string):string|null $alternativeNamesResolver + */ + public function __construct( + protected ResolveRelativePathAction $resolveRelativePathAction = new ResolveRelativePathAction(), + protected ?Closure $alternativeNamesResolver = null, + ) { + } + + public function execute( + Location $location, + ReferenceMap $referenceMap, + ): ImportsCollection { + $collection = new ImportsCollection(); + + $usedNamesInModule = array_values( + array_map(fn (Transformed $transformed) => $transformed->getName(), $location->transformed) + ); + + foreach ($location->transformed as $transformedItem) { + foreach ($transformedItem->references as $reference) { + $referencedTransformed = $referenceMap->get($reference); + + if ($referencedTransformed->location === $location->segments) { + continue; + } + + if ($collection->hasReferenceImported($referencedTransformed->reference)) { + continue; + } + + $name = $referencedTransformed->getName(); + + $resolveImportedName = $this->resolveImportedName($usedNamesInModule, $name); + + $usedNamesInModule[] = $resolveImportedName; + + $importName = new ImportName( + $name, + $referencedTransformed->reference, + $name === $resolveImportedName ? null : $resolveImportedName, + ); + + $relativePath = $this->resolveRelativePathAction->execute( + $location->segments, + $referencedTransformed->location, + ); + + $collection->add($relativePath, $importName); + } + } + + return $collection; + } + + protected function resolveImportedName( + array $usedNamesInScope, + string $name, + ): string { + if ($this->alternativeNamesResolver) { + return ($this->alternativeNamesResolver)($usedNamesInScope, $name); + } + + if (! in_array($name, $usedNamesInScope)) { + return $name; + } + + if (! in_array("{$name}Import", $usedNamesInScope)) { + return "{$name}Alt"; + } + + $counter = 2; + + while (in_array("{$name}Import{$counter}", $usedNamesInScope)) { + $counter++; + } + + return "{$name}Import{$counter}"; + } +} diff --git a/src/Actions/ResolveRelativePathAction.php b/src/Actions/ResolveRelativePathAction.php index 222e3976..50647f8c 100644 --- a/src/Actions/ResolveRelativePathAction.php +++ b/src/Actions/ResolveRelativePathAction.php @@ -4,43 +4,32 @@ class ResolveRelativePathAction { - public function execute( - array $currentNamespaceSegments, - array $requestedNamespaceSegments, - ): ?string { - $currentNamespaceSegments = array_values($currentNamespaceSegments); - $requestedNamespaceSegments = array_values($requestedNamespaceSegments); - - $maxIndex = max( - count($currentNamespaceSegments), - count($requestedNamespaceSegments) - ); - - for ($i = 0; $i < $maxIndex; $i++) { - if ( - array_key_exists($i, $currentNamespaceSegments) - && array_key_exists($i, $requestedNamespaceSegments) - && $currentNamespaceSegments[$i] === $requestedNamespaceSegments[$i]) { - unset($currentNamespaceSegments[$i]); - unset($requestedNamespaceSegments[$i]); - } + public function execute(array $from, array $to): ?string + { + if ($from === $to) { + return null; } - $currentNamespaceSegments = array_values($currentNamespaceSegments); - $requestedNamespaceSegments = array_values($requestedNamespaceSegments); + $commonDepth = 0; + $maxDepth = min(count($from), count($to)); - if (empty($currentNamespaceSegments) && empty($requestedNamespaceSegments)) { - return null; + for ($i = 0; $i < $maxDepth; $i++) { + if ($from[$i] !== $to[$i]) { + break; + } + $commonDepth++; } - $segments = ['.']; + $relativeSegments = []; - foreach ($currentNamespaceSegments as $i) { - $segments[] = '..'; + for ($i = $commonDepth; $i < count($from); $i++) { + $relativeSegments[] = '..'; } - array_push($segments, ...$requestedNamespaceSegments); + for ($i = $commonDepth; $i < count($to); $i++) { + $relativeSegments[] = $to[$i]; + } - return implode('/', $segments); - } + return implode('/', $relativeSegments); + } } diff --git a/src/Actions/WriteFilesAction.php b/src/Actions/WriteFilesAction.php new file mode 100644 index 00000000..1471e1f6 --- /dev/null +++ b/src/Actions/WriteFilesAction.php @@ -0,0 +1,34 @@ + $writeableFiles */ + public function execute( + array $writeableFiles + ): void { + foreach ($writeableFiles as $writeableFile) { + $this->writeFile($writeableFile); + } + } + + protected function writeFile(WriteableFile $file): void + { + $directory = dirname($file->path); + + if (is_dir($directory) === false) { + mkdir($directory, recursive: true); + } + + file_put_contents($file->path, $file->contents); + } +} diff --git a/src/Actions/WriteTypesAction.php b/src/Actions/WriteTypesAction.php deleted file mode 100644 index bb4fd1b5..00000000 --- a/src/Actions/WriteTypesAction.php +++ /dev/null @@ -1,25 +0,0 @@ - */ - public function execute( - TransformedCollection $collection, - ReferenceMap $referenceMap - ): array { - return $this->config->writer->output($collection, $referenceMap); - } -} diff --git a/src/Collections/ImportsCollection.php b/src/Collections/ImportsCollection.php new file mode 100644 index 00000000..210e1b2f --- /dev/null +++ b/src/Collections/ImportsCollection.php @@ -0,0 +1,67 @@ + $imports + */ + public function __construct( + protected array $imports = [], + ) { + } + + public function add(string $relativePath, ImportName $name): void + { + if (! array_key_exists($relativePath, $this->imports)) { + $this->imports[$relativePath] = new ImportLocation($relativePath); + } + + $this->imports[$relativePath]->addName($name); + } + + public function getAliasOrNameForReference(Reference $reference): ?string + { + foreach ($this->imports as $import) { + if ($aliasOrName = $import->getAliasOrNameForReference($reference)) { + return $aliasOrName; + } + } + + return null; + } + + public function hasReferenceImported(Reference $reference): bool + { + return $this->getAliasOrNameForReference($reference) !== null; + } + + public function isEmpty(): bool + { + return empty($this->imports); + } + + public function getIterator(): Traversable + { + return new \ArrayIterator($this->imports); + } + + /** + * @return array + */ + public function getTypeScriptNodes(): array + { + return array_map( + fn (ImportLocation $import) => $import->toTypeScriptNode(), + $this->imports, + ); + } +} diff --git a/src/Collections/ReferenceMap.php b/src/Collections/ReferenceMap.php index 40b899b7..19bcc036 100644 --- a/src/Collections/ReferenceMap.php +++ b/src/Collections/ReferenceMap.php @@ -8,8 +8,19 @@ class ReferenceMap { + /** @var array */ protected array $references = []; + /** + * @param array $references + */ + public function __construct(array $references = []) + { + foreach ($references as $reference) { + $this->add($reference); + } + } + public function add( Transformed $transformed ): void { diff --git a/src/Laravel/LaravelTypesProvider.php b/src/Laravel/LaravelTypesProvider.php index 35321f90..89bac86f 100644 --- a/src/Laravel/LaravelTypesProvider.php +++ b/src/Laravel/LaravelTypesProvider.php @@ -65,7 +65,7 @@ protected function eloquentCollection(): Transformed [new TypeScriptIdentifier('T')], ), new TypeScriptGeneric( - new TypeScriptIdentifier('Array'), + new TypeReference(new ClassStringReference(Collection::class)), [new TypeScriptIdentifier('T')], ), ), diff --git a/src/Support/ImportLocation.php b/src/Support/ImportLocation.php new file mode 100644 index 00000000..f95c7124 --- /dev/null +++ b/src/Support/ImportLocation.php @@ -0,0 +1,49 @@ + $importNames + */ + public function __construct( + protected string $relativePath, + protected array $importNames = [], + ) { + } + + public function addName(ImportName $name): void + { + $this->importNames[] = $name; + } + + public function getAliasOrNameForReference(Reference $reference): ?string + { + foreach ($this->importNames as $importName) { + if ($importName->reference->getKey() === $reference->getKey()) { + return $importName->alias ?? $importName->name; + } + } + + return null; + } + + public function toTypeScriptNode(): ?TypeScriptImport + { + if ($this->relativePath === null) { + // current path + return null; + } + + $names = array_unique(array_map( + fn (ImportName $name) => (string) $name, + $this->importNames, + )); + + return new TypeScriptImport($this->relativePath, $names); + } +} diff --git a/src/Support/ImportName.php b/src/Support/ImportName.php index 1483081b..190b8e6c 100644 --- a/src/Support/ImportName.php +++ b/src/Support/ImportName.php @@ -2,10 +2,14 @@ namespace Spatie\TypeScriptTransformer\Support; +use Spatie\TypeScriptTransformer\References\Reference; +use Spatie\TypeScriptTransformer\Transformed\Transformed; + class ImportName { public function __construct( public string $name, + public Reference $reference, public ?string $alias = null, ) { } @@ -23,11 +27,4 @@ public function isAliased(): bool { return $this->alias !== null; } - - public static function fromNameAndImportedName( - string $name, - string $importedName, - ): self { - return new self($name, $name === $importedName ? null : $importedName); - } } diff --git a/src/Support/WrittenFile.php b/src/Support/WriteableFile.php similarity index 71% rename from src/Support/WrittenFile.php rename to src/Support/WriteableFile.php index 2e2397b5..ad5948b2 100644 --- a/src/Support/WrittenFile.php +++ b/src/Support/WriteableFile.php @@ -2,10 +2,11 @@ namespace Spatie\TypeScriptTransformer\Support; -class WrittenFile +class WriteableFile { public function __construct( public string $path, + public string $contents, ) { } } diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index b1290179..d2082ae2 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -6,7 +6,7 @@ use Spatie\TypeScriptTransformer\Actions\DiscoverTypesAction; use Spatie\TypeScriptTransformer\Actions\FormatFilesAction; use Spatie\TypeScriptTransformer\Actions\ProvideTypesAction; -use Spatie\TypeScriptTransformer\Actions\WriteTypesAction; +use Spatie\TypeScriptTransformer\Actions\WriteFilesAction; use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; @@ -17,7 +17,7 @@ public function __construct( protected DiscoverTypesAction $discoverTypesAction, protected ProvideTypesAction $appendDefaultTypesAction, protected ConnectReferencesAction $connectReferencesAction, - protected WriteTypesAction $writeTypesAction, + protected WriteFilesAction $writeFilesAction, protected FormatFilesAction $formatFilesAction, ) { @@ -47,8 +47,10 @@ public function execute(bool $watch = false): void $referenceMap = $this->connectReferencesAction->execute($transformedCollection); - $writtenFiles = $this->writeTypesAction->execute($transformedCollection, $referenceMap); + $writeableFiles = $this->config->writer->output($transformedCollection, $referenceMap); - $this->formatFilesAction->execute($writtenFiles); + $this->writeFilesAction->execute($writeableFiles); + + $this->formatFilesAction->execute($writeableFiles); } } diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index de664541..1db70b52 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -2,6 +2,7 @@ namespace Spatie\TypeScriptTransformer\Writers; +use Spatie\TypeScriptTransformer\Actions\ResolveModuleImportsAction; use Spatie\TypeScriptTransformer\Actions\ResolveRelativePathAction; use Spatie\TypeScriptTransformer\Actions\SplitTransformedPerLocationAction; use Spatie\TypeScriptTransformer\Collections\ReferenceMap; @@ -9,27 +10,18 @@ use Spatie\TypeScriptTransformer\Support\ImportName; use Spatie\TypeScriptTransformer\Support\Location; use Spatie\TypeScriptTransformer\Support\TransformedCollection; +use Spatie\TypeScriptTransformer\Support\WriteableFile; use Spatie\TypeScriptTransformer\Support\WritingContext; -use Spatie\TypeScriptTransformer\Support\WrittenFile; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptImport; class ModuleWriter implements Writer { - protected string $path; - - protected SplitTransformedPerLocationAction $transformedPerLocationAction; - - protected ResolveRelativePathAction $resolveRelativePathAction; - public function __construct( - string $path, - protected string $filename = 'types', - protected string $extension = 'ts', + protected string $path, + protected SplitTransformedPerLocationAction $transformedPerLocationAction = new SplitTransformedPerLocationAction(), + protected ResolveModuleImportsAction $resolveModuleImportsAction = new ResolveModuleImportsAction(), ) { - $this->path = rtrim($path, '/'); - $this->transformedPerLocationAction = new SplitTransformedPerLocationAction(); - $this->resolveRelativePathAction = new ResolveRelativePathAction(); } public function output(TransformedCollection $collection, ReferenceMap $referenceMap): array @@ -50,118 +42,47 @@ public function output(TransformedCollection $collection, ReferenceMap $referenc protected function writeLocation( Location $location, ReferenceMap $referenceMap, - ): WrittenFile { - $imports = $this->resolveImports($location, $referenceMap); - - $path = "{$this->path}/".implode('/', $location->segments).'/'; + ): WriteableFile { + $imports = $this->resolveModuleImportsAction->execute( + $location, + $referenceMap, + ); $output = ''; - $writingContext = new WritingContext(function (Reference $reference) use ($referenceMap) { + $writingContext = new WritingContext(function (Reference $reference) use ($referenceMap, $imports) { + if($name = $imports->getAliasOrNameForReference($reference)){ + return $name; + } + + // Type declared somewhere else in the module return $referenceMap->get($reference)->getName(); }); - foreach ($imports as $import) { + foreach ($imports->getTypeScriptNodes() as $import) { $output .= $import->write($writingContext); } - $output .= PHP_EOL; + if($imports->isEmpty() === false){ + $output .= PHP_EOL; + } foreach ($location->transformed as $transformedItem) { $output .= $transformedItem->prepareForWrite()->write($writingContext); } - if (is_dir($path) === false) { - mkdir($path, recursive: true); - } - - file_put_contents("{$path}/{$this->filename}.{$this->extension}", $output); - - return new WrittenFile($path); + return new WriteableFile("{$this->resolvePath($location)}/index.ts", $output); } - /** - * @return array - */ - protected function resolveImports( + protected function resolvePath( Location $location, - ReferenceMap $referenceMap, - ): array { - /** @var array, names: array}> $imports */ - $imports = []; - - // TODO: right now, we import directly the name - // Take a look at the LengthAwarePaginator interface, it imports LengthAwarePaginator which does not work - // - - $usedNamesInScope = array_values( - array_map(fn (Transformed $transformed) => $transformed->getName(), $location->transformed) - ); - - foreach ($location->transformed as $transformedItem) { - foreach ($transformedItem->references as $reference) { - $transformedReference = $referenceMap->get($reference); - - if (! array_key_exists(implode($transformedReference->location), $imports)) { - $imports[implode($transformedReference->location)] = [ - 'location' => $transformedReference->location, - 'names' => [], - ]; - } - - $resolveImportedName = $this->resolveImportedName($usedNamesInScope, $transformedReference->getName()); - - $usedNamesInScope[] = $resolveImportedName; - - $imports[implode($transformedReference->location)]['names'][] = ImportName::fromNameAndImportedName( - $transformedReference->getName(), - $resolveImportedName, - ); - - // TODO: the reference should now point to the alias if used - // This is not possible at the moment because we don't have any idea about the node - // Ideally the references list is actually a list with pointers to the nodes - } - } - - return array_filter(array_map(function (array $import) use ($location) { - $names = array_values($import['names']); - - $path = $this->resolveRelativePathAction->execute( - $location->segments, - $import['location'], - ); - - if ($path === null) { - // current path - return null; - } - - return new TypeScriptImport( - "{$path}/{$this->filename}", - $names - ); - }, $imports)); - } - - protected function resolveImportedName( - array $usedNamesInScope, - string $name, ): string { - if (! in_array($name, $usedNamesInScope)) { - return $name; - } - - if (! in_array("{$name}Alt", $usedNamesInScope)) { - return "{$name}Alt"; - } - - $counter = 2; + $basePath = rtrim($this->path, '/'); - while (in_array("{$name}Alt{$counter}", $usedNamesInScope)) { - $counter++; + if (count($location->segments) === 0) { + return $basePath; } - return "{$name}Alt{$counter}"; + return $basePath.'/'.implode('/', $location->segments); } } diff --git a/src/Writers/NamespaceWriter.php b/src/Writers/NamespaceWriter.php index 55100436..33e75664 100644 --- a/src/Writers/NamespaceWriter.php +++ b/src/Writers/NamespaceWriter.php @@ -6,8 +6,8 @@ use Spatie\TypeScriptTransformer\Collections\ReferenceMap; use Spatie\TypeScriptTransformer\References\Reference; use Spatie\TypeScriptTransformer\Support\TransformedCollection; +use Spatie\TypeScriptTransformer\Support\WriteableFile; use Spatie\TypeScriptTransformer\Support\WritingContext; -use Spatie\TypeScriptTransformer\Support\WrittenFile; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNamespace; @@ -61,24 +61,6 @@ public function output( $output .= $namespace->write($writingContext); } - $this->writeFile($output); - - return [ - new WrittenFile($this->filename), - ]; - } - - private function writeFile(string $output): void - { - $directory = dirname($this->filename); - - if (! file_exists($directory)) { - mkdir($directory, recursive: true); - } - - file_put_contents( - $this->filename, - $output, - ); + return [new WriteableFile($this->filename, $output)]; } } diff --git a/src/Writers/Writer.php b/src/Writers/Writer.php index 503a925d..f5369178 100644 --- a/src/Writers/Writer.php +++ b/src/Writers/Writer.php @@ -5,11 +5,11 @@ use Spatie\TypeScriptTransformer\Collections\ReferenceMap; use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; -use Spatie\TypeScriptTransformer\Support\WrittenFile; +use Spatie\TypeScriptTransformer\Support\WriteableFile; interface Writer { - /** @return array */ + /** @return array */ public function output( TransformedCollection $collection, ReferenceMap $referenceMap, diff --git a/tests/Actions/ResolveRelativePathActionTest.php b/tests/Actions/ResolveRelativePathActionTest.php index 0f4474cc..ce8554c2 100644 --- a/tests/Actions/ResolveRelativePathActionTest.php +++ b/tests/Actions/ResolveRelativePathActionTest.php @@ -22,22 +22,27 @@ [ ['a', 'b', 'c'], ['a', 'd', 'e'], - './../../d/e', + '../../d/e', ], [ ['a', 'b', 'c', 'd'], ['a', 'd', 'e'], - './../../../d/e', + '../../../d/e', ], [ ['a', 'b', 'c'], ['a', 'd', 'e', 'f'], - './../../d/e/f', + '../../d/e/f', ], [ ['a'], ['b'], - './../b', + '../b', ], + [ + ['a', 'b', 'c', 'd'], + ['a', 'b', 'e', 'd'], + '../../e/d' + ] ] ); diff --git a/tests/Factories/TransformedFactory.php b/tests/Factories/TransformedFactory.php new file mode 100644 index 00000000..231aafc0 --- /dev/null +++ b/tests/Factories/TransformedFactory.php @@ -0,0 +1,54 @@ +reference ?? new CustomReference('factory', Str::random(6)); + $location = $this->location ?? []; + $export = $this->export ?? true; + $references = $this->references ?? []; + + return new Transformed( + typeScriptNode: $this->typeScriptNode, + reference: $reference, + location: $location, + export: $export, + references: $references, + ); + } +} diff --git a/tests/Writers/ModuleWriterTest.php b/tests/Writers/ModuleWriterTest.php new file mode 100644 index 00000000..678b670d --- /dev/null +++ b/tests/Writers/ModuleWriterTest.php @@ -0,0 +1,205 @@ +path = '/some/path'; + + $this->writer = new ModuleWriter($this->path); +}); + +it('can write modules', function () { + $transformedCollection = new TransformedCollection([ + TransformedFactory::alias('RootType', new TypeScriptString())->build(), + TransformedFactory::alias('RootType2', new TypeScriptString())->build(), + TransformedFactory::alias('Level1Type', new TypeScriptString(), location: ['level1'])->build(), + TransformedFactory::alias('Level1Type2', new TypeScriptString(), location: ['level1'])->build(), + TransformedFactory::alias('Level2Type', new TypeScriptString(), location: ['level1', 'level2'])->build(), + ]); + + $files = $this->writer->output( + $transformedCollection, + new ReferenceMap(), + ); + + expect($files) + ->toHaveCount(3) + ->each->toBeInstanceOf(WriteableFile::class); + + expect($files[0]) + ->path->toBe($this->path.'/index.ts') + ->contents->toBe('export type RootType = string;'.PHP_EOL.'export type RootType2 = string;'.PHP_EOL); + + expect($files[1]) + ->path->toBe($this->path.'/level1/index.ts') + ->contents->toBe('export type Level1Type = string;'.PHP_EOL.'export type Level1Type2 = string;'.PHP_EOL); + + expect($files[2]) + ->path->toBe($this->path.'/level1/level2/index.ts') + ->contents->toBe('export type Level2Type = string;'.PHP_EOL); +}); + +it('can define paths in different ways', function () { + $rootTransformed = TransformedFactory::alias('Type', new TypeScriptString())->build(); + $nestedTransformed = TransformedFactory::alias('Type', new TypeScriptString(), location: ['nested'])->build(); + + $withEndWriter = new ModuleWriter('/some-path/'); + $withoutEndWriter = new ModuleWriter('/some-path'); + + $transformedCollection = new TransformedCollection([$rootTransformed, $nestedTransformed]); + $referenceMap = new ReferenceMap(); + + $withEndFiles = $withEndWriter->output($transformedCollection, $referenceMap); + $withoutEndFiles = $withoutEndWriter->output($transformedCollection, $referenceMap); + + expect($withEndFiles)->toEqual($withoutEndFiles); +}); + +it('can reference other types within the module', function () { + $reference = new CustomReference('test', 'A'); + + $transformedCollection = new TransformedCollection([ + TransformedFactory::alias('A', new TypeScriptString(), reference: $reference)->build(), + TransformedFactory::alias('B', new TypeReference($reference))->build(), + ]); + + $files = $this->writer->output( + $transformedCollection, + (new ConnectReferencesAction())->execute($transformedCollection), + ); + + expect($files) + ->toHaveCount(1) + ->each->toBeInstanceOf(WriteableFile::class); + + expect($files[0]) + ->path->toBe($this->path.'/index.ts') + ->contents->toBe('export type A = string;'.PHP_EOL.'export type B = A;'.PHP_EOL); +}); + +it('can reference other types within a nested module', function () { + $referenceA = new CustomReference('test', 'A'); + $referenceB = new CustomReference('test', 'B'); + + $transformedCollection = new TransformedCollection([ + TransformedFactory::alias('A', new TypeScriptString(), reference: $referenceA, location: ['nested'])->build(), + TransformedFactory::alias('B', new TypeScriptString(), reference: $referenceB, location: ['nested', 'subNested'])->build(), + TransformedFactory::alias('C', new TypeScriptObject([ + new TypeScriptProperty('a', new TypeReference($referenceA)), + new TypeScriptProperty('b', new TypeReference($referenceB)), + ]))->build(), + ]); + + $files = $this->writer->output( + $transformedCollection, + (new ConnectReferencesAction())->execute($transformedCollection), + ); + + expect($files) + ->toHaveCount(3) + ->each->toBeInstanceOf(WriteableFile::class); + + expect($files[0]) + ->path->toBe($this->path.'/index.ts') + ->contents->toBe(<<path->toBe($this->path.'/nested/index.ts') + ->contents->toBe('export type A = string;'.PHP_EOL); + + expect($files[2]) + ->path->toBe($this->path.'/nested/subNested/index.ts') + ->contents->toBe('export type B = string;'.PHP_EOL); +}); + +it('can combine imports from nested modules', function () { + $referenceA = new CustomReference('test', 'A'); + $referenceB = new CustomReference('test', 'B'); + + $transformedCollection = new TransformedCollection([ + TransformedFactory::alias('A', new TypeScriptString(), reference: $referenceA, location: ['nested'])->build(), + TransformedFactory::alias('B', new TypeScriptString(), reference: $referenceB, location: ['nested'])->build(), + TransformedFactory::alias('C', new TypeScriptObject([ + new TypeScriptProperty('a', new TypeReference($referenceA)), + new TypeScriptProperty('b', new TypeReference($referenceB)), + ]))->build(), + ]); + + $files = $this->writer->output( + $transformedCollection, + (new ConnectReferencesAction())->execute($transformedCollection), + ); + + expect($files) + ->toHaveCount(2) + ->each->toBeInstanceOf(WriteableFile::class); + + expect($files[0]) + ->path->toBe($this->path.'/index.ts') + ->contents->toBe(<<path->toBe($this->path.'/nested/index.ts') + ->contents->toBe('export type A = string;'.PHP_EOL.'export type B = string;'.PHP_EOL); +}); + +it('can import from root into a nested module', function () { + $reference = new CustomReference('test', 'A'); + + $transformedCollection = new TransformedCollection([ + TransformedFactory::alias('A', new TypeScriptString(), reference: $reference)->build(), + TransformedFactory::alias('B', new TypeReference($reference), location: ['nested'])->build(), + ]); + + $files = $this->writer->output( + $transformedCollection, + (new ConnectReferencesAction())->execute($transformedCollection), + ); + + expect($files) + ->toHaveCount(2) + ->each->toBeInstanceOf(WriteableFile::class); + + expect($files[0]) + ->path->toBe($this->path.'/index.ts') + ->contents->toBe('export type A = string;'.PHP_EOL); + + expect($files[1]) + ->path->toBe($this->path.'/nested/index.ts') + ->contents->toBe(<<<'TypeScript' +import { A } from './..'; + +export type B = A; + +TypeScript); +}); From 0440e11bd58d76acce8b5f86d662b382880dfcfc Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 9 Aug 2023 15:24:00 +0200 Subject: [PATCH 24/51] Better testing and importing --- src/Actions/ConnectReferencesAction.php | 6 +- src/Actions/ResolveModuleImportsAction.php | 7 +- src/Actions/ResolveRelativePathAction.php | 10 +- src/Collections/ImportsCollection.php | 10 +- src/Support/ImportLocation.php | 7 +- src/Support/Location.php | 17 +++ src/Transformed/Transformed.php | 3 +- src/Writers/ModuleWriter.php | 15 +-- .../ResolveModuleImportsActionTest.php | 120 ++++++++++++++++++ .../Actions/ResolveRelativePathActionTest.php | 5 + tests/Factories/TransformedFactory.php | 9 +- tests/Writers/ModuleWriterTest.php | 39 +++++- 12 files changed, 213 insertions(+), 35 deletions(-) create mode 100644 tests/Actions/ResolveModuleImportsActionTest.php diff --git a/src/Actions/ConnectReferencesAction.php b/src/Actions/ConnectReferencesAction.php index 097be241..07a35440 100644 --- a/src/Actions/ConnectReferencesAction.php +++ b/src/Actions/ConnectReferencesAction.php @@ -38,8 +38,10 @@ public function execute(TransformedCollection $collection): ReferenceMap return; } - $metadata['references'][] = $reference; - $typeReference->connect($referenceMap->get($reference)); + $transformed = $referenceMap->get($reference); + + $metadata['references'][] = $transformed; + $typeReference->connect($transformed); }, [TypeReference::class]); foreach ($collection as $transformed) { diff --git a/src/Actions/ResolveModuleImportsAction.php b/src/Actions/ResolveModuleImportsAction.php index 5e0dc9b1..0f2ff1ab 100644 --- a/src/Actions/ResolveModuleImportsAction.php +++ b/src/Actions/ResolveModuleImportsAction.php @@ -23,7 +23,6 @@ public function __construct( public function execute( Location $location, - ReferenceMap $referenceMap, ): ImportsCollection { $collection = new ImportsCollection(); @@ -32,9 +31,7 @@ public function execute( ); foreach ($location->transformed as $transformedItem) { - foreach ($transformedItem->references as $reference) { - $referencedTransformed = $referenceMap->get($reference); - + foreach ($transformedItem->references as $referencedTransformed) { if ($referencedTransformed->location === $location->segments) { continue; } @@ -80,7 +77,7 @@ protected function resolveImportedName( } if (! in_array("{$name}Import", $usedNamesInScope)) { - return "{$name}Alt"; + return "{$name}Import"; } $counter = 2; diff --git a/src/Actions/ResolveRelativePathAction.php b/src/Actions/ResolveRelativePathAction.php index 50647f8c..00aad8f2 100644 --- a/src/Actions/ResolveRelativePathAction.php +++ b/src/Actions/ResolveRelativePathAction.php @@ -26,10 +26,16 @@ public function execute(array $from, array $to): ?string $relativeSegments[] = '..'; } + $hasSuffixedSegments = false; + for ($i = $commonDepth; $i < count($to); $i++) { $relativeSegments[] = $to[$i]; + + $hasSuffixedSegments = true; } - return implode('/', $relativeSegments); - } + $relativePath = implode('/', $relativeSegments); + + return $hasSuffixedSegments ? $relativePath : $relativePath.'/'; + } } diff --git a/src/Collections/ImportsCollection.php b/src/Collections/ImportsCollection.php index 210e1b2f..a8ed8ff8 100644 --- a/src/Collections/ImportsCollection.php +++ b/src/Collections/ImportsCollection.php @@ -59,9 +59,15 @@ public function getIterator(): Traversable */ public function getTypeScriptNodes(): array { - return array_map( + return array_values(array_map( fn (ImportLocation $import) => $import->toTypeScriptNode(), $this->imports, - ); + )); + } + + /** @return array */ + public function toArray(): array + { + return array_values($this->imports); } } diff --git a/src/Support/ImportLocation.php b/src/Support/ImportLocation.php index f95c7124..d5008801 100644 --- a/src/Support/ImportLocation.php +++ b/src/Support/ImportLocation.php @@ -39,11 +39,6 @@ public function toTypeScriptNode(): ?TypeScriptImport return null; } - $names = array_unique(array_map( - fn (ImportName $name) => (string) $name, - $this->importNames, - )); - - return new TypeScriptImport($this->relativePath, $names); + return new TypeScriptImport($this->relativePath, $this->importNames); } } diff --git a/src/Support/Location.php b/src/Support/Location.php index 8ed839bf..e28626cd 100644 --- a/src/Support/Location.php +++ b/src/Support/Location.php @@ -2,6 +2,7 @@ namespace Spatie\TypeScriptTransformer\Support; +use Spatie\TypeScriptTransformer\References\Reference; use Spatie\TypeScriptTransformer\Transformed\Transformed; class Location @@ -15,4 +16,20 @@ public function __construct( public array $transformed, ) { } + + public function getTransformedByReference(Reference $reference): ?Transformed + { + foreach ($this->transformed as $transformed) { + if ($transformed->reference->getKey() === $reference->getKey()) { + return $transformed; + } + } + + return null; + } + + public function hasReference(Reference $reference): bool + { + return $this->getTransformedByReference($reference) !== null; + } } diff --git a/src/Transformed/Transformed.php b/src/Transformed/Transformed.php index 668b224e..ebe5753d 100644 --- a/src/Transformed/Transformed.php +++ b/src/Transformed/Transformed.php @@ -15,7 +15,7 @@ class Transformed /** * @param array $location - * @param array $references + * @param array $references */ public function __construct( public TypeScriptNode $typeScriptNode, @@ -26,7 +26,6 @@ public function __construct( ) { } - public function getName(): ?string { if (isset($this->name)) { diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index 1db70b52..335aba6a 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -3,17 +3,13 @@ namespace Spatie\TypeScriptTransformer\Writers; use Spatie\TypeScriptTransformer\Actions\ResolveModuleImportsAction; -use Spatie\TypeScriptTransformer\Actions\ResolveRelativePathAction; use Spatie\TypeScriptTransformer\Actions\SplitTransformedPerLocationAction; use Spatie\TypeScriptTransformer\Collections\ReferenceMap; use Spatie\TypeScriptTransformer\References\Reference; -use Spatie\TypeScriptTransformer\Support\ImportName; use Spatie\TypeScriptTransformer\Support\Location; use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\WriteableFile; use Spatie\TypeScriptTransformer\Support\WritingContext; -use Spatie\TypeScriptTransformer\Transformed\Transformed; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptImport; class ModuleWriter implements Writer { @@ -43,15 +39,12 @@ protected function writeLocation( Location $location, ReferenceMap $referenceMap, ): WriteableFile { - $imports = $this->resolveModuleImportsAction->execute( - $location, - $referenceMap, - ); + $imports = $this->resolveModuleImportsAction->execute($location); $output = ''; - $writingContext = new WritingContext(function (Reference $reference) use ($referenceMap, $imports) { - if($name = $imports->getAliasOrNameForReference($reference)){ + $writingContext = new WritingContext(function (Reference $reference) use ($location, $referenceMap, $imports) { + if ($name = $imports->getAliasOrNameForReference($reference)) { return $name; } @@ -63,7 +56,7 @@ protected function writeLocation( $output .= $import->write($writingContext); } - if($imports->isEmpty() === false){ + if ($imports->isEmpty() === false) { $output .= PHP_EOL; } diff --git a/tests/Actions/ResolveModuleImportsActionTest.php b/tests/Actions/ResolveModuleImportsActionTest.php new file mode 100644 index 00000000..823e71ea --- /dev/null +++ b/tests/Actions/ResolveModuleImportsActionTest.php @@ -0,0 +1,120 @@ +action = new ResolveModuleImportsAction(); +}); + +it('wont resolve imports when types are in the same module', function () { + $location = new Location([], [ + $reference = TransformedFactory::alias('A', new TypeScriptString())->build(), + TransformedFactory::alias('B', new TypeReference($reference->reference), references: [ + $reference, + ])->build(), + ]); + + expect($this->action->execute($location)->isEmpty())->toBe(true); +}); + +it('will import a type from another module', function () { + $nestedReference = TransformedFactory::alias('Nested', new TypeScriptString(), location: ['parent', 'level', 'nested'])->build(); + $parentReference = TransformedFactory::alias('Parent', new TypeScriptString(), location: ['parent'])->build(); + $deeperParent = TransformedFactory::alias('DeeperParent', new TypeScriptString(), location: ['parent', 'deeper'])->build(); + $rootReference = TransformedFactory::alias('Root', new TypeScriptString(), location: [])->build(); + + $location = new Location(['parent', 'level'], [ + TransformedFactory::alias('Type', new TypeScriptString(), references: [ + $nestedReference, + $parentReference, + $deeperParent, + $rootReference, + ])->build(), + ]); + + $imports = $this->action->execute($location); + + expect($imports->toArray()) + ->toHaveCount(4) + ->each->toBeInstanceOf(ImportLocation::class); + + expect($imports->getTypeScriptNodes())->toEqual([ + new TypeScriptImport('nested', [new ImportName('Nested', $nestedReference->reference)]), + new TypeScriptImport('../', [new ImportName('Parent', $parentReference->reference)]), + new TypeScriptImport('../deeper', [new ImportName('DeeperParent', $deeperParent->reference)]), + new TypeScriptImport('../../', [new ImportName('Root', $rootReference->reference)]), + ]); +}); + +it('wont import the same type twice', function () { + $nestedReference = TransformedFactory::alias('Nested', new TypeScriptString(), location: ['nested'])->build(); + + $location = new Location([], [ + TransformedFactory::alias('TypeA', new TypeScriptString(), references: [ + $nestedReference, + ])->build(), + TransformedFactory::alias('TypeB', new TypeScriptString(), references: [ + $nestedReference, + ])->build(), + ]); + + $imports = $this->action->execute($location); + + expect($imports->toArray()) + ->toHaveCount(1) + ->each->toBeInstanceOf(ImportLocation::class); + + expect($imports->getTypeScriptNodes())->toEqual([ + new TypeScriptImport('nested', [new ImportName('Nested', $nestedReference->reference)]), + ]); +}); + +it('will alias a reference if it is already in the module', function (){ + $nestedCollection = TransformedFactory::alias('Collection', new TypeScriptString(), location: ['nested'])->build(); + + $location = new Location([], [ + TransformedFactory::alias('Collection', new TypeScriptString(), references: [ + $nestedCollection, + ])->build(), + ]); + + $imports = $this->action->execute($location); + + expect($imports->toArray()) + ->toHaveCount(1) + ->each->toBeInstanceOf(ImportLocation::class); + + expect($imports->getTypeScriptNodes())->toEqual([ + new TypeScriptImport('nested', [new ImportName('Collection', $nestedCollection->reference, 'CollectionImport')]), + ]); +}); + +it('will alias a reference if it is already in the module and already aliased by another import', function (){ + $nestedCollection = TransformedFactory::alias('Collection', new TypeScriptString(), location: ['nested'])->build(); + $otherNestedCollection = TransformedFactory::alias('Collection', new TypeScriptString(), location: ['otherNested'])->build(); + + $location = new Location([], [ + TransformedFactory::alias('Collection', new TypeScriptString(), references: [ + $nestedCollection, + $otherNestedCollection, + ])->build(), + ]); + + $imports = $this->action->execute($location); + + expect($imports->toArray()) + ->toHaveCount(2) + ->each->toBeInstanceOf(ImportLocation::class); + + expect($imports->getTypeScriptNodes())->toEqual([ + new TypeScriptImport('nested', [new ImportName('Collection', $nestedCollection->reference, 'CollectionImport')]), + new TypeScriptImport('otherNested', [new ImportName('Collection', $otherNestedCollection->reference, 'CollectionImport2')]), + ]); +}); diff --git a/tests/Actions/ResolveRelativePathActionTest.php b/tests/Actions/ResolveRelativePathActionTest.php index ce8554c2..2dda2de7 100644 --- a/tests/Actions/ResolveRelativePathActionTest.php +++ b/tests/Actions/ResolveRelativePathActionTest.php @@ -43,6 +43,11 @@ ['a', 'b', 'c', 'd'], ['a', 'b', 'e', 'd'], '../../e/d' + ], + [ + ['a', 'b'], + ['a'], + '../' ] ] ); diff --git a/tests/Factories/TransformedFactory.php b/tests/Factories/TransformedFactory.php index 231aafc0..3b3776be 100644 --- a/tests/Factories/TransformedFactory.php +++ b/tests/Factories/TransformedFactory.php @@ -27,12 +27,19 @@ public static function alias( ?Reference $reference = null, ?array $location = null, bool $export = true, + ?array $references = null, ): TransformedFactory { + $reference = $reference ?? new CustomReference( + 'factory_alias', + ($location !== null ? implode('.', $location) : '').Str::slug($name) + ); + return new self( typeScriptNode: new TypeScriptAlias(new TypeScriptIdentifier($name), $typeScriptNode), - reference: $reference ?? new CustomReference('factory_alias', Str::slug($name)), + reference: $reference, location: $location, export: $export, + references: $references ); } diff --git a/tests/Writers/ModuleWriterTest.php b/tests/Writers/ModuleWriterTest.php index 678b670d..bed43c18 100644 --- a/tests/Writers/ModuleWriterTest.php +++ b/tests/Writers/ModuleWriterTest.php @@ -113,8 +113,8 @@ expect($files[0]) ->path->toBe($this->path.'/index.ts') ->contents->toBe(<<path->toBe($this->path.'/index.ts') ->contents->toBe(<<path->toBe($this->path.'/nested/index.ts') ->contents->toBe(<<<'TypeScript' -import { A } from './..'; +import { A } from '../'; export type B = A; TypeScript); }); + +it('can automatically alias imported types', function () { + $reference = new CustomReference('test', 'A'); + + $transformedCollection = new TransformedCollection([ + TransformedFactory::alias('A', new TypeScriptString(), reference: $reference)->build(), + TransformedFactory::alias('A', new TypeReference($reference), location: ['nested'])->build(), + ]); + + $files = $this->writer->output( + $transformedCollection, + (new ConnectReferencesAction())->execute($transformedCollection), + ); + + expect($files) + ->toHaveCount(2) + ->each->toBeInstanceOf(WriteableFile::class); + + expect($files[0]) + ->path->toBe($this->path.'/index.ts') + ->contents->toBe('export type A = string;'.PHP_EOL); + + expect($files[1]) + ->path->toBe($this->path.'/nested/index.ts') + ->contents->toBe(<<<'TypeScript' +import { A as AImport } from '../'; + +export type A = AImport; + +TypeScript); +}); From 6e379d2ef0faa577c515c44c30f8cc5bf911d502 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Wed, 9 Aug 2023 13:25:13 +0000 Subject: [PATCH 25/51] Fix styling --- src/Actions/ConnectReferencesAction.php | 1 - src/Actions/FormatFilesAction.php | 3 +-- src/Actions/ProvideTypesAction.php | 1 - src/Actions/ResolveModuleImportsAction.php | 4 +--- src/Actions/WriteFilesAction.php | 2 +- src/Collections/ImportsCollection.php | 2 +- src/Collections/ReferenceMap.php | 2 +- src/Laravel/LaravelNamedRouteTypesProvider.php | 1 - src/Laravel/LaravelRouteActionTypesProvider.php | 1 - src/Laravel/LaravelTypesProvider.php | 3 +-- src/Laravel/SpatieLaravelTypesProvider.php | 1 - src/Laravel/TypeScriptTransformerServiceProvider.php | 1 - src/Support/ImportLocation.php | 2 +- src/Support/ImportName.php | 1 - src/Transformed/Transformed.php | 4 ++-- src/TypeProviders/TransformerTypesProvider.php | 1 - src/TypeProviders/TypesProvider.php | 1 - src/TypeScript/TypeScriptImport.php | 3 +-- src/TypeScriptTransformer.php | 1 - src/Writers/ModuleWriter.php | 2 +- src/Writers/Writer.php | 1 - tests/Actions/ResolveModuleImportsActionTest.php | 4 ++-- tests/Actions/ResolveRelativePathActionTest.php | 6 +++--- tests/Factories/TransformedFactory.php | 6 +++--- tests/Writers/ModuleWriterTest.php | 5 ++--- 25 files changed, 21 insertions(+), 38 deletions(-) diff --git a/src/Actions/ConnectReferencesAction.php b/src/Actions/ConnectReferencesAction.php index 07a35440..3d98c405 100644 --- a/src/Actions/ConnectReferencesAction.php +++ b/src/Actions/ConnectReferencesAction.php @@ -7,7 +7,6 @@ use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; -use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; use Spatie\TypeScriptTransformer\Visitor\Visitor; class ConnectReferencesAction diff --git a/src/Actions/FormatFilesAction.php b/src/Actions/FormatFilesAction.php index c941246b..0fec01b4 100644 --- a/src/Actions/FormatFilesAction.php +++ b/src/Actions/FormatFilesAction.php @@ -2,7 +2,6 @@ namespace Spatie\TypeScriptTransformer\Actions; -use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Support\WriteableFile; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; @@ -14,7 +13,7 @@ public function __construct( } /** - * @param array $writtenFiles + * @param array $writtenFiles */ public function execute(array $writtenFiles): void { diff --git a/src/Actions/ProvideTypesAction.php b/src/Actions/ProvideTypesAction.php index 38b9f48a..10e2663e 100644 --- a/src/Actions/ProvideTypesAction.php +++ b/src/Actions/ProvideTypesAction.php @@ -3,7 +3,6 @@ namespace Spatie\TypeScriptTransformer\Actions; use Spatie\TypeScriptTransformer\Support\TransformedCollection; -use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; diff --git a/src/Actions/ResolveModuleImportsAction.php b/src/Actions/ResolveModuleImportsAction.php index 0f2ff1ab..d8bc3b38 100644 --- a/src/Actions/ResolveModuleImportsAction.php +++ b/src/Actions/ResolveModuleImportsAction.php @@ -4,7 +4,6 @@ use Closure; use Spatie\TypeScriptTransformer\Collections\ImportsCollection; -use Spatie\TypeScriptTransformer\Collections\ReferenceMap; use Spatie\TypeScriptTransformer\Support\ImportName; use Spatie\TypeScriptTransformer\Support\Location; use Spatie\TypeScriptTransformer\Transformed\Transformed; @@ -12,8 +11,7 @@ class ResolveModuleImportsAction { /** - * @param ResolveRelativePathAction $resolveRelativePathAction - * @param Closure(array,string):string|null $alternativeNamesResolver + * @param Closure(array,string):string|null $alternativeNamesResolver */ public function __construct( protected ResolveRelativePathAction $resolveRelativePathAction = new ResolveRelativePathAction(), diff --git a/src/Actions/WriteFilesAction.php b/src/Actions/WriteFilesAction.php index 1471e1f6..4bc6293b 100644 --- a/src/Actions/WriteFilesAction.php +++ b/src/Actions/WriteFilesAction.php @@ -12,7 +12,7 @@ public function __construct( ) { } - /** @param array $writeableFiles */ + /** @param array $writeableFiles */ public function execute( array $writeableFiles ): void { diff --git a/src/Collections/ImportsCollection.php b/src/Collections/ImportsCollection.php index a8ed8ff8..b68f8261 100644 --- a/src/Collections/ImportsCollection.php +++ b/src/Collections/ImportsCollection.php @@ -12,7 +12,7 @@ class ImportsCollection implements IteratorAggregate { /** - * @param array $imports + * @param array $imports */ public function __construct( protected array $imports = [], diff --git a/src/Collections/ReferenceMap.php b/src/Collections/ReferenceMap.php index 19bcc036..768e659a 100644 --- a/src/Collections/ReferenceMap.php +++ b/src/Collections/ReferenceMap.php @@ -12,7 +12,7 @@ class ReferenceMap protected array $references = []; /** - * @param array $references + * @param array $references */ public function __construct(array $references = []) { diff --git a/src/Laravel/LaravelNamedRouteTypesProvider.php b/src/Laravel/LaravelNamedRouteTypesProvider.php index 258a0c89..d144bf57 100644 --- a/src/Laravel/LaravelNamedRouteTypesProvider.php +++ b/src/Laravel/LaravelNamedRouteTypesProvider.php @@ -13,7 +13,6 @@ use Spatie\TypeScriptTransformer\Laravel\Support\WithoutRoutes; use Spatie\TypeScriptTransformer\References\CustomReference; use Spatie\TypeScriptTransformer\Support\TransformedCollection; -use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; diff --git a/src/Laravel/LaravelRouteActionTypesProvider.php b/src/Laravel/LaravelRouteActionTypesProvider.php index 679c3fd9..bb87e292 100644 --- a/src/Laravel/LaravelRouteActionTypesProvider.php +++ b/src/Laravel/LaravelRouteActionTypesProvider.php @@ -12,7 +12,6 @@ use Spatie\TypeScriptTransformer\Laravel\Support\WithoutRoutes; use Spatie\TypeScriptTransformer\References\CustomReference; use Spatie\TypeScriptTransformer\Support\TransformedCollection; -use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; diff --git a/src/Laravel/LaravelTypesProvider.php b/src/Laravel/LaravelTypesProvider.php index 89bac86f..a8c30807 100644 --- a/src/Laravel/LaravelTypesProvider.php +++ b/src/Laravel/LaravelTypesProvider.php @@ -8,7 +8,6 @@ use Illuminate\Support\Collection; use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\Support\TransformedCollection; -use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; @@ -86,7 +85,7 @@ protected function lengthAwarePaginator(): Transformed new TypeScriptProperty('data', new TypeScriptGeneric( new TypeScriptIdentifier('Array'), [new TypeScriptIdentifier('T')], - ),), + ), ), new TypeScriptProperty('links', new TypeScriptObject([ new TypeScriptProperty('url', new TypeScriptUnion([ new TypeScriptIdentifier('string'), diff --git a/src/Laravel/SpatieLaravelTypesProvider.php b/src/Laravel/SpatieLaravelTypesProvider.php index 4bf6fa20..3d9c8bb0 100644 --- a/src/Laravel/SpatieLaravelTypesProvider.php +++ b/src/Laravel/SpatieLaravelTypesProvider.php @@ -4,7 +4,6 @@ use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\Support\TransformedCollection; -use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; diff --git a/src/Laravel/TypeScriptTransformerServiceProvider.php b/src/Laravel/TypeScriptTransformerServiceProvider.php index 63b92b8b..9c100386 100644 --- a/src/Laravel/TypeScriptTransformerServiceProvider.php +++ b/src/Laravel/TypeScriptTransformerServiceProvider.php @@ -7,7 +7,6 @@ use Spatie\TypeScriptTransformer\Laravel\Commands\InstallTypeScriptTransformerCommand; use Spatie\TypeScriptTransformer\Laravel\Commands\TransformTypeScriptCommand; use Spatie\TypeScriptTransformer\Laravel\Commands\WatchTypeScriptCommand; -use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; class TypeScriptTransformerServiceProvider extends PackageServiceProvider { diff --git a/src/Support/ImportLocation.php b/src/Support/ImportLocation.php index d5008801..6cc5d589 100644 --- a/src/Support/ImportLocation.php +++ b/src/Support/ImportLocation.php @@ -8,7 +8,7 @@ class ImportLocation { /** - * @param array $importNames + * @param array $importNames */ public function __construct( protected string $relativePath, diff --git a/src/Support/ImportName.php b/src/Support/ImportName.php index 190b8e6c..5cd9a6a9 100644 --- a/src/Support/ImportName.php +++ b/src/Support/ImportName.php @@ -3,7 +3,6 @@ namespace Spatie\TypeScriptTransformer\Support; use Spatie\TypeScriptTransformer\References\Reference; -use Spatie\TypeScriptTransformer\Transformed\Transformed; class ImportName { diff --git a/src/Transformed/Transformed.php b/src/Transformed/Transformed.php index ebe5753d..3408648b 100644 --- a/src/Transformed/Transformed.php +++ b/src/Transformed/Transformed.php @@ -14,8 +14,8 @@ class Transformed protected ?string $name; /** - * @param array $location - * @param array $references + * @param array $location + * @param array $references */ public function __construct( public TypeScriptNode $typeScriptNode, diff --git a/src/TypeProviders/TransformerTypesProvider.php b/src/TypeProviders/TransformerTypesProvider.php index 97de8517..5e040613 100644 --- a/src/TypeProviders/TransformerTypesProvider.php +++ b/src/TypeProviders/TransformerTypesProvider.php @@ -7,7 +7,6 @@ use Spatie\TypeScriptTransformer\Actions\DiscoverTypesAction; use Spatie\TypeScriptTransformer\Support\TransformationContext; use Spatie\TypeScriptTransformer\Support\TransformedCollection; -use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\Transformers\Transformer; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; diff --git a/src/TypeProviders/TypesProvider.php b/src/TypeProviders/TypesProvider.php index 1616273a..f4721c16 100644 --- a/src/TypeProviders/TypesProvider.php +++ b/src/TypeProviders/TypesProvider.php @@ -3,7 +3,6 @@ namespace Spatie\TypeScriptTransformer\TypeProviders; use Spatie\TypeScriptTransformer\Support\TransformedCollection; -use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; interface TypesProvider diff --git a/src/TypeScript/TypeScriptImport.php b/src/TypeScript/TypeScriptImport.php index 9622b6d2..ecd80dd2 100644 --- a/src/TypeScript/TypeScriptImport.php +++ b/src/TypeScript/TypeScriptImport.php @@ -8,8 +8,7 @@ class TypeScriptImport implements TypeScriptNode { /** - * @param string $path - * @param array $names + * @param array $names */ public function __construct( public string $path, diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index d2082ae2..63f2bb88 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -8,7 +8,6 @@ use Spatie\TypeScriptTransformer\Actions\ProvideTypesAction; use Spatie\TypeScriptTransformer\Actions\WriteFilesAction; use Spatie\TypeScriptTransformer\Support\TransformedCollection; -use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; class TypeScriptTransformer { diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index 335aba6a..ebb48c86 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -43,7 +43,7 @@ protected function writeLocation( $output = ''; - $writingContext = new WritingContext(function (Reference $reference) use ($location, $referenceMap, $imports) { + $writingContext = new WritingContext(function (Reference $reference) use ($referenceMap, $imports) { if ($name = $imports->getAliasOrNameForReference($reference)) { return $name; } diff --git a/src/Writers/Writer.php b/src/Writers/Writer.php index f5369178..5b02f67f 100644 --- a/src/Writers/Writer.php +++ b/src/Writers/Writer.php @@ -4,7 +4,6 @@ use Spatie\TypeScriptTransformer\Collections\ReferenceMap; use Spatie\TypeScriptTransformer\Support\TransformedCollection; -use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Support\WriteableFile; interface Writer diff --git a/tests/Actions/ResolveModuleImportsActionTest.php b/tests/Actions/ResolveModuleImportsActionTest.php index 823e71ea..64d1661b 100644 --- a/tests/Actions/ResolveModuleImportsActionTest.php +++ b/tests/Actions/ResolveModuleImportsActionTest.php @@ -76,7 +76,7 @@ ]); }); -it('will alias a reference if it is already in the module', function (){ +it('will alias a reference if it is already in the module', function () { $nestedCollection = TransformedFactory::alias('Collection', new TypeScriptString(), location: ['nested'])->build(); $location = new Location([], [ @@ -96,7 +96,7 @@ ]); }); -it('will alias a reference if it is already in the module and already aliased by another import', function (){ +it('will alias a reference if it is already in the module and already aliased by another import', function () { $nestedCollection = TransformedFactory::alias('Collection', new TypeScriptString(), location: ['nested'])->build(); $otherNestedCollection = TransformedFactory::alias('Collection', new TypeScriptString(), location: ['otherNested'])->build(); diff --git a/tests/Actions/ResolveRelativePathActionTest.php b/tests/Actions/ResolveRelativePathActionTest.php index 2dda2de7..0cd5464c 100644 --- a/tests/Actions/ResolveRelativePathActionTest.php +++ b/tests/Actions/ResolveRelativePathActionTest.php @@ -42,12 +42,12 @@ [ ['a', 'b', 'c', 'd'], ['a', 'b', 'e', 'd'], - '../../e/d' + '../../e/d', ], [ ['a', 'b'], ['a'], - '../' - ] + '../', + ], ] ); diff --git a/tests/Factories/TransformedFactory.php b/tests/Factories/TransformedFactory.php index 3b3776be..b18f4b7b 100644 --- a/tests/Factories/TransformedFactory.php +++ b/tests/Factories/TransformedFactory.php @@ -24,10 +24,10 @@ public function __construct( public static function alias( string $name, TypeScriptNode $typeScriptNode, - ?Reference $reference = null, - ?array $location = null, + Reference $reference = null, + array $location = null, bool $export = true, - ?array $references = null, + array $references = null, ): TransformedFactory { $reference = $reference ?? new CustomReference( 'factory_alias', diff --git a/tests/Writers/ModuleWriterTest.php b/tests/Writers/ModuleWriterTest.php index bed43c18..08954e09 100644 --- a/tests/Writers/ModuleWriterTest.php +++ b/tests/Writers/ModuleWriterTest.php @@ -1,6 +1,5 @@ path->toBe($this->path.'/index.ts') - ->contents->toBe(<<contents->toBe(<<<'TypeScript' import { A } from 'nested'; import { B } from 'nested/subNested'; @@ -157,7 +156,7 @@ expect($files[0]) ->path->toBe($this->path.'/index.ts') - ->contents->toBe(<<contents->toBe(<<<'TypeScript' import { A, B } from 'nested'; export type C = { From cbc39abbe08454df0225b83dc1bb50cf9a20bad2 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 1 Dec 2023 15:57:21 +0100 Subject: [PATCH 26/51] wip --- src/Transformers/ClassTransformer.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index f6e48d74..bf41c61f 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -54,6 +54,8 @@ abstract protected function shouldTransform(ReflectionClass $reflection): bool; /** @return array */ protected function classPropertyProcessors(): array { + // Call this once per class we're transforming for some performance reasons + return []; } From 3345c39b54a7c6c0e579d33d195fcb9964da3664 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Fri, 1 Dec 2023 14:57:50 +0000 Subject: [PATCH 27/51] Fix styling --- src/TypeScript/TypeReference.php | 2 +- src/TypeScript/TypeScriptAlias.php | 2 +- src/TypeScript/TypeScriptEnum.php | 2 +- src/TypeScript/TypeScriptFunctionDefinition.php | 2 +- src/TypeScript/TypeScriptGeneric.php | 2 +- src/TypeScript/TypeScriptIdentifier.php | 2 +- src/TypeScript/TypeScriptInterface.php | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/TypeScript/TypeReference.php b/src/TypeScript/TypeReference.php index 5bd735bc..244eda83 100644 --- a/src/TypeScript/TypeReference.php +++ b/src/TypeScript/TypeReference.php @@ -6,7 +6,7 @@ use Spatie\TypeScriptTransformer\Support\WritingContext; use Spatie\TypeScriptTransformer\Transformed\Transformed; -class TypeReference implements TypeScriptNode, TypeScriptExportableNode +class TypeReference implements TypeScriptExportableNode, TypeScriptNode { public function __construct( public Reference $reference, diff --git a/src/TypeScript/TypeScriptAlias.php b/src/TypeScript/TypeScriptAlias.php index 43de622f..0c8663fc 100644 --- a/src/TypeScript/TypeScriptAlias.php +++ b/src/TypeScript/TypeScriptAlias.php @@ -5,7 +5,7 @@ use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptAlias implements TypeScriptNode, TypeScriptVisitableNode, TypeScriptForwardingExportableNode +class TypeScriptAlias implements TypeScriptForwardingExportableNode, TypeScriptNode, TypeScriptVisitableNode { public function __construct( public TypeScriptIdentifier|TypeScriptGeneric $identifier, diff --git a/src/TypeScript/TypeScriptEnum.php b/src/TypeScript/TypeScriptEnum.php index 9d5bcd43..8dd8a2b0 100644 --- a/src/TypeScript/TypeScriptEnum.php +++ b/src/TypeScript/TypeScriptEnum.php @@ -4,7 +4,7 @@ use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptEnum implements TypeScriptNode, TypeScriptExportableNode +class TypeScriptEnum implements TypeScriptExportableNode, TypeScriptNode { public function __construct( public string $name, diff --git a/src/TypeScript/TypeScriptFunctionDefinition.php b/src/TypeScript/TypeScriptFunctionDefinition.php index 31a1f36f..39981f0e 100644 --- a/src/TypeScript/TypeScriptFunctionDefinition.php +++ b/src/TypeScript/TypeScriptFunctionDefinition.php @@ -5,7 +5,7 @@ use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptFunctionDefinition implements TypeScriptNode, TypeScriptVisitableNode, TypeScriptForwardingExportableNode +class TypeScriptFunctionDefinition implements TypeScriptForwardingExportableNode, TypeScriptNode, TypeScriptVisitableNode { public function __construct( public TypeScriptGeneric|TypeScriptIdentifier $identifier, diff --git a/src/TypeScript/TypeScriptGeneric.php b/src/TypeScript/TypeScriptGeneric.php index 7f2387d4..cc26984a 100644 --- a/src/TypeScript/TypeScriptGeneric.php +++ b/src/TypeScript/TypeScriptGeneric.php @@ -5,7 +5,7 @@ use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptGeneric implements TypeScriptNode, TypeScriptVisitableNode, TypeScriptForwardingExportableNode +class TypeScriptGeneric implements TypeScriptForwardingExportableNode, TypeScriptNode, TypeScriptVisitableNode { /** * @param array $genericTypes diff --git a/src/TypeScript/TypeScriptIdentifier.php b/src/TypeScript/TypeScriptIdentifier.php index 66dac38e..a409a56d 100644 --- a/src/TypeScript/TypeScriptIdentifier.php +++ b/src/TypeScript/TypeScriptIdentifier.php @@ -4,7 +4,7 @@ use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptIdentifier implements TypeScriptNode, TypeScriptExportableNode +class TypeScriptIdentifier implements TypeScriptExportableNode, TypeScriptNode { public function __construct( public string $name, diff --git a/src/TypeScript/TypeScriptInterface.php b/src/TypeScript/TypeScriptInterface.php index d0661eb3..d1e165c3 100644 --- a/src/TypeScript/TypeScriptInterface.php +++ b/src/TypeScript/TypeScriptInterface.php @@ -5,7 +5,7 @@ use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptInterface implements TypeScriptNode, TypeScriptVisitableNode, TypeScriptForwardingExportableNode +class TypeScriptInterface implements TypeScriptForwardingExportableNode, TypeScriptNode, TypeScriptVisitableNode { /** * @param array $properties From c1fa741e71c5c70be8e1268ecd4520be1cb39d08 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 1 Mar 2024 14:55:52 +0100 Subject: [PATCH 28/51] Add Husky --- .husky/pre-commit | 1 + .php-cs-fixer.dist.php | 35 ++ composer.json | 1 + lint-staged.config.js | 3 + package-lock.json | 835 +++++++++++++++++++++++++++++++++++++++++ package.json | 5 + yarn.lock | 110 ------ 7 files changed, 880 insertions(+), 110 deletions(-) create mode 100644 .husky/pre-commit create mode 100644 .php-cs-fixer.dist.php create mode 100644 lint-staged.config.js create mode 100644 package-lock.json delete mode 100644 yarn.lock diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..37236231 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +yarn lint-staged diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 00000000..ea229dfc --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,35 @@ +in([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + ->name('*.php') + ->notName('*.blade.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PSR12' => true, + 'array_syntax' => ['syntax' => 'short'], + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'no_unused_imports' => true, + 'not_operator_with_successor_space' => true, + 'trailing_comma_in_multiline' => true, + 'phpdoc_scalar' => true, + 'unary_operator_spaces' => true, + 'binary_operator_spaces' => true, + 'blank_line_before_statement' => [ + 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], + ], + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_var_without_name' => true, + 'method_argument_space' => [ + 'on_multiline' => 'ensure_fully_multiline', + 'keep_multiple_spaces_after_comma' => true, + ], + 'single_trait_insert_per_statement' => true, + ]) + ->setFinder($finder); diff --git a/composer.json b/composer.json index d0988ca9..6336bb3f 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ }, "require-dev": { "laravel/pint": "^1.0", + "friendsofphp/php-cs-fixer": "^3.0", "nunomaduro/collision": "^7.9", "nunomaduro/larastan": "^2.0.1", "orchestra/testbench": "^8.0", diff --git a/lint-staged.config.js b/lint-staged.config.js new file mode 100644 index 00000000..33f947c8 --- /dev/null +++ b/lint-staged.config.js @@ -0,0 +1,3 @@ +module.exports = { + '**/*.php': ['php ./vendor/bin/php-cs-fixer fix --config .php-cs-fixer.dist.php --allow-risky=yes'], +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..9739ef63 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,835 @@ +{ + "name": "typescript-transformer", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "chokidar": "^3.5.3" + }, + "devDependencies": { + "husky": "^9.0.11", + "lint-staged": "^15.2.2", + "typescript": "^5.1.6" + } + }, + "node_modules/ansi-escapes": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", + "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", + "dev": true, + "dependencies": { + "type-fest": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/husky": { + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", + "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", + "dev": true, + "bin": { + "husky": "bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/lint-staged": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.2.tgz", + "integrity": "sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==", + "dev": true, + "dependencies": { + "chalk": "5.3.0", + "commander": "11.1.0", + "debug": "4.3.4", + "execa": "8.0.1", + "lilconfig": "3.0.0", + "listr2": "8.0.1", + "micromatch": "4.0.5", + "pidtree": "0.6.0", + "string-argv": "0.3.2", + "yaml": "2.3.4" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/listr2": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.0.1.tgz", + "integrity": "sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.0.0", + "rfdc": "^1.3.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/log-update": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz", + "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", + "dev": true, + "dependencies": { + "ansi-escapes": "^6.2.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^7.0.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.1.6", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/package.json b/package.json index d89dad0a..59149313 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,13 @@ { "devDependencies": { + "husky": "^9.0.11", + "lint-staged": "^15.2.2", "typescript": "^5.1.6" }, "dependencies": { "chokidar": "^3.5.3" + }, + "scripts": { + "prepare": "husky" } } diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index b28679fe..00000000 --- a/yarn.lock +++ /dev/null @@ -1,110 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -chokidar@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -typescript@^5.1.6: - version "5.1.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" - integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== From 841f6d2d4327a4217d9c62a9b7c129e313ddb87f Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Fri, 1 Mar 2024 13:56:17 +0000 Subject: [PATCH 29/51] Fix styling --- src/Transformers/ClassTransformer.php | 2 +- src/Visitor/Visitor.php | 4 ++-- tests/Factories/TransformedFactory.php | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index bf41c61f..3ef0a196 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -102,7 +102,7 @@ protected function getTypeScriptNode( protected function resolveTypeByAttribute( ReflectionClass $reflectionClass, - ReflectionProperty $property = null, + ?ReflectionProperty $property = null, ): ?TypeScriptNode { $subject = $property ?? $reflectionClass; diff --git a/src/Visitor/Visitor.php b/src/Visitor/Visitor.php index 7ea9db18..ad762497 100644 --- a/src/Visitor/Visitor.php +++ b/src/Visitor/Visitor.php @@ -26,7 +26,7 @@ public function __construct( public function before( Closure $closure, - array $allowedNodes = null, + ?array $allowedNodes = null, ): self { $this->beforeClosures[] = new VisitorClosure($closure, $allowedNodes); @@ -35,7 +35,7 @@ public function before( public function after( Closure $closure, - array $allowedNodes = null, + ?array $allowedNodes = null, ): self { $this->afterClosures[] = new VisitorClosure($closure, $allowedNodes); diff --git a/tests/Factories/TransformedFactory.php b/tests/Factories/TransformedFactory.php index b18f4b7b..3b3776be 100644 --- a/tests/Factories/TransformedFactory.php +++ b/tests/Factories/TransformedFactory.php @@ -24,10 +24,10 @@ public function __construct( public static function alias( string $name, TypeScriptNode $typeScriptNode, - Reference $reference = null, - array $location = null, + ?Reference $reference = null, + ?array $location = null, bool $export = true, - array $references = null, + ?array $references = null, ): TransformedFactory { $reference = $reference ?? new CustomReference( 'factory_alias', From 60a0ed3771cbb9e2affba48a8b7ea8971b26e858 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 1 Mar 2024 14:56:33 +0100 Subject: [PATCH 30/51] Use CS fixer --- .../workflows/fix-php-code-style-issues.yml | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml index 2f32b5f5..96e9a2d0 100644 --- a/.github/workflows/fix-php-code-style-issues.yml +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -1,27 +1,23 @@ -name: Fix PHP code style issues +name: Check & fix styling -on: - push: - paths: - - '**.php' - -permissions: - contents: write +on: [push] jobs: - php-code-styling: + php-cs-fixer: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} - - name: Fix PHP code style issues - uses: aglipanci/laravel-pint-action@1.0.0 + - name: Run PHP CS Fixer + uses: docker://oskarstark/php-cs-fixer-ga + with: + args: --config=.php-cs-fixer.dist.php --allow-risky=yes - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v4 + uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: Fix styling From feab27a73d74241ae9a512289f18782ca2369fe5 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Fri, 1 Mar 2024 13:56:57 +0000 Subject: [PATCH 31/51] Fix styling --- src/Actions/ProvideTypesAction.php | 2 +- src/Laravel/LaravelNamedRouteTypesProvider.php | 12 ++++++++---- .../LaravelRouteActionTypesProvider.php | 18 ++++++++++++------ src/Laravel/LaravelTypesProvider.php | 12 ++++++++---- src/Laravel/SpatieLaravelTypesProvider.php | 3 ++- src/Transformers/ClassTransformer.php | 3 ++- src/Transformers/EnumTransformer.php | 3 ++- src/TypeScriptTransformerConfigBuilder.php | 8 ++++---- tests/Writers/ModuleWriterTest.php | 6 ++++-- 9 files changed, 43 insertions(+), 24 deletions(-) diff --git a/src/Actions/ProvideTypesAction.php b/src/Actions/ProvideTypesAction.php index 10e2663e..febaf95e 100644 --- a/src/Actions/ProvideTypesAction.php +++ b/src/Actions/ProvideTypesAction.php @@ -18,7 +18,7 @@ public function execute(TransformedCollection $collection): void foreach ($this->config->typeProviders as $defaultTypeProvider) { $defaultTypeProvider = $defaultTypeProvider instanceof TypesProvider ? $defaultTypeProvider - : new $defaultTypeProvider; + : new $defaultTypeProvider(); $defaultTypeProvider->provide( $this->config, diff --git a/src/Laravel/LaravelNamedRouteTypesProvider.php b/src/Laravel/LaravelNamedRouteTypesProvider.php index d144bf57..a962c594 100644 --- a/src/Laravel/LaravelNamedRouteTypesProvider.php +++ b/src/Laravel/LaravelNamedRouteTypesProvider.php @@ -60,7 +60,8 @@ public function provide(TypeScriptTransformerConfig $config, TransformedCollecti $this->parseRouteCollection($routeCollection), ), $routesListReference = new CustomReference('laravel_named_routes', 'routes_list'), - $this->location, true, + $this->location, + true, ); $jsonEncodedRoutes = $this->routeCollectionToJson($routeCollection); @@ -85,10 +86,12 @@ public function provide(TypeScriptTransformerConfig $config, TransformedCollecti new TypeScriptIdentifier('TRoute'), new TypeScriptIdentifier('"parameters"'), ]), - isOptional: true), + isOptional: true + ), ], new TypeScriptString(), - new TypeScriptRaw(<<location, true, + $this->location, + true, ); $types->add($transformedRoutes, $transformedRoute); diff --git a/src/Laravel/LaravelRouteActionTypesProvider.php b/src/Laravel/LaravelRouteActionTypesProvider.php index bb87e292..00cfea51 100644 --- a/src/Laravel/LaravelRouteActionTypesProvider.php +++ b/src/Laravel/LaravelRouteActionTypesProvider.php @@ -62,7 +62,8 @@ public function provide(TypeScriptTransformerConfig $config, TransformedCollecti $this->parseRouteCollection($routeCollection), ), $routesListReference = new CustomReference('laravel_route_actions', 'routes_list'), - $this->location, true, + $this->location, + true, ); $isInvokableControllerCondition = TypeScriptOperator::extends( @@ -94,7 +95,8 @@ public function provide(TypeScriptTransformerConfig $config, TransformedCollecti ) ), $actionControllerReference = new CustomReference('laravel_route_actions', 'action_controller'), - $this->location, true, + $this->location, + true, ); $actionParameters = new Transformed( @@ -121,7 +123,8 @@ public function provide(TypeScriptTransformerConfig $config, TransformedCollecti ) ), $actionParametersReference = new CustomReference('laravel_route_actions', 'action_parameters'), - $this->location, true, + $this->location, + true, ); $jsonEncodedRoutes = $this->routeCollectionToJson($routeCollection); @@ -171,7 +174,8 @@ public function provide(TypeScriptTransformerConfig $config, TransformedCollecti ), isOptional: true), ], new TypeScriptString(), - new TypeScriptRaw(<<location, true, + $this->location, + true, ); $types->add($transformedRoutes, $actionController, $actionParameters, $transformedAction); @@ -256,7 +261,8 @@ protected function parseRouteParameter(RouteParameter $parameter): TypeScriptNod protected function routeCollectionToJson(RouteCollection $collection): string { return collect($collection->controllers) - ->map(fn (RouteController|RouteInvokableController $controller) => $controller instanceof RouteInvokableController + ->map( + fn (RouteController|RouteInvokableController $controller) => $controller instanceof RouteInvokableController ? [ 'url' => $controller->url, 'methods' => array_values($controller->methods), diff --git a/src/Laravel/LaravelTypesProvider.php b/src/Laravel/LaravelTypesProvider.php index a8c30807..fc7fc3a3 100644 --- a/src/Laravel/LaravelTypesProvider.php +++ b/src/Laravel/LaravelTypesProvider.php @@ -51,7 +51,8 @@ protected function collection(): Transformed ), ), new ClassStringReference(Collection::class), - ['Illuminate', 'Support'], true, + ['Illuminate', 'Support'], + true, ); } @@ -69,7 +70,8 @@ protected function eloquentCollection(): Transformed ), ), new ClassStringReference(EloquentCollection::class), - ['Illuminate', 'Database', 'Eloquent', 'Collection'], true, + ['Illuminate', 'Database', 'Eloquent', 'Collection'], + true, ); } @@ -122,7 +124,8 @@ protected function lengthAwarePaginator(): Transformed ]), ), new ClassStringReference(LengthAwarePaginator::class), - ['Illuminate', 'Pagination'], true, + ['Illuminate', 'Pagination'], + true, ); } @@ -140,7 +143,8 @@ protected function lengthAwarePaginatorInterface(): Transformed ), ), new ClassStringReference(LengthAwarePaginatorInterface::class), - ['Illuminate', 'Contracts', 'Pagination'], true, + ['Illuminate', 'Contracts', 'Pagination'], + true, ); } } diff --git a/src/Laravel/SpatieLaravelTypesProvider.php b/src/Laravel/SpatieLaravelTypesProvider.php index 3d9c8bb0..6fe82492 100644 --- a/src/Laravel/SpatieLaravelTypesProvider.php +++ b/src/Laravel/SpatieLaravelTypesProvider.php @@ -45,7 +45,8 @@ public function provide(TypeScriptTransformerConfig $config, TransformedCollecti ) ), new ClassStringReference(\Spatie\LaravelOptions\Options::class), - ['Spatie', 'LaravelOptions'], true, + ['Spatie', 'LaravelOptions'], + true, ); $types->add($optionsType); diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index 3ef0a196..836cc778 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -45,7 +45,8 @@ public function transform(ReflectionClass $reflectionClass, TransformationContex $this->getTypeScriptNode($reflectionClass) ), new ReflectionClassReference($reflectionClass), - $context->nameSpaceSegments, true, + $context->nameSpaceSegments, + true, ); } diff --git a/src/Transformers/EnumTransformer.php b/src/Transformers/EnumTransformer.php index 98406926..b5def7b9 100644 --- a/src/Transformers/EnumTransformer.php +++ b/src/Transformers/EnumTransformer.php @@ -38,7 +38,8 @@ public function transform( ? $this->transformAsNativeEnum($context->name, $cases) : $this->transformAsUnion($context->name, $cases), new ReflectionClassReference($reflectionClass), - $context->nameSpaceSegments, true, + $context->nameSpaceSegments, + true, ); } diff --git a/src/TypeScriptTransformerConfigBuilder.php b/src/TypeScriptTransformerConfigBuilder.php index 8fd5ef78..92be28b4 100644 --- a/src/TypeScriptTransformerConfigBuilder.php +++ b/src/TypeScriptTransformerConfigBuilder.php @@ -64,13 +64,13 @@ public function get(): TypeScriptTransformerConfig $this->ensureConfigIsValid(); $typeProviders = array_map( - fn (TypesProvider|string $typeProvider) => is_string($typeProvider) ? new $typeProvider : $typeProvider, + fn (TypesProvider|string $typeProvider) => is_string($typeProvider) ? new $typeProvider() : $typeProvider, $this->typeProviders ); if (! empty($this->transformers)) { $transformers = array_map( - fn (Transformer|string $transformer) => is_string($transformer) ? new $transformer : $transformer, + fn (Transformer|string $transformer) => is_string($transformer) ? new $transformer() : $transformer, $this->transformers ); @@ -82,10 +82,10 @@ public function get(): TypeScriptTransformerConfig ); if (is_string($writer)) { - $writer = new $writer; + $writer = new $writer(); } - $formatter = is_string($this->formatter) ? new $this->formatter : $this->formatter; + $formatter = is_string($this->formatter) ? new $this->formatter() : $this->formatter; return new TypeScriptTransformerConfig( $typeProviders, diff --git a/tests/Writers/ModuleWriterTest.php b/tests/Writers/ModuleWriterTest.php index 08954e09..122c15a3 100644 --- a/tests/Writers/ModuleWriterTest.php +++ b/tests/Writers/ModuleWriterTest.php @@ -111,7 +111,8 @@ expect($files[0]) ->path->toBe($this->path.'/index.ts') - ->contents->toBe(<<<'TypeScript' + ->contents->toBe( + <<<'TypeScript' import { A } from 'nested'; import { B } from 'nested/subNested'; @@ -156,7 +157,8 @@ expect($files[0]) ->path->toBe($this->path.'/index.ts') - ->contents->toBe(<<<'TypeScript' + ->contents->toBe( + <<<'TypeScript' import { A, B } from 'nested'; export type C = { From 3f32d0c2ad8b4f3bdd493eb362db40c84faeb5f3 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 15 Mar 2024 15:57:23 +0100 Subject: [PATCH 32/51] Typescript v3 updates --- README.md | 460 +++++++++++++++++- composer.json | 2 +- src/Actions/ConnectReferencesAction.php | 1 + src/Actions/ProvideTypesAction.php | 6 +- src/Actions/ReplaceNodesAction.php | 47 ++ ...pilePhpStanTypeToTypeScriptNodeAction.php} | 9 +- ...eReflectionTypeToTypeScriptNodeAction.php} | 2 +- src/Attributes/Hidden.php | 10 + src/Attributes/TypeScriptType.php | 4 +- src/Collections/ImportsCollection.php | 2 +- src/{Support => Data}/ImportLocation.php | 3 +- ...CollectionOfInfoClassPropertyProcessor.php | 61 +++ ...moveDataLazyTypeClassPropertyProcessor.php | 16 +- ...ollectionByArrayClassPropertyProcessor.php | 8 +- .../Commands/TransformTypeScriptCommand.php | 14 +- .../Transformers/DataClassTransformer.php | 2 + ...tTransformerApplicationServiceProvider.php | 6 +- ...mer.php => AttributedClassTransformer.php} | 2 +- src/Transformers/ClassTransformer.php | 32 +- src/TypeScript/TypeReference.php | 10 + src/TypeScriptTransformer.php | 33 +- src/TypeScriptTransformerConfig.php | 3 + ...=> TypeScriptTransformerConfigFactory.php} | 32 +- .../TypeScriptTransformerServiceProvider.stub | 8 +- .../ResolveModuleImportsActionTest.php | 2 +- ...PhpStanTypeToTypeScriptNodeActionTest.php} | 18 +- ...lectionTypeToTypeScriptNodeActionTest.php} | 4 +- .../Attributes/LiteralTypeScriptTypeTest.php | 5 + tests/Attributes/TypeScriptTypeTest.php | 5 + tests/ExampleTest.php | 5 - ...ectionOfInfoClassPropertyProcessorTest.php | 3 + ...DataLazyTypeClassPropertyProcessorTest.php | 3 + ...ctionByArrayClassPropertyProcessorTest.php | 129 +++++ tests/Pest.php | 52 ++ tests/Stubs/PhpDocTypesStub.php | 3 + tests/Transformers/ClassTransformerTest.php | 323 ++++++++++++ 36 files changed, 1273 insertions(+), 52 deletions(-) create mode 100644 src/Actions/ReplaceNodesAction.php rename src/Actions/{TranspilePhpStanTypeToTypeScriptTypeAction.php => TranspilePhpStanTypeToTypeScriptNodeAction.php} (97%) rename src/Actions/{TranspileReflectionTypeToTypeScriptTypeAction.php => TranspileReflectionTypeToTypeScriptNodeAction.php} (98%) create mode 100644 src/Attributes/Hidden.php rename src/{Support => Data}/ImportLocation.php (91%) create mode 100644 src/Laravel/ClassPropertyProcessors/AddDataCollectionOfInfoClassPropertyProcessor.php rename src/Transformers/{AttributeTransformer.php => AttributedClassTransformer.php} (95%) rename src/{TypeScriptTransformerConfigBuilder.php => TypeScriptTransformerConfigFactory.php} (74%) rename tests/Actions/{TranspilePhpStanTypeToTypeScriptTypeActionTest.php => TranspilePhpStanTypeToTypeScriptNodeActionTest.php} (93%) rename tests/Actions/{TranspileReflectionTypeToTypeScriptTypeActionTest.php => TranspileReflectionTypeToTypeScriptNodeActionTest.php} (97%) create mode 100644 tests/Attributes/LiteralTypeScriptTypeTest.php create mode 100644 tests/Attributes/TypeScriptTypeTest.php delete mode 100644 tests/ExampleTest.php create mode 100644 tests/Laravel/ClassPropertyProcessors/AddDataCollectionOfInfoClassPropertyProcessorTest.php create mode 100644 tests/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessorTest.php create mode 100644 tests/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessorTest.php create mode 100644 tests/Transformers/ClassTransformerTest.php diff --git a/README.md b/README.md index 8a36235c..d83ce207 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,56 @@ [![Tests](https://img.shields.io/github/actions/workflow/status/spatie/typescript-transformer/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/spatie/typescript-transformer/actions/workflows/run-tests.yml) [![Total Downloads](https://img.shields.io/packagist/dt/spatie/typescript-transformer.svg?style=flat-square)](https://packagist.org/packages/spatie/typescript-transformer) -This is where your description should go. Try and limit it to a paragraph or two. Consider adding a small example. +This package allows you to convert PHP classes to TypeScript. + +This class... + +```php +/** @typescript */ +class User +{ + public int $id; + public string $name; + public ?string $address; +} +``` + +... will be converted to this TypeScript type: + +```ts +export type User = { + id: number; + name: string; + address: string | null; +} +``` + +Here's another example. + +```php +class Languages extends Enum +{ + const TYPESCRIPT = 'typescript'; + const PHP = 'php'; +} +``` + +The `Languages` enum will be converted to: + +```tsx +export type Languages = 'typescript' | 'php'; +``` ## Support us [](https://spatie.be/github-ad-click/typescript-transformer) -We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). +We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can +support us by [buying one of our paid products](https://spatie.be/open-source/support-us). -We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). +We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. +You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards +on [our virtual postcard wall](https://spatie.be/open-source/postcards). ## Installation @@ -22,13 +63,420 @@ You can install the package via composer: composer require spatie/typescript-transformer ``` -## Usage +## Setting up TypeScript transformer + +We first need to initialize typescript-transformer and configure what it exactly should do. If you're using Laravel, +please skip to the next section. + +Since TypeScript transformer is framework-agnostic, we cannot provide you exact steps on how to integrate it into your +application. However, we can provide you with a general idea of how to do it. + +Ideally, TypeScript transformer is a CLI command within your application, that can be quickly called when you need to +generate TypeScript types. + +Within Symphony, for example, you can create a command like this: + +```php +use Spatie\TypeScriptTransformer\TypeScriptTransformer; +use Spatie\TypeScriptTransformer\TypeScriptTransformerConfigFactory; + +class GenerateTypeScriptCommand extends Command +{ + protected static $defaultName = 'typescript:transform'; + + protected function configure() + { + $this->setDescription('Transform TypeScript types'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $config = TypeScriptTransformerConfigFactory::create(); // We'll come back to this in a minute + + TypeScriptTransformer::create($config)->execute(); + } +} +``` + +When you've registered the command, it can be executed as such: + +```bash +php bin/console typescript:transform +``` + +Since we haven't configured TypeScript transformer yet, this command won't do anything. Skip the Laravel section and +continue with the next section to learn how to configure TypeScript transformer. + +### Laravel + +When using Laravel, first install the specific `TypeScriptTransformerServiceProvider`: + +```bash +php artisan typescript:install +``` + +This command will create a `TypeScriptTransformerServiceProvider` in your `app/Providers` directory. Which looks like +this: + +```php +class TypeScriptTransformerServiceProvider extends BaseTypeScriptTransformerServiceProvider +{ + protected function configure(TypeScriptTransformerConfigFactory $config): void + { + $config; // We'll come back to this in a minute + } +} +``` + +Now you can transform types as such: + +```bash +php artisan typescript:transform +``` + +Since we haven't configured TypeScript transformer yet, this command won't do anything. Let's do that now. + +## Running TypeScript Transformer for the first time + +TypeScript transformer is a highly configurable framework to transform PHP classes and more into TypeScript types, we +provide some highly used functionality out of the box, but you can configure it to your needs. + +We're going to start with transforming basic PHP classes to TypeScript types, this is what the package actually does: + +1. It starts searching for PHP classes within your application +2. It makes a ReflectionClass from each of these found classes +3. These ReflectionClasses are then processed by a list of transformers (they take a ReflectionClass and try to make a + TypeScript type from it) +4. If a ReflectionClass is transformed, it is added to a list to be written to TypeScript otherwise the class is ignored +5. That list is then written to a TypeScript file + +Transformers are the most important part in this whole process, they implement the `Transformer` interface which looks +like this: + +```php +interface Transformer +{ + public function transform(ReflectionClass $reflectionClass, TransformationContext $context): Transformed|Untransformable; +} +``` + +By default, the package comes with a few transformers: + +- `EnumTransformer`: Transforms PHP enums to TypeScript enums +- `ClassTransformer`: Transforms PHP classes with its properties to TypeScript types +- `AttributedClassTransformer`: A special version of the `ClassTransformer` that only transforms classes with + the `#[TypeScript]` attribute +- `LaravelClassTransformer`: A special version of the `ClassTransformer` with some goodies for Laravel users + +You're free to mix and match these transformers to your needs, or even create your own transformers. + +Registering can be done as such within your TypeScript CLI command or `TypeScriptTransformerServiceProvider` (if you're +using Laravel): + +```php +$config->transformer(AttributedClassTransformer::class); +``` + +Since transformers are just PHP classes, you can also pass them arguments when initializing them: + +```php +$config->transformer(new EnumTransformer(useNativeEnums: true)); // transformers enums as TypeScript native enums and not as a union of strings +``` + +Quick note: transformers are executed in the order they are registered in the configuration, when a transformer cannot +transform a class, the next transformer is executed. + +Transformers work on PHP classes, we need to tell TypeScript transformer where to look for these classes. This can be +done by adding a directory to the configuration: + +```php +$config->watchDirectories(app_path()); +``` + +We're almost done! The last thing we need to do is tell TypeScript transformer how to write types, this can be done by +using the `NamespaceWriter` which writes all types to a single TypeScript file with namespaces: + +```ts +declare namespace App.Data { + export type PostData = { + title: string; + slug: string; + type: App.Enums.PostType; + tags: Array; + publish_date: string | null; + published: boolean; + }; +} +declare namespace App.Enums { + export type PostType = 'news' | 'blog'; +} +``` + +You can configure this writer and where it should put the file as such: + +```php +$config->writeTypes(new NamespaceWriter(resource_path('types/generated.d.ts'))); +``` + +If you want a file per namespace, then you can use the `ModuleWriter`, it will write a structure like this: + +```ts +// app/data/index.d.ts +export type PostData = { + title: string; + slug: string; + type: App.Enums.PostType; + tags: Array; + publish_date: string | null; + published: boolean; +}; + +// app/enums/index.d.ts +export type PostType = 'news' | 'blog'; +``` + +You can configure it like this: + +```php +$config->writeTypes(new ModuleWriter(resource_path('types'))); +``` + +That's it! You're now ready to transform your PHP classes to TypeScript types. If you've configured +the `EnumTransformer` then, every enum should be transformed to TypeScript. When using the `AttributedClassTransformer`, +be sure to add the `#[TypeScript]` attribute to classes you want transformed. + +## Making sure PHP classes are typed + +The first run of TypeScript transformer might not have the desired result, a lot of property types could be `undefined` +because TypeScript transformer doesn't know what type these properties are, let's fix that! + +Typescript transformer will automatically transform basic PHP types as such: + +```php +class Types +{ + public string $property; // string + public int $property; // number + public float $property; // number + public bool $property; // boolean + public mixed $property; // any + public object $property; // object +} +``` + +When a type is nullable, TypeScript transformer will transform it as such: + +```php +class Types +{ + public ?string $property; // string | null +} +``` + +Unions and intersections are also supported: + +```php +class Types +{ + public string | int $property; // string | number + public string & int $property; // string & number +} +``` + +Arrays in PHP can be transformed to two types in TypeScript, if no types are annotated, an array will become an `Array`. +When an array is typed with integer keys it will still be an Array. An array typed with string keys will become +a `Record`: + +```php +class Types +{ + public array $property; // Array + + /** @var bool[] */ + public array $property; // Array + + /** @var array */ + public array $property; // Array + + /** @var array */ + public array $property; // Record +} +``` + +As you an see, when an array value is typed correctly, it will also be typed correctly in TypeScript. + +It is also possible to use non-typical array key types, like an enum: + +```php +class Types +{ + /** @var array */ + public array $property; // Record<'news'|'blog', string> +} +``` + +There are multiple locations where you can add property annotations: + +```php +/** +* @property string[] $propertyA + */ +class Types +{ + public array $propertyA; + + /** @var string[] */ + public array $propertyB; + + /** + * @param string[] $propertyC + */ + public function __construct( + public array $propertyC + ) { + + } +} +``` + +Typing objects works like magic: + +```php +class Types +{ + // App.Enums.PostType (when using the NamespaceWriter) + // Import { PostType } from '../enums' + PostType (when using the ModuleWriter) + public PostType $property; +} +``` + +If an typed object is not transformed and thus we don't know how it will look like in TypeScript, it will be replaced +by `unknown`. It is possible to replace these unknown types with a TypeScript type, without transforming them, keep +reading to learn how to do that. + +You can also type generic properties: + +```php +class Types +{ + /** @var Collection */ + public Collection $property; // Illuminate.Support.Collection +} +``` + +Properties can be made optional in TypeScript by adding the `#[Optional]` attribute: ```php -$skeleton = new Spatie\TypescriptTransformer(); -echo $skeleton->echoPhrase('Hello, Spatie!'); +class Types +{ + #[Optional] + public string $property; +} +``` + +Transforming this class will result in the following object: + +```ts +export type Types = { + property?: string; +} ``` +It is possible to hide properties from the TypeScript object by adding the `#[Hidden]` attribute: + +```php +class Types +{ + #[Hidden] + public string $property; +} +``` + +When you want to replace a property type with a literal TypeScript type, you can use the `#[LiteralTypeScriptType]` attribute: + +```php +class Types +{ + #[LiteralTypeScriptType('Record, string>')] + public array $property; +} +``` + +You can also create a TypeScript object from literal types: + +```php +class Types +{ + #[LiteralTypeScriptType([ + 'age' => 'number', + 'name' => 'string', + ])] + public array $property; +} +``` + +This will result in the following TypeScript object: + +```ts +export type Types = { + property: { + age: number; + name: string; + }; +} +``` + +It is also possible to type properties using php types within an attribute using the `#[TypeScriptType]` attribute: + +```php +class Types +{ + #[TypeScriptType('string')] + public $property; +} +``` + +Also this attribute can be used to type an object, but this time the types can be PHP types: + +```php +class Types +{ + #[TypeScriptType([ + 'age' => 'int', + 'name' => 'string', + ])] + public $property; +} +``` + +## Replacing common types + +## TypeScript nodes + +## Visiting TypeScript nodes + +## Creating a transformer + +### Extending the class Transformer + +-> PropertyTypeProcessors + +## Creating a typesprovider + +## Formatting TypeScript + +## Laravel + +### Getting routes as TypeScript + +## Live updates + +## Advanced concepts + +### Building your own Writer + +### Building your own Formatter + ## Testing ```bash diff --git a/composer.json b/composer.json index 6336bb3f..34cfa4ce 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "phpstan/phpdoc-parser": "^1.13", "spatie/file-system-watcher": "^1.1", "spatie/laravel-package-tools": "^1.14.0", - "spatie/php-structure-discoverer": "^1.1", + "spatie/php-structure-discoverer": "^2.1", "spatie/temporary-directory": "^2.1" }, "require-dev": { diff --git a/src/Actions/ConnectReferencesAction.php b/src/Actions/ConnectReferencesAction.php index 3d98c405..d4b76306 100644 --- a/src/Actions/ConnectReferencesAction.php +++ b/src/Actions/ConnectReferencesAction.php @@ -40,6 +40,7 @@ public function execute(TransformedCollection $collection): ReferenceMap $transformed = $referenceMap->get($reference); $metadata['references'][] = $transformed; + $typeReference->connect($transformed); }, [TypeReference::class]); diff --git a/src/Actions/ProvideTypesAction.php b/src/Actions/ProvideTypesAction.php index febaf95e..bf3214dd 100644 --- a/src/Actions/ProvideTypesAction.php +++ b/src/Actions/ProvideTypesAction.php @@ -13,8 +13,10 @@ public function __construct( ) { } - public function execute(TransformedCollection $collection): void + public function execute(): TransformedCollection { + $collection = new TransformedCollection(); + foreach ($this->config->typeProviders as $defaultTypeProvider) { $defaultTypeProvider = $defaultTypeProvider instanceof TypesProvider ? $defaultTypeProvider @@ -25,5 +27,7 @@ public function execute(TransformedCollection $collection): void $collection ); } + + return $collection; } } diff --git a/src/Actions/ReplaceNodesAction.php b/src/Actions/ReplaceNodesAction.php new file mode 100644 index 00000000..f438f1b5 --- /dev/null +++ b/src/Actions/ReplaceNodesAction.php @@ -0,0 +1,47 @@ + $replacements + */ + public function execute( + TransformedCollection $transformedCollection, + ): void { + if (empty($this->config->nodeReplacements)) { + return; + } + + $visitor = Visitor::create(); + + foreach ($this->config->nodeReplacements as $replacement) { + $visitor->before( + function (TypeScriptNode $node) use ($replacement) { + if ($node != $replacement['search']) { + return; + } + + return VisitorOperation::replace($replacement['replacement']); + }, + [$replacement['search']::class] + ); + } + + foreach ($transformedCollection as $transformed) { + $visitor->execute($transformed->typeScriptNode); + } + } +} diff --git a/src/Actions/TranspilePhpStanTypeToTypeScriptTypeAction.php b/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php similarity index 97% rename from src/Actions/TranspilePhpStanTypeToTypeScriptTypeAction.php rename to src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php index da674e67..c9777086 100644 --- a/src/Actions/TranspilePhpStanTypeToTypeScriptTypeAction.php +++ b/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php @@ -34,7 +34,7 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnknown; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptVoid; -class TranspilePhpStanTypeToTypeScriptTypeAction +class TranspilePhpStanTypeToTypeScriptNodeAction { public function __construct( protected FindClassNameFqcnAction $findClassNameFqcnAction = new FindClassNameFqcnAction() @@ -101,6 +101,13 @@ protected function identifierNode( return new TypeScriptObject([]); } + if($node->name === 'array-key') { + return new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNumber(), + ]); + } + if (class_exists($node->name) || interface_exists($node->name)) { return new TypeReference(new ClassStringReference($node->name)); } diff --git a/src/Actions/TranspileReflectionTypeToTypeScriptTypeAction.php b/src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php similarity index 98% rename from src/Actions/TranspileReflectionTypeToTypeScriptTypeAction.php rename to src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php index a8883bfe..97655149 100644 --- a/src/Actions/TranspileReflectionTypeToTypeScriptTypeAction.php +++ b/src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php @@ -22,7 +22,7 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnknown; -class TranspileReflectionTypeToTypeScriptTypeAction +class TranspileReflectionTypeToTypeScriptNodeAction { public function execute( ReflectionType $reflectionType, diff --git a/src/Attributes/Hidden.php b/src/Attributes/Hidden.php new file mode 100644 index 00000000..ec96e505 --- /dev/null +++ b/src/Attributes/Hidden.php @@ -0,0 +1,10 @@ +type)) { return $transpiler->execute($docResolver->type($this->type), $class); diff --git a/src/Collections/ImportsCollection.php b/src/Collections/ImportsCollection.php index b68f8261..5db7903c 100644 --- a/src/Collections/ImportsCollection.php +++ b/src/Collections/ImportsCollection.php @@ -3,8 +3,8 @@ namespace Spatie\TypeScriptTransformer\Collections; use IteratorAggregate; +use Spatie\TypeScriptTransformer\Data\ImportLocation; use Spatie\TypeScriptTransformer\References\Reference; -use Spatie\TypeScriptTransformer\Support\ImportLocation; use Spatie\TypeScriptTransformer\Support\ImportName; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptImport; use Traversable; diff --git a/src/Support/ImportLocation.php b/src/Data/ImportLocation.php similarity index 91% rename from src/Support/ImportLocation.php rename to src/Data/ImportLocation.php index 6cc5d589..a53ffd75 100644 --- a/src/Support/ImportLocation.php +++ b/src/Data/ImportLocation.php @@ -1,8 +1,9 @@ buildVisitor(); + } + + public function execute(ReflectionProperty $reflection, ?TypeNode $annotation, TypeScriptProperty $property): ?TypeScriptProperty + { + $attributes = $reflection->getAttributes('Spatie\LaravelData\Attributes\DataCollectionOf'); + + if (empty($attributes)) { + return $property; + } + + $attribute = $attributes[0]; + + $metadata = [ + 'dataClass' => $attribute->getArguments()[0], + ]; + + $property->type = $this->visitor->execute($property->type, $metadata); + + return $property; + } + + protected function buildVisitor(): void + { + $this->visitor = Visitor::create()->before(function (TypeReference $node, &$metadata) { + if ( + $node->reference instanceof ClassStringReference + && is_a($node->reference->classString, 'Spatie\LaravelData\DataCollection', true) + ) { + return VisitorOperation::replace(new TypeScriptGeneric( + $node, + [ + new TypeReference(new ClassStringReference($metadata['dataClass'])), + ] + )); + } + + return $node; + }, [TypeReference::class]); + } +} diff --git a/src/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessor.php b/src/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessor.php index 17d6cdfc..eb70acd6 100644 --- a/src/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessor.php +++ b/src/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessor.php @@ -4,6 +4,7 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use ReflectionProperty; +use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\Transformers\ClassPropertyProcessors\ClassPropertyProcessor; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; @@ -11,6 +12,15 @@ class RemoveDataLazyTypeClassPropertyProcessor implements ClassPropertyProcessor { + protected array $lazyTypes = [ + 'Spatie\LaravelData\Lazy', + 'Spatie\LaravelData\Support\Lazy\ClosureLazy', + 'Spatie\LaravelData\Support\Lazy\ConditionalLazy', + 'Spatie\LaravelData\Support\Lazy\DefaultLazy', + 'Spatie\LaravelData\Support\Lazy\InertiaLazy', + 'Spatie\LaravelData\Support\Lazy\RelationalLazy', + ]; + public function execute(ReflectionProperty $reflection, ?TypeNode $annotation, TypeScriptProperty $property): ?TypeScriptProperty { if (! $property->type instanceof TypeScriptUnion) { @@ -20,7 +30,7 @@ public function execute(ReflectionProperty $reflection, ?TypeNode $annotation, T for ($i = 0; $i < count($property->type->types); $i++) { $subType = $property->type->types[$i]; - if ($subType instanceof TypeReference && is_a($subType->reference, \Spatie\LaravelData\Lazy::class, true)) { + if ($subType instanceof TypeReference && $subType->reference instanceof ClassStringReference && in_array($subType->reference->classString, $this->lazyTypes)) { $property->isOptional = true; unset($property->type->types[$i]); @@ -29,6 +39,10 @@ public function execute(ReflectionProperty $reflection, ?TypeNode $annotation, T $property->type->types = array_values($property->type->types); + if (count($property->type->types) === 1) { + $property->type = $property->type->types[0]; + } + return $property; } } diff --git a/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php b/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php index b88d6e20..0c33b18c 100644 --- a/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php +++ b/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php @@ -36,11 +36,17 @@ public function __construct() return; } - if (count($generic->genericTypes) !== 2) { + $genericTypesCount = count($generic->genericTypes); + + if ($genericTypesCount > 2 || $genericTypesCount === 0) { // Someone messed with the type, let's skip it return; } + if($genericTypesCount === 1) { + return VisitorOperation::replace(new TypeScriptArray([$generic->genericTypes[0]])); + } + $isRecord = $generic->genericTypes[0] instanceof TypeScriptUnion || $generic->genericTypes[0] instanceof TypeScriptString; if ($isRecord) { diff --git a/src/Laravel/Commands/TransformTypeScriptCommand.php b/src/Laravel/Commands/TransformTypeScriptCommand.php index cc242edd..45efe6f9 100644 --- a/src/Laravel/Commands/TransformTypeScriptCommand.php +++ b/src/Laravel/Commands/TransformTypeScriptCommand.php @@ -5,6 +5,7 @@ use Illuminate\Console\Command; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\TypeScriptTransformer; +use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class TransformTypeScriptCommand extends Command { @@ -12,10 +13,15 @@ class TransformTypeScriptCommand extends Command public $description = 'Transforms PHP to TypeScript'; - public function handle( - TypeScriptTransformer $typeScriptTransformer - ): int { - $typeScriptTransformer->execute(); + public function handle(): int + { + if (! app()->has(TypeScriptTransformerConfig::class)) { + $this->error('Please, first publish the TypeScriptTransformerServiceProvider and configure it.'); + + return self::FAILURE; + } + + app(TypeScriptTransformer::class)->execute(); $log = TypeScriptTransformerLog::resolve(); diff --git a/src/Laravel/Transformers/DataClassTransformer.php b/src/Laravel/Transformers/DataClassTransformer.php index e60926ed..e9795637 100644 --- a/src/Laravel/Transformers/DataClassTransformer.php +++ b/src/Laravel/Transformers/DataClassTransformer.php @@ -3,6 +3,7 @@ namespace Spatie\TypeScriptTransformer\Laravel\Transformers; use ReflectionClass; +use Spatie\TypeScriptTransformer\Laravel\ClassPropertyProcessors\AddDataCollectionOfInfoClassPropertyProcessor; use Spatie\TypeScriptTransformer\Laravel\ClassPropertyProcessors\RemoveDataLazyTypeClassPropertyProcessor; class DataClassTransformer extends LaravelClassTransformer @@ -16,6 +17,7 @@ protected function classPropertyProcessors(): array { return array_merge(parent::classPropertyProcessors(), [ new RemoveDataLazyTypeClassPropertyProcessor(), + new AddDataCollectionOfInfoClassPropertyProcessor(), ]); } } diff --git a/src/Laravel/TypeScriptTransformerApplicationServiceProvider.php b/src/Laravel/TypeScriptTransformerApplicationServiceProvider.php index f0fbdfa6..396737b6 100644 --- a/src/Laravel/TypeScriptTransformerApplicationServiceProvider.php +++ b/src/Laravel/TypeScriptTransformerApplicationServiceProvider.php @@ -4,15 +4,15 @@ use Illuminate\Support\ServiceProvider; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; -use Spatie\TypeScriptTransformer\TypeScriptTransformerConfigBuilder; +use Spatie\TypeScriptTransformer\TypeScriptTransformerConfigFactory; abstract class TypeScriptTransformerApplicationServiceProvider extends ServiceProvider { - abstract protected function configure(TypeScriptTransformerConfigBuilder $config): void; + abstract protected function configure(TypeScriptTransformerConfigFactory $config): void; public function register(): void { - $builder = new TypeScriptTransformerConfigBuilder(); + $builder = new TypeScriptTransformerConfigFactory(); $this->configure($builder); diff --git a/src/Transformers/AttributeTransformer.php b/src/Transformers/AttributedClassTransformer.php similarity index 95% rename from src/Transformers/AttributeTransformer.php rename to src/Transformers/AttributedClassTransformer.php index e9fe4c9c..b44ff021 100644 --- a/src/Transformers/AttributeTransformer.php +++ b/src/Transformers/AttributedClassTransformer.php @@ -8,7 +8,7 @@ use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\Transformed\Untransformable; -class AttributeTransformer extends ClassTransformer +class AttributedClassTransformer extends ClassTransformer { protected function shouldTransform(ReflectionClass $reflection): bool { diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index 836cc778..e744e59d 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -6,8 +6,9 @@ use ReflectionClass; use ReflectionProperty; use Spatie\TypeScriptTransformer\Actions\ParseUseDefinitionsAction; -use Spatie\TypeScriptTransformer\Actions\TranspilePhpStanTypeToTypeScriptTypeAction; -use Spatie\TypeScriptTransformer\Actions\TranspileReflectionTypeToTypeScriptTypeAction; +use Spatie\TypeScriptTransformer\Actions\TranspilePhpStanTypeToTypeScriptNodeAction; +use Spatie\TypeScriptTransformer\Actions\TranspileReflectionTypeToTypeScriptNodeAction; +use Spatie\TypeScriptTransformer\Attributes\Hidden; use Spatie\TypeScriptTransformer\Attributes\Optional; use Spatie\TypeScriptTransformer\Attributes\TypeScriptTypeAttributeContract; use Spatie\TypeScriptTransformer\References\ReflectionClassReference; @@ -27,10 +28,11 @@ abstract class ClassTransformer implements Transformer { public function __construct( protected DocTypeResolver $docTypeResolver = new DocTypeResolver(), - protected TranspilePhpStanTypeToTypeScriptTypeAction $transpilePhpStanTypeToTypeScriptTypeAction = new TranspilePhpStanTypeToTypeScriptTypeAction(), - protected TranspileReflectionTypeToTypeScriptTypeAction $transpileReflectionTypeToTypeScriptTypeAction = new TranspileReflectionTypeToTypeScriptTypeAction(), + protected TranspilePhpStanTypeToTypeScriptNodeAction $transpilePhpStanTypeToTypeScriptTypeAction = new TranspilePhpStanTypeToTypeScriptNodeAction(), + protected TranspileReflectionTypeToTypeScriptNodeAction $transpileReflectionTypeToTypeScriptTypeAction = new TranspileReflectionTypeToTypeScriptNodeAction(), protected ParseUseDefinitionsAction $parseUseDefinitionsAction = new ParseUseDefinitionsAction(), ) { + } public function transform(ReflectionClass $reflectionClass, TransformationContext $context): Transformed|Untransformable @@ -87,6 +89,10 @@ protected function getTypeScriptNode( $annotation?->type ); + if ($property === null) { + continue; + } + $property = $this->runClassPropertyProcessors( $reflectionProperty, $annotation?->type, @@ -131,19 +137,25 @@ protected function createProperty( ReflectionClass $reflectionClass, ReflectionProperty $reflectionProperty, ?TypeNode $annotation, - ): TypeScriptProperty { + ): ?TypeScriptProperty { $type = $this->resolveTypeForProperty( $reflectionClass, $reflectionProperty, $annotation ); - return new TypeScriptProperty( + $property = new TypeScriptProperty( $reflectionProperty->getName(), $type, $this->isPropertyOptional($reflectionProperty, $reflectionClass, $type), $this->isPropertyReadonly($reflectionProperty, $reflectionClass, $type) ); + + if ($this->isPropertyHidden($reflectionProperty, $reflectionClass, $property)) { + return null; + } + + return $property; } protected function resolveTypeForProperty( @@ -188,6 +200,14 @@ protected function isPropertyReadonly( return $reflectionProperty->isReadOnly() || $reflectionClass->isReadOnly(); } + protected function isPropertyHidden( + ReflectionProperty $reflectionProperty, + ReflectionClass $reflectionClass, + TypeScriptProperty $property, + ): bool { + return count($reflectionProperty->getAttributes(Hidden::class)) > 0; + } + protected function runClassPropertyProcessors( ReflectionProperty $reflectionProperty, ?TypeNode $annotation, diff --git a/src/TypeScript/TypeReference.php b/src/TypeScript/TypeReference.php index 244eda83..18f36771 100644 --- a/src/TypeScript/TypeReference.php +++ b/src/TypeScript/TypeReference.php @@ -2,12 +2,18 @@ namespace Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\References\Reference; use Spatie\TypeScriptTransformer\Support\WritingContext; use Spatie\TypeScriptTransformer\Transformed\Transformed; class TypeReference implements TypeScriptExportableNode, TypeScriptNode { + public static function referencingPhpClass(string $class): self + { + return new self(new ClassStringReference($class)); + } + public function __construct( public Reference $reference, public ?Transformed $referenced = null, @@ -21,6 +27,10 @@ public function connect(Transformed $transformed): void public function write(WritingContext $context): string { + if($this->referenced === null) { + return 'undefined'; + } + return ($context->referenceWriter)($this->reference); } diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index 63f2bb88..83ef9b7a 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -6,15 +6,16 @@ use Spatie\TypeScriptTransformer\Actions\DiscoverTypesAction; use Spatie\TypeScriptTransformer\Actions\FormatFilesAction; use Spatie\TypeScriptTransformer\Actions\ProvideTypesAction; +use Spatie\TypeScriptTransformer\Actions\ReplaceNodesAction; use Spatie\TypeScriptTransformer\Actions\WriteFilesAction; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; class TypeScriptTransformer { public function __construct( protected TypeScriptTransformerConfig $config, protected DiscoverTypesAction $discoverTypesAction, - protected ProvideTypesAction $appendDefaultTypesAction, + protected ProvideTypesAction $provideTypesAction, + protected ReplaceNodesAction $replaceNodesAction, protected ConnectReferencesAction $connectReferencesAction, protected WriteFilesAction $writeFilesAction, protected FormatFilesAction $formatFilesAction, @@ -22,6 +23,19 @@ public function __construct( } + public static function create(TypeScriptTransformerConfig $config): self + { + return new self( + $config, + new DiscoverTypesAction(), + new ProvideTypesAction($config), + new ReplaceNodesAction($config), + new ConnectReferencesAction(), + new WriteFilesAction($config), + new FormatFilesAction($config), + ); + } + public function execute(bool $watch = false): void { // Parallelize @@ -33,6 +47,17 @@ public function execute(bool $watch = false): void // watch -> only reload when the config changes (difficult, maybe skip for now) + /** + * Watch implementation + * - We care about file create, update and delete + * - Directory changes are basically combined operations of file changes + * - File create + * - Run the file though `TransformerTypesProvider` and check if a ReflectionClass can be created + * - If so, add it to the types collection + * - Add it to the reference map + * - Rewrite the file (partially) + */ + /** * Notes after knowledge sharing * - Split Laravel part again? @@ -40,9 +65,9 @@ public function execute(bool $watch = false): void * - When generating routes where we have the full namespace, prepend with ., check Laravel Echo for this * - Prettier can run on complete directories, so formatting single files is maybe not required */ - $transformedCollection = new TransformedCollection(); + $transformedCollection = $this->provideTypesAction->execute(); - $this->appendDefaultTypesAction->execute($transformedCollection); + $this->replaceNodesAction->execute($transformedCollection); $referenceMap = $this->connectReferencesAction->execute($transformedCollection); diff --git a/src/TypeScriptTransformerConfig.php b/src/TypeScriptTransformerConfig.php index 5836dd58..878e9132 100755 --- a/src/TypeScriptTransformerConfig.php +++ b/src/TypeScriptTransformerConfig.php @@ -4,6 +4,7 @@ use Spatie\TypeScriptTransformer\Formatters\Formatter; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; use Spatie\TypeScriptTransformer\Writers\Writer; class TypeScriptTransformerConfig @@ -11,12 +12,14 @@ class TypeScriptTransformerConfig /** * @param array $typeProviders * @param array $directoriesToWatch + * @param array $nodeReplacements */ public function __construct( readonly public array $typeProviders, readonly public Writer $writer, readonly public ?Formatter $formatter, readonly public array $directoriesToWatch = [], + readonly public array $nodeReplacements = [], ) { } } diff --git a/src/TypeScriptTransformerConfigBuilder.php b/src/TypeScriptTransformerConfigFactory.php similarity index 74% rename from src/TypeScriptTransformerConfigBuilder.php rename to src/TypeScriptTransformerConfigFactory.php index 92be28b4..3e6ea582 100644 --- a/src/TypeScriptTransformerConfigBuilder.php +++ b/src/TypeScriptTransformerConfigFactory.php @@ -3,17 +3,22 @@ namespace Spatie\TypeScriptTransformer; use Spatie\TypeScriptTransformer\Formatters\Formatter; +use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\Transformers\Transformer; use Spatie\TypeScriptTransformer\TypeProviders\TransformerTypesProvider; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; +use Spatie\TypeScriptTransformer\TypeScript\TypeReference; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; use Spatie\TypeScriptTransformer\Writers\NamespaceWriter; use Spatie\TypeScriptTransformer\Writers\Writer; -class TypeScriptTransformerConfigBuilder +class TypeScriptTransformerConfigFactory { /** - * @param array $typeProviders - * @param array $transformers + * @param array $typeProviders + * @param array $transformers + * @param array $directoriesToWatch + * @param array $nodeReplacements */ public function __construct( protected array $typeProviders = [], @@ -21,9 +26,15 @@ public function __construct( protected string|Formatter|null $formatter = null, protected array $transformers = [], protected array $directoriesToWatch = [], + protected array $nodeReplacements = [], ) { } + public static function create(): self + { + return new self(); + } + public function typesProvider(TypesProvider|string ...$typesProvider): self { array_push($this->typeProviders, ...$typesProvider); @@ -59,6 +70,18 @@ public function formatter(Formatter|string $formatter): self return $this; } + public function replaceType( + string $search, + TypeScriptNode $replacement + ): self { + $this->nodeReplacements[] = [ + 'search' => new TypeReference(new ClassStringReference($search)), + 'replacement' => $replacement, + ]; + + return $this; + } + public function get(): TypeScriptTransformerConfig { $this->ensureConfigIsValid(); @@ -91,7 +114,8 @@ public function get(): TypeScriptTransformerConfig $typeProviders, $writer, $formatter, - $this->directoriesToWatch + $this->directoriesToWatch, + $this->nodeReplacements ); } diff --git a/stubs/TypeScriptTransformerServiceProvider.stub b/stubs/TypeScriptTransformerServiceProvider.stub index a212c000..aa68177f 100644 --- a/stubs/TypeScriptTransformerServiceProvider.stub +++ b/stubs/TypeScriptTransformerServiceProvider.stub @@ -3,18 +3,18 @@ namespace App\Providers; use Spatie\TypeScriptTransformer\Formatters\PrettierFormatter; -use Spatie\TypeScriptTransformer\Transformers\AttributeTransformer; +use Spatie\TypeScriptTransformer\Transformers\AttributedClassTransformer; use Spatie\TypeScriptTransformer\Transformers\EnumTransformer; -use Spatie\TypeScriptTransformer\TypeScriptTransformerConfigBuilder; +use Spatie\TypeScriptTransformer\TypeScriptTransformerConfigFactory; use Spatie\TypeScriptTransformer\Writers\NamespaceWriter; use Spatie\TypeScriptTransformer\Laravel\TypeScriptTransformerApplicationServiceProvider as BaseTypeScriptTransformerServiceProvider; class TypeScriptTransformerServiceProvider extends BaseTypeScriptTransformerServiceProvider { - protected function configure(TypeScriptTransformerConfigBuilder $config): void + protected function configure(TypeScriptTransformerConfigFactory $config): void { $config - ->transformer(AttributeTransformer::class) + ->transformer(AttributedClassTransformer::class) ->transformer(EnumTransformer::class) ->watchDirectories(app_path()) ->writer(new NamespaceWriter(resource_path('types/generated.d.ts'))) diff --git a/tests/Actions/ResolveModuleImportsActionTest.php b/tests/Actions/ResolveModuleImportsActionTest.php index 64d1661b..b53405ed 100644 --- a/tests/Actions/ResolveModuleImportsActionTest.php +++ b/tests/Actions/ResolveModuleImportsActionTest.php @@ -1,7 +1,7 @@ execute( $docTypeResolver->property(new ReflectionProperty(PhpDocTypesStub::class, $property))->type, @@ -192,6 +192,20 @@ ), ]; + yield [ + 'arrayGenericWithArrayKey', + new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [ + new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNumber(), + ]), + new TypeScriptString(), + ] + ), + ]; + yield [ 'typeArray', new TypeScriptGeneric(new TypeScriptIdentifier('Array'), [new TypeScriptString()]), diff --git a/tests/Actions/TranspileReflectionTypeToTypeScriptTypeActionTest.php b/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php similarity index 97% rename from tests/Actions/TranspileReflectionTypeToTypeScriptTypeActionTest.php rename to tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php index 80953c7f..96a60019 100644 --- a/tests/Actions/TranspileReflectionTypeToTypeScriptTypeActionTest.php +++ b/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php @@ -2,7 +2,7 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Collection; -use Spatie\TypeScriptTransformer\Actions\TranspileReflectionTypeToTypeScriptTypeAction; +use Spatie\TypeScriptTransformer\Actions\TranspileReflectionTypeToTypeScriptNodeAction; use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\Tests\Stubs\PhpTypesStub; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; @@ -22,7 +22,7 @@ string $property, TypeScriptNode $expectedTypeScriptNode, ) { - $transpiler = new TranspileReflectionTypeToTypeScriptTypeAction(); + $transpiler = new TranspileReflectionTypeToTypeScriptNodeAction(); $typeScriptNode = $transpiler->execute( (new ReflectionProperty(PhpTypesStub::class, $property))->getType(), diff --git a/tests/Attributes/LiteralTypeScriptTypeTest.php b/tests/Attributes/LiteralTypeScriptTypeTest.php new file mode 100644 index 00000000..a44fcc0b --- /dev/null +++ b/tests/Attributes/LiteralTypeScriptTypeTest.php @@ -0,0 +1,5 @@ +todo(); + +it('can output an object type')->todo(); diff --git a/tests/Attributes/TypeScriptTypeTest.php b/tests/Attributes/TypeScriptTypeTest.php new file mode 100644 index 00000000..a44fcc0b --- /dev/null +++ b/tests/Attributes/TypeScriptTypeTest.php @@ -0,0 +1,5 @@ +todo(); + +it('can output an object type')->todo(); diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php deleted file mode 100644 index 5d363218..00000000 --- a/tests/ExampleTest.php +++ /dev/null @@ -1,5 +0,0 @@ -toBeTrue(); -}); diff --git a/tests/Laravel/ClassPropertyProcessors/AddDataCollectionOfInfoClassPropertyProcessorTest.php b/tests/Laravel/ClassPropertyProcessors/AddDataCollectionOfInfoClassPropertyProcessorTest.php new file mode 100644 index 00000000..95736d43 --- /dev/null +++ b/tests/Laravel/ClassPropertyProcessors/AddDataCollectionOfInfoClassPropertyProcessorTest.php @@ -0,0 +1,3 @@ +todo(); diff --git a/tests/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessorTest.php b/tests/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessorTest.php new file mode 100644 index 00000000..2f30131a --- /dev/null +++ b/tests/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessorTest.php @@ -0,0 +1,3 @@ + */ + public Collection $int_key_collection; + + /** @var Collection */ + public Collection $string_key_collection; + + /** @var Collection */ + public Collection $array_key_collection; + + /** @var Collection */ + public Collection $union_key_collection; + + /** @var Collection */ + public Collection $missing_key_collection; + + /** @var Collection */ + public Collection $missing_types_collection; + + /** @var Collection */ + public Collection $too_much_types_collection; + + public Collection $no_annotation_collection; + }; + + $propertyNode = (new ReplaceLaravelCollectionByArrayClassPropertyProcessor())->execute( + reflection: new ReflectionProperty($class, $property), + annotation: null, + property: resolvePropertyNode($class, $property) + ); + + expect($propertyNode->type)->toEqual( + $expected + ); +})->with(function () { + yield 'int key collection' => [ + 'int_key_collection', + new TypeScriptArray([ + new TypeScriptBoolean(), + ]), + ]; + + yield 'string key collection' => [ + 'string_key_collection', + new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [ + new TypeScriptString(), + new TypeScriptBoolean(), + ], + ), + ]; + + yield 'array key collection' => [ + 'array_key_collection', + new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [ + new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNumber(), + ]), + new TypeScriptBoolean(), + ], + ), + ]; + + yield 'union key collection' => [ + 'union_key_collection', + new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [ + new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNumber(), + ]), + new TypeScriptBoolean(), + ], + ), + ]; + + yield 'missing key collection' => [ + 'missing_key_collection', + new TypeScriptArray([ + new TypeScriptBoolean(), + ]), + ]; + + yield 'missing types collection' => [ + 'missing_types_collection', + new TypeReference(new ClassStringReference(Collection::class)), + ]; + + yield 'too much types collection' => [ + 'too_much_types_collection', + new TypeScriptGeneric( + new TypeReference(new ClassStringReference(Collection::class)), + [ + new TypeScriptString(), + new TypeScriptNumber(), + new TypeScriptBoolean(), + ], + ), + ]; + + yield 'no annotation collection' => [ + 'no_annotation_collection', + new TypeReference(new ClassStringReference(Collection::class)), + ]; +}); diff --git a/tests/Pest.php b/tests/Pest.php index b3d9bbc7..232d0ad4 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1 +1,53 @@ transform(new ReflectionClass($class), $transformationContext); +} + +function resolveObjectNode( + string|object $class, + ?TransformationContext $transformationContext = null, + ?ClassTransformer $transformer = null +): TypeScriptObject { + return transformClass($class, $transformationContext, $transformer)->typeScriptNode->type; +} + +function resolvePropertyNode( + string|object $class, + string $property, + ?TransformationContext $transformationContext = null, + ?ClassTransformer $transformer = null +): TypeScriptProperty { + $objectNode = resolveObjectNode($class, $transformationContext, $transformer); + + foreach ($objectNode->properties as $propertyNode) { + if ($propertyNode->name instanceof TypeScriptIdentifier && $propertyNode->name->name === $property) { + return $propertyNode; + } + } + + throw new Exception("Could not find node for property {$property}"); +} diff --git a/tests/Stubs/PhpDocTypesStub.php b/tests/Stubs/PhpDocTypesStub.php index 2ecebc2c..17296e35 100644 --- a/tests/Stubs/PhpDocTypesStub.php +++ b/tests/Stubs/PhpDocTypesStub.php @@ -82,6 +82,9 @@ class PhpDocTypesStub extends stdClass /** @var array */ public $arrayGenericWithKey; + /** @var array */ + public $arrayGenericWithArrayKey; + /** @var string[] */ public $typeArray; diff --git a/tests/Transformers/ClassTransformerTest.php b/tests/Transformers/ClassTransformerTest.php new file mode 100644 index 00000000..2981ee9f --- /dev/null +++ b/tests/Transformers/ClassTransformerTest.php @@ -0,0 +1,323 @@ +getName())->toBe('ClassName'); + expect($transformed->typeScriptNode)->toEqual( + new TypeScriptAlias( + new TypeScriptIdentifier('ClassName'), + new TypeScriptObject([ + new TypeScriptProperty( + new TypeScriptIdentifier('name'), + new TypeScriptString() + ), + ]) + ) + ); + expect($transformed->reference)->toEqual( + new ReflectionClassReference(new ReflectionClass($class)) + ); + expect($transformed->location)->toEqual(['App', 'Data']); + expect($transformed->export)->toBeTrue(); + expect($transformed->references)->toEqual([]); +}); + +it('can transform a class by depending on a TypeScriptTypeAttributeContract attribute type', function () { + #[LiteralTypeScriptType('string')] + class TestTypeScriptTypeAttributeContractForClass + { + } + + $transformed = transformClass(TestTypeScriptTypeAttributeContractForClass::class); + + expect($transformed->typeScriptNode)->toEqual( + new TypeScriptAlias( + new TypeScriptIdentifier(TestTypeScriptTypeAttributeContractForClass::class), + new TypeScriptRaw('string'), + ) + ); +}); + +it('transforms only public non static properties by default', function () { + $class = new class () { + public string $public; + + protected string $protected; + + private string $private; + + public static string $publicStatic; + + protected static string $protectedStatic; + + private static string $privateStatic; + }; + + expect(resolveObjectNode($class))->toEqual( + new TypeScriptObject([ + new TypeScriptProperty( + new TypeScriptIdentifier('public'), + new TypeScriptString() + ), + ]) + ); +}); + + +it('can type a property using php reflection types', function () { + $class = new class () { + public string $name; + }; + + expect(resolveObjectNode($class))->toEqual( + new TypeScriptObject([ + new TypeScriptProperty( + new TypeScriptIdentifier('name'), + new TypeScriptString() + ), + ]) + ); +}); + +it('can type a property using a var annotation', function () { + $class = new class () { + /** @var string */ + public $name; + }; + + expect(resolveObjectNode($class))->toEqual( + new TypeScriptObject([ + new TypeScriptProperty( + new TypeScriptIdentifier('name'), + new TypeScriptString() + ), + ]) + ); +}); + + +it('can type a property using a constructor annotation', function () { + $class = new class ('') { + /** + * @param string $name + */ + public function __construct( + public $name, + ) { + } + }; + + expect(resolveObjectNode($class))->toEqual( + new TypeScriptObject([ + new TypeScriptProperty( + new TypeScriptIdentifier('name'), + new TypeScriptString() + ), + ]) + ); +}); + +it('can type a property using a class property annotation', function () { + /** + * @property string $name + */ + class TestClassPropertyAnnotation + { + public $name; + } + + expect(resolveObjectNode(TestClassPropertyAnnotation::class))->toEqual( + new TypeScriptObject([ + new TypeScriptProperty( + new TypeScriptIdentifier('name'), + new TypeScriptString() + ), + ]) + ); +}); + +it('can type a property using a TypeScriptTypeAttributeContract attribute type', function () { + $class = new class () { + #[TypeScriptType('string')] + public $name; + }; + + expect(resolveObjectNode($class))->toEqual( + new TypeScriptObject([ + new TypeScriptProperty( + new TypeScriptIdentifier('name'), + new TypeScriptString() + ), + ]) + ); +}); + +it('can make a typescript property optional by annotation', function () { + $class = new class () { + #[Optional] + public string $name; + }; + + expect(resolveObjectNode($class))->toEqual( + new TypeScriptObject([ + new TypeScriptProperty( + new TypeScriptIdentifier('name'), + new TypeScriptString(), + isOptional: true + ), + ]) + ); +}); + +it('will type an untyped property as unknown', function () { + $class = new class () { + public $name; + }; + + expect(resolveObjectNode($class))->toEqual( + new TypeScriptObject([ + new TypeScriptProperty( + new TypeScriptIdentifier('name'), + new TypeScriptUnknown() + ), + ]) + ); +}); + +it('can make a TypeScript property readonly by adding the modifier to the property', function () { + $class = new class () { + public readonly string $name; + }; + + expect(resolveObjectNode($class))->toEqual( + new TypeScriptObject([ + new TypeScriptProperty( + new TypeScriptIdentifier('name'), + new TypeScriptString(), + isReadonly: true + ), + ]) + ); +}); + +it('can make a TypeScript property readonly by adding the modifier to the class', function () { + $class = eval('$class = new readonly class {public string $name;};'); + + expect(resolveObjectNode($class))->toEqual( + new TypeScriptObject([ + new TypeScriptProperty( + new TypeScriptIdentifier('name'), + new TypeScriptString(), + isReadonly: true + ), + ]) + ); +})->skip(fn () => PHP_VERSION_ID < 80300); + +it('can hide a property by adding a hidden attribute', function () { + $class = new class () { + #[Hidden] + public string $property; + }; + + expect(resolveObjectNode($class))->toEqual( + new TypeScriptObject([]) + ); +}); + +it('can run a class property processor', function () { + $class = new class () { + public string $name; + }; + + $object = resolveObjectNode($class, transformer: new class () extends ClassTransformer { + protected function shouldTransform(ReflectionClass $reflection): bool + { + return true; + } + + protected function classPropertyProcessors(): array + { + return [ + new class () implements ClassPropertyProcessor { + public function execute(ReflectionProperty $reflection, ?TypeNode $annotation, TypeScriptProperty $property): ?TypeScriptProperty + { + $property->name = new TypeScriptIdentifier('newName'); + $property->type = new TypeScriptNumber(); + $property->isOptional = true; + $property->isReadonly = true; + + return $property; + } + }, + ]; + } + }); + + expect($object)->toEqual( + new TypeScriptObject([ + new TypeScriptProperty( + new TypeScriptIdentifier('newName'), + new TypeScriptNumber(), + isOptional: true, + isReadonly: true + ), + ]) + ); +}); + +it('can use a class property processor to remove a property', function () { + $class = new class () { + public string $name; + }; + + $object = resolveObjectNode($class, transformer: new class () extends ClassTransformer { + protected function shouldTransform(ReflectionClass $reflection): bool + { + return true; + } + + protected function classPropertyProcessors(): array + { + return [ + new class () implements ClassPropertyProcessor { + public function execute(ReflectionProperty $reflection, ?TypeNode $annotation, TypeScriptProperty $property): ?TypeScriptProperty + { + return null; + } + }, + ]; + } + }); + + expect($object)->toEqual( + new TypeScriptObject([]) + ); +}); From 0803e35e3e2751dbbf51e08a771481761c363a31 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 22 Mar 2024 17:31:41 +0100 Subject: [PATCH 33/51] wip --- README.md | 98 ++++++++++++- src/Actions/ParseUserDefinedTypeAction.php | 34 +++++ src/Actions/ReplaceNodesAction.php | 47 ------- ...spilePhpStanTypeToTypeScriptNodeAction.php | 4 + ...avelDataTypeScriptTransformerExtension.php | 15 ++ .../LaravelTypeScriptTransformerExtension.php | 21 +++ src/Support/Concerns/Instanceable.php | 13 ++ .../TypeScriptTransformerExtension.php | 10 ++ src/Support/TransformedCollection.php | 23 ++- src/TypeScriptTransformer.php | 20 ++- src/TypeScriptTransformerConfig.php | 10 +- src/TypeScriptTransformerConfigFactory.php | 133 ++++++++++++++++-- .../Common/ReplaceTypesVisitorClosure.php | 76 ++++++++++ src/Visitor/Visitor.php | 38 +++-- src/Visitor/VisitorClosure.php | 11 ++ src/Visitor/VisitorClosureType.php | 9 ++ .../ParseUserDefinedTypeActionTest.php | 17 +++ tests/Stubs/PhpTypesStub.php | 4 +- tests/Support/InlineTypesProvider.php | 34 +++++ tests/Support/MemoryWriter.php | 42 ++++++ tests/TypeReplacements.php | 132 +++++++++++++++++ tests/VisitorClosures.php | 51 +++++++ 22 files changed, 755 insertions(+), 87 deletions(-) create mode 100644 src/Actions/ParseUserDefinedTypeAction.php delete mode 100644 src/Actions/ReplaceNodesAction.php create mode 100644 src/Laravel/LaravelDataTypeScriptTransformerExtension.php create mode 100644 src/Laravel/LaravelTypeScriptTransformerExtension.php create mode 100644 src/Support/Concerns/Instanceable.php create mode 100644 src/Support/Extensions/TypeScriptTransformerExtension.php create mode 100644 src/Visitor/Common/ReplaceTypesVisitorClosure.php create mode 100644 src/Visitor/VisitorClosureType.php create mode 100644 tests/Actions/ParseUserDefinedTypeActionTest.php create mode 100644 tests/Support/InlineTypesProvider.php create mode 100644 tests/Support/MemoryWriter.php create mode 100644 tests/TypeReplacements.php create mode 100644 tests/VisitorClosures.php diff --git a/README.md b/README.md index d83ce207..aba3d5ef 100644 --- a/README.md +++ b/README.md @@ -392,7 +392,8 @@ class Types } ``` -When you want to replace a property type with a literal TypeScript type, you can use the `#[LiteralTypeScriptType]` attribute: +When you want to replace a property type with a literal TypeScript type, you can use the `#[LiteralTypeScriptType]` +attribute: ```php class Types @@ -451,17 +452,108 @@ class Types ## Replacing common types +Some PHP classes should be transformed into a TypeScript object, an example of this is the `DateTime` class. When you +send such an object to the front it will be represented by a string rather than an object. TypeScript transformer allows +you to replace these kinds types with an appropriate TypeScript type. + +Replacing types can be done in the config: + +```php +$config->replaceType(DateTime::class, 'string'); +``` + +Now all `DateTime` objects will be transformed to a string in TypeScript. This also includes inherited classes +like `Carbon`, those will also be transformed to a string. + +When using an interface like `DateTimeInterface` you can also replace it with a TypeScript type: + +```php +$config->replaceType(DateTimeInterface::class, 'string'); +``` + +All classes that implement `DateTimeInterface` will be transformed to a string in TypeScript. + +### Replacements + +As we've seen before it is possible to replace types by writing them out like you would do in an annotation, this allows +you to build complex types, for example: + +```php +$config->replaceType(DateTimeInterface::class, 'array{day: int, month: int, year: int}'); +``` + +From now on, all `DateTimeInterface` objects will be replaced by the following TypeScript object: + +```ts +{ + day: number; + month: number; + year: number; +} +``` + +It is also possible to define a replacement as an internal TypeScript node(more on that later): + +```php +$config->replaceType(DateTimeInterface::class, new TypeScriptString()); +``` + +Or use a closure to define the replacement: + +```php +use Spatie\TypeScriptTransformer\TypeScript\TypeReference; + +$config->replaceType(DateTimeInterface::class, function (TypeReference $reference) { + return new TypeScriptString(); +}); +``` + ## TypeScript nodes -## Visiting TypeScript nodes +Internally the package uses TypeScript nodes to represent TypeScript types, these nodes can be used to build complex +types and it is possible to create your own nodes. + +For example, a TypeScript alias is representing a User object looks like this: + +```php +use Spatie\TypeScriptTransformer\TypeScript; + +new TypeScriptAlias( + new TypeScriptIdentifier('User'), + new TypeScriptObject([ + new TypeScriptProperty('id', new TypeScriptNumber()), + new TypeScriptProperty('name', new TypeScriptString()), + new TypeScriptProperty('address', new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNull(), + ])), + ]), +); +``` + +Transforming this alias to TypeScript will result in the following type: + +```ts +type User = { + id: number; + name: string; + address: string | null; +} +``` + +There are a lot of TypeScript nodes available, you can find them in the `Spatie\TypeScriptTransformer\TypeScript` namespace. ## Creating a transformer + + ### Extending the class Transformer -> PropertyTypeProcessors -## Creating a typesprovider +## Creating a TypesProvider + +## Visiting TypeScript nodes ## Formatting TypeScript diff --git a/src/Actions/ParseUserDefinedTypeAction.php b/src/Actions/ParseUserDefinedTypeAction.php new file mode 100644 index 00000000..a1f1b318 --- /dev/null +++ b/src/Actions/ParseUserDefinedTypeAction.php @@ -0,0 +1,34 @@ +typeParser = new TypeParser($constExprParser); + } + + public function execute(string $type, ?ReflectionClass $reflectionClass = null): TypeScriptNode + { + return $this->transpilePhpStanTypeToTypeScriptNodeAction->execute( + $this->typeParser->parse(new TokenIterator($this->lexer->tokenize($type))), + $reflectionClass, + ); + } +} diff --git a/src/Actions/ReplaceNodesAction.php b/src/Actions/ReplaceNodesAction.php deleted file mode 100644 index f438f1b5..00000000 --- a/src/Actions/ReplaceNodesAction.php +++ /dev/null @@ -1,47 +0,0 @@ - $replacements - */ - public function execute( - TransformedCollection $transformedCollection, - ): void { - if (empty($this->config->nodeReplacements)) { - return; - } - - $visitor = Visitor::create(); - - foreach ($this->config->nodeReplacements as $replacement) { - $visitor->before( - function (TypeScriptNode $node) use ($replacement) { - if ($node != $replacement['search']) { - return; - } - - return VisitorOperation::replace($replacement['replacement']); - }, - [$replacement['search']::class] - ); - } - - foreach ($transformedCollection as $transformed) { - $visitor->execute($transformed->typeScriptNode); - } - } -} diff --git a/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php b/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php index c9777086..bcbfd38a 100644 --- a/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php +++ b/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php @@ -112,6 +112,10 @@ protected function identifierNode( return new TypeReference(new ClassStringReference($node->name)); } + if($reflectionClass === null) { + return new TypeScriptUnknown(); + } + $referenced = $this->findClassNameFqcnAction->execute( $reflectionClass, $node->name diff --git a/src/Laravel/LaravelDataTypeScriptTransformerExtension.php b/src/Laravel/LaravelDataTypeScriptTransformerExtension.php new file mode 100644 index 00000000..8887b6fa --- /dev/null +++ b/src/Laravel/LaravelDataTypeScriptTransformerExtension.php @@ -0,0 +1,15 @@ +transformer(DataClassTransformer::class); + } +} diff --git a/src/Laravel/LaravelTypeScriptTransformerExtension.php b/src/Laravel/LaravelTypeScriptTransformerExtension.php new file mode 100644 index 00000000..33cbddb4 --- /dev/null +++ b/src/Laravel/LaravelTypeScriptTransformerExtension.php @@ -0,0 +1,21 @@ +typesProvider(LaravelTypesProvider::class) + ->replaceType(Collection::class, new TypeScriptIdentifier('Array')) + ->replaceType(CarbonInterface::class, new TypeScriptString()); + } +} diff --git a/src/Support/Concerns/Instanceable.php b/src/Support/Concerns/Instanceable.php new file mode 100644 index 00000000..abee350f --- /dev/null +++ b/src/Support/Concerns/Instanceable.php @@ -0,0 +1,13 @@ + */ -class TransformedCollection implements IteratorAggregate +class TransformedCollection implements IteratorAggregate, ArrayAccess { /** * @param array $items @@ -31,4 +32,24 @@ public function getIterator(): Traversable { return new ArrayIterator($this->items); } + + public function offsetExists(mixed $offset): bool + { + return isset($this->items[$offset]); + } + + public function offsetGet(mixed $offset): Transformed + { + return $this->items[$offset]; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + $this->items[$offset] = $value; + } + + public function offsetUnset(mixed $offset): void + { + unset($this->items[$offset]); + } } diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index 83ef9b7a..8a38a5d4 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -6,8 +6,8 @@ use Spatie\TypeScriptTransformer\Actions\DiscoverTypesAction; use Spatie\TypeScriptTransformer\Actions\FormatFilesAction; use Spatie\TypeScriptTransformer\Actions\ProvideTypesAction; -use Spatie\TypeScriptTransformer\Actions\ReplaceNodesAction; use Spatie\TypeScriptTransformer\Actions\WriteFilesAction; +use Spatie\TypeScriptTransformer\Visitor\Visitor; class TypeScriptTransformer { @@ -15,7 +15,6 @@ public function __construct( protected TypeScriptTransformerConfig $config, protected DiscoverTypesAction $discoverTypesAction, protected ProvideTypesAction $provideTypesAction, - protected ReplaceNodesAction $replaceNodesAction, protected ConnectReferencesAction $connectReferencesAction, protected WriteFilesAction $writeFilesAction, protected FormatFilesAction $formatFilesAction, @@ -29,7 +28,6 @@ public static function create(TypeScriptTransformerConfig $config): self $config, new DiscoverTypesAction(), new ProvideTypesAction($config), - new ReplaceNodesAction($config), new ConnectReferencesAction(), new WriteFilesAction($config), new FormatFilesAction($config), @@ -67,10 +65,24 @@ public function execute(bool $watch = false): void */ $transformedCollection = $this->provideTypesAction->execute(); - $this->replaceNodesAction->execute($transformedCollection); + if (! empty($this->config->providedVisitorClosures)) { + $visitor = Visitor::create()->closures(...$this->config->providedVisitorClosures); + + foreach ($transformedCollection as $transformed) { + $visitor->execute($transformed->typeScriptNode); + } + } $referenceMap = $this->connectReferencesAction->execute($transformedCollection); + if (! empty($this->config->connectedVisitorClosures)) { + $visitor = Visitor::create()->closures(...$this->config->connectedVisitorClosures); + + foreach ($transformedCollection as $transformed) { + $visitor->execute($transformed->typeScriptNode); + } + } + $writeableFiles = $this->config->writer->output($transformedCollection, $referenceMap); $this->writeFilesAction->execute($writeableFiles); diff --git a/src/TypeScriptTransformerConfig.php b/src/TypeScriptTransformerConfig.php index 878e9132..0ef71365 100755 --- a/src/TypeScriptTransformerConfig.php +++ b/src/TypeScriptTransformerConfig.php @@ -4,22 +4,24 @@ use Spatie\TypeScriptTransformer\Formatters\Formatter; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; +use Spatie\TypeScriptTransformer\Visitor\VisitorClosure; use Spatie\TypeScriptTransformer\Writers\Writer; class TypeScriptTransformerConfig { /** * @param array $typeProviders - * @param array $directoriesToWatch - * @param array $nodeReplacements + * @param array $directoriesToWatch + * @param array $providedVisitorClosures + * @param array $connectedVisitorClosures */ public function __construct( readonly public array $typeProviders, readonly public Writer $writer, readonly public ?Formatter $formatter, readonly public array $directoriesToWatch = [], - readonly public array $nodeReplacements = [], + readonly public array $providedVisitorClosures = [], + readonly public array $connectedVisitorClosures = [], ) { } } diff --git a/src/TypeScriptTransformerConfigFactory.php b/src/TypeScriptTransformerConfigFactory.php index 3e6ea582..91d6368f 100644 --- a/src/TypeScriptTransformerConfigFactory.php +++ b/src/TypeScriptTransformerConfigFactory.php @@ -2,15 +2,23 @@ namespace Spatie\TypeScriptTransformer; +use Closure; +use Exception; +use Spatie\TypeScriptTransformer\Actions\ParseUserDefinedTypeAction; use Spatie\TypeScriptTransformer\Formatters\Formatter; -use Spatie\TypeScriptTransformer\References\ClassStringReference; +use Spatie\TypeScriptTransformer\Support\Extensions\TypeScriptTransformerExtension; use Spatie\TypeScriptTransformer\Transformers\Transformer; use Spatie\TypeScriptTransformer\TypeProviders\TransformerTypesProvider; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; -use Spatie\TypeScriptTransformer\TypeScript\TypeReference; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptRaw; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnknown; +use Spatie\TypeScriptTransformer\Visitor\Common\ReplaceTypesVisitorClosure; +use Spatie\TypeScriptTransformer\Visitor\VisitorClosure; +use Spatie\TypeScriptTransformer\Visitor\VisitorClosureType; use Spatie\TypeScriptTransformer\Writers\NamespaceWriter; use Spatie\TypeScriptTransformer\Writers\Writer; +use Throwable; class TypeScriptTransformerConfigFactory { @@ -18,7 +26,10 @@ class TypeScriptTransformerConfigFactory * @param array $typeProviders * @param array $transformers * @param array $directoriesToWatch - * @param array $nodeReplacements + * @param array $typeReplacements + * @param array $extensions + * @param array $providedVisitorClosures + * @param array $connectedVisitorClosures */ public function __construct( protected array $typeProviders = [], @@ -26,7 +37,10 @@ public function __construct( protected string|Formatter|null $formatter = null, protected array $transformers = [], protected array $directoriesToWatch = [], - protected array $nodeReplacements = [], + protected array $typeReplacements = [], + protected array $extensions = [], + protected array $providedVisitorClosures = [], + protected array $connectedVisitorClosures = [], ) { } @@ -37,6 +51,12 @@ public static function create(): self public function typesProvider(TypesProvider|string ...$typesProvider): self { + foreach ($typesProvider as $provider) { + if ($provider === TransformerTypesProvider::class || $provider instanceof TransformerTypesProvider) { + throw new Exception("Please add transformers using the config's `transformer` method."); + } + } + array_push($this->typeProviders, ...$typesProvider); return $this; @@ -49,6 +69,27 @@ public function transformer(string|Transformer ...$transformer): self return $this; } + public function replaceTransformer( + string|Transformer $search, + string|Transformer $replacement + ): self { + $searchClass = is_string($search) ? $search : $search::class; + + foreach ($this->transformers as $key => $transformer) { + if (is_string($transformer) && $transformer === $searchClass) { + $this->transformers[$key] = $replacement; + + break; + } + + if (is_object($transformer) && $transformer::class === $searchClass) { + $this->transformers[$key] = $replacement; + } + } + + return $this; + } + public function watchDirectories(string ...$directories): self { array_push($this->directoriesToWatch, ...$directories); @@ -70,14 +111,73 @@ public function formatter(Formatter|string $formatter): self return $this; } + public function providedVisitor( + VisitorClosure|Closure $visitor, + ?array $allowedNodes = null, + VisitorClosureType $type = VisitorClosureType::Before + ): self { + if (! $visitor instanceof VisitorClosure) { + $visitor = new VisitorClosure($visitor, $allowedNodes, $type); + } + + $this->providedVisitorClosures[] = $visitor; + + return $this; + } + + public function connectedVisitor( + VisitorClosure|Closure $visitor, + ?array $allowedNodes = null, + VisitorClosureType $type = VisitorClosureType::Before + ): self { + if (! $visitor instanceof VisitorClosure) { + $visitor = new VisitorClosure($visitor, $allowedNodes, $type); + } + + $this->connectedVisitorClosures[] = $visitor; + + return $this; + } + public function replaceType( string $search, - TypeScriptNode $replacement + TypeScriptNode|string|Closure $replacement + ): self { + if ($replacement instanceof TypeScriptNode) { + $this->typeReplacements[$search] = $replacement; + + return $this; + } + + if (is_string($replacement)) { + try { + $node = ParseUserDefinedTypeAction::instance()->execute($replacement); + + if ($node instanceof TypeScriptUnknown) { + $node = new TypeScriptRaw($replacement); + } + + $this->typeReplacements[$search] = $node; + } catch (Throwable $e) { + $this->typeReplacements[$search] = new TypeScriptRaw($replacement); + } + + return $this; + } + + if (! $replacement instanceof Closure) { + throw new Exception('Replacement must be a TypeScriptNode, a string or a Closure'); + } + + $this->typeReplacements[$search] = $replacement; + + return $this; + } + + public function extension( + TypeScriptTransformerExtension ...$extensions ): self { - $this->nodeReplacements[] = [ - 'search' => new TypeReference(new ClassStringReference($search)), - 'replacement' => $replacement, - ]; + array_push($this->extensions, ...$extensions); return $this; } @@ -100,9 +200,7 @@ public function get(): TypeScriptTransformerConfig $typeProviders[] = new TransformerTypesProvider($transformers, $this->directoriesToWatch); } - $writer = $this->writer ?? new NamespaceWriter( - resource_path('types/generated.d.ts') - ); + $writer = $this->writer ?? new NamespaceWriter(__DIR__.'/js/typed.ts'); if (is_string($writer)) { $writer = new $writer(); @@ -110,12 +208,21 @@ public function get(): TypeScriptTransformerConfig $formatter = is_string($this->formatter) ? new $this->formatter() : $this->formatter; + if ($this->typeReplacements) { + array_unshift($this->providedVisitorClosures, new ReplaceTypesVisitorClosure($this->typeReplacements)); + } + + foreach ($this->extensions as $extension) { + $extension->enrich($this); + } + return new TypeScriptTransformerConfig( $typeProviders, $writer, $formatter, $this->directoriesToWatch, - $this->nodeReplacements + $this->providedVisitorClosures, + $this->connectedVisitorClosures ); } diff --git a/src/Visitor/Common/ReplaceTypesVisitorClosure.php b/src/Visitor/Common/ReplaceTypesVisitorClosure.php new file mode 100644 index 00000000..7e7d2605 --- /dev/null +++ b/src/Visitor/Common/ReplaceTypesVisitorClosure.php @@ -0,0 +1,76 @@ + */ + protected static array $replacements = []; + + /** @var array */ + protected static array $skip = []; + + /** + * @param array $typeReplacements + */ + public function __construct( + protected array $typeReplacements + ) { + parent::__construct( + $this->resolveClosure(), + allowedNodes: [TypeReference::class], + type: VisitorClosureType::Before + ); + + static::$replacements = $typeReplacements; + } + + protected function resolveClosure(): Closure + { + return function (TypeReference $node) { + if (! $node->reference instanceof ClassStringReference) { + return $node; + } + + $class = $node->reference->classString; + + if (array_key_exists($class, static::$skip)) { + return $node; + } + + if (! array_key_exists($class, static::$replacements)) { + foreach ($this->typeReplacements as $type => $replacement) { + if ($class === $type || is_a($class, $type, true)) { + static::$replacements[$class] = $replacement; + + return $this->replaceNode($node, $replacement); + } + } + + return $node; + } + + return $this->replaceNode($node, static::$replacements[$class]); + }; + } + + protected function replaceNode( + TypeReference $node, + Closure|TypeScriptNode $replacement, + ): VisitorOperation { + if ($replacement instanceof Closure) { + $replacement = $replacement($node); + } + + return VisitorOperation::replace($replacement); + } +} diff --git a/src/Visitor/Visitor.php b/src/Visitor/Visitor.php index ad762497..8ef62779 100644 --- a/src/Visitor/Visitor.php +++ b/src/Visitor/Visitor.php @@ -15,12 +15,10 @@ public static function create(): self } /** - * @param array $beforeClosures - * @param array $afterClosures + * @param array $closures */ public function __construct( - protected array $beforeClosures = [], - protected array $afterClosures = [], + protected array $closures = [], ) { } @@ -28,7 +26,7 @@ public function before( Closure $closure, ?array $allowedNodes = null, ): self { - $this->beforeClosures[] = new VisitorClosure($closure, $allowedNodes); + $this->closures[] = new VisitorClosure($closure, $allowedNodes, VisitorClosureType::Before); return $this; } @@ -37,7 +35,15 @@ public function after( Closure $closure, ?array $allowedNodes = null, ): self { - $this->afterClosures[] = new VisitorClosure($closure, $allowedNodes); + $this->closures[] = new VisitorClosure($closure, $allowedNodes, VisitorClosureType::Before); + + return $this; + } + + public function closures( + VisitorClosure ...$closures + ): self { + array_push($this->closures, ...$closures); return $this; } @@ -46,9 +52,13 @@ public function execute( TypeScriptNode $node, array &$metadata = [], ): ?TypeScriptNode { - foreach ($this->beforeClosures as $beforeClosure) { - if ($beforeClosure->shouldRun($node)) { - $operation = $beforeClosure->run($node, $metadata); + foreach ($this->closures as $closure) { + if (! $closure->isBefore()) { + continue; + } + + if ($closure->shouldRun($node)) { + $operation = $closure->run($node, $metadata); if ($operation->type === VisitorOperationType::Remove) { return null; @@ -88,9 +98,13 @@ public function execute( } } - foreach ($this->afterClosures as $afterClosure) { - if ($afterClosure->shouldRun($node)) { - $operation = $afterClosure->run($node, $metadata); + foreach ($this->closures as $closure) { + if (! $closure->isAfter()) { + continue; + } + + if ($closure->shouldRun($node)) { + $operation = $closure->run($node, $metadata); if ($operation->type === VisitorOperationType::Remove) { return null; diff --git a/src/Visitor/VisitorClosure.php b/src/Visitor/VisitorClosure.php index 3d61f318..14587aa4 100644 --- a/src/Visitor/VisitorClosure.php +++ b/src/Visitor/VisitorClosure.php @@ -13,10 +13,21 @@ class VisitorClosure public function __construct( protected Closure $closure, protected ?array $allowedNodes, + protected VisitorClosureType $type, ) { $this->requiresMetadata = (new ReflectionFunction($this->closure))->getNumberOfParameters() === 2; } + public function isBefore(): bool + { + return $this->type === VisitorClosureType::Before; + } + + public function isAfter(): bool + { + return $this->type === VisitorClosureType::After; + } + public function shouldRun( TypeScriptNode $node ): bool { diff --git a/src/Visitor/VisitorClosureType.php b/src/Visitor/VisitorClosureType.php new file mode 100644 index 00000000..544b73ff --- /dev/null +++ b/src/Visitor/VisitorClosureType.php @@ -0,0 +1,9 @@ +execute('string'))->toBeInstanceOf(TypeScriptString::class); + expect($parser->execute('array'))->toEqual(new TypeScriptGeneric(new TypeScriptIdentifier('Record'), [new TypeScriptNumber(), new TypeScriptString()])); + expect($parser->execute('self', new ReflectionClass(DateTime::class)))->toEqual(new TypeReference(new ClassStringReference(DateTime::class))); +}); diff --git a/tests/Stubs/PhpTypesStub.php b/tests/Stubs/PhpTypesStub.php index 86a8973f..8c0f611b 100644 --- a/tests/Stubs/PhpTypesStub.php +++ b/tests/Stubs/PhpTypesStub.php @@ -30,9 +30,7 @@ class PhpTypesStub extends stdClass public Collection&Arrayable $intersection; - public (Collection&Arrayable) - -|null $bnf; + public (Collection&Arrayable)|null $bnf; public self $self; diff --git a/tests/Support/InlineTypesProvider.php b/tests/Support/InlineTypesProvider.php new file mode 100644 index 00000000..cb971830 --- /dev/null +++ b/tests/Support/InlineTypesProvider.php @@ -0,0 +1,34 @@ +transformed = is_array($transformed) ? $transformed : [$transformed]; + + foreach ($this->transformed as $key => $transformed) { + if ($transformed instanceof TransformedFactory) { + $this->transformed[$key] = $transformed->build(); + } + } + } + + public function provide(TypeScriptTransformerConfig $config, TransformedCollection $types): void + { + foreach ($this->transformed as $transformed) { + $types->add($transformed); + } + } +} diff --git a/tests/Support/MemoryWriter.php b/tests/Support/MemoryWriter.php new file mode 100644 index 00000000..b39e8c31 --- /dev/null +++ b/tests/Support/MemoryWriter.php @@ -0,0 +1,42 @@ +getName() === $name) { + return $transformed->typeScriptNode; + } + } + } + + public function getOutput(): string + { + $writer = new NamespaceWriter('test.ts'); + + [$writeableFile] = $writer->output(static::$collection, static::$referenceMap); + + return $writeableFile->contents; + } +} diff --git a/tests/TypeReplacements.php b/tests/TypeReplacements.php new file mode 100644 index 00000000..68594b79 --- /dev/null +++ b/tests/TypeReplacements.php @@ -0,0 +1,132 @@ +typesProvider(new InlineTypesProvider(TransformedFactory::alias( + 'date', + new TypeScriptObject([ + new TypeScriptProperty('datetime', new TypeReference(new ClassStringReference(DateTime::class))), + ]) + ))) + ->writer($writer = new MemoryWriter()) + ->replaceType(DateTime::class, $replacement) + ->get(); + + TypeScriptTransformer::create($config)->execute(); + + expect($writer->getTransformedNodeByName('date'))->toEqual(new TypeScriptAlias( + new TypeScriptIdentifier('date'), + $expected + )); +})->with(function () { + yield 'with a user defined PHP type' => [ + 'replacement' => 'string', + 'expected' => new TypeScriptObject([ + new TypeScriptProperty('datetime', new TypeScriptString()), + ]), + ]; + + yield 'with a user defined complex PHP type' => [ + 'replacement' => 'array{day: int, month: int, year: int}', + 'expected' => new TypeScriptObject([ + new TypeScriptProperty('datetime', new TypeScriptObject([ + new TypeScriptProperty('day', new TypeScriptNumber()), + new TypeScriptProperty('month', new TypeScriptNumber()), + new TypeScriptProperty('year', new TypeScriptNumber()), + ])), + ]), + ]; + + yield 'with a user defined type' => [ + 'replacement' => 'JsDate', + 'expected' => new TypeScriptObject([ + new TypeScriptProperty('datetime', new TypeScriptRaw('JsDate')), + ]), + ]; + + yield 'with a typescript node' => [ + 'replacement' => new TypeScriptObject([ + new TypeScriptProperty('date', new TypeScriptString()), + ]), + 'expected' => new TypeScriptObject([ + new TypeScriptProperty('datetime', new TypeScriptObject([ + new TypeScriptProperty('date', new TypeScriptString()), + ])), + ]), + ]; + + yield 'using a closure' => [ + 'replacement' => fn (TypeScriptNode $node) => new TypeScriptObject([ + new TypeScriptProperty('date', new TypeScriptString()), + ]), + 'expected' => new TypeScriptObject([ + new TypeScriptProperty('datetime', new TypeScriptObject([ + new TypeScriptProperty('date', new TypeScriptString()), + ])), + ]), + ]; +}); + +it('will replace inherited types', function () { + $config = TypeScriptTransformerConfigFactory::create() + ->typesProvider(new InlineTypesProvider(TransformedFactory::alias( + 'date', + new TypeScriptObject([ + new TypeScriptProperty('datetime', new TypeReference(new ClassStringReference(Carbon::class))), + ]) + ))) + ->writer($writer = new MemoryWriter()) + ->replaceType(DateTime::class, 'string') + ->get(); + + TypeScriptTransformer::create($config)->execute(); + + expect($writer->getTransformedNodeByName('date'))->toEqual(new TypeScriptAlias( + new TypeScriptIdentifier('date'), + new TypeScriptObject([ + new TypeScriptProperty('datetime', new TypeScriptString()), + ]) + )); +}); + +it('will replace implemented types', function () { + $config = TypeScriptTransformerConfigFactory::create() + ->typesProvider(new InlineTypesProvider(TransformedFactory::alias( + 'date', + new TypeScriptObject([ + new TypeScriptProperty('datetime', new TypeReference(new ClassStringReference(Carbon::class))), + ]) + ))) + ->writer($writer = new MemoryWriter()) + ->replaceType(DateTimeInterface::class, 'string') + ->get(); + + TypeScriptTransformer::create($config)->execute(); + + expect($writer->getTransformedNodeByName('date'))->toEqual(new TypeScriptAlias( + new TypeScriptIdentifier('date'), + new TypeScriptObject([ + new TypeScriptProperty('datetime', new TypeScriptString()), + ]) + )); +}); diff --git a/tests/VisitorClosures.php b/tests/VisitorClosures.php new file mode 100644 index 00000000..de940fb8 --- /dev/null +++ b/tests/VisitorClosures.php @@ -0,0 +1,51 @@ +typesProvider(new InlineTypesProvider(TransformedFactory::alias( + 'someObject', + new TypeScriptObject([ + new TypeScriptProperty('name', new TypeReference(new ClassStringReference(DateTime::class))), + ]) + ))) + ->writer($writer = new MemoryWriter()) + ->providedVisitor(function (TypeScriptObject $reference) { + return VisitorOperation::replace(new TypeScriptString()); + }, [TypeScriptObject::class]) + ->get(); + + TypeScriptTransformer::create($config)->execute(); + + expect($writer->getOutput())->toEqual('type someObject = string;'); +}); + +it('can run visitor closures when types are connected', function () { + $config = TypeScriptTransformerConfigFactory::create() + ->typesProvider(new InlineTypesProvider(TransformedFactory::alias( + 'someObject', + new TypeScriptObject([ + new TypeScriptProperty('name', new TypeReference(new ClassStringReference(DateTime::class))), + ]) + ))) + ->writer($writer = new MemoryWriter()) + ->connectedVisitor(function (TypeScriptObject $reference) { + return VisitorOperation::replace(new TypeScriptString()); + }, [TypeScriptObject::class]) + ->get(); + + TypeScriptTransformer::create($config)->execute(); + + expect($writer->getOutput())->toEqual('type someObject = string;'); +}); From 233961c36243347e433d8a65f0465dd2223ece7f Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 19 Apr 2024 11:04:50 +0200 Subject: [PATCH 34/51] Update readme --- README.md | 176 +++++++++++++++++++++++++- src/Transformers/ClassTransformer.php | 12 +- 2 files changed, 179 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index aba3d5ef..b8196e34 100644 --- a/README.md +++ b/README.md @@ -437,7 +437,7 @@ class Types } ``` -Also this attribute can be used to type an object, but this time the types can be PHP types: +Also, this attribute can be used to type an object, but this time the types can be PHP types: ```php class Types @@ -541,18 +541,186 @@ type User = { } ``` -There are a lot of TypeScript nodes available, you can find them in the `Spatie\TypeScriptTransformer\TypeScript` namespace. +There are a lot of TypeScript nodes available, you can find them in the `Spatie\TypeScriptTransformer\TypeScript` +namespace. In the advanced section we'll take a look at how to build your own TypeScript nodes. ## Creating a transformer +Transformers are the most important part of TypeScript transformer, they take a PHP class and try to transform it to a +TypeScript type. A transformer implements the `Transformer` interface: +```php +interface Transformer +{ + public function transform(ReflectionClass $reflectionClass, TransformationContext $context): Transformed|Untransformable; +} +``` + +The `TransformationContext` contains all the information you need to transform a class: + +```php +class TransformationContext +{ + public function __construct( + // The name for the class that is being transformed, can be user defined + public string $name, + // The segments of the namespace where the class is located + public array $nameSpaceSegments, + ) { + } +} +``` + +Within the method a `Transformed` data object should be created and returned which looks like this: + +```php +use Spatie\TypeScriptTransformer\References\ClassStringReference; + +new Transformed( + // The TypeScript node representing the transformed class + typeScriptNode: $typeScriptNode, + // A unique name for the transformed class + reference: new ClassStringReference($reflectionClass->getName()), + // A location where the class should be written to + // By default, this is the namespace of the class and the $nameSpaceSegments from the TransformationContext can be used + location: $context->nameSpaceSegments, + // Whether the type should be exported in TypeScript + export: true, +); +``` + +If a class cannot be transformed, the `Untransformable` object should be returned: + +```php +use Spatie\TypeScriptTransformer\Untransformable; + +Untransformable::create(); +``` + +When a class cannot be transformed, the next transformer in the list will be executed. ### Extending the class Transformer --> PropertyTypeProcessors +Most of the time, transforming a class comes down to taking all the properties and transforming them to a TypeScript +object with properties, the package provides an easy-to-extend class for this called `ClassTransformer`. + +You can create your own by extending the `ClassTransformer` and implementing the `shouldTransform` method: + +```php +use Spatie\TypeScriptTransformer\Transformers\ClassTransformer; + +class MyTransformer extends ClassTransformer +{ + protected function shouldTransform(ReflectionClass $reflection): bool + { + return $reflection->implementsInterface(\Spatie\LaravelData\Data::class); + } +} +``` + +In the case above, the transformer will only run when transforming classes which are data objects from +the [laravel-data](https://github.com/spatie/laravel-data) package. We encourage you to overwrite certain methods so +that the transformer fits your needs. + +#### Choosing properties to transform + +By default, all public non-static properties of a class are transformed, but you can overwrite the `properties` method to change this: + +```php +protected function getProperties(ReflectionClass $reflection): array +{ + return $reflection->getProperties(ReflectionProperty::IS_PUBLIC|ReflectionProperty::IS_PROTECTED); +} +``` + +#### Optional properties + +It is possible to make a property optional in TypeScript by overwriting the `isPropertyReadonly` method: + +```php +protected function isPropertyOptional( + ReflectionProperty $reflectionProperty, + ReflectionClass $reflectionClass, + TypeScriptNode $type, +): bool { + return str_starts_with($reflectionProperty->getName(), '_'); +} +``` + +By default, we check whether a property has an `#[Optional]` attribute. + +#### Readonly properties + +You can make a property readonly by overwriting the `isPropertyReadonly` method: + +```php +protected function isPropertyReadonly( + ReflectionProperty $reflectionProperty, + ReflectionClass $reflectionClass, + TypeScriptNode $type, +): bool { + return str_ends_with($reflectionProperty->getName(), 'Read'); +} +``` + +By default, we check whether a property was made readonly in PHP. + +#### Hiding properties + +It is possible to completely hide a property from the TypeScript object by overwriting the `isPropertyHidden` method: + +```php +protected function isPropertyHidden( + ReflectionProperty $reflectionProperty, + ReflectionClass $reflectionClass, + TypeScriptProperty $property, +): bool { + return count($reflectionProperty->getAttributes(Hidden::class)) > 0; +} +``` + +By default, we check whether a property has an `#[Hidden]` attribute. + +#### Class property processors + +Sometimes a more fine-grained control is needed over how a property is transformed, this is where class property processors come to play. They allow you to update the TypeScript Node of the property, you can create them by implementing the `ClassPropertyProcessor` interface: + +```php +use Spatie\TypeScriptTransformer\Transformers\ClassPropertyProcessors\ClassPropertyProcessor; + +class RemoveNullProcessor implements ClassPropertyProcessor +{ + public function execute( + ReflectionProperty $reflection, + ?TypeNode $annotation, + TypeScriptProperty $property + ): ?TypeScriptProperty { + if ($property->type instanceof TypeScriptUnion) { + $property->type = new TypeScriptUnion( + array_values(array_filter($property->type->types, fn (TypeScriptNode $type) => !$type instanceof TypeScriptNull)) + ); + } + + return $property; + } +} +``` + +You can add these processors to the transformer by overwriting the `classPropertyProcessors` method: + +```php +protected function classPropertyProcessors(): array +{ + return [ + new RemoveNullProcessor(), + ]; +} +``` ## Creating a TypesProvider + + ## Visiting TypeScript nodes ## Formatting TypeScript @@ -569,6 +737,8 @@ There are a lot of TypeScript nodes available, you can find them in the `Spatie\ ### Building your own Formatter +### Building your own TypeScript node + ## Testing ```bash diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index e744e59d..c679eb8e 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -26,13 +26,15 @@ abstract class ClassTransformer implements Transformer { + protected array $classPropertyProcessors; + public function __construct( protected DocTypeResolver $docTypeResolver = new DocTypeResolver(), protected TranspilePhpStanTypeToTypeScriptNodeAction $transpilePhpStanTypeToTypeScriptTypeAction = new TranspilePhpStanTypeToTypeScriptNodeAction(), protected TranspileReflectionTypeToTypeScriptNodeAction $transpileReflectionTypeToTypeScriptTypeAction = new TranspileReflectionTypeToTypeScriptNodeAction(), protected ParseUseDefinitionsAction $parseUseDefinitionsAction = new ParseUseDefinitionsAction(), ) { - + $this->classPropertyProcessors = $this->classPropertyProcessors(); } public function transform(ReflectionClass $reflectionClass, TransformationContext $context): Transformed|Untransformable @@ -57,8 +59,6 @@ abstract protected function shouldTransform(ReflectionClass $reflection): bool; /** @return array */ protected function classPropertyProcessors(): array { - // Call this once per class we're transforming for some performance reasons - return []; } @@ -127,10 +127,10 @@ protected function resolveTypeByAttribute( protected function getProperties(ReflectionClass $reflection): array { - return array_values(array_filter( + return array_filter( $reflection->getProperties(ReflectionProperty::IS_PUBLIC), fn (ReflectionProperty $property) => ! $property->isStatic() - )); + ); } protected function createProperty( @@ -213,7 +213,7 @@ protected function runClassPropertyProcessors( ?TypeNode $annotation, TypeScriptProperty $property, ): ?TypeScriptProperty { - $processors = $this->classPropertyProcessors(); + $processors = $this->classPropertyProcessors; foreach ($processors as $processor) { $property = $processor->execute($reflectionProperty, $annotation, $property); From 410c84dd1681c1fec9a1a0d9782f669e8135c185 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 19 Apr 2024 14:29:00 +0200 Subject: [PATCH 35/51] wip --- README.md | 75 ++++++++++- composer.json | 5 +- src/Actions/TransformTypesAction.php | 97 ++++++++++++++ src/Support/TransformationContext.php | 1 + src/Transformers/ClassTransformer.php | 29 ++++- .../TransformerTypesProvider.php | 63 ++------- src/TypeScriptTransformer.php | 1 - tests/Actions/FormatFilesActionTest.php | 71 ++++++++++ .../Attributes/LiteralTypeScriptTypeTest.php | 27 +++- tests/Attributes/TypeScriptTypeTest.php | 34 ++++- .../TypesToProvide/HiddenAttributedClass.php | 11 ++ tests/Fakes/TypesToProvide/SimpleClass.php | 13 ++ .../Fakes/TypesToProvide/StringBackedEnum.php | 11 ++ .../TypeScriptAttributedClass.php | 11 ++ .../TypeScriptLocationAttributedClass.php | 11 ++ tests/Pest.php | 46 ++++--- tests/Support/AllClassTransformer.php | 14 ++ tests/Transformers/ClassTransformerTest.php | 58 ++++++--- .../TransformerTypesProviderTest.php | 123 ++++++++++++++++++ ...tionTest__it_can_disable_formatting__1.txt | 1 + ...tionTest__it_can_disable_formatting__2.txt | 1 + ...mat_an_generated_file_with_prettier__1.txt | 14 ++ ...mat_an_generated_file_with_prettier__2.txt | 14 ++ ...peTest__it_can_output_a_single_type__1.txt | 6 + ...eTest__it_can_output_an_object_type__1.txt | 9 ++ ...peTest__it_can_output_a_single_type__1.txt | 13 ++ ...eTest__it_can_output_an_object_type__1.txt | 16 +++ 27 files changed, 670 insertions(+), 105 deletions(-) create mode 100644 src/Actions/TransformTypesAction.php create mode 100644 tests/Actions/FormatFilesActionTest.php create mode 100644 tests/Fakes/TypesToProvide/HiddenAttributedClass.php create mode 100644 tests/Fakes/TypesToProvide/SimpleClass.php create mode 100644 tests/Fakes/TypesToProvide/StringBackedEnum.php create mode 100644 tests/Fakes/TypesToProvide/TypeScriptAttributedClass.php create mode 100644 tests/Fakes/TypesToProvide/TypeScriptLocationAttributedClass.php create mode 100644 tests/Support/AllClassTransformer.php create mode 100644 tests/TypeProviders/TransformerTypesProviderTest.php create mode 100644 tests/__snapshots__/FormatFilesActionTest__it_can_disable_formatting__1.txt create mode 100644 tests/__snapshots__/FormatFilesActionTest__it_can_disable_formatting__2.txt create mode 100644 tests/__snapshots__/FormatFilesActionTest__it_can_format_an_generated_file_with_prettier__1.txt create mode 100644 tests/__snapshots__/FormatFilesActionTest__it_can_format_an_generated_file_with_prettier__2.txt create mode 100644 tests/__snapshots__/LiteralTypeScriptTypeTest__it_can_output_a_single_type__1.txt create mode 100644 tests/__snapshots__/LiteralTypeScriptTypeTest__it_can_output_an_object_type__1.txt create mode 100644 tests/__snapshots__/TypeScriptTypeTest__it_can_output_a_single_type__1.txt create mode 100644 tests/__snapshots__/TypeScriptTypeTest__it_can_output_an_object_type__1.txt diff --git a/README.md b/README.md index b8196e34..b028a1e4 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ interface Transformer By default, the package comes with a few transformers: - `EnumTransformer`: Transforms PHP enums to TypeScript enums -- `ClassTransformer`: Transforms PHP classes with its properties to TypeScript types +- `ClassTransformer`: Transforms PHP classes with its properties to TypeScript types (abstract, read on for more info) - `AttributedClassTransformer`: A special version of the `ClassTransformer` that only transforms classes with the `#[TypeScript]` attribute - `LaravelClassTransformer`: A special version of the `ClassTransformer` with some goodies for Laravel users @@ -245,6 +245,69 @@ That's it! You're now ready to transform your PHP classes to TypeScript types. I the `EnumTransformer` then, every enum should be transformed to TypeScript. When using the `AttributedClassTransformer`, be sure to add the `#[TypeScript]` attribute to classes you want transformed. +### Special attributes + +Classes can have attributes that change the way they are transformed, let's go through them. + +Using the `#[TypeScript]` attribute is not only a way to tell typescript-transformer to transform a class, but it can +also be used to change the name of the transformed class: + +```php +#[TypeScript(name: 'UserWithoutEmail')] +class User +{ + public int $id; + public string $name; +} +``` + +This will transform the `User` class to `UserWithoutEmail` in TypeScript. + +```ts +export type UserWithoutEmail = { + id: number; + name: string; +} +``` + +Each type will be located somewhere either being a file when using the `ModuleWriter` or in a single file when using +the `NamespaceWriter`. The location of the type can be changed by using the `#[TypeScript]` attribute: + +```php +#[TypeScript(location: ['Data', 'Users'])] +class User +{ + public int $id; + public string $name; +} +``` + +This will transform as such: + +```ts +declare namespace Data.Users { + export type User = { + id: number; + name: string; + }; +} +``` + +It is possible to completely remove a class from the TypeScript output by using the `#[Hidden]` attribute: + +```php +#[Hidden] +enum Members: string +{ + case John = 'john'; + case Paul = 'paul'; + case George = 'george'; + case Ringo = 'ringo'; +} +``` + +This is particularly useful when using the `EnumTransformer` and you want to hide certain enums from the TypeScript. + ## Making sure PHP classes are typed The first run of TypeScript transformer might not have the desired result, a lot of property types could be `undefined` @@ -624,7 +687,8 @@ that the transformer fits your needs. #### Choosing properties to transform -By default, all public non-static properties of a class are transformed, but you can overwrite the `properties` method to change this: +By default, all public non-static properties of a class are transformed, but you can overwrite the `properties` method +to change this: ```php protected function getProperties(ReflectionClass $reflection): array @@ -642,6 +706,7 @@ protected function isPropertyOptional( ReflectionProperty $reflectionProperty, ReflectionClass $reflectionClass, TypeScriptNode $type, + TransformationContext $context, ): bool { return str_starts_with($reflectionProperty->getName(), '_'); } @@ -683,7 +748,9 @@ By default, we check whether a property has an `#[Hidden]` attribute. #### Class property processors -Sometimes a more fine-grained control is needed over how a property is transformed, this is where class property processors come to play. They allow you to update the TypeScript Node of the property, you can create them by implementing the `ClassPropertyProcessor` interface: +Sometimes a more fine-grained control is needed over how a property is transformed, this is where class property +processors come to play. They allow you to update the TypeScript Node of the property, you can create them by +implementing the `ClassPropertyProcessor` interface: ```php use Spatie\TypeScriptTransformer\Transformers\ClassPropertyProcessors\ClassPropertyProcessor; @@ -719,8 +786,6 @@ protected function classPropertyProcessors(): array ## Creating a TypesProvider - - ## Visiting TypeScript nodes ## Formatting TypeScript diff --git a/composer.json b/composer.json index 34cfa4ce..c645a755 100644 --- a/composer.json +++ b/composer.json @@ -24,8 +24,8 @@ "spatie/temporary-directory": "^2.1" }, "require-dev": { - "laravel/pint": "^1.0", "friendsofphp/php-cs-fixer": "^3.0", + "laravel/pint": "^1.0", "nunomaduro/collision": "^7.9", "nunomaduro/larastan": "^2.0.1", "orchestra/testbench": "^8.0", @@ -35,7 +35,8 @@ "phpstan/extension-installer": "^1.1", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.0", - "spatie/laravel-ray": "^1.26" + "spatie/laravel-ray": "^1.26", + "spatie/pest-plugin-snapshots": "^2.1" }, "autoload": { "psr-4": { diff --git a/src/Actions/TransformTypesAction.php b/src/Actions/TransformTypesAction.php new file mode 100644 index 00000000..5f7c1d17 --- /dev/null +++ b/src/Actions/TransformTypesAction.php @@ -0,0 +1,97 @@ + $transformers + * @param array $discoveredClasses + * + * @return array + */ + public function execute( + array $transformers, + array $discoveredClasses, + ): array { + $types = []; + + foreach ($discoveredClasses as $discoveredClass) { + $transformed = $this->transformType( + $transformers, + $discoveredClass + ); + + if ($transformed) { + $types[] = $transformed; + } + } + + return $types; + } + + /** + * @param class-string $type + */ + protected function transformType( + array $transformers, + string $type + ): ?Transformed { + try { + $reflection = new ReflectionClass($type); + } catch (ReflectionException) { + // TODO: maybe add some kind of log? + + return null; + } + + if (count($reflection->getAttributes(Hidden::class)) > 0) { + return null; + } + + foreach ($transformers as $transformer) { + $transformed = $transformer->transform( + $reflection, + $this->createTransformationContext($reflection), + ); + + if ($transformed instanceof Transformed) { + return $transformed; + } + } + + return null; + } + + protected function createTransformationContext( + ReflectionClass $reflection + ): TransformationContext { + $attribute = $this->getTypeScriptAttribute($reflection); + + $name = $attribute->name ?? $reflection->getShortName(); + + $nameSpaceSegments = $attribute->location ?? explode('\\', $reflection->getNamespaceName()); + + return new TransformationContext( + $name, + $nameSpaceSegments, + count($reflection->getAttributes(Optional::class)) > 0, + ); + } + + protected function getTypeScriptAttribute(ReflectionClass $reflection): ?TypeScript + { + $attribute = $reflection->getAttributes(TypeScript::class)[0] ?? null; + + return $attribute?->newInstance(); + } +} diff --git a/src/Support/TransformationContext.php b/src/Support/TransformationContext.php index 0ff41ca6..2911a6b4 100644 --- a/src/Support/TransformationContext.php +++ b/src/Support/TransformationContext.php @@ -7,6 +7,7 @@ class TransformationContext public function __construct( public string $name, public array $nameSpaceSegments, + public bool $optional = false, ) { } } diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index c679eb8e..2b78adc5 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -39,6 +39,10 @@ public function __construct( public function transform(ReflectionClass $reflectionClass, TransformationContext $context): Transformed|Untransformable { + if ($reflectionClass->isEnum()) { + return Untransformable::create(); + } + if (! $this->shouldTransform($reflectionClass)) { return Untransformable::create(); } @@ -46,7 +50,7 @@ public function transform(ReflectionClass $reflectionClass, TransformationContex return new Transformed( new TypeScriptAlias( new TypeScriptIdentifier($context->name), - $this->getTypeScriptNode($reflectionClass) + $this->getTypeScriptNode($reflectionClass, $context) ), new ReflectionClassReference($reflectionClass), $context->nameSpaceSegments, @@ -63,7 +67,8 @@ protected function classPropertyProcessors(): array } protected function getTypeScriptNode( - ReflectionClass $reflectionClass + ReflectionClass $reflectionClass, + TransformationContext $context, ): TypeScriptNode { if ($resolvedAttributeType = $this->resolveTypeByAttribute($reflectionClass)) { return $resolvedAttributeType; @@ -86,7 +91,8 @@ protected function getTypeScriptNode( $property = $this->createProperty( $reflectionClass, $reflectionProperty, - $annotation?->type + $annotation?->type, + $context ); if ($property === null) { @@ -137,6 +143,7 @@ protected function createProperty( ReflectionClass $reflectionClass, ReflectionProperty $reflectionProperty, ?TypeNode $annotation, + TransformationContext $context, ): ?TypeScriptProperty { $type = $this->resolveTypeForProperty( $reflectionClass, @@ -147,8 +154,17 @@ protected function createProperty( $property = new TypeScriptProperty( $reflectionProperty->getName(), $type, - $this->isPropertyOptional($reflectionProperty, $reflectionClass, $type), - $this->isPropertyReadonly($reflectionProperty, $reflectionClass, $type) + $this->isPropertyOptional( + $reflectionProperty, + $reflectionClass, + $type, + $context + ), + $this->isPropertyReadonly( + $reflectionProperty, + $reflectionClass, + $type, + ) ); if ($this->isPropertyHidden($reflectionProperty, $reflectionClass, $property)) { @@ -188,8 +204,9 @@ protected function isPropertyOptional( ReflectionProperty $reflectionProperty, ReflectionClass $reflectionClass, TypeScriptNode $type, + TransformationContext $context, ): bool { - return count($reflectionProperty->getAttributes(Optional::class)) > 0; + return $context->optional || count($reflectionProperty->getAttributes(Optional::class)) > 0; } protected function isPropertyReadonly( diff --git a/src/TypeProviders/TransformerTypesProvider.php b/src/TypeProviders/TransformerTypesProvider.php index 5e040613..bd2703ac 100644 --- a/src/TypeProviders/TransformerTypesProvider.php +++ b/src/TypeProviders/TransformerTypesProvider.php @@ -2,20 +2,17 @@ namespace Spatie\TypeScriptTransformer\TypeProviders; -use ReflectionClass; -use ReflectionException; use Spatie\TypeScriptTransformer\Actions\DiscoverTypesAction; -use Spatie\TypeScriptTransformer\Support\TransformationContext; +use Spatie\TypeScriptTransformer\Actions\TransformTypesAction; use Spatie\TypeScriptTransformer\Support\TransformedCollection; -use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\Transformers\Transformer; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class TransformerTypesProvider implements TypesProvider { /** - * @param array $transformers - * @param array $directories + * @param array $transformers + * @param array $directories */ public function __construct( protected array $transformers, @@ -27,54 +24,14 @@ public function provide( TypeScriptTransformerConfig $config, TransformedCollection $types ): void { - $discoveredClasses = (new DiscoverTypesAction())->execute($this->directories); + $discoverTypesAction = new DiscoverTypesAction(); + $transformTypesAction = new TransformTypesAction(); - foreach ($discoveredClasses as $discoveredClass) { - $transformed = $this->transformType($discoveredClass); + $discoveredClasses = $discoverTypesAction->execute($this->directories); - if ($transformed) { - $types->add($transformed); - } - } - } - - /** - * @param class-string $type - */ - protected function transformType(string $type): ?Transformed - { - try { - $reflection = new ReflectionClass($type); - } catch (ReflectionException) { - // TODO: maybe add some kind of log? - - return null; - } - - foreach ($this->transformers as $transformer) { - $transformed = $transformer->transform( - $reflection, - $this->createTransformationContext($reflection), - ); - - if ($transformed instanceof Transformed) { - return $transformed; - } - } - - return null; - } - - protected function createTransformationContext( - ReflectionClass $reflection - ): TransformationContext { - $name = $reflection->getShortName(); - - $nameSpaceSegments = explode('\\', $reflection->getNamespaceName()); - - return new TransformationContext( - $name, - $nameSpaceSegments, - ); + $types->add(...$transformTypesAction->execute( + $this->transformers, + $discoveredClasses, + )); } } diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index 8a38a5d4..ff7e1a47 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -61,7 +61,6 @@ public function execute(bool $watch = false): void * - Split Laravel part again? * - Make it possible to hijack the PHPStan types, or some way to rename a Laravel Collection to an array? Would be easier * - When generating routes where we have the full namespace, prepend with ., check Laravel Echo for this - * - Prettier can run on complete directories, so formatting single files is maybe not required */ $transformedCollection = $this->provideTypesAction->execute(); diff --git a/tests/Actions/FormatFilesActionTest.php b/tests/Actions/FormatFilesActionTest.php new file mode 100644 index 00000000..9dfd79f2 --- /dev/null +++ b/tests/Actions/FormatFilesActionTest.php @@ -0,0 +1,71 @@ +temporaryDirectory = (new TemporaryDirectory())->create(); + + $this->outputFile = $this->temporaryDirectory->path('types.d.ts'); +}); + +it('can format an generated file with prettier', function () { + $writeableFileA = new WriteableFile( + $this->temporaryDirectory->path('testA.ts'), + "export type Enum='yes'|'no';export type OtherDto={name:string}" + ); + + $writeableFileB = new WriteableFile( + $this->temporaryDirectory->path('testA.ts'), + '{int: number;overwritable: number | boolean;object: {an_int:number;a_bool:boolean;}pure_typescript: never;pure_typescript_object: {an_any:any;a_never:never;}regular_type: number;}' + ); + + file_put_contents($writeableFileA->path, $writeableFileA->contents); + file_put_contents($writeableFileB->path, $writeableFileB->contents); + + $action = new FormatFilesAction( + TypeScriptTransformerConfigFactory::create() + ->formatter(PrettierFormatter::class) + ->get() + ); + + $action->execute([ + $writeableFileA, + $writeableFileB, + ]); + + assertMatchesSnapshot(file_get_contents($writeableFileA->path)); + assertMatchesSnapshot(file_get_contents($writeableFileB->path)); +}); + +it('can disable formatting', function () { + $writeableFileA = new WriteableFile( + $this->temporaryDirectory->path('testA.ts'), + "export type Enum='yes'|'no';export type OtherDto={name:string}" + ); + + $writeableFileB = new WriteableFile( + $this->temporaryDirectory->path('testA.ts'), + '{int: number;overwritable: number | boolean;object: {an_int:number;a_bool:boolean;}pure_typescript: never;pure_typescript_object: {an_any:any;a_never:never;}regular_type: number;}' + ); + + file_put_contents($writeableFileA->path, $writeableFileA->contents); + file_put_contents($writeableFileB->path, $writeableFileB->contents); + + $action = new FormatFilesAction( + TypeScriptTransformerConfigFactory::create()->get() + ); + + $action->execute([ + $writeableFileA, + $writeableFileB, + ]); + + assertMatchesSnapshot(file_get_contents($writeableFileA->path)); + assertMatchesSnapshot(file_get_contents($writeableFileB->path)); +}); diff --git a/tests/Attributes/LiteralTypeScriptTypeTest.php b/tests/Attributes/LiteralTypeScriptTypeTest.php index a44fcc0b..54da1ec3 100644 --- a/tests/Attributes/LiteralTypeScriptTypeTest.php +++ b/tests/Attributes/LiteralTypeScriptTypeTest.php @@ -1,5 +1,28 @@ todo(); +use function Spatie\Snapshots\assertMatchesSnapshot; -it('can output an object type')->todo(); +use Spatie\TypeScriptTransformer\Attributes\LiteralTypeScriptType; + +it('can output a single type', function () { + class TestSingleLiteralTypeScriptTypeAttribute + { + #[LiteralTypeScriptType('Array<{label: string, value: string}>')] + public array $property; + } + + assertMatchesSnapshot(classesToTypeScript([TestSingleLiteralTypeScriptTypeAttribute::class])); +}); + +it('can output an object type', function () { + class TestObjectLiteralTypeScriptTypeAttribute + { + #[LiteralTypeScriptType([ + 'label' => 'string', + 'value' => 'string', + ])] + public array $property; + } + + assertMatchesSnapshot(classesToTypeScript([TestObjectLiteralTypeScriptTypeAttribute::class])); +}); diff --git a/tests/Attributes/TypeScriptTypeTest.php b/tests/Attributes/TypeScriptTypeTest.php index a44fcc0b..7ee236a2 100644 --- a/tests/Attributes/TypeScriptTypeTest.php +++ b/tests/Attributes/TypeScriptTypeTest.php @@ -1,5 +1,35 @@ todo(); +use function Spatie\Snapshots\assertMatchesSnapshot; -it('can output an object type')->todo(); +use Spatie\TypeScriptTransformer\Attributes\TypeScriptType; +use Spatie\TypeScriptTransformer\Support\WriteableFile; + +it('can output a single type', closure: function () { + class TestSingleTypeScriptTypeAttribute + { + #[TypeScriptType('array')] + public array $property; + } + + assertMatchesSnapshot(classesToTypeScript([ + WriteableFile::class, + TestSingleTypeScriptTypeAttribute::class, + ])); +}); + +it('can output an object type', function () { + class TestObjectTypeScriptTypeAttribute + { + #[TypeScriptType([ + 'name' => 'string', + 'file' => WriteableFile::class, + ])] + public array $property; + } + + assertMatchesSnapshot(classesToTypeScript([ + WriteableFile::class, + TestObjectTypeScriptTypeAttribute::class, + ])); +}); diff --git a/tests/Fakes/TypesToProvide/HiddenAttributedClass.php b/tests/Fakes/TypesToProvide/HiddenAttributedClass.php new file mode 100644 index 00000000..8070f957 --- /dev/null +++ b/tests/Fakes/TypesToProvide/HiddenAttributedClass.php @@ -0,0 +1,11 @@ +add(transformClass($class, $transformationContext)); + } + + $referenceMap = (new ConnectReferencesAction())->execute($collection); + + $writeableFile = (new NamespaceWriter('fakeFile'))->output($collection, $referenceMap)[0]; + + return $writeableFile->contents; +} function transformClass( string|object $class, - ?TransformationContext $transformationContext = null, ?ClassTransformer $transformer = null ): Transformed { - $transformer ??= new class () extends ClassTransformer { - protected function shouldTransform(ReflectionClass $reflection): bool - { - return true; - } - }; + $transformer ??= new AllClassTransformer(); - $transformationContext ??= new TransformationContext( - is_object($class) ? get_class($class) : $class, - [] + $transformTypesAction = new TransformTypesAction(); + + [$transformed] = $transformTypesAction->execute( + [$transformer], + [is_string($class) ? $class : $class::class], ); - return $transformer->transform(new ReflectionClass($class), $transformationContext); + return $transformed; } function resolveObjectNode( string|object $class, - ?TransformationContext $transformationContext = null, ?ClassTransformer $transformer = null ): TypeScriptObject { - return transformClass($class, $transformationContext, $transformer)->typeScriptNode->type; + return transformClass($class, $transformer)->typeScriptNode->type; } function resolvePropertyNode( string|object $class, string $property, - ?TransformationContext $transformationContext = null, ?ClassTransformer $transformer = null ): TypeScriptProperty { - $objectNode = resolveObjectNode($class, $transformationContext, $transformer); + $objectNode = resolveObjectNode($class, $transformer); foreach ($objectNode->properties as $propertyNode) { if ($propertyNode->name instanceof TypeScriptIdentifier && $propertyNode->name->name === $property) { diff --git a/tests/Support/AllClassTransformer.php b/tests/Support/AllClassTransformer.php new file mode 100644 index 00000000..3ef63393 --- /dev/null +++ b/tests/Support/AllClassTransformer.php @@ -0,0 +1,14 @@ +getName())->toBe('ClassName'); + expect($transformed->getName())->toBe('SimpleClass'); expect($transformed->typeScriptNode)->toEqual( new TypeScriptAlias( - new TypeScriptIdentifier('ClassName'), + new TypeScriptIdentifier('SimpleClass'), new TypeScriptObject([ new TypeScriptProperty( - new TypeScriptIdentifier('name'), + new TypeScriptIdentifier('stringProperty'), + new TypeScriptString() + ), + new TypeScriptProperty( + new TypeScriptIdentifier('constructorPromotedStringProperty'), new TypeScriptString() ), ]) ) ); expect($transformed->reference)->toEqual( - new ReflectionClassReference(new ReflectionClass($class)) + new ReflectionClassReference(new ReflectionClass(SimpleClass::class)) ); - expect($transformed->location)->toEqual(['App', 'Data']); + expect($transformed->location)->toEqual(['Spatie', 'TypeScriptTransformer', 'Tests', 'Fakes', 'TypesToProvide']); expect($transformed->export)->toBeTrue(); expect($transformed->references)->toEqual([]); }); @@ -59,7 +60,7 @@ class TestTypeScriptTypeAttributeContractForClass expect($transformed->typeScriptNode)->toEqual( new TypeScriptAlias( - new TypeScriptIdentifier(TestTypeScriptTypeAttributeContractForClass::class), + new TypeScriptIdentifier('TestTypeScriptTypeAttributeContractForClass'), new TypeScriptRaw('string'), ) ); @@ -179,7 +180,7 @@ class TestClassPropertyAnnotation ); }); -it('can make a typescript property optional by annotation', function () { +it('can make a typescript property optional by attribute', function () { $class = new class () { #[Optional] public string $name; @@ -196,6 +197,30 @@ class TestClassPropertyAnnotation ); }); +it('can make a complete class optional by attribute', function () { + #[Optional] + class TestAllPropertiesOptionalByClassAttribute + { + public string $name; + public int $age; + } + + expect(resolveObjectNode(TestAllPropertiesOptionalByClassAttribute::class))->toEqual( + new TypeScriptObject([ + new TypeScriptProperty( + new TypeScriptIdentifier('name'), + new TypeScriptString(), + isOptional: true + ), + new TypeScriptProperty( + new TypeScriptIdentifier('age'), + new TypeScriptNumber(), + isOptional: true + ), + ]) + ); +}); + it('will type an untyped property as unknown', function () { $class = new class () { public $name; @@ -257,12 +282,7 @@ class TestClassPropertyAnnotation public string $name; }; - $object = resolveObjectNode($class, transformer: new class () extends ClassTransformer { - protected function shouldTransform(ReflectionClass $reflection): bool - { - return true; - } - + $object = resolveObjectNode($class, transformer: new class () extends AllClassTransformer { protected function classPropertyProcessors(): array { return [ diff --git a/tests/TypeProviders/TransformerTypesProviderTest.php b/tests/TypeProviders/TransformerTypesProviderTest.php new file mode 100644 index 00000000..7d75e658 --- /dev/null +++ b/tests/TypeProviders/TransformerTypesProviderTest.php @@ -0,0 +1,123 @@ +provide( + TypeScriptTransformerConfigFactory::create()->get(), + $collection = new TransformedCollection() + ); + + return $collection; +} + +it('will find types and takes attributes into account', function () { + $collection = getTestProvidedTypes(); + + expect($collection)->toHaveCount(3); + expect(iterator_to_array($collection))->sequence( + fn (Expectation $transformed) => $transformed + ->toBeInstanceOf(Transformed::class) + ->getName()->toBe('JustAnotherName') + ->typeScriptNode->toEqual(new TypeScriptAlias( + new TypeScriptIdentifier('JustAnotherName'), + new TypeScriptObject([ + new TypeScriptProperty('property', new TypeScriptString()), + ]) + )) + ->reference->toBeInstanceOf(ReflectionClassReference::class) + ->reference->classString->toBe(TypeScriptAttributedClass::class) + ->location->toBe(['Spatie', 'TypeScriptTransformer', 'Tests', 'Fakes', 'TypesToProvide']), + fn (Expectation $transformed) => $transformed + ->toBeInstanceOf(Transformed::class) + ->getName()->toBe('TypeScriptLocationAttributedClass') + ->typeScriptNode->toEqual(new TypeScriptAlias( + new TypeScriptIdentifier('TypeScriptLocationAttributedClass'), + new TypeScriptObject([ + new TypeScriptProperty('property', new TypeScriptString()), + ]) + )) + ->reference->toBeInstanceOf(ReflectionClassReference::class) + ->reference->classString->toBe(TypeScriptLocationAttributedClass::class) + ->location->toBe(['App', 'Here']), + fn (Expectation $transformed) => $transformed + ->toBeInstanceOf(Transformed::class) + ->getName()->toBe('SimpleClass') + ->typeScriptNode->toEqual(new TypeScriptAlias( + new TypeScriptIdentifier('SimpleClass'), + new TypeScriptObject([ + new TypeScriptProperty('stringProperty', new TypeScriptString()), + new TypeScriptProperty('constructorPromotedStringProperty', new TypeScriptString()), + ]) + )) + ->reference->toBeInstanceOf(ReflectionClassReference::class) + ->reference->classString->toBe(SimpleClass::class) + ->location->toBe(['Spatie', 'TypeScriptTransformer', 'Tests', 'Fakes', 'TypesToProvide']), + ); +}); + +it('will not find hidden classes', function () { + $typeNames = array_map( + fn (Transformed $transformed) => $transformed->reference->classString, + iterator_to_array(getTestProvidedTypes()) + ); + + expect($typeNames) + ->not->toContain(HiddenAttributedClass::class) + ->toContain(SimpleClass::class); +}); + +it('will only transform types it can transform', function () { + $classTypes = array_map( + fn (Transformed $transformed) => $transformed->reference->classString, + iterator_to_array(getTestProvidedTypes([new AllClassTransformer()])) + ); + + expect($classTypes) + ->not->toContain(StringBackedEnum::class) + ->toContain(SimpleClass::class); + + $enumTypes = array_map( + fn (Transformed $transformed) => $transformed->reference->classString, + iterator_to_array(getTestProvidedTypes([new EnumTransformer()])) + ); + + expect($enumTypes) + ->toContain(StringBackedEnum::class) + ->not->toContain(SimpleClass::class); + + $allTypes = array_map( + fn (Transformed $transformed) => $transformed->reference->classString, + iterator_to_array(getTestProvidedTypes([new EnumTransformer(), new AllClassTransformer()])) + ); + + expect($allTypes) + ->toContain(StringBackedEnum::class) + ->toContain(SimpleClass::class); +}); diff --git a/tests/__snapshots__/FormatFilesActionTest__it_can_disable_formatting__1.txt b/tests/__snapshots__/FormatFilesActionTest__it_can_disable_formatting__1.txt new file mode 100644 index 00000000..c886afd0 --- /dev/null +++ b/tests/__snapshots__/FormatFilesActionTest__it_can_disable_formatting__1.txt @@ -0,0 +1 @@ +{int: number;overwritable: number | boolean;object: {an_int:number;a_bool:boolean;}pure_typescript: never;pure_typescript_object: {an_any:any;a_never:never;}regular_type: number;} \ No newline at end of file diff --git a/tests/__snapshots__/FormatFilesActionTest__it_can_disable_formatting__2.txt b/tests/__snapshots__/FormatFilesActionTest__it_can_disable_formatting__2.txt new file mode 100644 index 00000000..c886afd0 --- /dev/null +++ b/tests/__snapshots__/FormatFilesActionTest__it_can_disable_formatting__2.txt @@ -0,0 +1 @@ +{int: number;overwritable: number | boolean;object: {an_int:number;a_bool:boolean;}pure_typescript: never;pure_typescript_object: {an_any:any;a_never:never;}regular_type: number;} \ No newline at end of file diff --git a/tests/__snapshots__/FormatFilesActionTest__it_can_format_an_generated_file_with_prettier__1.txt b/tests/__snapshots__/FormatFilesActionTest__it_can_format_an_generated_file_with_prettier__1.txt new file mode 100644 index 00000000..2b002f8b --- /dev/null +++ b/tests/__snapshots__/FormatFilesActionTest__it_can_format_an_generated_file_with_prettier__1.txt @@ -0,0 +1,14 @@ +{ + int: number; + overwritable: number | boolean; + object: { + an_int: number; + a_bool: boolean; + } + pure_typescript: never; + pure_typescript_object: { + an_any: any; + a_never: never; + } + regular_type: number; +} diff --git a/tests/__snapshots__/FormatFilesActionTest__it_can_format_an_generated_file_with_prettier__2.txt b/tests/__snapshots__/FormatFilesActionTest__it_can_format_an_generated_file_with_prettier__2.txt new file mode 100644 index 00000000..2b002f8b --- /dev/null +++ b/tests/__snapshots__/FormatFilesActionTest__it_can_format_an_generated_file_with_prettier__2.txt @@ -0,0 +1,14 @@ +{ + int: number; + overwritable: number | boolean; + object: { + an_int: number; + a_bool: boolean; + } + pure_typescript: never; + pure_typescript_object: { + an_any: any; + a_never: never; + } + regular_type: number; +} diff --git a/tests/__snapshots__/LiteralTypeScriptTypeTest__it_can_output_a_single_type__1.txt b/tests/__snapshots__/LiteralTypeScriptTypeTest__it_can_output_a_single_type__1.txt new file mode 100644 index 00000000..122c9d4d --- /dev/null +++ b/tests/__snapshots__/LiteralTypeScriptTypeTest__it_can_output_a_single_type__1.txt @@ -0,0 +1,6 @@ +declare namespace { +export type TestSingleLiteralTypeScriptTypeAttribute = { +property: Array<{label: string, value: string}> +}; + +} diff --git a/tests/__snapshots__/LiteralTypeScriptTypeTest__it_can_output_an_object_type__1.txt b/tests/__snapshots__/LiteralTypeScriptTypeTest__it_can_output_an_object_type__1.txt new file mode 100644 index 00000000..af1795f7 --- /dev/null +++ b/tests/__snapshots__/LiteralTypeScriptTypeTest__it_can_output_an_object_type__1.txt @@ -0,0 +1,9 @@ +declare namespace { +export type TestObjectLiteralTypeScriptTypeAttribute = { +property: { +label: string +value: string +} +}; + +} diff --git a/tests/__snapshots__/TypeScriptTypeTest__it_can_output_a_single_type__1.txt b/tests/__snapshots__/TypeScriptTypeTest__it_can_output_a_single_type__1.txt new file mode 100644 index 00000000..32231e03 --- /dev/null +++ b/tests/__snapshots__/TypeScriptTypeTest__it_can_output_a_single_type__1.txt @@ -0,0 +1,13 @@ +declare namespace { +export type TestSingleTypeScriptTypeAttribute = { +property: Record +}; + +} +declare namespace Spatie.TypeScriptTransformer.Support{ +export type WriteableFile = { +path: string +contents: string +}; + +} diff --git a/tests/__snapshots__/TypeScriptTypeTest__it_can_output_an_object_type__1.txt b/tests/__snapshots__/TypeScriptTypeTest__it_can_output_an_object_type__1.txt new file mode 100644 index 00000000..16abc147 --- /dev/null +++ b/tests/__snapshots__/TypeScriptTypeTest__it_can_output_an_object_type__1.txt @@ -0,0 +1,16 @@ +declare namespace { +export type TestObjectTypeScriptTypeAttribute = { +property: { +name: string +file: Spatie.TypeScriptTransformer.Support.WriteableFile +} +}; + +} +declare namespace Spatie.TypeScriptTransformer.Support{ +export type WriteableFile = { +path: string +contents: string +}; + +} From 3e6d125fd7b00c1fe8cfaa285de0dda66fb9dc79 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 25 Apr 2024 14:55:38 +0200 Subject: [PATCH 36/51] wip --- README.md | 4 +- composer.json | 3 +- src/Actions/ConnectReferencesAction.php | 27 ++--- src/Actions/ProvideTypesAction.php | 10 +- src/Actions/TransformTypesAction.php | 32 +----- src/Collections/ReferenceMap.php | 5 + src/Support/TransformationContext.php | 20 ++++ src/Support/TransformedCollection.php | 5 + src/Support/TypeScriptTransformerLog.php | 9 ++ src/Transformers/ClassTransformer.php | 2 - .../EnumProviders/EnumProvider.php | 14 +++ .../EnumProviders/PhpEnumProvider.php | 38 +++++++ src/Transformers/EnumTransformer.php | 44 +++----- src/TypeScript/TypeScriptEnum.php | 18 +++- src/TypeScript/TypeScriptExport.php | 2 +- src/TypeScript/TypeScriptImport.php | 2 +- src/TypeScript/TypeScriptNamespace.php | 2 +- src/Writers/FlatWriter.php | 40 +++++++ src/Writers/ModuleWriter.php | 4 +- src/Writers/NamespaceWriter.php | 4 +- tests/Actions/ConnectReferencesActionTest.php | 100 ++++++++++++++++++ tests/Actions/DiscoverTypesActionTest.php | 30 ++++++ tests/Actions/ProvideTypesActionTest.php | 38 +++++++ .../SplitTransformedPerLocationActionTest.php | 38 +++++++ tests/Actions/TransformTypesActionTest.php | 69 ++++++++++++ ...ePhpStanTypeToTypeScriptNodeActionTest.php | 2 +- ...flectionTypeToTypeScriptNodeActionTest.php | 2 +- tests/Actions/WriteFilesActionTest.php | 30 ++++++ tests/Fakes/Circular/CircularA.php | 8 ++ tests/Fakes/Circular/CircularB.php | 8 ++ .../PropertyTypes}/PhpDocTypesStub.php | 2 +- .../PropertyTypes}/PhpTypesStub.php | 2 +- tests/Fakes/TypesToProvide/IntBackedEnum.php | 11 ++ .../OptionalAttributedClass.php | 11 ++ tests/Fakes/TypesToProvide/ReadonlyClass.php | 8 ++ .../Fakes/TypesToProvide/StringBackedEnum.php | 8 +- tests/Fakes/TypesToProvide/UnitEnum.php | 11 ++ ...ctionByArrayClassPropertyProcessorTest.php | 10 +- tests/Pest.php | 50 +++------ tests/Support/MemoryWriter.php | 4 +- tests/Support/TransformationContextTest.php | 41 +++++++ tests/Transformers/ClassTransformerTest.php | 43 ++++---- tests/Transformers/EnumTransformerTest.php | 68 ++++++++++++ .../TransformerTypesProviderTest.php | 29 ++++- tests/TypeScript/TypeScriptEnumTest.php | 70 ++++++++++++ .../VisitorTest.php} | 0 tests/Writers/FlatWriterTest.php | 79 ++++++++++++++ tests/Writers/NamespaceWriterTest.php | 97 +++++++++++++++++ ...peTest__it_can_output_a_single_type__1.txt | 3 - ...eTest__it_can_output_an_object_type__1.txt | 3 - ...peTest__it_can_output_a_single_type__1.txt | 12 +-- ...eTest__it_can_output_an_object_type__1.txt | 16 +-- 52 files changed, 997 insertions(+), 191 deletions(-) create mode 100644 src/Transformers/EnumProviders/EnumProvider.php create mode 100644 src/Transformers/EnumProviders/PhpEnumProvider.php create mode 100644 src/Writers/FlatWriter.php create mode 100644 tests/Actions/ConnectReferencesActionTest.php create mode 100644 tests/Actions/DiscoverTypesActionTest.php create mode 100644 tests/Actions/ProvideTypesActionTest.php create mode 100644 tests/Actions/SplitTransformedPerLocationActionTest.php create mode 100644 tests/Actions/TransformTypesActionTest.php create mode 100644 tests/Actions/WriteFilesActionTest.php create mode 100644 tests/Fakes/Circular/CircularA.php create mode 100644 tests/Fakes/Circular/CircularB.php rename tests/{Stubs => Fakes/PropertyTypes}/PhpDocTypesStub.php (96%) rename tests/{Stubs => Fakes/PropertyTypes}/PhpTypesStub.php (90%) create mode 100644 tests/Fakes/TypesToProvide/IntBackedEnum.php create mode 100644 tests/Fakes/TypesToProvide/OptionalAttributedClass.php create mode 100644 tests/Fakes/TypesToProvide/ReadonlyClass.php create mode 100644 tests/Fakes/TypesToProvide/UnitEnum.php create mode 100644 tests/Support/TransformationContextTest.php create mode 100644 tests/Transformers/EnumTransformerTest.php create mode 100644 tests/TypeScript/TypeScriptEnumTest.php rename tests/{Actions/VisitTypeScriptTreeActionTest.php => Visitor/VisitorTest.php} (100%) create mode 100644 tests/Writers/FlatWriterTest.php create mode 100644 tests/Writers/NamespaceWriterTest.php diff --git a/README.md b/README.md index b028a1e4..78c6d686 100644 --- a/README.md +++ b/README.md @@ -786,8 +786,6 @@ protected function classPropertyProcessors(): array ## Creating a TypesProvider -## Visiting TypeScript nodes - ## Formatting TypeScript ## Laravel @@ -804,6 +802,8 @@ protected function classPropertyProcessors(): array ### Building your own TypeScript node +### Visiting TypeScript nodes + ## Testing ```bash diff --git a/composer.json b/composer.json index c645a755..55d6f99b 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.0", "spatie/laravel-ray": "^1.26", - "spatie/pest-plugin-snapshots": "^2.1" + "spatie/pest-plugin-snapshots": "^2.1", + "spatie/ray": "^1.41" }, "autoload": { "psr-4": { diff --git a/src/Actions/ConnectReferencesAction.php b/src/Actions/ConnectReferencesAction.php index d4b76306..dc7283c0 100644 --- a/src/Actions/ConnectReferencesAction.php +++ b/src/Actions/ConnectReferencesAction.php @@ -11,8 +11,8 @@ class ConnectReferencesAction { - public function __construct( - ) { + public function __construct() + { } public function execute(TransformedCollection $collection): ReferenceMap @@ -20,39 +20,32 @@ public function execute(TransformedCollection $collection): ReferenceMap $referenceMap = new ReferenceMap(); foreach ($collection as $transformed) { - if ($transformed->reference) { - $referenceMap->add($transformed); - } + $referenceMap->add($transformed); } $visitor = Visitor::create()->before(function (TypeReference $typeReference, array &$metadata) use ($referenceMap) { - $reference = $typeReference->reference; + /** @var Transformed $transformed */ + $transformed = $metadata['transformed']; - if (! $referenceMap->has($reference)) { - /** @var Transformed $transformed */ - $transformed = $metadata['transformed']; - - TypeScriptTransformerLog::resolve()->warning("Tried replacing reference to `{$reference->humanFriendlyName()}` in `{$transformed->reference->humanFriendlyName()}` but it was not found in the transformed types"); + if (! $referenceMap->has($typeReference->reference)) { + TypeScriptTransformerLog::resolve()->warning("Tried replacing reference to `{$typeReference->reference->humanFriendlyName()}` in `{$transformed->reference->humanFriendlyName()}` but it was not found in the transformed types"); return; } - $transformed = $referenceMap->get($reference); + $transformedReference = $referenceMap->get($typeReference->reference); - $metadata['references'][] = $transformed; + $transformed->references[] = $transformedReference; - $typeReference->connect($transformed); + $typeReference->connect($transformedReference); }, [TypeReference::class]); foreach ($collection as $transformed) { $metadata = [ 'transformed' => $transformed, - 'references' => [], ]; $visitor->execute($transformed->typeScriptNode, $metadata); - - $transformed->references = $metadata['references']; } return $referenceMap; diff --git a/src/Actions/ProvideTypesAction.php b/src/Actions/ProvideTypesAction.php index bf3214dd..05ec092e 100644 --- a/src/Actions/ProvideTypesAction.php +++ b/src/Actions/ProvideTypesAction.php @@ -17,12 +17,12 @@ public function execute(): TransformedCollection { $collection = new TransformedCollection(); - foreach ($this->config->typeProviders as $defaultTypeProvider) { - $defaultTypeProvider = $defaultTypeProvider instanceof TypesProvider - ? $defaultTypeProvider - : new $defaultTypeProvider(); + foreach ($this->config->typeProviders as $typeProvider) { + $typeProvider = $typeProvider instanceof TypesProvider + ? $typeProvider + : new $typeProvider(); - $defaultTypeProvider->provide( + $typeProvider->provide( $this->config, $collection ); diff --git a/src/Actions/TransformTypesAction.php b/src/Actions/TransformTypesAction.php index 5f7c1d17..64a602a8 100644 --- a/src/Actions/TransformTypesAction.php +++ b/src/Actions/TransformTypesAction.php @@ -5,9 +5,8 @@ use ReflectionClass; use ReflectionException; use Spatie\TypeScriptTransformer\Attributes\Hidden; -use Spatie\TypeScriptTransformer\Attributes\Optional; -use Spatie\TypeScriptTransformer\Attributes\TypeScript; use Spatie\TypeScriptTransformer\Support\TransformationContext; +use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\Transformers\Transformer; @@ -49,7 +48,9 @@ protected function transformType( try { $reflection = new ReflectionClass($type); } catch (ReflectionException) { - // TODO: maybe add some kind of log? + TypeScriptTransformerLog::resolve()->error( + "Failed to reflect class `{$type}`" + ); return null; } @@ -61,7 +62,7 @@ protected function transformType( foreach ($transformers as $transformer) { $transformed = $transformer->transform( $reflection, - $this->createTransformationContext($reflection), + TransformationContext::createFromReflection($reflection), ); if ($transformed instanceof Transformed) { @@ -71,27 +72,4 @@ protected function transformType( return null; } - - protected function createTransformationContext( - ReflectionClass $reflection - ): TransformationContext { - $attribute = $this->getTypeScriptAttribute($reflection); - - $name = $attribute->name ?? $reflection->getShortName(); - - $nameSpaceSegments = $attribute->location ?? explode('\\', $reflection->getNamespaceName()); - - return new TransformationContext( - $name, - $nameSpaceSegments, - count($reflection->getAttributes(Optional::class)) > 0, - ); - } - - protected function getTypeScriptAttribute(ReflectionClass $reflection): ?TypeScript - { - $attribute = $reflection->getAttributes(TypeScript::class)[0] ?? null; - - return $attribute?->newInstance(); - } } diff --git a/src/Collections/ReferenceMap.php b/src/Collections/ReferenceMap.php index 768e659a..212c5d5f 100644 --- a/src/Collections/ReferenceMap.php +++ b/src/Collections/ReferenceMap.php @@ -41,4 +41,9 @@ public function get( ): Transformed { return $this->references[$reference->getKey()]; } + + public function all(): array + { + return $this->references; + } } diff --git a/src/Support/TransformationContext.php b/src/Support/TransformationContext.php index 2911a6b4..13e23854 100644 --- a/src/Support/TransformationContext.php +++ b/src/Support/TransformationContext.php @@ -2,6 +2,10 @@ namespace Spatie\TypeScriptTransformer\Support; +use ReflectionClass; +use Spatie\TypeScriptTransformer\Attributes\Optional; +use Spatie\TypeScriptTransformer\Attributes\TypeScript; + class TransformationContext { public function __construct( @@ -10,4 +14,20 @@ public function __construct( public bool $optional = false, ) { } + + public static function createFromReflection( + ReflectionClass $reflection + ): TransformationContext { + $attribute = ($reflection->getAttributes(TypeScript::class)[0] ?? null)?->newInstance(); + + $name = $attribute?->name ?? $reflection->getShortName(); + + $nameSpaceSegments = $attribute?->location ?? explode('\\', $reflection->getNamespaceName()); + + return new TransformationContext( + $name, + $nameSpaceSegments, + count($reflection->getAttributes(Optional::class)) > 0, + ); + } } diff --git a/src/Support/TransformedCollection.php b/src/Support/TransformedCollection.php index 18f3b11f..2846a0bd 100644 --- a/src/Support/TransformedCollection.php +++ b/src/Support/TransformedCollection.php @@ -52,4 +52,9 @@ public function offsetUnset(mixed $offset): void { unset($this->items[$offset]); } + + public function all(): array + { + return $this->items; + } } diff --git a/src/Support/TypeScriptTransformerLog.php b/src/Support/TypeScriptTransformerLog.php index 23501dcf..d30c09e3 100644 --- a/src/Support/TypeScriptTransformerLog.php +++ b/src/Support/TypeScriptTransformerLog.php @@ -8,6 +8,8 @@ class TypeScriptTransformerLog public array $warningMessages = []; + public array $errorMessages = []; + protected static self $instance; private function __construct() @@ -32,4 +34,11 @@ public function warning(string $message): self return $this; } + + public function error(string $message): self + { + $this->errorMessages[] = $message; + + return $this; + } } diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index 2b78adc5..582a26c4 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -5,7 +5,6 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use ReflectionClass; use ReflectionProperty; -use Spatie\TypeScriptTransformer\Actions\ParseUseDefinitionsAction; use Spatie\TypeScriptTransformer\Actions\TranspilePhpStanTypeToTypeScriptNodeAction; use Spatie\TypeScriptTransformer\Actions\TranspileReflectionTypeToTypeScriptNodeAction; use Spatie\TypeScriptTransformer\Attributes\Hidden; @@ -32,7 +31,6 @@ public function __construct( protected DocTypeResolver $docTypeResolver = new DocTypeResolver(), protected TranspilePhpStanTypeToTypeScriptNodeAction $transpilePhpStanTypeToTypeScriptTypeAction = new TranspilePhpStanTypeToTypeScriptNodeAction(), protected TranspileReflectionTypeToTypeScriptNodeAction $transpileReflectionTypeToTypeScriptTypeAction = new TranspileReflectionTypeToTypeScriptNodeAction(), - protected ParseUseDefinitionsAction $parseUseDefinitionsAction = new ParseUseDefinitionsAction(), ) { $this->classPropertyProcessors = $this->classPropertyProcessors(); } diff --git a/src/Transformers/EnumProviders/EnumProvider.php b/src/Transformers/EnumProviders/EnumProvider.php new file mode 100644 index 00000000..b3bad589 --- /dev/null +++ b/src/Transformers/EnumProviders/EnumProvider.php @@ -0,0 +1,14 @@ +isEnum(); + } + + public function isValidUnion(ReflectionClass $reflection): bool + { + return (new ReflectionEnum($reflection->getName()))->isBacked(); + } + + /** + * @return array + */ + public function resolveCases(ReflectionClass $reflection): array + { + /** @var class-string $enumClass */ + $enumClass = $reflection->getName(); + + return array_map( + fn ($case) => [ + 'name' => $case->name, + 'value' => $case instanceof BackedEnum ? $case->value : null, + ], + $enumClass::cases() + ); + } +} diff --git a/src/Transformers/EnumTransformer.php b/src/Transformers/EnumTransformer.php index b5def7b9..0e8768e3 100644 --- a/src/Transformers/EnumTransformer.php +++ b/src/Transformers/EnumTransformer.php @@ -2,24 +2,25 @@ namespace Spatie\TypeScriptTransformer\Transformers; -use BackedEnum; use ReflectionClass; use Spatie\TypeScriptTransformer\References\ReflectionClassReference; use Spatie\TypeScriptTransformer\Support\TransformationContext; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\Transformed\Untransformable; +use Spatie\TypeScriptTransformer\Transformers\EnumProviders\EnumProvider; +use Spatie\TypeScriptTransformer\Transformers\EnumProviders\PhpEnumProvider; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptEnum; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptLiteral; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; -use UnitEnum; class EnumTransformer implements Transformer { public function __construct( - public bool $useNativeEnums = false, + public bool $useUnionEnums = true, + public EnumProvider $enumProvider = new PhpEnumProvider() ) { } @@ -27,43 +28,26 @@ public function transform( ReflectionClass $reflectionClass, TransformationContext $context ): Transformed|Untransformable { - if (! $this->isEnum($reflectionClass)) { + if (! $this->enumProvider->isEnum($reflectionClass)) { return Untransformable::create(); } - $cases = $this->resolveCases($reflectionClass); + if ($this->useUnionEnums === true && ! $this->enumProvider->isValidUnion($reflectionClass)) { + return Untransformable::create(); + } + + $cases = $this->enumProvider->resolveCases($reflectionClass); return new Transformed( - $this->useNativeEnums - ? $this->transformAsNativeEnum($context->name, $cases) - : $this->transformAsUnion($context->name, $cases), + $this->useUnionEnums + ? $this->transformAsUnion($context->name, $cases) + : $this->transformAsNativeEnum($context->name, $cases), new ReflectionClassReference($reflectionClass), $context->nameSpaceSegments, true, ); } - protected function isEnum(ReflectionClass $reflection): bool - { - return $reflection->isEnum(); - } - - protected function resolveCases(ReflectionClass $reflection): array - { - /** @var class-string $enumClass */ - $enumClass = $reflection->getName(); - - $cases = []; - - foreach ($enumClass::cases() as $case) { - $cases[$case->name] = $case instanceof BackedEnum - ? $case->value - : $case->name; - } - - return $cases; - } - protected function transformAsNativeEnum( string $name, array $cases @@ -79,7 +63,7 @@ protected function transformAsUnion( new TypeScriptIdentifier($name), new TypeScriptUnion( array_map( - fn (string $case) => new TypeScriptLiteral($case), + fn (array $case) => new TypeScriptLiteral($case['value']), $cases, ), ), diff --git a/src/TypeScript/TypeScriptEnum.php b/src/TypeScript/TypeScriptEnum.php index 8dd8a2b0..a2086600 100644 --- a/src/TypeScript/TypeScriptEnum.php +++ b/src/TypeScript/TypeScriptEnum.php @@ -6,6 +6,10 @@ class TypeScriptEnum implements TypeScriptExportableNode, TypeScriptNode { + /** + * @param string $name + * @param array $cases + */ public function __construct( public string $name, public array $cases, @@ -14,13 +18,21 @@ public function __construct( public function write(WritingContext $context): string { - $output = 'export enum '.$this->name.' {'.PHP_EOL; + $output = 'enum '.$this->name.' {'.PHP_EOL; foreach ($this->cases as $case) { - $output .= ' '.$case.','.PHP_EOL; + $output .= ' '; + + $output .= match (true) { + is_int($case['value']) => "{$case['name']} = {$case['value']},", + is_string($case['value']) => "{$case['name']} = '{$case['value']}',", + default => "{$case['name']},", + }; + + $output .= PHP_EOL; } - $output .= '}'.PHP_EOL; + $output .= '}'; return $output; } diff --git a/src/TypeScript/TypeScriptExport.php b/src/TypeScript/TypeScriptExport.php index eb5aba7a..c78de3e4 100644 --- a/src/TypeScript/TypeScriptExport.php +++ b/src/TypeScript/TypeScriptExport.php @@ -14,7 +14,7 @@ public function __construct( public function write(WritingContext $context): string { - return "export {$this->node->write($context)}".PHP_EOL; + return "export {$this->node->write($context)}"; } public function visitorProfile(): VisitorProfile diff --git a/src/TypeScript/TypeScriptImport.php b/src/TypeScript/TypeScriptImport.php index ecd80dd2..75272c71 100644 --- a/src/TypeScript/TypeScriptImport.php +++ b/src/TypeScript/TypeScriptImport.php @@ -20,6 +20,6 @@ public function write(WritingContext $context): string { $names = implode(', ', $this->names); - return "import { {$names} } from '{$this->path}';".PHP_EOL; + return "import { {$names} } from '{$this->path}';"; } } diff --git a/src/TypeScript/TypeScriptNamespace.php b/src/TypeScript/TypeScriptNamespace.php index 5af4a7db..69c6d477 100644 --- a/src/TypeScript/TypeScriptNamespace.php +++ b/src/TypeScript/TypeScriptNamespace.php @@ -23,7 +23,7 @@ public function write(WritingContext $context): string $output .= $type->write($context).PHP_EOL; } - $output .= '}'.PHP_EOL; + $output .= '}'; return $output; } diff --git a/src/Writers/FlatWriter.php b/src/Writers/FlatWriter.php new file mode 100644 index 00000000..9cc81f26 --- /dev/null +++ b/src/Writers/FlatWriter.php @@ -0,0 +1,40 @@ +get($reference); + + if (empty($transformable->location)) { + return $transformable->getName(); + } + + return $transformable->getName(); + }); + + foreach ($collection as $transformed) { + $output .= $transformed->prepareForWrite()->write($writingContext) . PHP_EOL; + } + + return [new WriteableFile($this->filename, $output)]; + } +} diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index ebb48c86..ff8e563e 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -53,7 +53,7 @@ protected function writeLocation( }); foreach ($imports->getTypeScriptNodes() as $import) { - $output .= $import->write($writingContext); + $output .= $import->write($writingContext) . PHP_EOL; } if ($imports->isEmpty() === false) { @@ -61,7 +61,7 @@ protected function writeLocation( } foreach ($location->transformed as $transformedItem) { - $output .= $transformedItem->prepareForWrite()->write($writingContext); + $output .= $transformedItem->prepareForWrite()->write($writingContext) . PHP_EOL; } return new WriteableFile("{$this->resolvePath($location)}/index.ts", $output); diff --git a/src/Writers/NamespaceWriter.php b/src/Writers/NamespaceWriter.php index 33e75664..b5349639 100644 --- a/src/Writers/NamespaceWriter.php +++ b/src/Writers/NamespaceWriter.php @@ -44,7 +44,7 @@ public function output( foreach ($split as $splitConstruct) { if (count($splitConstruct->segments) === 0) { foreach ($splitConstruct->transformed as $transformable) { - $output .= $transformable->typeScriptNode->write($writingContext); + $output .= $transformable->prepareForWrite()->write($writingContext) . PHP_EOL; } continue; @@ -58,7 +58,7 @@ public function output( ), ); - $output .= $namespace->write($writingContext); + $output .= $namespace->write($writingContext) . PHP_EOL; } return [new WriteableFile($this->filename, $output)]; diff --git a/tests/Actions/ConnectReferencesActionTest.php b/tests/Actions/ConnectReferencesActionTest.php new file mode 100644 index 00000000..171b06c7 --- /dev/null +++ b/tests/Actions/ConnectReferencesActionTest.php @@ -0,0 +1,100 @@ +execute($collection)->all(); + + expect($referenceMap) + ->toHaveCount(2) + ->toBe([ + $transformedEnum->reference->getKey() => $transformedEnum, + $transformedClass->reference->getKey() => $transformedClass, + ]); + + expect($transformedEnum->references)->toHaveCount(0); + + expect($transformedClass->references) + ->toHaveCount(1) + ->toBe([$transformedEnum]); + + expect($transformedClass->typeScriptNode->type->properties[0]->type) + ->toBeInstanceOf(TypeReference::class) + ->referenced->toBe($transformedEnum); +}); + +it('can connect two objects referencing each other', function () { + $collection = new TransformedCollection([ + $circularA = transformSingle(CircularA::class, new AllClassTransformer()), + $circularB = transformSingle(CircularB::class, new AllClassTransformer()), + ]); + + $referenceMap = app(ConnectReferencesAction::class)->execute($collection)->all(); + + expect($referenceMap) + ->toHaveCount(2) + ->toBe([ + $circularA->reference->getKey() => $circularA, + $circularB->reference->getKey() => $circularB, + ]); + + expect($circularA->references) + ->toHaveCount(1) + ->toBe([$circularB]); + + expect($circularB->references) + ->toHaveCount(1) + ->toBe([$circularA]); + + expect($circularA->typeScriptNode->type->properties[0]->type) + ->toBeInstanceOf(TypeReference::class) + ->referenced->toBe($circularB); + + expect($circularB->typeScriptNode->type->properties[0]->type) + ->toBeInstanceOf(TypeReference::class) + ->referenced->toBe($circularA); +}); + +it('will write to the log when a reference cannot be found', function () { + $class = new class () { + public StringBackedEnum $enum; + }; + + $collection = new TransformedCollection([ + $transformedClass = transformSingle($class, new AllClassTransformer()), + ]); + + $referenceMap = app(ConnectReferencesAction::class)->execute($collection)->all(); + + expect($referenceMap) + ->toHaveCount(1) + ->toBe([ + $transformedClass->reference->getKey() => $transformedClass, + ]); + + expect($transformedClass->references) + ->toHaveCount(0); + + expect($transformedClass->typeScriptNode->type->properties[0]->type) + ->toBeInstanceOf(TypeReference::class) + ->referenced->toBeNull(); + + expect(TypeScriptTransformerLog::resolve()->warningMessages)->not()->toBeEmpty(); +}); diff --git a/tests/Actions/DiscoverTypesActionTest.php b/tests/Actions/DiscoverTypesActionTest.php new file mode 100644 index 00000000..c4a50d86 --- /dev/null +++ b/tests/Actions/DiscoverTypesActionTest.php @@ -0,0 +1,30 @@ +execute([ + __DIR__.'/../Fakes/TypesToProvide', + ]); + + expect($types)->toBe([ + StringBackedEnum::class, + HiddenAttributedClass::class, + TypeScriptAttributedClass::class, + TypeScriptLocationAttributedClass::class, + OptionalAttributedClass::class, + ReadonlyClass::class, + SimpleClass::class, + UnitEnum::class, + IntBackedEnum::class, + ]); +}); diff --git a/tests/Actions/ProvideTypesActionTest.php b/tests/Actions/ProvideTypesActionTest.php new file mode 100644 index 00000000..ea44d24a --- /dev/null +++ b/tests/Actions/ProvideTypesActionTest.php @@ -0,0 +1,38 @@ +add( + TransformedFactory::alias('Foo', new TypeScriptString())->build(), + ); + } + }; + + $config = TypeScriptTransformerConfigFactory::create() + ->typesProvider( + new InlineTypesProvider([ + TransformedFactory::alias('Bar', new TypeScriptString()), + ]), + $stringProvider::class + ) + ->get(); + + $types = (new ProvideTypesAction($config))->execute(); + + expect($types)->toHaveCount(2); + expect($types[0]->getName())->toBe('Bar'); + expect($types[1]->getName())->toBe('Foo'); +}); diff --git a/tests/Actions/SplitTransformedPerLocationActionTest.php b/tests/Actions/SplitTransformedPerLocationActionTest.php new file mode 100644 index 00000000..a4bab933 --- /dev/null +++ b/tests/Actions/SplitTransformedPerLocationActionTest.php @@ -0,0 +1,38 @@ +build(), + $root1 = TransformedFactory::alias('RootType', new TypeScriptString())->build(), + $level2 = TransformedFactory::alias('Level2Type', new TypeScriptString(), location: ['level1', 'level2'])->build(), + $level12 = TransformedFactory::alias('Level1Type2', new TypeScriptString(), location: ['level1'])->build(), + $root2 = TransformedFactory::alias('RootType2', new TypeScriptString())->build(), + ]); + + $split = (new SplitTransformedPerLocationAction())->execute( + $transformedCollection + ); + + expect($split)->toHaveCount(3); + + expect($split[0]) + ->toBeInstanceOf(Location::class) + ->segments->toBeEmpty() + ->transformed->toEqual([$root1, $root2]); + + expect($split[1]) + ->toBeInstanceOf(Location::class) + ->segments->toBe(['level1']) + ->transformed->toEqual([$level11, $level12]); + + expect($split[2]) + ->toBeInstanceOf(Location::class) + ->segments->toBe(['level1', 'level2']) + ->transformed->toEqual([$level2]); +}); diff --git a/tests/Actions/TransformTypesActionTest.php b/tests/Actions/TransformTypesActionTest.php new file mode 100644 index 00000000..ad12c2a8 --- /dev/null +++ b/tests/Actions/TransformTypesActionTest.php @@ -0,0 +1,69 @@ +execute( + [ + new EnumTransformer(), + new AllClassTransformer(), + ], + [ + StringBackedEnum::class, + SimpleClass::class, + ] + ); + + expect($types) + ->toHaveCount(2) + ->each->toBeInstanceOf(Transformed::class); +}); + +it('will not transform untransformable types', function () { + $types = (new TransformTypesAction())->execute( + [ + new EnumTransformer(), + ], + [ + SimpleClass::class, + ] + ); + + expect($types)->toBeEmpty(); +}); + +it('can hide classes using an attribute', function () { + $types = (new TransformTypesAction())->execute( + [ + new AllClassTransformer(), + ], + [ + HiddenAttributedClass::class, + ] + ); + + expect($types)->toBeEmpty(); +}); + +it('will log errors when a type cannot be reflected', function () { + $types = (new TransformTypesAction())->execute( + [ + new AllClassTransformer(), + ], + [ + 'NonExistentClass', + ] + ); + + expect($types)->toBeEmpty(); + + expect(TypeScriptTransformerLog::resolve()->errorMessages) + ->toHaveCount(1); +}); diff --git a/tests/Actions/TranspilePhpStanTypeToTypeScriptNodeActionTest.php b/tests/Actions/TranspilePhpStanTypeToTypeScriptNodeActionTest.php index f46b81ab..df807c18 100644 --- a/tests/Actions/TranspilePhpStanTypeToTypeScriptNodeActionTest.php +++ b/tests/Actions/TranspilePhpStanTypeToTypeScriptNodeActionTest.php @@ -3,7 +3,7 @@ use Illuminate\Support\Collection; use Spatie\TypeScriptTransformer\Actions\TranspilePhpStanTypeToTypeScriptNodeAction; use Spatie\TypeScriptTransformer\References\ClassStringReference; -use Spatie\TypeScriptTransformer\Tests\Stubs\PhpDocTypesStub; +use Spatie\TypeScriptTransformer\Tests\Fakes\PropertyTypes\PhpDocTypesStub; use Spatie\TypeScriptTransformer\TypeResolvers\DocTypeResolver; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAny; diff --git a/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php b/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php index 96a60019..922601d4 100644 --- a/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php +++ b/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php @@ -4,7 +4,7 @@ use Illuminate\Support\Collection; use Spatie\TypeScriptTransformer\Actions\TranspileReflectionTypeToTypeScriptNodeAction; use Spatie\TypeScriptTransformer\References\ClassStringReference; -use Spatie\TypeScriptTransformer\Tests\Stubs\PhpTypesStub; +use Spatie\TypeScriptTransformer\Tests\Fakes\PropertyTypes\PhpTypesStub; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAny; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptArray; diff --git a/tests/Actions/WriteFilesActionTest.php b/tests/Actions/WriteFilesActionTest.php new file mode 100644 index 00000000..9052aa47 --- /dev/null +++ b/tests/Actions/WriteFilesActionTest.php @@ -0,0 +1,30 @@ +temporaryDirectory = TemporaryDirectory::make(); +}); + +it('can write files in a directory', function () { + $fileA = new WriteableFile($this->temporaryDirectory->path('fileA.ts'), 'fileA contents'); + $fileB = new WriteableFile($this->temporaryDirectory->path('fileB.ts'), 'fileB contents'); + + (new WriteFilesAction(TypeScriptTransformerConfigFactory::create()->get()))->execute([$fileA, $fileB]); + + expect(file_get_contents($fileA->path))->toBe('fileA contents'); + expect(file_get_contents($fileB->path))->toBe('fileB contents'); +}); + +it('can write files in a directory with subdirectories', function () { + $fileA = new WriteableFile($this->temporaryDirectory->path('sub/fileA.ts'), 'fileA contents'); + $fileB = new WriteableFile($this->temporaryDirectory->path('sub/sub2/fileB.ts'), 'fileB contents'); + + (new WriteFilesAction(TypeScriptTransformerConfigFactory::create()->get()))->execute([$fileA, $fileB]); + + expect(file_get_contents($fileA->path))->toBe('fileA contents'); + expect(file_get_contents($fileB->path))->toBe('fileB contents'); +}); diff --git a/tests/Fakes/Circular/CircularA.php b/tests/Fakes/Circular/CircularA.php new file mode 100644 index 00000000..a2179914 --- /dev/null +++ b/tests/Fakes/Circular/CircularA.php @@ -0,0 +1,8 @@ +typeScriptNode->type; + + [$propertyNode] = array_values(array_filter( + $object->properties, + fn (TypeScriptProperty $propertyNode) => $propertyNode->name instanceof TypeScriptIdentifier && $propertyNode->name->name === $property + )); + $propertyNode = (new ReplaceLaravelCollectionByArrayClassPropertyProcessor())->execute( reflection: new ReflectionProperty($class, $property), annotation: null, - property: resolvePropertyNode($class, $property) + property: $propertyNode ); expect($propertyNode->type)->toEqual( diff --git a/tests/Pest.php b/tests/Pest.php index b3826bfc..3e0b522e 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -2,37 +2,36 @@ use Spatie\TypeScriptTransformer\Actions\ConnectReferencesAction; use Spatie\TypeScriptTransformer\Actions\TransformTypesAction; -use Spatie\TypeScriptTransformer\Support\TransformationContext; use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Tests\Support\AllClassTransformer; +use Spatie\TypeScriptTransformer\Tests\Support\MemoryWriter; use Spatie\TypeScriptTransformer\Transformed\Transformed; -use Spatie\TypeScriptTransformer\Transformers\ClassTransformer; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -use Spatie\TypeScriptTransformer\Writers\NamespaceWriter; +use Spatie\TypeScriptTransformer\Transformed\Untransformable; +use Spatie\TypeScriptTransformer\Transformers\Transformer; function classesToTypeScript( array $classes, - ?TransformationContext $transformationContext = null, + ?Transformer $transformer = null ): string { $collection = new TransformedCollection(); foreach ($classes as $class) { - $collection->add(transformClass($class, $transformationContext)); + $collection->add(transformSingle($class, $transformer)); } $referenceMap = (new ConnectReferencesAction())->execute($collection); - $writeableFile = (new NamespaceWriter('fakeFile'))->output($collection, $referenceMap)[0]; + $writer = new MemoryWriter(); - return $writeableFile->contents; + ($writer)->output($collection, $referenceMap); + + return $writer->getOutput(); } -function transformClass( +function transformSingle( string|object $class, - ?ClassTransformer $transformer = null -): Transformed { + ?Transformer $transformer = null +): Transformed|Untransformable { $transformer ??= new AllClassTransformer(); $transformTypesAction = new TransformTypesAction(); @@ -42,28 +41,5 @@ function transformClass( [is_string($class) ? $class : $class::class], ); - return $transformed; -} - -function resolveObjectNode( - string|object $class, - ?ClassTransformer $transformer = null -): TypeScriptObject { - return transformClass($class, $transformer)->typeScriptNode->type; -} - -function resolvePropertyNode( - string|object $class, - string $property, - ?ClassTransformer $transformer = null -): TypeScriptProperty { - $objectNode = resolveObjectNode($class, $transformer); - - foreach ($objectNode->properties as $propertyNode) { - if ($propertyNode->name instanceof TypeScriptIdentifier && $propertyNode->name->name === $property) { - return $propertyNode; - } - } - - throw new Exception("Could not find node for property {$property}"); + return $transformed ?? Untransformable::create(); } diff --git a/tests/Support/MemoryWriter.php b/tests/Support/MemoryWriter.php index b39e8c31..7c2293b0 100644 --- a/tests/Support/MemoryWriter.php +++ b/tests/Support/MemoryWriter.php @@ -5,7 +5,7 @@ use Spatie\TypeScriptTransformer\Collections\ReferenceMap; use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\Writers\NamespaceWriter; +use Spatie\TypeScriptTransformer\Writers\FlatWriter; use Spatie\TypeScriptTransformer\Writers\Writer; class MemoryWriter implements Writer @@ -33,7 +33,7 @@ public function getTransformedNodeByName(string $name): ?TypeScriptNode public function getOutput(): string { - $writer = new NamespaceWriter('test.ts'); + $writer = new FlatWriter('test.ts'); [$writeableFile] = $writer->output(static::$collection, static::$referenceMap); diff --git a/tests/Support/TransformationContextTest.php b/tests/Support/TransformationContextTest.php new file mode 100644 index 00000000..61d756b5 --- /dev/null +++ b/tests/Support/TransformationContextTest.php @@ -0,0 +1,41 @@ +name)->toBe('SimpleClass'); + expect($context->nameSpaceSegments)->toBe(['Spatie', 'TypeScriptTransformer', 'Tests', 'Fakes', 'TypesToProvide']); + expect($context->optional)->toBeFalse(); +}); + +it('can make a class optional by attribute in its context', function () { + $reflection = new ReflectionClass(OptionalAttributedClass::class); + + $context = TransformationContext::createFromReflection($reflection); + + expect($context->optional)->toBeTrue(); +}); + +it('can set the name by attribute', function () { + $reflection = new ReflectionClass(TypeScriptAttributedClass::class); + + $context = TransformationContext::createFromReflection($reflection); + + expect($context->name)->toBe('JustAnotherName'); +}); + +it('can set the location by attribute', function () { + $reflection = new ReflectionClass(TypeScriptLocationAttributedClass::class); + + $context = TransformationContext::createFromReflection($reflection); + + expect($context->nameSpaceSegments)->toBe(['App', 'Here']); +}); diff --git a/tests/Transformers/ClassTransformerTest.php b/tests/Transformers/ClassTransformerTest.php index ff0d3b6f..9a33f655 100644 --- a/tests/Transformers/ClassTransformerTest.php +++ b/tests/Transformers/ClassTransformerTest.php @@ -10,6 +10,7 @@ use Spatie\TypeScriptTransformer\Attributes\Optional; use Spatie\TypeScriptTransformer\Attributes\TypeScriptType; use Spatie\TypeScriptTransformer\References\ReflectionClassReference; +use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\ReadonlyClass; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\SimpleClass; use Spatie\TypeScriptTransformer\Tests\Support\AllClassTransformer; use Spatie\TypeScriptTransformer\Transformers\ClassPropertyProcessors\ClassPropertyProcessor; @@ -24,7 +25,7 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnknown; it('can transform a class', function () { - $transformed = transformClass(SimpleClass::class); + $transformed = transformSingle(SimpleClass::class); expect($transformed->getName())->toBe('SimpleClass'); expect($transformed->typeScriptNode)->toEqual( @@ -56,7 +57,7 @@ class TestTypeScriptTypeAttributeContractForClass { } - $transformed = transformClass(TestTypeScriptTypeAttributeContractForClass::class); + $transformed = transformSingle(TestTypeScriptTypeAttributeContractForClass::class); expect($transformed->typeScriptNode)->toEqual( new TypeScriptAlias( @@ -81,7 +82,7 @@ class TestTypeScriptTypeAttributeContractForClass private static string $privateStatic; }; - expect(resolveObjectNode($class))->toEqual( + expect(transformSingle($class)->typeScriptNode->type)->toEqual( new TypeScriptObject([ new TypeScriptProperty( new TypeScriptIdentifier('public'), @@ -97,7 +98,7 @@ class TestTypeScriptTypeAttributeContractForClass public string $name; }; - expect(resolveObjectNode($class))->toEqual( + expect(transformSingle($class)->typeScriptNode->type)->toEqual( new TypeScriptObject([ new TypeScriptProperty( new TypeScriptIdentifier('name'), @@ -113,7 +114,7 @@ class TestTypeScriptTypeAttributeContractForClass public $name; }; - expect(resolveObjectNode($class))->toEqual( + expect(transformSingle($class)->typeScriptNode->type)->toEqual( new TypeScriptObject([ new TypeScriptProperty( new TypeScriptIdentifier('name'), @@ -135,7 +136,7 @@ public function __construct( } }; - expect(resolveObjectNode($class))->toEqual( + expect(transformSingle($class)->typeScriptNode->type)->toEqual( new TypeScriptObject([ new TypeScriptProperty( new TypeScriptIdentifier('name'), @@ -154,7 +155,7 @@ class TestClassPropertyAnnotation public $name; } - expect(resolveObjectNode(TestClassPropertyAnnotation::class))->toEqual( + expect(transformSingle(TestClassPropertyAnnotation::class)->typeScriptNode->type)->toEqual( new TypeScriptObject([ new TypeScriptProperty( new TypeScriptIdentifier('name'), @@ -170,7 +171,7 @@ class TestClassPropertyAnnotation public $name; }; - expect(resolveObjectNode($class))->toEqual( + expect(transformSingle($class)->typeScriptNode->type)->toEqual( new TypeScriptObject([ new TypeScriptProperty( new TypeScriptIdentifier('name'), @@ -186,7 +187,7 @@ class TestClassPropertyAnnotation public string $name; }; - expect(resolveObjectNode($class))->toEqual( + expect(transformSingle($class)->typeScriptNode->type)->toEqual( new TypeScriptObject([ new TypeScriptProperty( new TypeScriptIdentifier('name'), @@ -205,7 +206,7 @@ class TestAllPropertiesOptionalByClassAttribute public int $age; } - expect(resolveObjectNode(TestAllPropertiesOptionalByClassAttribute::class))->toEqual( + expect(transformSingle(TestAllPropertiesOptionalByClassAttribute::class)->typeScriptNode->type)->toEqual( new TypeScriptObject([ new TypeScriptProperty( new TypeScriptIdentifier('name'), @@ -226,7 +227,7 @@ class TestAllPropertiesOptionalByClassAttribute public $name; }; - expect(resolveObjectNode($class))->toEqual( + expect(transformSingle($class)->typeScriptNode->type)->toEqual( new TypeScriptObject([ new TypeScriptProperty( new TypeScriptIdentifier('name'), @@ -241,7 +242,7 @@ class TestAllPropertiesOptionalByClassAttribute public readonly string $name; }; - expect(resolveObjectNode($class))->toEqual( + expect(transformSingle($class)->typeScriptNode->type)->toEqual( new TypeScriptObject([ new TypeScriptProperty( new TypeScriptIdentifier('name'), @@ -253,18 +254,16 @@ class TestAllPropertiesOptionalByClassAttribute }); it('can make a TypeScript property readonly by adding the modifier to the class', function () { - $class = eval('$class = new readonly class {public string $name;};'); - - expect(resolveObjectNode($class))->toEqual( + expect(transformSingle(ReadonlyClass::class)->typeScriptNode->type)->toEqual( new TypeScriptObject([ new TypeScriptProperty( - new TypeScriptIdentifier('name'), + new TypeScriptIdentifier('property'), new TypeScriptString(), isReadonly: true ), ]) ); -})->skip(fn () => PHP_VERSION_ID < 80300); +}); it('can hide a property by adding a hidden attribute', function () { $class = new class () { @@ -272,7 +271,7 @@ class TestAllPropertiesOptionalByClassAttribute public string $property; }; - expect(resolveObjectNode($class))->toEqual( + expect(transformSingle($class)->typeScriptNode->type)->toEqual( new TypeScriptObject([]) ); }); @@ -282,7 +281,7 @@ class TestAllPropertiesOptionalByClassAttribute public string $name; }; - $object = resolveObjectNode($class, transformer: new class () extends AllClassTransformer { + $object = transformSingle($class, transformer: new class () extends AllClassTransformer { protected function classPropertyProcessors(): array { return [ @@ -299,7 +298,7 @@ public function execute(ReflectionProperty $reflection, ?TypeNode $annotation, T }, ]; } - }); + })->typeScriptNode->type; expect($object)->toEqual( new TypeScriptObject([ @@ -318,7 +317,7 @@ public function execute(ReflectionProperty $reflection, ?TypeNode $annotation, T public string $name; }; - $object = resolveObjectNode($class, transformer: new class () extends ClassTransformer { + $object = transformSingle($class, transformer: new class () extends ClassTransformer { protected function shouldTransform(ReflectionClass $reflection): bool { return true; @@ -335,7 +334,7 @@ public function execute(ReflectionProperty $reflection, ?TypeNode $annotation, T }, ]; } - }); + })->typeScriptNode->type; expect($object)->toEqual( new TypeScriptObject([]) diff --git a/tests/Transformers/EnumTransformerTest.php b/tests/Transformers/EnumTransformerTest.php new file mode 100644 index 00000000..97846921 --- /dev/null +++ b/tests/Transformers/EnumTransformerTest.php @@ -0,0 +1,68 @@ +toBeInstanceOf(Transformed::class); + expect(transformSingle(DateTime::class, new EnumTransformer()))->toBeInstanceOf(Untransformable::class); +}); + +it('does not transform a unit enum when using union enums', function () { + expect(transformSingle(UnitEnum::class, new EnumTransformer()))->toBeInstanceOf(Untransformable::class); +}); + +it('can transform an unit backed enum into a native enum', function () { + expect(classesToTypeScript([UnitEnum::class], new EnumTransformer(useUnionEnums: false))) + ->toBe(<<toBe('export type IntBackedEnum = 1 | 2 | 3 | 4;'.PHP_EOL); +}); + +it('can transform an int backed enum into a native enum', function () { + expect(classesToTypeScript([IntBackedEnum::class], new EnumTransformer(useUnionEnums: false))) + ->toBe(<<toBe('export type StringBackedEnum = "john" | "paul" | "george" | "ringo";' . PHP_EOL); +}); + +it('can transform a string backed enum into a native enum', function () { + expect(classesToTypeScript([StringBackedEnum::class], new EnumTransformer(useUnionEnums: false))) + ->toBe( + <<toHaveCount(3); + expect($collection)->toHaveCount(5); + ray(iterator_to_array($collection)); expect(iterator_to_array($collection))->sequence( fn (Expectation $transformed) => $transformed ->toBeInstanceOf(Transformed::class) @@ -66,6 +69,30 @@ function getTestProvidedTypes( ->reference->toBeInstanceOf(ReflectionClassReference::class) ->reference->classString->toBe(TypeScriptLocationAttributedClass::class) ->location->toBe(['App', 'Here']), + fn (Expectation $transformed) => $transformed + ->toBeInstanceOf(Transformed::class) + ->getName()->toBe('OptionalAttributedClass') + ->typeScriptNode->toEqual(new TypeScriptAlias( + new TypeScriptIdentifier('OptionalAttributedClass'), + new TypeScriptObject([ + new TypeScriptProperty('property', new TypeScriptString(), isOptional: true), + ]) + )) + ->reference->toBeInstanceOf(ReflectionClassReference::class) + ->reference->classString->toBe(OptionalAttributedClass::class) + ->location->toBe(['Spatie', 'TypeScriptTransformer', 'Tests', 'Fakes', 'TypesToProvide']), + fn (Expectation $transformed) => $transformed + ->toBeInstanceOf(Transformed::class) + ->getName()->toBe('ReadonlyClass') + ->typeScriptNode->toEqual(new TypeScriptAlias( + new TypeScriptIdentifier('ReadonlyClass'), + new TypeScriptObject([ + new TypeScriptProperty('property', new TypeScriptString(), isReadonly: true), + ]) + )) + ->reference->toBeInstanceOf(ReflectionClassReference::class) + ->reference->classString->toBe(ReadonlyClass::class) + ->location->toBe(['Spatie', 'TypeScriptTransformer', 'Tests', 'Fakes', 'TypesToProvide']), fn (Expectation $transformed) => $transformed ->toBeInstanceOf(Transformed::class) ->getName()->toBe('SimpleClass') diff --git a/tests/TypeScript/TypeScriptEnumTest.php b/tests/TypeScript/TypeScriptEnumTest.php new file mode 100644 index 00000000..dcee8a6b --- /dev/null +++ b/tests/TypeScript/TypeScriptEnumTest.php @@ -0,0 +1,70 @@ +write(new WritingContext(fn () => '')))->toBe($expected); +})->with(function () { + yield 'numeric enum without indexes' => [ + 'cases' => [ + ['name' => 'Up', 'value' => null], + ['name' => 'Down', 'value' => null], + ['name' => 'Left', 'value' => null], + ['name' => 'Right', 'value' => null], + ], + 'expected' => << [ + 'cases' => [ + ['name' => 'Up', 'value' => null], + ['name' => 'Down', 'value' => 3], + ['name' => 'Left', 'value' => null], + ['name' => 'Right', 'value' => null], + ], + 'expected' => << [ + 'cases' => [ + ['name' => 'Up', 'value' => 'up'], + ['name' => 'Down', 'value' => 'down'], + ['name' => 'Left', 'value' => 'left'], + ['name' => 'Right', 'value' => 'right'], + ], + 'expected' => <<path = '/some/path'; + + $this->writer = new FlatWriter($this->path); +}); + + +it('can write everything in one flat file', function () { + $transformedCollection = new TransformedCollection([ + TransformedFactory::alias('RootType', new TypeScriptString())->build(), + TransformedFactory::alias('RootType2', new TypeScriptString())->build(), + TransformedFactory::alias('Level1Type', new TypeScriptString(), location: ['level1'])->build(), + TransformedFactory::alias('Level1Type2', new TypeScriptString(), location: ['level1'])->build(), + TransformedFactory::alias('Level2Type', new TypeScriptString(), location: ['level1', 'level2'])->build(), + ]); + + [$file] = $this->writer->output( + $transformedCollection, + new ReferenceMap(), + ); + + expect($file) + ->toBeInstanceOf(WriteableFile::class) + ->path->toBe($this->path) + ->contents->toBe(<<build(), + TransformedFactory::alias('B', new TypeScriptString(), reference: $referenceB, location: ['nested', 'subNested'])->build(), + TransformedFactory::alias('C', new TypeScriptObject([ + new TypeScriptProperty('a', new TypeReference($referenceA)), + new TypeScriptProperty('b', new TypeReference($referenceB)), + ]))->build(), + ]); + + [$file] = $this->writer->output( + $transformedCollection, + (new ConnectReferencesAction())->execute($transformedCollection), + ); + + expect($file) + ->toBeInstanceOf(WriteableFile::class) + ->path->toBe($this->path) + ->contents->toBe(<<build(), + TransformedFactory::alias('RootType2', new TypeScriptString())->build(), + TransformedFactory::alias('Level1Type', new TypeScriptString(), location: ['level1'])->build(), + TransformedFactory::alias('Level1Type2', new TypeScriptString(), location: ['level1'])->build(), + TransformedFactory::alias('Level2Type', new TypeScriptString(), location: ['level1', 'level2'])->build(), + ]); + + $filename = 'types.ts'; + + $files = (new NamespaceWriter($filename))->output( + $transformedCollection, + new ReferenceMap(), + ); + + expect($files) + ->toHaveCount(1) + ->each->toBeInstanceOf(WriteableFile::class); + + $file = $files[0]; + + expect($file) + ->path->toBe($filename) + ->contents->toEqual( + <<build(), + TransformedFactory::alias('B', new TypeScriptString(), reference: $referenceB, location: ['nested', 'subNested'])->build(), + TransformedFactory::alias('C', new TypeScriptObject([ + new TypeScriptProperty('a', new TypeReference($referenceA)), + new TypeScriptProperty('b', new TypeReference($referenceB)), + ]))->build(), + ]); + + $filename = 'types.ts'; + + $files = (new NamespaceWriter($filename))->output( + $transformedCollection, + (new ConnectReferencesAction())->execute($transformedCollection), + ); + + expect($files) + ->toHaveCount(1) + ->each->toBeInstanceOf(WriteableFile::class); + + $file = $files[0]; + + expect($file) + ->path->toBe($filename) + ->contents->toEqual(<< }; - -} diff --git a/tests/__snapshots__/LiteralTypeScriptTypeTest__it_can_output_an_object_type__1.txt b/tests/__snapshots__/LiteralTypeScriptTypeTest__it_can_output_an_object_type__1.txt index af1795f7..530d0354 100644 --- a/tests/__snapshots__/LiteralTypeScriptTypeTest__it_can_output_an_object_type__1.txt +++ b/tests/__snapshots__/LiteralTypeScriptTypeTest__it_can_output_an_object_type__1.txt @@ -1,9 +1,6 @@ -declare namespace { export type TestObjectLiteralTypeScriptTypeAttribute = { property: { label: string value: string } }; - -} diff --git a/tests/__snapshots__/TypeScriptTypeTest__it_can_output_a_single_type__1.txt b/tests/__snapshots__/TypeScriptTypeTest__it_can_output_a_single_type__1.txt index 32231e03..4896da2b 100644 --- a/tests/__snapshots__/TypeScriptTypeTest__it_can_output_a_single_type__1.txt +++ b/tests/__snapshots__/TypeScriptTypeTest__it_can_output_a_single_type__1.txt @@ -1,13 +1,7 @@ -declare namespace { -export type TestSingleTypeScriptTypeAttribute = { -property: Record -}; - -} -declare namespace Spatie.TypeScriptTransformer.Support{ export type WriteableFile = { path: string contents: string }; - -} +export type TestSingleTypeScriptTypeAttribute = { +property: Record +}; diff --git a/tests/__snapshots__/TypeScriptTypeTest__it_can_output_an_object_type__1.txt b/tests/__snapshots__/TypeScriptTypeTest__it_can_output_an_object_type__1.txt index 16abc147..84743746 100644 --- a/tests/__snapshots__/TypeScriptTypeTest__it_can_output_an_object_type__1.txt +++ b/tests/__snapshots__/TypeScriptTypeTest__it_can_output_an_object_type__1.txt @@ -1,16 +1,10 @@ -declare namespace { -export type TestObjectTypeScriptTypeAttribute = { -property: { -name: string -file: Spatie.TypeScriptTransformer.Support.WriteableFile -} -}; - -} -declare namespace Spatie.TypeScriptTransformer.Support{ export type WriteableFile = { path: string contents: string }; - +export type TestObjectTypeScriptTypeAttribute = { +property: { +name: string +file: WriteableFile } +}; From ef88161ee537667ab02270e2849a054b18174d2a Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 25 Apr 2024 15:38:44 +0200 Subject: [PATCH 37/51] wip --- src/Transformers/InterfaceTransformer.php | 226 ++++++++++++++++++ src/TypeScript/TypeScriptInterface.php | 4 +- ...thod.php => TypeScriptInterfaceMethod.php} | 2 +- src/TypeScriptTransformer.php | 4 +- tests/Fakes/Integration/Enum.php | 9 + tests/Fakes/Integration/IntegrationClass.php | 82 +++++++ tests/Fakes/Integration/IntegrationItem.php | 8 + .../Fakes/Integration/Level/LevelUpClass.php | 8 + tests/Integration.php | 56 +++++ ...DataLazyTypeClassPropertyProcessorTest.php | 2 +- ...he_integration_test_with_a_flat_file__1.ts | 39 +++ ...gration_test_with_a_module_structure__1.ts | 38 +++ ...gration_test_with_a_module_structure__2.ts | 3 + ...egration_test_with_a_namespaced_file__1.ts | 43 ++++ 14 files changed, 519 insertions(+), 5 deletions(-) create mode 100644 src/Transformers/InterfaceTransformer.php rename src/TypeScript/{TypeScriptMethod.php => TypeScriptInterfaceMethod.php} (91%) create mode 100644 tests/Fakes/Integration/Enum.php create mode 100644 tests/Fakes/Integration/IntegrationClass.php create mode 100644 tests/Fakes/Integration/IntegrationItem.php create mode 100644 tests/Fakes/Integration/Level/LevelUpClass.php create mode 100644 tests/Integration.php create mode 100644 tests/__snapshots__/files/Integration__it_can_handle_the_integration_test_with_a_flat_file__1.ts create mode 100644 tests/__snapshots__/files/Integration__it_can_handle_the_integration_test_with_a_module_structure__1.ts create mode 100644 tests/__snapshots__/files/Integration__it_can_handle_the_integration_test_with_a_module_structure__2.ts create mode 100644 tests/__snapshots__/files/Integration__it_can_handle_the_integration_test_with_a_namespaced_file__1.ts diff --git a/src/Transformers/InterfaceTransformer.php b/src/Transformers/InterfaceTransformer.php new file mode 100644 index 00000000..7ee7f21f --- /dev/null +++ b/src/Transformers/InterfaceTransformer.php @@ -0,0 +1,226 @@ +isEnum()) { + return Untransformable::create(); + } + + if (! $this->shouldTransform($reflectionClass)) { + return Untransformable::create(); + } + + return new Transformed( + new TypeScriptAlias( + new TypeScriptIdentifier($context->name), + $this->getTypeScriptNode($reflectionClass, $context) + ), + new ReflectionClassReference($reflectionClass), + $context->nameSpaceSegments, + true, + ); + } + + abstract protected function shouldTransform(ReflectionClass $reflection): bool; + + protected function getTypeScriptNode( + ReflectionClass $reflectionClass, + TransformationContext $context, + ): TypeScriptNode { + if ($resolvedAttributeType = $this->resolveTypeByAttribute($reflectionClass)) { + return $resolvedAttributeType; + } + + $constructorAnnotations = $reflectionClass->hasMethod('__construct') + ? $this->docTypeResolver->method($reflectionClass->getMethod('__construct'))?->parameters ?? [] + : []; + + $properties = []; + + foreach ($this->getMethods($reflectionClass) as $reflectionMethod) { + $property = $this->createProperty( + $reflectionClass, + $reflectionMethod, + $annotation?->type, + $context + ); + + if ($property === null) { + continue; + } + + $property = $this->runClassPropertyProcessors( + $reflectionMethod, + $annotation?->type, + $property + ); + + if ($property !== null) { + $properties[] = $property; + } + } + + return new Type($properties); + } + + protected function resolveTypeByAttribute( + ReflectionClass $reflectionClass, + ?ReflectionProperty $property = null, + ): ?TypeScriptNode { + $subject = $property ?? $reflectionClass; + + foreach ($subject->getAttributes() as $attribute) { + if (is_a($attribute->getName(), TypeScriptTypeAttributeContract::class, true)) { + /** @var TypeScriptTypeAttributeContract $attributeInstance */ + $attributeInstance = $attribute->newInstance(); + + return $attributeInstance->getType($reflectionClass); + } + } + + return null; + } + + protected function getMethods(ReflectionClass $reflection): array + { + return array_filter( + $reflection->getMethods(), + fn (ReflectionMethod $method) => ! $method->isStatic() + ); + } + + protected function createProperty( + ReflectionClass $reflectionClass, + ReflectionProperty $reflectionProperty, + ?TypeNode $annotation, + TransformationContext $context, + ): ?TypeScriptProperty { + $type = $this->resolveTypeForProperty( + $reflectionClass, + $reflectionProperty, + $annotation + ); + + $property = new TypeScriptProperty( + $reflectionProperty->getName(), + $type, + $this->isPropertyOptional( + $reflectionProperty, + $reflectionClass, + $type, + $context + ), + $this->isPropertyReadonly( + $reflectionProperty, + $reflectionClass, + $type, + ) + ); + + if ($this->isPropertyHidden($reflectionProperty, $reflectionClass, $property)) { + return null; + } + + return $property; + } + + protected function resolveTypeForProperty( + ReflectionClass $reflectionClass, + ReflectionProperty $reflectionProperty, + ?TypeNode $annotation, + ): TypeScriptNode { + if ($resolvedAttributeType = $this->resolveTypeByAttribute($reflectionClass, $reflectionProperty)) { + return $resolvedAttributeType; + } + + if ($annotation) { + return $this->transpilePhpStanTypeToTypeScriptTypeAction->execute( + $annotation, + $reflectionClass, + ); + } + + if ($reflectionProperty->hasType()) { + return $this->transpileReflectionTypeToTypeScriptTypeAction->execute( + $reflectionProperty->getType(), + $reflectionClass + ); + } + + return new TypeScriptUnknown(); + } + + protected function isPropertyOptional( + ReflectionProperty $reflectionProperty, + ReflectionClass $reflectionClass, + TypeScriptNode $type, + TransformationContext $context, + ): bool { + return $context->optional || count($reflectionProperty->getAttributes(Optional::class)) > 0; + } + + protected function isPropertyReadonly( + ReflectionProperty $reflectionProperty, + ReflectionClass $reflectionClass, + TypeScriptNode $type, + ): bool { + return $reflectionProperty->isReadOnly() || $reflectionClass->isReadOnly(); + } + + protected function isPropertyHidden( + ReflectionProperty $reflectionProperty, + ReflectionClass $reflectionClass, + TypeScriptProperty $property, + ): bool { + return count($reflectionProperty->getAttributes(Hidden::class)) > 0; + } + + protected function runClassPropertyProcessors( + ReflectionProperty $reflectionProperty, + ?TypeNode $annotation, + TypeScriptProperty $property, + ): ?TypeScriptProperty { + $processors = $this->classPropertyProcessors; + + foreach ($processors as $processor) { + $property = $processor->execute($reflectionProperty, $annotation, $property); + + if ($property === null) { + return null; + } + } + + return $property; + } +} diff --git a/src/TypeScript/TypeScriptInterface.php b/src/TypeScript/TypeScriptInterface.php index d1e165c3..b28c5bf5 100644 --- a/src/TypeScript/TypeScriptInterface.php +++ b/src/TypeScript/TypeScriptInterface.php @@ -9,7 +9,7 @@ class TypeScriptInterface implements TypeScriptForwardingExportableNode, TypeScr { /** * @param array $properties - * @param array $methods + * @param array $methods */ public function __construct( public TypeScriptIdentifier $name, @@ -24,7 +24,7 @@ public function write(WritingContext $context): string $items = array_reduce( $combined, - fn (string $carry, TypeScriptProperty|TypeScriptMethod $item) => $carry.$item->write($context).PHP_EOL, + fn (string $carry, TypeScriptProperty|TypeScriptInterfaceMethod $item) => $carry.$item->write($context).PHP_EOL, empty($combined) ? '' : PHP_EOL ); diff --git a/src/TypeScript/TypeScriptMethod.php b/src/TypeScript/TypeScriptInterfaceMethod.php similarity index 91% rename from src/TypeScript/TypeScriptMethod.php rename to src/TypeScript/TypeScriptInterfaceMethod.php index ea946382..0ca2c3a9 100644 --- a/src/TypeScript/TypeScriptMethod.php +++ b/src/TypeScript/TypeScriptInterfaceMethod.php @@ -5,7 +5,7 @@ use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptMethod implements TypeScriptNode, TypeScriptVisitableNode +class TypeScriptInterfaceMethod implements TypeScriptNode, TypeScriptVisitableNode { /** * @param array $parameters diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index ff7e1a47..68a87376 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -22,8 +22,10 @@ public function __construct( } - public static function create(TypeScriptTransformerConfig $config): self + public static function create(TypeScriptTransformerConfig|TypeScriptTransformerConfigFactory $config): self { + $config = $config instanceof TypeScriptTransformerConfigFactory ? $config->get() : $config; + return new self( $config, new DiscoverTypesAction(), diff --git a/tests/Fakes/Integration/Enum.php b/tests/Fakes/Integration/Enum.php new file mode 100644 index 00000000..10517116 --- /dev/null +++ b/tests/Fakes/Integration/Enum.php @@ -0,0 +1,9 @@ + */ + public $complex_union; + + public Enum $enum; + + public Exception $non_typescript_type; + + /** @var IntegrationItem[] */ + public array $array_of_reference; + + public DateTime $replacement_type; + + /** @var \DateTime */ + public $annotated_replacement_type; + + /** @var \DateTime[] */ + public array $array_annotated_replacement_type; + + public LevelUpClass $level_up_class; + + #[Hidden] + public string $hidden; + + public readonly string $readonly; + + #[Optional] + public string $optional; + + /** + * @param array $constructor_annotated_array + */ + public function __construct( + public array $constructor_annotated_array, + /** @var array */ + public array $constructor_inline_annotated_array, + ) { + } +} diff --git a/tests/Fakes/Integration/IntegrationItem.php b/tests/Fakes/Integration/IntegrationItem.php new file mode 100644 index 00000000..1a7bd1ff --- /dev/null +++ b/tests/Fakes/Integration/IntegrationItem.php @@ -0,0 +1,8 @@ +temporaryDirectory = TemporaryDirectory::make(); +}); + +it('can handle the integration test with a flat file', function () { + $config = TypeScriptTransformerConfigFactory::create() + ->transformer(new EnumTransformer()) + ->transformer(new AllClassTransformer()) + ->watchDirectories(__DIR__ . '/Fakes/Integration') + ->replaceType(DateTime::class, 'string') + ->writer(new FlatWriter($this->temporaryDirectory->path('flat.d.ts'))); + + TypeScriptTransformer::create($config)->execute(watch: false); + + assertMatchesFileSnapshot($this->temporaryDirectory->path('flat.d.ts')); +}); + +it('can handle the integration test with a namespaced file', function () { + $config = TypeScriptTransformerConfigFactory::create() + ->transformer(new EnumTransformer()) + ->transformer(new AllClassTransformer()) + ->watchDirectories(__DIR__ . '/Fakes/Integration') + ->replaceType(DateTime::class, 'string') + ->writer(new NamespaceWriter($this->temporaryDirectory->path('flat.d.ts'))); + + TypeScriptTransformer::create($config)->execute(watch: false); + + assertMatchesFileSnapshot($this->temporaryDirectory->path('flat.d.ts')); +}); + +it('can handle the integration test with a module structure', function () { + $config = TypeScriptTransformerConfigFactory::create() + ->transformer(new EnumTransformer()) + ->transformer(new AllClassTransformer()) + ->watchDirectories(__DIR__ . '/Fakes/Integration') + ->replaceType(DateTime::class, 'string') + ->writer(new ModuleWriter($this->temporaryDirectory->path())); + + TypeScriptTransformer::create($config)->execute(watch: false); + + assertMatchesFileSnapshot($this->temporaryDirectory->path('Spatie/TypeScriptTransformer/Tests/Fakes/Integration/index.ts')); + assertMatchesFileSnapshot($this->temporaryDirectory->path('Spatie/TypeScriptTransformer/Tests/Fakes/Integration/Level/index.ts')); +}); diff --git a/tests/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessorTest.php b/tests/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessorTest.php index 2f30131a..e9b05724 100644 --- a/tests/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessorTest.php +++ b/tests/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessorTest.php @@ -1,3 +1,3 @@ todo(); diff --git a/tests/__snapshots__/files/Integration__it_can_handle_the_integration_test_with_a_flat_file__1.ts b/tests/__snapshots__/files/Integration__it_can_handle_the_integration_test_with_a_flat_file__1.ts new file mode 100644 index 00000000..aaedb671 --- /dev/null +++ b/tests/__snapshots__/files/Integration__it_can_handle_the_integration_test_with_a_flat_file__1.ts @@ -0,0 +1,39 @@ +export type IntegrationClass = { +string: string +nullable: string | null +default: string +int: number +boolean: boolean +float: number +object: object +array: [] +mixed: any +none: unknown +var_annotated: string +union: number | string +annotated_array: Array +complex_annotated_array: { +int: number +string: string +level_up: LevelUpClass +} +complex_union: number | string | Array +enum: Enum +non_typescript_type: undefined +array_of_reference: Array +replacement_type: string +annotated_replacement_type: string +array_annotated_replacement_type: Array +level_up_class: LevelUpClass +readonly readonly: string +optional?: string +constructor_annotated_array: Array +constructor_inline_annotated_array: Array +}; +export type Enum = "yes" | "no"; +export type IntegrationItem = { +name: string +}; +export type LevelUpClass = { +name: string +}; diff --git a/tests/__snapshots__/files/Integration__it_can_handle_the_integration_test_with_a_module_structure__1.ts b/tests/__snapshots__/files/Integration__it_can_handle_the_integration_test_with_a_module_structure__1.ts new file mode 100644 index 00000000..4e822f1c --- /dev/null +++ b/tests/__snapshots__/files/Integration__it_can_handle_the_integration_test_with_a_module_structure__1.ts @@ -0,0 +1,38 @@ +import { LevelUpClass } from 'Level'; + +export type Enum = "yes" | "no"; +export type IntegrationClass = { +string: string +nullable: string | null +default: string +int: number +boolean: boolean +float: number +object: object +array: [] +mixed: any +none: unknown +var_annotated: string +union: number | string +annotated_array: Array +complex_annotated_array: { +int: number +string: string +level_up: LevelUpClass +} +complex_union: number | string | Array +enum: Enum +non_typescript_type: undefined +array_of_reference: Array +replacement_type: string +annotated_replacement_type: string +array_annotated_replacement_type: Array +level_up_class: LevelUpClass +readonly readonly: string +optional?: string +constructor_annotated_array: Array +constructor_inline_annotated_array: Array +}; +export type IntegrationItem = { +name: string +}; diff --git a/tests/__snapshots__/files/Integration__it_can_handle_the_integration_test_with_a_module_structure__2.ts b/tests/__snapshots__/files/Integration__it_can_handle_the_integration_test_with_a_module_structure__2.ts new file mode 100644 index 00000000..2e3ce50e --- /dev/null +++ b/tests/__snapshots__/files/Integration__it_can_handle_the_integration_test_with_a_module_structure__2.ts @@ -0,0 +1,3 @@ +export type LevelUpClass = { +name: string +}; diff --git a/tests/__snapshots__/files/Integration__it_can_handle_the_integration_test_with_a_namespaced_file__1.ts b/tests/__snapshots__/files/Integration__it_can_handle_the_integration_test_with_a_namespaced_file__1.ts new file mode 100644 index 00000000..0b2aabae --- /dev/null +++ b/tests/__snapshots__/files/Integration__it_can_handle_the_integration_test_with_a_namespaced_file__1.ts @@ -0,0 +1,43 @@ +declare namespace Spatie.TypeScriptTransformer.Tests.Fakes.Integration{ +export type Enum = "yes" | "no"; +export type IntegrationClass = { +string: string +nullable: string | null +default: string +int: number +boolean: boolean +float: number +object: object +array: [] +mixed: any +none: unknown +var_annotated: string +union: number | string +annotated_array: Array +complex_annotated_array: { +int: number +string: string +level_up: Spatie.TypeScriptTransformer.Tests.Fakes.Integration.Level.LevelUpClass +} +complex_union: number | string | Array +enum: Spatie.TypeScriptTransformer.Tests.Fakes.Integration.Enum +non_typescript_type: undefined +array_of_reference: Array +replacement_type: string +annotated_replacement_type: string +array_annotated_replacement_type: Array +level_up_class: Spatie.TypeScriptTransformer.Tests.Fakes.Integration.Level.LevelUpClass +readonly readonly: string +optional?: string +constructor_annotated_array: Array +constructor_inline_annotated_array: Array +}; +export type IntegrationItem = { +name: string +}; +} +declare namespace Spatie.TypeScriptTransformer.Tests.Fakes.Integration.Level{ +export type LevelUpClass = { +name: string +}; +} From 241795f957843f21cd313c5a9490f56156fb5462 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 13 Jun 2024 17:24:43 +0200 Subject: [PATCH 38/51] wip --- README.md | 342 +++++++++++++++++- src/Transformed/Transformed.php | 18 +- src/TypeScript/TypeReference.php | 4 +- src/TypeScript/TypeScriptAlias.php | 4 +- src/TypeScript/TypeScriptEnum.php | 4 +- src/TypeScript/TypeScriptExport.php | 2 +- src/TypeScript/TypeScriptExportableNode.php | 8 - .../TypeScriptForwardingExportableNode.php | 8 - .../TypeScriptForwardingNamedNode.php | 8 + .../TypeScriptFunctionDefinition.php | 4 +- src/TypeScript/TypeScriptGeneric.php | 4 +- src/TypeScript/TypeScriptIdentifier.php | 4 +- src/TypeScript/TypeScriptInterface.php | 4 +- src/TypeScript/TypeScriptNamedNode.php | 8 + src/TypeScriptTransformer.php | 25 +- 15 files changed, 390 insertions(+), 57 deletions(-) delete mode 100644 src/TypeScript/TypeScriptExportableNode.php delete mode 100644 src/TypeScript/TypeScriptForwardingExportableNode.php create mode 100644 src/TypeScript/TypeScriptForwardingNamedNode.php create mode 100644 src/TypeScript/TypeScriptNamedNode.php diff --git a/README.md b/README.md index 78c6d686..fae8654c 100644 --- a/README.md +++ b/README.md @@ -784,10 +784,213 @@ protected function classPropertyProcessors(): array } ``` +A class property processor can also be used to remove properties from the TypeScript object: + +```php +class RemoveAllStrings implements ClassPropertyProcessor +{ + public function execute( + ReflectionProperty $reflection, + ?TypeNode $annotation, + TypeScriptProperty $property + ): ?TypeScriptProperty { + if ($property->type instanceof TypeScriptString) { + return null; + } + + return $property; + } +} +``` + ## Creating a TypesProvider +Until now we've only taken a look at transforming PHP classes to TypeScript, but what if you want to transform something +else? This is where the `TypesProvider` comes in, it is a class that provides TypeScript types. The transformers +we've seen before are actually bundled in a default `TypesProvider` provided by the package. + +A `TypesProvider` implements the `TypeProvider` interface: + +```php +namespace Spatie\TypeScriptTransformer\TypeProviders; + +use Spatie\TypeScriptTransformer\Support\TransformedCollection; +use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; + +interface TypesProvider +{ + public function provide( + TypeScriptTransformerConfig $config, + TransformedCollection $types + ): void; +} +``` + +The `provide` method is called when the TypeScript transformer is executed, it should add `Transformed` objects to the +collection provided. We could for example add a generic type which transforms Laravel collections: + +```php +class AddLaravelCollectionProvider implements TypesProvider +{ + public function provide( + TypeScriptTransformerConfig $config, + TransformedCollection $types + ): void { + $types->add(new Transformed( + typeScriptNode: new TypeScriptAlias( + new TypeScriptGeneric( + new TypeScriptIdentifier('Collection'), + [new TypeScriptIdentifier('T')], + ), + new TypeScriptGeneric( + new TypeScriptIdentifier('Array'), + [new TypeScriptIdentifier('T')], + ), + ), + reference: new ClassStringReference(Collection::class), + location: ['Illuminate', 'Support'] + )); + } +} +``` + +When we register the provider as such in the configuration: + +```php +$config->addProvider(new AddLaravelCollectionProvider()); +``` + +Our transformed TypeScript will have the following type: + +```ts +namespace Illuminate.Support { + export type Collection = Array; +} +``` + +When referencing a Laravel collection in one of our PHP classes like this: + +```php +class Data +{ + /** @var Collection */ + public Collection $collection; +} +``` + +The transformed TypeScript will look like this: + +```ts +export type Data = { + collection: Illuminate.Support.Collection; +} +``` + +## Referencing types + +Types sometimes reference other types like PHP classes referencing other PHP classes. Within the package a concept of +references is used to link these types together. + +When creating `Transformed` objects we've always used the `ClassStringReference` since we were referencing PHP classes, +sometimes you might be transforming something which is not a PHP class for example a list of strings. In this case, you +can use a `CustomReference`: + +```php +use Spatie\TypeScriptTransformer\References\CustomReference; + +new Transformed( + typeScriptNode: new TypeScriptAlias( + new TypeScriptIdentifier('Type'), + new TypeScriptUnion([new TypeScriptLiteral('PHP'), new TypeScriptLiteral('TypeScript')]), + ), + reference: new CustomReference('my_languages_package', 'some_languages'), + location: ['App', 'Languages'], +); +``` + +A custom reference should be unique for each type, that's why it is built up from a group and a name. We advise you when +creating a package (or if you're implementing a feature within your app) to choose a custom group name in order not to +conflict with other packages. + +In the end the transformed TypeScript will look like this: + +```ts +namespace App.Languages { + export type Type = 'PHP' | 'TypeScript'; +} +``` + +It is possible to reference this type in another `Transformed` object: + +```php +new Transformed( + typeScriptNode: new TypeScriptAlias( + new TypeScriptIdentifier('Compiler'), + new TypeScriptObject([ + new TypeScriptProperty('type', new CustomTypeReference('my_languages_package', 'some_languages')), + ]), + ), + reference: new ClassStringReference(Compiler::class), + location: ['App', 'Compilers'], +); +``` + +The transformed TypeScript now will look like this: + +```ts +namespace App.Compilers { + export type Compiler = { + type: App.Languages.Type; + }; +} +``` + +Since we're using the same reference, the package is smart enough to link them together when transforming to TypeScript. + +Off course, you can also reference PHP classes in the same way: + +```php +new Transformed( + typeScriptNode: new TypeScriptAlias( + new TypeScriptIdentifier('Post'), + new TypeScriptObject([ + new TypeScriptProperty('publisher', new TypeScriptReference(new ClassStringReference(User::class))), + ]), + ), + reference: new ClassStringReference(User::class), + location: ['App', 'Models'], +); +``` + ## Formatting TypeScript +The package tries to format the transformed TypeScript as good as possible, but sometimes this could be far from +perfect. That's why it is possible to automatically format the TypeScript code after transforming. + +By default, the package has support for two formatters: + +- `PrettierFormatter`: Formats the TypeScript code using Prettier +- `EslintFormatter`: Formats the TypeScript code using ESLint + +You can add a formatter to the configuration like this: + +```php +use Spatie\TypeScriptTransformer\Formatters\PrettierFormatter; + +$config->formatter(new PrettierFormatter()); +``` + +It is possible to create your own formatter by implementing the `Formatter` interface: + +```php +interface Formatter +{ + public function format(array $files): void; +} +``` + +The `$files` array contains the TypeScript files that need to be formatted, you can format them in any way you like. + ## Laravel ### Getting routes as TypeScript @@ -796,14 +999,147 @@ protected function classPropertyProcessors(): array ## Advanced concepts -### Building your own Writer - -### Building your own Formatter +The package is highly configurable and can be extended in many ways, let's take a look at some advanced concepts. ### Building your own TypeScript node +The package comes with a lot of TypeScript nodes, but sometimes it might be necessary to build your own. + +A TypeScript node is a regular PHP class that implements the `TypeScriptNode` interface: + +```php +use Spatie\TypeScriptTransformer\Support\WritingContext; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNamedNode; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; + +class PickNode implements TypeScriptNode, TypeScriptNamedNode +{ + public function __construct( + private TypeScriptNode $type, + private array $properties, + ) {} + + public function write(WritingContext $context): string + { + return 'Pick<' . $this->type->write($context) . ', ' . implode(' | ', $this->properties) . '>'; + } + + public function getName(): string + { + return 'Pick'; + } +} +``` + +The write method is responsible for transforming the TypeScript node to a string, the `WritingContext` object is passed +to +lower level TypeScript nodes to reference other TypeScript types and can generally be ignored. + +Some TypeScript nodes represent a type with a name like an interface, enum, ... these nodes should implement +the `TypeScriptNamedNode` interface. The `getName` method should return the name of the TypeScript node so that it can +be referenced by other TypeScript nodes. + +When you've got a node which itself contains another TypeScript node that can be a `TypeScriptNamedNode` we recommend +you to implement `TypeScriptForwardingNamedNode`. This interface requires you to implement the `getForwardedNamedNode` +method which should return the TypeScript node that either is another `TypeScriptForwardingNamedNode` +or `TypeScriptNamedNode`. An example of such a node is the `TypeScriptAlias`: + +```php +class TypeScriptAlias implements TypeScriptForwardingNamedNode, TypeScriptNode +{ + public function __construct( + public TypeScriptIdentifier|TypeScriptGeneric $identifier, + public TypeScriptNode $type, + ) { + } + + public function write(WritingContext $context): string + { + return "type {$this->identifier->write($context)} = {$this->type->write($context)};"; + } + + public function getForwardedNamedNode(): TypeScriptNamedNode|TypeScriptForwardingNamedNode + { + return $this->identifier; + } +} +``` + +Lastly, the package also provides some tooling to visit a tree of all TypeScript nodes, when your custom node is +encapsulating other TypeScript nodes you should implement the `TypeScriptVisitableNode` interface which requires you to +implement the visitorProfile method. + +The `visitorProfile` method should return a `VisitorProfile` object which contains information about the properties +of your TypeScript node PHP class that can be visited. We extinguish two types of properties: single nodes properties +containing a single node and iterable properties containing a set of properties. + +The `TypeScriptGeneric` node is an excellent example: + +```php +class TypeScriptGeneric implements TypeScriptForwardingNamedNode, TypeScriptNode, TypeScriptVisitableNode +{ + /** + * @param array $genericTypes + */ + public function __construct( + public TypeScriptIdentifier|TypeReference $type, + public array $genericTypes, + ) { + } + + public function visitorProfile(): VisitorProfile + { + return VisitorProfile::create()->single('type')->iterable('genericTypes'); + } + + // .... +} +``` + ### Visiting TypeScript nodes +### Building your own Writer + +Writers are responsible for writing out the TypeScript types, the package comes with three writers: + +- `NamespaceWriter`: Writes all types to a single TypeScript file with namespaces +- `ModuleWriter`: Writes all types to a file per namespace +- `FlatWriter`: Writes all types to a single TypeScript file without namespaces + +It is possible to create your own writer by implementing the `Writer` interface: + +```php +use Spatie\TypeScriptTransformer\Support\WriteableFile; + +interface Writer +{ + /** @return array */ + public function write( + TransformedCollection $collection, + ReferenceMap $referenceMap, + ): void; +} +``` + +In the end the `write` method should return an array of `WriteableFile` objects, these objects contain the TypeScript +code and the location where it should be written to. + +In the writer you should loop over each `Transformed` object in the collection, decide in which file it should be stored +and transform the TypeScript node to a string: + +```php +foreach ($collection as $transformed) { + $output .= $transformed->prepareForWrite()->write($writingContext) . PHP_EOL; +} +``` + +The `prepareForWrite` method will make sure that a TypeScript node is exported when required. The `write` method will +transform the TypeScript node to a string and requires a `WritingContext` object with information about the writing +context. + +For now the `WritingContext` consists of a Closure returning a string referencing other TypeScript types. We recommend +you to take a look at the `FlatWriter` to see how this is implemented. + ## Testing ```bash diff --git a/src/Transformed/Transformed.php b/src/Transformed/Transformed.php index 3408648b..e94d9dec 100644 --- a/src/Transformed/Transformed.php +++ b/src/Transformed/Transformed.php @@ -5,8 +5,8 @@ use Spatie\TypeScriptTransformer\References\Reference; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptExport; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptExportableNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptForwardingExportableNode; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptForwardingNamedNode; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNamedNode; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; class Transformed @@ -32,18 +32,18 @@ public function getName(): ?string return $this->name; } - if ($this->typeScriptNode instanceof TypeScriptExportableNode) { - return $this->name = $this->typeScriptNode->getExportedName(); + if ($this->typeScriptNode instanceof TypeScriptNamedNode) { + return $this->name = $this->typeScriptNode->getName(); } - if ($this->typeScriptNode instanceof TypeScriptForwardingExportableNode) { + if ($this->typeScriptNode instanceof TypeScriptForwardingNamedNode) { $exportableNode = $this->typeScriptNode; - while ($exportableNode instanceof TypeScriptForwardingExportableNode) { - $exportableNode = $exportableNode->getForwardedExportableNode(); + while ($exportableNode instanceof TypeScriptForwardingNamedNode) { + $exportableNode = $exportableNode->getForwardedNamedNode(); } - return $this->name = $exportableNode->getExportedName(); + return $this->name = $exportableNode->getName(); } return null; @@ -62,7 +62,7 @@ public function prepareForWrite(): TypeScriptNode return $this->typeScriptNode; } - if (! $this->typeScriptNode instanceof TypeScriptExportableNode && ! $this->typeScriptNode instanceof TypeScriptForwardingExportableNode) { + if (! $this->typeScriptNode instanceof TypeScriptNamedNode && ! $this->typeScriptNode instanceof TypeScriptForwardingNamedNode) { TypeScriptTransformerLog::resolve()->warning("Could not export `{$this->reference->humanFriendlyName()}` because it is not exportable"); return $this->typeScriptNode; diff --git a/src/TypeScript/TypeReference.php b/src/TypeScript/TypeReference.php index 18f36771..b7c817ca 100644 --- a/src/TypeScript/TypeReference.php +++ b/src/TypeScript/TypeReference.php @@ -7,7 +7,7 @@ use Spatie\TypeScriptTransformer\Support\WritingContext; use Spatie\TypeScriptTransformer\Transformed\Transformed; -class TypeReference implements TypeScriptExportableNode, TypeScriptNode +class TypeReference implements TypeScriptNamedNode, TypeScriptNode { public static function referencingPhpClass(string $class): self { @@ -34,7 +34,7 @@ public function write(WritingContext $context): string return ($context->referenceWriter)($this->reference); } - public function getExportedName(): string + public function getName(): string { return $this->referenced->getName(); } diff --git a/src/TypeScript/TypeScriptAlias.php b/src/TypeScript/TypeScriptAlias.php index 0c8663fc..f400bf3f 100644 --- a/src/TypeScript/TypeScriptAlias.php +++ b/src/TypeScript/TypeScriptAlias.php @@ -5,7 +5,7 @@ use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptAlias implements TypeScriptForwardingExportableNode, TypeScriptNode, TypeScriptVisitableNode +class TypeScriptAlias implements TypeScriptForwardingNamedNode, TypeScriptNode, TypeScriptVisitableNode { public function __construct( public TypeScriptIdentifier|TypeScriptGeneric $identifier, @@ -23,7 +23,7 @@ public function visitorProfile(): VisitorProfile return VisitorProfile::create()->single('identifier', 'type'); } - public function getForwardedExportableNode(): TypeScriptExportableNode|TypeScriptForwardingExportableNode + public function getForwardedNamedNode(): TypeScriptNamedNode|TypeScriptForwardingNamedNode { return $this->identifier; } diff --git a/src/TypeScript/TypeScriptEnum.php b/src/TypeScript/TypeScriptEnum.php index a2086600..f74d516f 100644 --- a/src/TypeScript/TypeScriptEnum.php +++ b/src/TypeScript/TypeScriptEnum.php @@ -4,7 +4,7 @@ use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptEnum implements TypeScriptExportableNode, TypeScriptNode +class TypeScriptEnum implements TypeScriptNamedNode, TypeScriptNode { /** * @param string $name @@ -37,7 +37,7 @@ public function write(WritingContext $context): string return $output; } - public function getExportedName(): string + public function getName(): string { return $this->name; } diff --git a/src/TypeScript/TypeScriptExport.php b/src/TypeScript/TypeScriptExport.php index c78de3e4..5a2948f5 100644 --- a/src/TypeScript/TypeScriptExport.php +++ b/src/TypeScript/TypeScriptExport.php @@ -8,7 +8,7 @@ class TypeScriptExport implements TypeScriptNode, TypeScriptVisitableNode { public function __construct( - public TypeScriptExportableNode|TypeScriptForwardingExportableNode $node, + public TypeScriptNamedNode|TypeScriptForwardingNamedNode $node, ) { } diff --git a/src/TypeScript/TypeScriptExportableNode.php b/src/TypeScript/TypeScriptExportableNode.php deleted file mode 100644 index c8bcf88d..00000000 --- a/src/TypeScript/TypeScriptExportableNode.php +++ /dev/null @@ -1,8 +0,0 @@ -single('identifier', 'returnType', 'body')->iterable('parameters'); } - public function getForwardedExportableNode(): TypeScriptExportableNode|TypeScriptForwardingExportableNode + public function getForwardedNamedNode(): TypeScriptNamedNode|TypeScriptForwardingNamedNode { return $this->identifier; } diff --git a/src/TypeScript/TypeScriptGeneric.php b/src/TypeScript/TypeScriptGeneric.php index cc26984a..ff0ef31a 100644 --- a/src/TypeScript/TypeScriptGeneric.php +++ b/src/TypeScript/TypeScriptGeneric.php @@ -5,7 +5,7 @@ use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptGeneric implements TypeScriptForwardingExportableNode, TypeScriptNode, TypeScriptVisitableNode +class TypeScriptGeneric implements TypeScriptForwardingNamedNode, TypeScriptNode, TypeScriptVisitableNode { /** * @param array $genericTypes @@ -31,7 +31,7 @@ public function visitorProfile(): VisitorProfile return VisitorProfile::create()->single('type')->iterable('genericTypes'); } - public function getForwardedExportableNode(): TypeScriptExportableNode|TypeScriptForwardingExportableNode + public function getForwardedNamedNode(): TypeScriptNamedNode|TypeScriptForwardingNamedNode { return $this->type; } diff --git a/src/TypeScript/TypeScriptIdentifier.php b/src/TypeScript/TypeScriptIdentifier.php index a409a56d..99efc8e0 100644 --- a/src/TypeScript/TypeScriptIdentifier.php +++ b/src/TypeScript/TypeScriptIdentifier.php @@ -4,7 +4,7 @@ use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptIdentifier implements TypeScriptExportableNode, TypeScriptNode +class TypeScriptIdentifier implements TypeScriptNamedNode, TypeScriptNode { public function __construct( public string $name, @@ -16,7 +16,7 @@ public function write(WritingContext $context): string return (str_contains($this->name, '.') || str_contains($this->name, '\\')) ? "'{$this->name}'" : $this->name; } - public function getExportedName(): string + public function getName(): string { return $this->name; } diff --git a/src/TypeScript/TypeScriptInterface.php b/src/TypeScript/TypeScriptInterface.php index b28c5bf5..cf98326e 100644 --- a/src/TypeScript/TypeScriptInterface.php +++ b/src/TypeScript/TypeScriptInterface.php @@ -5,7 +5,7 @@ use Spatie\TypeScriptTransformer\Support\VisitorProfile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class TypeScriptInterface implements TypeScriptForwardingExportableNode, TypeScriptNode, TypeScriptVisitableNode +class TypeScriptInterface implements TypeScriptForwardingNamedNode, TypeScriptNode, TypeScriptVisitableNode { /** * @param array $properties @@ -39,7 +39,7 @@ public function visitorProfile(): VisitorProfile ->iterable('methods'); } - public function getForwardedExportableNode(): TypeScriptExportableNode|TypeScriptForwardingExportableNode + public function getForwardedNamedNode(): TypeScriptNamedNode|TypeScriptForwardingNamedNode { return $this->name; } diff --git a/src/TypeScript/TypeScriptNamedNode.php b/src/TypeScript/TypeScriptNamedNode.php new file mode 100644 index 00000000..1af1020f --- /dev/null +++ b/src/TypeScript/TypeScriptNamedNode.php @@ -0,0 +1,8 @@ + only reload when the config changes (difficult, maybe skip for now) + /** + * TODO: + * - Add interface implementation + tests + * - Split off Laravel specific code and test + * - Split off data specific code and test + * - Add support for watching files + * - Further write docs + check them + * - Check old Laravel tests if we missed something + * - Check in Flare whether everything is working as expected + * - Release + */ /** * Watch implementation @@ -58,12 +61,6 @@ public function execute(bool $watch = false): void * - Rewrite the file (partially) */ - /** - * Notes after knowledge sharing - * - Split Laravel part again? - * - Make it possible to hijack the PHPStan types, or some way to rename a Laravel Collection to an array? Would be easier - * - When generating routes where we have the full namespace, prepend with ., check Laravel Echo for this - */ $transformedCollection = $this->provideTypesAction->execute(); if (! empty($this->config->providedVisitorClosures)) { From f75d83db01203ecfe655d99f8fe6ce00032c102b Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 21 Jun 2024 10:27:44 +0200 Subject: [PATCH 39/51] wip --- README.md | 134 ++++++++++++++++++++- src/TypeScriptTransformerConfigFactory.php | 4 +- src/Visitor/Visitor.php | 2 +- tests/Visitor/VisitorTest.php | 27 +++++ tests/VisitorClosures.php | 4 +- 5 files changed, 164 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fae8654c..10288210 100644 --- a/README.md +++ b/README.md @@ -1032,8 +1032,7 @@ class PickNode implements TypeScriptNode, TypeScriptNamedNode ``` The write method is responsible for transforming the TypeScript node to a string, the `WritingContext` object is passed -to -lower level TypeScript nodes to reference other TypeScript types and can generally be ignored. +to lower level TypeScript nodes to reference other TypeScript types and can generally be ignored. Some TypeScript nodes represent a type with a name like an interface, enum, ... these nodes should implement the `TypeScriptNamedNode` interface. The `getName` method should return the name of the TypeScript node so that it can @@ -1096,8 +1095,139 @@ class TypeScriptGeneric implements TypeScriptForwardingNamedNode, TypeScriptNode } ``` +It contains a single node property `type` and an iterable property `genericTypes`. From now on the package will visit +the nodes within these properties. + ### Visiting TypeScript nodes +When working with TypeScript nodes in a class property processor or a custom TypeScript node, it might be necessary to +visit and alter nodes in the tree. The `Visitor` class can be used to visit such a tree of TypeScript +nodes. + +The visitor will start in a node and then traverse the tree of TypeScript nodes, it is possible to register a `before` +and `after` callback for each node it visits. The `before` callback is called before visiting the children of a node and +the `after` callback is called after visiting the children of a node. + +```php +use Spatie\TypeScriptTransformer\Visitor\Visitor; + +Visitor::create() + ->before(function (TypeScriptNode $node){ + echo 'Before visiting ' . $node::class . PHP_EOL; + }) + ->after(function (TypeScriptNode $node) { + echo 'After visiting ' . $node::class . PHP_EOL; + }) + ->execute($rootNode); +``` + +When running the visitor on the following node: + +```php +$rootNode = new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNumber(), +]); +``` + +The output will be (redacted for readability): + +``` +Before visiting TypeScriptUnion +Before visiting TypeScriptString +After visiting TypeScriptString +Before visiting TypeScriptNumber +After visiting TypeScriptNumber +After visiting TypeScriptUnion +``` + +By default, the visitor will visit the tree of nodes and run the callback on each node within the tree. It is possible +to limit the types of nodes the callback runs on: + +```php +Visitor::create() + ->after(function (TypeScriptUnion $node, [TypeScriptUnion::class]) { + // Do something with TypeScriptUnion nodes + }) + ->execute($rootNode); +``` + +When not returning a TypeScript node from the callback, the visitor will continue traversing the tree. It is possible to +replace a node in the tree like this: + +```php +use Spatie\TypeScriptTransformer\Visitor\VisitorOperation; + +Visitor::create() + ->after(function (TypeScriptUnion $node, [TypeScriptUnion::class]) { + if(count($node->types) === 1) { + return VisitorOperation::replace(array_values($node->types)[0]); + } + }) + ->execute($rootNode); +``` + +The visitor above will replace all union nodes with a single type with that type. + +It is also possible to remove a node from the tree: + +```php +Visitor::create() + ->after(function (TypeScriptString $node, [TypeScriptString::class]) { + return VisitorOperation::remove(); + }) + ->execute($rootNode); +``` + +### Hooking into TypeScript transformer + +Every time the TypeScript transformer is executed, it will go through a series of steps, it is possible to run a visitor +in between some of these steps. + +The steps look as following: + +1. Running of the TypeProviders creating a collection of Transformed types +2. Possible hooking point: `providedVisitorHook` +3. Connecting references between Transformed types +4. Possible hooking point: `connectedVisitorHook` +5. Create a collection of WriteableFiles +6. Write those files to disk +7. Format the files + +The two hooking points above can be used to run a visitor on the collection of Transformed types: + +```php +use Spatie\TypeScriptTransformer\Visitor\VisitorClosureType; + +$config->providedVisitorHook( + fn(TransformedCollection $collection) => Visitor::create()->execute($collection), + [TypeScriptUnion::class], + VisitorClosureType::Before +); + +$config->connectedVisitorHook( + fn(TransformedCollection $collection) => Visitor::create()->execute($collection), + [TypeScriptUnion::class], + VisitorClosureType::Before +); +``` + +Running visitors as an after hook is also possible: + +```php +$config->providedVisitorHook( + fn(TransformedCollection $collection) => Visitor::create()->execute($collection), + [TypeScriptUnion::class], + VisitorClosureType::After +); + +$config->connectedVisitorHook( + fn(TransformedCollection $collection) => Visitor::create()->execute($collection), + [TypeScriptUnion::class], + VisitorClosureType::After +); +``` + ### Building your own Writer Writers are responsible for writing out the TypeScript types, the package comes with three writers: diff --git a/src/TypeScriptTransformerConfigFactory.php b/src/TypeScriptTransformerConfigFactory.php index 91d6368f..ed88555c 100644 --- a/src/TypeScriptTransformerConfigFactory.php +++ b/src/TypeScriptTransformerConfigFactory.php @@ -111,7 +111,7 @@ public function formatter(Formatter|string $formatter): self return $this; } - public function providedVisitor( + public function providedVisitorHook( VisitorClosure|Closure $visitor, ?array $allowedNodes = null, VisitorClosureType $type = VisitorClosureType::Before @@ -125,7 +125,7 @@ public function providedVisitor( return $this; } - public function connectedVisitor( + public function connectedVisitorHook( VisitorClosure|Closure $visitor, ?array $allowedNodes = null, VisitorClosureType $type = VisitorClosureType::Before diff --git a/src/Visitor/Visitor.php b/src/Visitor/Visitor.php index 8ef62779..99d1eb67 100644 --- a/src/Visitor/Visitor.php +++ b/src/Visitor/Visitor.php @@ -35,7 +35,7 @@ public function after( Closure $closure, ?array $allowedNodes = null, ): self { - $this->closures[] = new VisitorClosure($closure, $allowedNodes, VisitorClosureType::Before); + $this->closures[] = new VisitorClosure($closure, $allowedNodes, VisitorClosureType::After); return $this; } diff --git a/tests/Visitor/VisitorTest.php b/tests/Visitor/VisitorTest.php index 041edbf6..435b9285 100644 --- a/tests/Visitor/VisitorTest.php +++ b/tests/Visitor/VisitorTest.php @@ -87,3 +87,30 @@ expect($visited)->toBe($unionNode); expect($unionNode->types)->toEqual([$stringNode, new TypeScriptBoolean()]); }); + +it('will execute a before and after closure correctly', function () { + $rootNode = new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNumber(), + ]); + + $order = []; + + Visitor::create() + ->before(function (TypeScriptNode $node) use (&$order) { + $order[] = 'before '. $node::class; + }) + ->after(function (TypeScriptNode $node) use (&$order) { + $order[] = 'after '. $node::class; + }) + ->execute($rootNode); + + expect($order)->toEqual([ + 'before '. TypeScriptUnion::class, + 'before '. TypeScriptString::class, + 'after '. TypeScriptString::class, + 'before '. TypeScriptNumber::class, + 'after '. TypeScriptNumber::class, + 'after '. TypeScriptUnion::class, + ]); +}); diff --git a/tests/VisitorClosures.php b/tests/VisitorClosures.php index de940fb8..49dacf59 100644 --- a/tests/VisitorClosures.php +++ b/tests/VisitorClosures.php @@ -21,7 +21,7 @@ ]) ))) ->writer($writer = new MemoryWriter()) - ->providedVisitor(function (TypeScriptObject $reference) { + ->providedVisitorHook(function (TypeScriptObject $reference) { return VisitorOperation::replace(new TypeScriptString()); }, [TypeScriptObject::class]) ->get(); @@ -40,7 +40,7 @@ ]) ))) ->writer($writer = new MemoryWriter()) - ->connectedVisitor(function (TypeScriptObject $reference) { + ->connectedVisitorHook(function (TypeScriptObject $reference) { return VisitorOperation::replace(new TypeScriptString()); }, [TypeScriptObject::class]) ->get(); From e987b0e7f803bbbd26ce89dffe2ee193ef922c63 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 21 Jun 2024 13:58:22 +0200 Subject: [PATCH 40/51] wip --- README.md | 7 +- composer.json | 7 +- ...spilePhpStanTypeToTypeScriptNodeAction.php | 54 ++-- ...ayLikeStructuresClassPropertyProcessor.php | 88 ++++++ ...CollectionOfInfoClassPropertyProcessor.php | 61 ----- .../DataClassPropertyProcessor.php | 83 ++++++ ...moveDataLazyTypeClassPropertyProcessor.php | 48 ---- .../InstallTypeScriptTransformerCommand.php | 46 +++- .../Transformers/DataClassTransformer.php | 41 ++- .../Transformers/LaravelClassTransformer.php | 17 +- src/Transformers/ClassTransformer.php | 5 +- .../ParseUserDefinedTypeActionTest.php | 6 +- ...ePhpStanTypeToTypeScriptNodeActionTest.php | 20 +- ...keStructuresClassPropertyProcessorTest.php | 256 ++++++++++++++++++ tests/Fakes/PropertyTypes/PhpDocTypesStub.php | 7 +- ...ectionOfInfoClassPropertyProcessorTest.php | 3 - ...DataLazyTypeClassPropertyProcessorTest.php | 3 - ...peTest__it_can_output_a_single_type__1.txt | 2 +- 18 files changed, 575 insertions(+), 179 deletions(-) create mode 100644 src/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessor.php delete mode 100644 src/Laravel/ClassPropertyProcessors/AddDataCollectionOfInfoClassPropertyProcessor.php create mode 100644 src/Laravel/ClassPropertyProcessors/DataClassPropertyProcessor.php delete mode 100644 src/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessor.php create mode 100644 tests/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessorTest.php delete mode 100644 tests/Laravel/ClassPropertyProcessors/AddDataCollectionOfInfoClassPropertyProcessorTest.php delete mode 100644 tests/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessorTest.php diff --git a/README.md b/README.md index 10288210..a3b99863 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,9 @@ class TypeScriptTransformerServiceProvider extends BaseTypeScriptTransformerServ } ``` +And it will also register the service provider in your `bootstrap/providers.php` file (when running Laravel 11 or +above). Or in your `config/app.php` file when running Laravel 10 or below. + Now you can transform types as such: ```bash @@ -995,7 +998,7 @@ The `$files` array contains the TypeScript files that need to be formatted, you ### Getting routes as TypeScript -## Live updates +## Watching changes and live updating TypeScript ## Advanced concepts @@ -1182,7 +1185,7 @@ Visitor::create() ### Hooking into TypeScript transformer Every time the TypeScript transformer is executed, it will go through a series of steps, it is possible to run a visitor -in between some of these steps. +in between some of these steps. The steps look as following: diff --git a/composer.json b/composer.json index 55d6f99b..177905ca 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ ], "require": { "php": "^8.2", - "illuminate/contracts": "^10.0", + "illuminate/contracts": "^10.0|^11.0", "phpstan/phpdoc-parser": "^1.13", "spatie/file-system-watcher": "^1.1", "spatie/laravel-package-tools": "^1.14.0", @@ -28,7 +28,7 @@ "laravel/pint": "^1.0", "nunomaduro/collision": "^7.9", "nunomaduro/larastan": "^2.0.1", - "orchestra/testbench": "^8.0", + "orchestra/testbench": "^8.0|^9.0", "pestphp/pest": "^2.0", "pestphp/pest-plugin-arch": "^2.0", "pestphp/pest-plugin-laravel": "^2.0", @@ -37,7 +37,8 @@ "phpstan/phpstan-phpunit": "^1.0", "spatie/laravel-ray": "^1.26", "spatie/pest-plugin-snapshots": "^2.1", - "spatie/ray": "^1.41" + "spatie/ray": "^1.41", + "spatie/laravel-data": "^4.0" }, "autoload": { "psr-4": { diff --git a/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php b/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php index bcbfd38a..f387248a 100644 --- a/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php +++ b/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php @@ -101,7 +101,7 @@ protected function identifierNode( return new TypeScriptObject([]); } - if($node->name === 'array-key') { + if ($node->name === 'array-key') { return new TypeScriptUnion([ new TypeScriptString(), new TypeScriptNumber(), @@ -112,7 +112,7 @@ protected function identifierNode( return new TypeReference(new ClassStringReference($node->name)); } - if($reflectionClass === null) { + if ($reflectionClass === null) { return new TypeScriptUnknown(); } @@ -132,8 +132,7 @@ protected function arrayTypeNode( ArrayTypeNode $node, ?ReflectionClass $reflectionClass ): TypeScriptNode { - return new TypeScriptGeneric( - new TypeScriptIdentifier('Array'), + return new TypeScriptArray( [$this->execute($node->type, $reflectionClass)] ); } @@ -195,22 +194,8 @@ protected function genericNode( GenericTypeNode $node, ?ReflectionClass $reflectionClass ): TypeScriptNode { - if ($node->type->name === 'array') { - return match (count($node->genericTypes)) { - 0 => new TypeScriptArray([]), - 1 => new TypeScriptGeneric( - new TypeScriptIdentifier('Array'), - [$this->execute($node->genericTypes[0], $reflectionClass)] - ), - 2 => new TypeScriptGeneric( - new TypeScriptIdentifier('Record'), - [ - $this->execute($node->genericTypes[0], $reflectionClass), - $this->execute($node->genericTypes[1], $reflectionClass), - ] - ), - default => throw new Exception('Invalid number of generic types for array'), - }; + if ($node->type->name === 'array' || $node->type->name === 'Array') { + return $this->genericArrayNode($node, $reflectionClass); } $type = $this->execute($node->type, $reflectionClass); @@ -227,4 +212,33 @@ protected function genericNode( ) ); } + + private function genericArrayNode(GenericTypeNode $node, ?ReflectionClass $reflectionClass): TypeScriptGeneric|TypeScriptArray + { + $genericTypes = count($node->genericTypes); + + if ($genericTypes === 0) { + return new TypeScriptArray([]); + } + + if ($genericTypes === 1) { + return new TypeScriptArray([$this->execute($node->genericTypes[0], $reflectionClass)]); + } + + if ($genericTypes > 2) { + throw new Exception('Invalid number of generic types for array'); + } + + $key = $this->execute($node->genericTypes[0], $reflectionClass); + $value = $this->execute($node->genericTypes[1], $reflectionClass); + + if ($key instanceof TypeScriptNumber) { + return new TypeScriptArray([$value]); + } + + return new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [$key, $value,] + ); + } } diff --git a/src/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessor.php b/src/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessor.php new file mode 100644 index 00000000..c7bf051f --- /dev/null +++ b/src/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessor.php @@ -0,0 +1,88 @@ +visitor = Visitor::create()->before(function (TypeScriptGeneric $generic) { + $isCollection = $generic->type instanceof TypeReference + && $generic->type->reference instanceof ClassStringReference + && in_array($generic->type->reference->classString, $this->arrayLikeClassesToReplace); + + $isArrayToReplace = $this->replaceArrays + && $generic->type instanceof TypeScriptIdentifier + && $generic->type->name === 'Array' + && count($generic->genericTypes) === 2; // One type is totally valid + + if (! $isCollection && ! $isArrayToReplace) { + return VisitorOperation::keep(); + } + + $genericTypesCount = count($generic->genericTypes); + + if ($genericTypesCount > 2 || $genericTypesCount === 0) { + // Someone messed with the type, let's skip it + return VisitorOperation::keep(); + } + + if ($genericTypesCount === 1) { + return VisitorOperation::replace(new TypeScriptArray([$generic->genericTypes[0]])); + } + + $isRecord = $generic->genericTypes[0] instanceof TypeScriptUnion || $generic->genericTypes[0] instanceof TypeScriptString; + + if ($isRecord) { + return VisitorOperation::replace(new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [ + $generic->genericTypes[0], + $generic->genericTypes[1], + ] + )); + } + + return VisitorOperation::replace(new TypeScriptArray([$generic->genericTypes[1]])); + }, [TypeScriptGeneric::class]); + } + + public function replaceArrayLikeClass(string ...$class): self + { + array_push($this->arrayLikeClassesToReplace, ...$class); + + return $this; + } + + public function execute( + ReflectionProperty $reflection, + ?TypeNode $annotation, + TypeScriptProperty $property + ): ?TypeScriptProperty { + $property->type = $this->visitor->execute($property->type); + + return $property; + } +} diff --git a/src/Laravel/ClassPropertyProcessors/AddDataCollectionOfInfoClassPropertyProcessor.php b/src/Laravel/ClassPropertyProcessors/AddDataCollectionOfInfoClassPropertyProcessor.php deleted file mode 100644 index 0abb7c3d..00000000 --- a/src/Laravel/ClassPropertyProcessors/AddDataCollectionOfInfoClassPropertyProcessor.php +++ /dev/null @@ -1,61 +0,0 @@ -buildVisitor(); - } - - public function execute(ReflectionProperty $reflection, ?TypeNode $annotation, TypeScriptProperty $property): ?TypeScriptProperty - { - $attributes = $reflection->getAttributes('Spatie\LaravelData\Attributes\DataCollectionOf'); - - if (empty($attributes)) { - return $property; - } - - $attribute = $attributes[0]; - - $metadata = [ - 'dataClass' => $attribute->getArguments()[0], - ]; - - $property->type = $this->visitor->execute($property->type, $metadata); - - return $property; - } - - protected function buildVisitor(): void - { - $this->visitor = Visitor::create()->before(function (TypeReference $node, &$metadata) { - if ( - $node->reference instanceof ClassStringReference - && is_a($node->reference->classString, 'Spatie\LaravelData\DataCollection', true) - ) { - return VisitorOperation::replace(new TypeScriptGeneric( - $node, - [ - new TypeReference(new ClassStringReference($metadata['dataClass'])), - ] - )); - } - - return $node; - }, [TypeReference::class]); - } -} diff --git a/src/Laravel/ClassPropertyProcessors/DataClassPropertyProcessor.php b/src/Laravel/ClassPropertyProcessors/DataClassPropertyProcessor.php new file mode 100644 index 00000000..5743f442 --- /dev/null +++ b/src/Laravel/ClassPropertyProcessors/DataClassPropertyProcessor.php @@ -0,0 +1,83 @@ +lazyTypes = array_merge($this->lazyTypes, $this->customLazyTypes); + } + + public function execute( + ReflectionProperty $reflection, + ?TypeNode $annotation, + TypeScriptProperty $property + ): ?TypeScriptProperty { + $dataClass = $this->dataConfig->getDataClass($reflection->getDeclaringClass()->getName()); + $dataProperty = $dataClass->properties->get($reflection->getName()); + + if ($dataProperty->hidden) { + return null; + } + + if ($dataProperty->outputMappedName) { + $property->name = new TypeScriptIdentifier($dataProperty->outputMappedName); + } + + if (! $property->type instanceof TypeScriptUnion) { + return $property; + } + + for ($i = 0; $i < count($property->type->types); $i++) { + $subType = $property->type->types[$i]; + + if ($subType instanceof TypeReference && $this->shouldHideReference($subType)) { + $property->isOptional = true; + + unset($property->type->types[$i]); + } + } + + $property->type->types = array_values($property->type->types); + + if (count($property->type->types) === 1) { + $property->type = $property->type->types[0]; + } + + return $property; + } + + protected function shouldHideReference( + TypeReference $reference + ): bool { + if (! $reference->reference instanceof ClassStringReference) { + return false; + } + + return in_array($reference->reference->classString, $this->lazyTypes) + || $reference->reference->classString === Optional::class; + } +} diff --git a/src/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessor.php b/src/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessor.php deleted file mode 100644 index eb70acd6..00000000 --- a/src/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessor.php +++ /dev/null @@ -1,48 +0,0 @@ -type instanceof TypeScriptUnion) { - return $property; - } - - for ($i = 0; $i < count($property->type->types); $i++) { - $subType = $property->type->types[$i]; - - if ($subType instanceof TypeReference && $subType->reference instanceof ClassStringReference && in_array($subType->reference->classString, $this->lazyTypes)) { - $property->isOptional = true; - - unset($property->type->types[$i]); - } - } - - $property->type->types = array_values($property->type->types); - - if (count($property->type->types) === 1) { - $property->type = $property->type->types[0]; - } - - return $property; - } -} diff --git a/src/Laravel/Commands/InstallTypeScriptTransformerCommand.php b/src/Laravel/Commands/InstallTypeScriptTransformerCommand.php index 4175be39..a74fe2b9 100644 --- a/src/Laravel/Commands/InstallTypeScriptTransformerCommand.php +++ b/src/Laravel/Commands/InstallTypeScriptTransformerCommand.php @@ -19,31 +19,51 @@ public function handle(): void $this->comment('Publishing TypeScript Transformer Service Provider...'); $this->callSilent('vendor:publish', ['--tag' => 'typescript-transformer-provider']); - $this->registerTypescriptTransformerServiceProvider(); + $namespace = Str::replaceLast('\\', '', $this->laravel->getNamespace()); - $this->info('TypeScript Transformer scaffolding installed successfully.'); + $this->installServiceProvider($namespace); + $this->registerServiceProvider($namespace); } - protected function registerTypescriptTransformerServiceProvider(): void + protected function installServiceProvider(string $namespace): void { - $namespace = Str::replaceLast('\\', '', $this->laravel->getNamespace()); + $serviceProviderPath = app_path('Providers/TypeScriptTransformerServiceProvider.php'); + + if (file_exists($serviceProviderPath)) { + $this->info('TypeScript Transformer Service Provider already installed.'); + + return; + } + + file_put_contents($serviceProviderPath, str_replace( + "namespace App\Providers;", + "namespace {$namespace}\Providers;", + file_get_contents($serviceProviderPath) + )); + + $this->info('TypeScript Transformer Service Provider installed.'); + } + + protected function registerServiceProvider(string $namespace): void + { + $configFile = version_compare($this->laravel->version(), '11.0.0', '>=') ? + base_path('bootstrap/providers.php') : + config_path('app.php'); - $appConfig = file_get_contents(config_path('app.php')); + $appConfig = file_get_contents($configFile); if (Str::contains($appConfig, $namespace.'\\Providers\\TypeScriptTransformerServiceProvider::class')) { + $this->info('TypeScript Transformer Service Provider already registered.'); + return; } - file_put_contents(config_path('app.php'), str_replace( - "{$namespace}\\Providers\RouteServiceProvider::class,".PHP_EOL, - "{$namespace}\\Providers\RouteServiceProvider::class,".PHP_EOL.PHP_EOL."{$namespace}\Providers\TypeScriptTransformerServiceProvider::class,".PHP_EOL, + file_put_contents($configFile, str_replace( + "{$namespace}\\Providers\AppServiceProvider::class,".PHP_EOL, + "{$namespace}\\Providers\AppServiceProvider::class,".PHP_EOL." {$namespace}\Providers\TypeScriptTransformerServiceProvider::class,".PHP_EOL, $appConfig )); - file_put_contents(app_path('Providers/TypeScriptTransformerServiceProvider.php'), str_replace( - "namespace App\Providers;", - "namespace {$namespace}\Providers;", - file_get_contents(app_path('Providers/TypeScriptTransformerServiceProvider.php')) - )); + $this->info('TypeScript Transformer Service Provider registered.'); } } diff --git a/src/Laravel/Transformers/DataClassTransformer.php b/src/Laravel/Transformers/DataClassTransformer.php index e9795637..b69bf9bc 100644 --- a/src/Laravel/Transformers/DataClassTransformer.php +++ b/src/Laravel/Transformers/DataClassTransformer.php @@ -3,21 +3,52 @@ namespace Spatie\TypeScriptTransformer\Laravel\Transformers; use ReflectionClass; +use Spatie\LaravelData\Contracts\BaseData; +use Spatie\LaravelData\Support\DataConfig; +use Spatie\TypeScriptTransformer\Actions\TranspilePhpStanTypeToTypeScriptNodeAction; +use Spatie\TypeScriptTransformer\Actions\TranspileReflectionTypeToTypeScriptNodeAction; +use Spatie\TypeScriptTransformer\ClassPropertyProcessors\FixArrayLikeStructuresClassPropertyProcessor; use Spatie\TypeScriptTransformer\Laravel\ClassPropertyProcessors\AddDataCollectionOfInfoClassPropertyProcessor; use Spatie\TypeScriptTransformer\Laravel\ClassPropertyProcessors\RemoveDataLazyTypeClassPropertyProcessor; +use Spatie\TypeScriptTransformer\TypeResolvers\DocTypeResolver; class DataClassTransformer extends LaravelClassTransformer { + protected DataConfig $dataConfig; + + public function __construct( + protected array $customLazyTypes = [], + protected array $customDataCollections = [], + DocTypeResolver $docTypeResolver = new DocTypeResolver(), + TranspilePhpStanTypeToTypeScriptNodeAction $transpilePhpStanTypeToTypeScriptTypeAction = new TranspilePhpStanTypeToTypeScriptNodeAction(), + TranspileReflectionTypeToTypeScriptNodeAction $transpileReflectionTypeToTypeScriptTypeAction = new TranspileReflectionTypeToTypeScriptNodeAction(), + ) { + $this->dataConfig = app(DataConfig::class); + + parent::__construct($docTypeResolver, $transpilePhpStanTypeToTypeScriptTypeAction, $transpileReflectionTypeToTypeScriptTypeAction); + } + protected function shouldTransform(ReflectionClass $reflection): bool { - return $reflection->implementsInterface(\Spatie\LaravelData\Contracts\BaseData::class); + return $reflection->implementsInterface(BaseData::class); } protected function classPropertyProcessors(): array { - return array_merge(parent::classPropertyProcessors(), [ - new RemoveDataLazyTypeClassPropertyProcessor(), - new AddDataCollectionOfInfoClassPropertyProcessor(), - ]); + $processors = parent::classPropertyProcessors(); + + foreach ($processors as $processor) { + if ($processor instanceof FixArrayLikeStructuresClassPropertyProcessor) { + $processor->replaceArrayLikeClass( + \Spatie\LaravelData\DataCollection::class, + ...$this->customDataCollections + ); + } + } + + $processors[] = new AddDataCollectionOfInfoClassPropertyProcessor(); + $processors[] = new RemoveDataLazyTypeClassPropertyProcessor($this->customLazyTypes); + + return $processors; } } diff --git a/src/Laravel/Transformers/LaravelClassTransformer.php b/src/Laravel/Transformers/LaravelClassTransformer.php index 8991d7a5..89119db2 100644 --- a/src/Laravel/Transformers/LaravelClassTransformer.php +++ b/src/Laravel/Transformers/LaravelClassTransformer.php @@ -2,15 +2,24 @@ namespace Spatie\TypeScriptTransformer\Laravel\Transformers; -use Spatie\TypeScriptTransformer\Laravel\ClassPropertyProcessors\ReplaceLaravelCollectionByArrayClassPropertyProcessor; +use Spatie\TypeScriptTransformer\ClassPropertyProcessors\FixArrayLikeStructuresClassPropertyProcessor; use Spatie\TypeScriptTransformer\Transformers\ClassTransformer; abstract class LaravelClassTransformer extends ClassTransformer { protected function classPropertyProcessors(): array { - return array_merge(parent::classPropertyProcessors(), [ - new ReplaceLaravelCollectionByArrayClassPropertyProcessor(), - ]); + $processors = parent::classPropertyProcessors(); + + foreach ($processors as $processor) { + if ($processor instanceof FixArrayLikeStructuresClassPropertyProcessor) { + $processor->replaceArrayLikeClass( + \Illuminate\Support\Collection::class, + \Illuminate\Database\Eloquent\Collection::class, + ); + } + } + + return $processors; } } diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index 582a26c4..283412b8 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -10,6 +10,7 @@ use Spatie\TypeScriptTransformer\Attributes\Hidden; use Spatie\TypeScriptTransformer\Attributes\Optional; use Spatie\TypeScriptTransformer\Attributes\TypeScriptTypeAttributeContract; +use Spatie\TypeScriptTransformer\ClassPropertyProcessors\FixArrayLikeStructuresClassPropertyProcessor; use Spatie\TypeScriptTransformer\References\ReflectionClassReference; use Spatie\TypeScriptTransformer\Support\TransformationContext; use Spatie\TypeScriptTransformer\Transformed\Transformed; @@ -61,7 +62,9 @@ abstract protected function shouldTransform(ReflectionClass $reflection): bool; /** @return array */ protected function classPropertyProcessors(): array { - return []; + return [ + new FixArrayLikeStructuresClassPropertyProcessor(), + ]; } protected function getTypeScriptNode( diff --git a/tests/Actions/ParseUserDefinedTypeActionTest.php b/tests/Actions/ParseUserDefinedTypeActionTest.php index 2d2fbb3c..0ce4bc64 100644 --- a/tests/Actions/ParseUserDefinedTypeActionTest.php +++ b/tests/Actions/ParseUserDefinedTypeActionTest.php @@ -3,15 +3,13 @@ use Spatie\TypeScriptTransformer\Actions\ParseUserDefinedTypeAction; use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\TypeScript\TypeReference; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGeneric; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNumber; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptArray; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; it('can parse a user defined type', function () { $parser = new ParseUserDefinedTypeAction(); expect($parser->execute('string'))->toBeInstanceOf(TypeScriptString::class); - expect($parser->execute('array'))->toEqual(new TypeScriptGeneric(new TypeScriptIdentifier('Record'), [new TypeScriptNumber(), new TypeScriptString()])); + expect($parser->execute('array'))->toEqual(new TypeScriptArray([new TypeScriptString()])); expect($parser->execute('self', new ReflectionClass(DateTime::class)))->toEqual(new TypeReference(new ClassStringReference(DateTime::class))); }); diff --git a/tests/Actions/TranspilePhpStanTypeToTypeScriptNodeActionTest.php b/tests/Actions/TranspilePhpStanTypeToTypeScriptNodeActionTest.php index df807c18..273163c1 100644 --- a/tests/Actions/TranspilePhpStanTypeToTypeScriptNodeActionTest.php +++ b/tests/Actions/TranspilePhpStanTypeToTypeScriptNodeActionTest.php @@ -175,18 +175,20 @@ yield [ 'arrayGeneric', - new TypeScriptGeneric( - new TypeScriptIdentifier('Array'), - [new TypeScriptString()] - ), + new TypeScriptArray([new TypeScriptString()]), + ]; + + yield [ + 'arrayGenericWithIntKey', + new TypeScriptArray([new TypeScriptString()]), ]; yield [ - 'arrayGenericWithKey', + 'arrayGenericWithStringKey', new TypeScriptGeneric( new TypeScriptIdentifier('Record'), [ - new TypeScriptNumber(), + new TypeScriptString(), new TypeScriptString(), ] ), @@ -208,7 +210,7 @@ yield [ 'typeArray', - new TypeScriptGeneric(new TypeScriptIdentifier('Array'), [new TypeScriptString()]), + new TypeScriptArray([new TypeScriptString()]), ]; yield [ @@ -216,8 +218,8 @@ new TypeScriptGeneric( new TypeScriptIdentifier('Record'), [ - new TypeScriptNumber(), - new TypeScriptGeneric(new TypeScriptIdentifier('Array'), [new TypeScriptString()]), + new TypeScriptString(), + new TypeScriptArray([new TypeScriptString()]), ] ), ]; diff --git a/tests/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessorTest.php b/tests/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessorTest.php new file mode 100644 index 00000000..092315f9 --- /dev/null +++ b/tests/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessorTest.php @@ -0,0 +1,256 @@ + */ + public array $int_key_array; + + /** @var array */ + public array $string_key_array; + + /** @var array */ + public array $array_key_array; + + /** @var array */ + public array $union_key_array; + + /** @var array */ + public array $correct_array; + + /** @var bool[] */ + public array $correct_array_alternative; + + /** @var array */ + public array $missing_types_array; + + public array $no_annotation_array; + }; + + $object = transformSingle($class)->typeScriptNode->type; + + [$propertyNode] = array_values(array_filter( + $object->properties, + fn (TypeScriptProperty $propertyNode) => $propertyNode->name instanceof TypeScriptIdentifier && $propertyNode->name->name === $property + )); + + $propertyNode = (new FixArrayLikeStructuresClassPropertyProcessor())->execute( + reflection: new ReflectionProperty($class, $property), + annotation: null, + property: $propertyNode + ); + + expect($propertyNode->type)->toEqual( + $expected + ); +})->with(function () { + yield 'int key array' => [ + 'int_key_array', + new TypeScriptArray([ + new TypeScriptBoolean(), + ]), + ]; + + yield 'string key array' => [ + 'string_key_array', + new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [ + new TypeScriptString(), + new TypeScriptBoolean(), + ], + ), + ]; + + yield 'array key array' => [ + 'array_key_array', + new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [ + new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNumber(), + ]), + new TypeScriptBoolean(), + ], + ), + ]; + + yield 'union key array' => [ + 'union_key_array', + new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [ + new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNumber(), + ]), + new TypeScriptBoolean(), + ], + ), + ]; + + yield 'correct array' => [ + 'correct_array', + new TypeScriptArray([ + new TypeScriptBoolean(), + ]), + ]; + + yield 'correct array alternative' => [ + 'correct_array_alternative', + new TypeScriptArray([ + new TypeScriptBoolean(), + ]), + ]; + + yield 'missing types array' => [ + 'missing_types_array', + new TypeScriptArray([]), + ]; + + yield 'no annotation array' => [ + 'no_annotation_array', + new TypeScriptArray([]), + ]; +}); + +it('replaces array like classes', function ( + string $property, + TypeScriptNode $expected, +) { + $class = new class () { + /** @var Collection */ + public Collection $int_key_collection; + + /** @var Collection */ + public Collection $string_key_collection; + + /** @var Collection */ + public Collection $array_key_collection; + + /** @var Collection */ + public Collection $union_key_collection; + + /** @var Collection */ + public Collection $missing_key_collection; + + /** @var Collection */ + public Collection $missing_types_collection; + + /** @var Collection */ + public Collection $too_much_types_collection; + + public Collection $no_annotation_collection; + }; + + $object = transformSingle($class)->typeScriptNode->type; + + [$propertyNode] = array_values(array_filter( + $object->properties, + fn (TypeScriptProperty $propertyNode) => $propertyNode->name instanceof TypeScriptIdentifier && $propertyNode->name->name === $property + )); + + $propertyNode = (new FixArrayLikeStructuresClassPropertyProcessor( + arrayLikeClassesToReplace: [Collection::class], + ))->execute( + reflection: new ReflectionProperty($class, $property), + annotation: null, + property: $propertyNode + ); + + expect($propertyNode->type)->toEqual( + $expected + ); +})->with(function () { + yield 'int key collection' => [ + 'int_key_collection', + new TypeScriptArray([ + new TypeScriptBoolean(), + ]), + ]; + + yield 'string key collection' => [ + 'string_key_collection', + new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [ + new TypeScriptString(), + new TypeScriptBoolean(), + ], + ), + ]; + + yield 'array key collection' => [ + 'array_key_collection', + new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [ + new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNumber(), + ]), + new TypeScriptBoolean(), + ], + ), + ]; + + yield 'union key collection' => [ + 'union_key_collection', + new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [ + new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNumber(), + ]), + new TypeScriptBoolean(), + ], + ), + ]; + + yield 'missing key collection' => [ + 'missing_key_collection', + new TypeScriptArray([ + new TypeScriptBoolean(), + ]), + ]; + + yield 'missing types collection' => [ + 'missing_types_collection', + new TypeReference(new ClassStringReference(Collection::class)), + ]; + + yield 'too much types collection' => [ + 'too_much_types_collection', + new TypeScriptGeneric( + new TypeReference(new ClassStringReference(Collection::class)), + [ + new TypeScriptString(), + new TypeScriptNumber(), + new TypeScriptBoolean(), + ], + ), + ]; + + yield 'no annotation collection' => [ + 'no_annotation_collection', + new TypeReference(new ClassStringReference(Collection::class)), + ]; +}); diff --git a/tests/Fakes/PropertyTypes/PhpDocTypesStub.php b/tests/Fakes/PropertyTypes/PhpDocTypesStub.php index d4c62355..40cf2164 100644 --- a/tests/Fakes/PropertyTypes/PhpDocTypesStub.php +++ b/tests/Fakes/PropertyTypes/PhpDocTypesStub.php @@ -80,7 +80,10 @@ class PhpDocTypesStub extends stdClass public $arrayGeneric; /** @var array */ - public $arrayGenericWithKey; + public $arrayGenericWithIntKey; + + /** @var array */ + public $arrayGenericWithStringKey; /** @var array */ public $arrayGenericWithArrayKey; @@ -88,7 +91,7 @@ class PhpDocTypesStub extends stdClass /** @var string[] */ public $typeArray; - /** @var array> */ + /** @var array> */ public $nestedArray; /** @var array{a: int, 'b': int, "c": int, d?: int} */ diff --git a/tests/Laravel/ClassPropertyProcessors/AddDataCollectionOfInfoClassPropertyProcessorTest.php b/tests/Laravel/ClassPropertyProcessors/AddDataCollectionOfInfoClassPropertyProcessorTest.php deleted file mode 100644 index 95736d43..00000000 --- a/tests/Laravel/ClassPropertyProcessors/AddDataCollectionOfInfoClassPropertyProcessorTest.php +++ /dev/null @@ -1,3 +0,0 @@ -todo(); diff --git a/tests/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessorTest.php b/tests/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessorTest.php deleted file mode 100644 index e9b05724..00000000 --- a/tests/Laravel/ClassPropertyProcessors/RemoveDataLazyTypeClassPropertyProcessorTest.php +++ /dev/null @@ -1,3 +0,0 @@ -todo(); diff --git a/tests/__snapshots__/TypeScriptTypeTest__it_can_output_a_single_type__1.txt b/tests/__snapshots__/TypeScriptTypeTest__it_can_output_a_single_type__1.txt index 4896da2b..c8b9c6aa 100644 --- a/tests/__snapshots__/TypeScriptTypeTest__it_can_output_a_single_type__1.txt +++ b/tests/__snapshots__/TypeScriptTypeTest__it_can_output_a_single_type__1.txt @@ -3,5 +3,5 @@ path: string contents: string }; export type TestSingleTypeScriptTypeAttribute = { -property: Record +property: [WriteableFile] }; From 0ee5696a66352115a29b2033503408bba8feb555 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 21 Jun 2024 15:15:12 +0200 Subject: [PATCH 41/51] wip --- ...ollectionByArrayClassPropertyProcessor.php | 75 ---------- ...avelDataTypeScriptTransformerExtension.php | 15 +- src/Laravel/LaravelDataTypesProvider.php | 60 ++++++++ .../LaravelTypeScriptTransformerExtension.php | 9 +- src/Laravel/LaravelTypesProvider.php | 116 +++++++++------ .../Transformers/DataClassTransformer.php | 34 ++--- ... => LaravelAttributedClassTransformer.php} | 4 +- ...tTransformerApplicationServiceProvider.php | 10 +- src/TypeScriptTransformerConfigFactory.php | 37 +++-- ...ctionByArrayClassPropertyProcessorTest.php | 137 ------------------ 10 files changed, 200 insertions(+), 297 deletions(-) delete mode 100644 src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php create mode 100644 src/Laravel/LaravelDataTypesProvider.php rename src/Laravel/Transformers/{LaravelClassTransformer.php => LaravelAttributedClassTransformer.php} (82%) delete mode 100644 tests/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessorTest.php diff --git a/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php b/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php deleted file mode 100644 index 0c33b18c..00000000 --- a/src/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessor.php +++ /dev/null @@ -1,75 +0,0 @@ -visitor = Visitor::create()->before(function (TypeScriptGeneric $generic) { - $isCollection = $generic->type instanceof TypeReference - && $generic->type->reference instanceof ClassStringReference - && in_array($generic->type->reference->classString, [ - Collection::class, - EloquentCollection::class, - ]); - - if (! $isCollection) { - return; - } - - $genericTypesCount = count($generic->genericTypes); - - if ($genericTypesCount > 2 || $genericTypesCount === 0) { - // Someone messed with the type, let's skip it - return; - } - - if($genericTypesCount === 1) { - return VisitorOperation::replace(new TypeScriptArray([$generic->genericTypes[0]])); - } - - $isRecord = $generic->genericTypes[0] instanceof TypeScriptUnion || $generic->genericTypes[0] instanceof TypeScriptString; - - if ($isRecord) { - return VisitorOperation::replace(new TypeScriptGeneric( - new TypeScriptIdentifier('Record'), - [ - $generic->genericTypes[0], - $generic->genericTypes[1], - ] - )); - } - - return VisitorOperation::replace(new TypeScriptArray([$generic->genericTypes[1]])); - }, [TypeScriptGeneric::class]); - } - - public function execute( - ReflectionProperty $reflection, - ?TypeNode $annotation, - TypeScriptProperty $property - ): ?TypeScriptProperty { - $property->type = $this->visitor->execute($property->type); - - return $property; - } -} diff --git a/src/Laravel/LaravelDataTypeScriptTransformerExtension.php b/src/Laravel/LaravelDataTypeScriptTransformerExtension.php index 8887b6fa..9f27a4b5 100644 --- a/src/Laravel/LaravelDataTypeScriptTransformerExtension.php +++ b/src/Laravel/LaravelDataTypeScriptTransformerExtension.php @@ -8,8 +8,21 @@ class LaravelDataTypeScriptTransformerExtension implements TypeScriptTransformerExtension { + public function __construct( + protected array $customLazyTypes = [], + protected array $customDataCollections = [], + ) { + } + public function enrich(TypeScriptTransformerConfigFactory $factory): void { - $factory->transformer(DataClassTransformer::class); + $factory->extension(new LaravelTypeScriptTransformerExtension()); + + $factory->prependTransformer(new DataClassTransformer( + customLazyTypes: $this->customLazyTypes, + customDataCollections: $this->customDataCollections, + )); + + $factory->typesProvider(LaravelDataTypesProvider::class); } } diff --git a/src/Laravel/LaravelDataTypesProvider.php b/src/Laravel/LaravelDataTypesProvider.php new file mode 100644 index 00000000..d8a2dec4 --- /dev/null +++ b/src/Laravel/LaravelDataTypesProvider.php @@ -0,0 +1,60 @@ +add( + $this->paginatedCollection(), + $this->cursorPaginatedCollection(), + ); + } + + protected function paginatedCollection(): Transformed + { + return new Transformed( + new TypeScriptAlias( + new TypeScriptGeneric( + new TypeScriptIdentifier('PaginatedDataCollection'), + [new TypeScriptIdentifier('TKey'), new TypeScriptIdentifier('TValue')], + ), + new TypeReference(new ClassStringReference(LengthAwarePaginator::class)) + ), + new ClassStringReference(PaginatedDataCollection::class), + ['Spatie', 'LaravelData'], + true, + ); + } + + protected function cursorPaginatedCollection(): Transformed + { + return new Transformed( + new TypeScriptAlias( + new TypeScriptGeneric( + new TypeScriptIdentifier('CursorPaginatedDataCollection'), + [new TypeScriptIdentifier('TKey'), new TypeScriptIdentifier('TValue')], + ), + new TypeReference(new ClassStringReference(CursorPaginator::class)) + ), + new ClassStringReference(CursorPaginatedDataCollection::class), + ['Spatie', 'LaravelData'], + true, + ); + } +} diff --git a/src/Laravel/LaravelTypeScriptTransformerExtension.php b/src/Laravel/LaravelTypeScriptTransformerExtension.php index 33cbddb4..4eaf22fe 100644 --- a/src/Laravel/LaravelTypeScriptTransformerExtension.php +++ b/src/Laravel/LaravelTypeScriptTransformerExtension.php @@ -3,9 +3,9 @@ namespace Spatie\TypeScriptTransformer\Laravel; use Carbon\CarbonInterface; -use Illuminate\Support\Collection; +use Spatie\TypeScriptTransformer\Laravel\Transformers\LaravelAttributedClassTransformer; use Spatie\TypeScriptTransformer\Support\Extensions\TypeScriptTransformerExtension; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\Transformers\AttributedClassTransformer; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfigFactory; @@ -14,8 +14,11 @@ class LaravelTypeScriptTransformerExtension implements TypeScriptTransformerExte public function enrich(TypeScriptTransformerConfigFactory $factory): void { $factory + ->replaceTransformer( + AttributedClassTransformer::class, + LaravelAttributedClassTransformer::class + ) ->typesProvider(LaravelTypesProvider::class) - ->replaceType(Collection::class, new TypeScriptIdentifier('Array')) ->replaceType(CarbonInterface::class, new TypeScriptString()); } } diff --git a/src/Laravel/LaravelTypesProvider.php b/src/Laravel/LaravelTypesProvider.php index fc7fc3a3..5b8d419c 100644 --- a/src/Laravel/LaravelTypesProvider.php +++ b/src/Laravel/LaravelTypesProvider.php @@ -2,10 +2,10 @@ namespace Spatie\TypeScriptTransformer\Laravel; +use Illuminate\Contracts\Pagination\CursorPaginator as CursorPaginatorInterface; use Illuminate\Contracts\Pagination\LengthAwarePaginator as LengthAwarePaginatorInterface; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; +use Illuminate\Pagination\CursorPaginator; use Illuminate\Pagination\LengthAwarePaginator; -use Illuminate\Support\Collection; use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Transformed\Transformed; @@ -27,66 +27,99 @@ class LaravelTypesProvider implements TypesProvider { public function provide(TypeScriptTransformerConfig $config, TransformedCollection $types): void { - /** @todo We should only keep these types if they are referenced otherwise they arent't required to be transformed */ - /** @todo writing types in phpdoc syntax would be a lot easier here */ $types->add( - $this->collection(), - $this->eloquentCollection(), $this->lengthAwarePaginator(), $this->lengthAwarePaginatorInterface(), + $this->cursorPaginator(), + $this->cursorPaginatorInterface(), ); } - protected function collection(): Transformed + protected function lengthAwarePaginator(): Transformed { return new Transformed( new TypeScriptAlias( new TypeScriptGeneric( - new TypeScriptIdentifier('Collection'), - [new TypeScriptIdentifier('T')], - ), - new TypeScriptGeneric( - new TypeScriptIdentifier('Array'), - [new TypeScriptIdentifier('T')], + new TypeScriptIdentifier('LengthAwarePaginator'), + [new TypeScriptIdentifier('TKey'), new TypeScriptIdentifier('TValue')], ), + new TypeScriptObject([ + new TypeScriptProperty('data', new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [new TypeScriptIdentifier('TKey'), new TypeScriptIdentifier('TValue')], + ), ), + new TypeScriptProperty('links', new TypeScriptObject([ + new TypeScriptProperty('url', new TypeScriptUnion([ + new TypeScriptIdentifier('string'), + new TypeScriptIdentifier('null'), + ])), + new TypeScriptProperty('label', new TypeScriptString()), + new TypeScriptProperty('active', new TypeScriptBoolean()), + ])), + new TypeScriptProperty('meta', new TypeScriptObject([ + new TypeScriptProperty('total', new TypeScriptNumber()), + new TypeScriptProperty('current_page', new TypeScriptNumber()), + new TypeScriptProperty('first_page_url', new TypeScriptString()), + new TypeScriptProperty('from', new TypeScriptUnion([ + new TypeScriptNumber(), + new TypeScriptNull(), + ])), + new TypeScriptProperty('last_page', new TypeScriptNumber()), + new TypeScriptProperty('last_page_url', new TypeScriptString()), + new TypeScriptProperty('next_page_url', new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNull(), + ])), + new TypeScriptProperty('path', new TypeScriptString()), + new TypeScriptProperty('per_page', new TypeScriptNumber()), + new TypeScriptProperty('prev_page_url', new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNull(), + ])), + new TypeScriptProperty('to', new TypeScriptUnion([ + new TypeScriptNumber(), + new TypeScriptNull(), + ])), + ])), + ]), ), - new ClassStringReference(Collection::class), - ['Illuminate', 'Support'], + new ClassStringReference(LengthAwarePaginator::class), + ['Illuminate'], true, ); } - protected function eloquentCollection(): Transformed + protected function lengthAwarePaginatorInterface(): Transformed { return new Transformed( new TypeScriptAlias( new TypeScriptGeneric( - new TypeScriptIdentifier('Collection'), + new TypeScriptIdentifier('LengthAwarePaginatorInterface'), [new TypeScriptIdentifier('T')], ), new TypeScriptGeneric( - new TypeReference(new ClassStringReference(Collection::class)), + new TypeReference(new ClassStringReference(LengthAwarePaginator::class)), [new TypeScriptIdentifier('T')], ), ), - new ClassStringReference(EloquentCollection::class), - ['Illuminate', 'Database', 'Eloquent', 'Collection'], + new ClassStringReference(LengthAwarePaginatorInterface::class), + ['Illuminate'], true, ); } - protected function lengthAwarePaginator(): Transformed + protected function cursorPaginator(): Transformed { return new Transformed( new TypeScriptAlias( new TypeScriptGeneric( - new TypeScriptIdentifier('LengthAwarePaginator'), - [new TypeScriptIdentifier('T')], + new TypeScriptIdentifier('CursorPaginator'), + [new TypeScriptIdentifier('TKey'), new TypeScriptIdentifier('TValue')], ), new TypeScriptObject([ new TypeScriptProperty('data', new TypeScriptGeneric( - new TypeScriptIdentifier('Array'), - [new TypeScriptIdentifier('T')], + new TypeScriptIdentifier('Record'), + [new TypeScriptIdentifier('TKey'), new TypeScriptIdentifier('TValue')], ), ), new TypeScriptProperty('links', new TypeScriptObject([ new TypeScriptProperty('url', new TypeScriptUnion([ @@ -97,53 +130,48 @@ protected function lengthAwarePaginator(): Transformed new TypeScriptProperty('active', new TypeScriptBoolean()), ])), new TypeScriptProperty('meta', new TypeScriptObject([ - new TypeScriptProperty('total', new TypeScriptNumber()), - new TypeScriptProperty('current_page', new TypeScriptNumber()), - new TypeScriptProperty('first_page_url', new TypeScriptString()), - new TypeScriptProperty('from', new TypeScriptUnion([ - new TypeScriptNumber(), + new TypeScriptProperty('path', new TypeScriptString()), + new TypeScriptProperty('per_page', new TypeScriptNumber()), + new TypeScriptProperty('next_cursor', new TypeScriptUnion([ + new TypeScriptString(), new TypeScriptNull(), ])), - new TypeScriptProperty('last_page', new TypeScriptNumber()), - new TypeScriptProperty('last_page_url', new TypeScriptString()), new TypeScriptProperty('next_page_url', new TypeScriptUnion([ new TypeScriptString(), new TypeScriptNull(), ])), - new TypeScriptProperty('path', new TypeScriptString()), - new TypeScriptProperty('per_page', new TypeScriptNumber()), - new TypeScriptProperty('prev_page_url', new TypeScriptUnion([ + new TypeScriptProperty('prev_cursor', new TypeScriptUnion([ new TypeScriptString(), new TypeScriptNull(), ])), - new TypeScriptProperty('to', new TypeScriptUnion([ - new TypeScriptNumber(), + new TypeScriptProperty('prev_page_url', new TypeScriptUnion([ + new TypeScriptString(), new TypeScriptNull(), ])), ])), ]), ), - new ClassStringReference(LengthAwarePaginator::class), - ['Illuminate', 'Pagination'], + new ClassStringReference(CursorPaginator::class), + ['Illuminate'], true, ); } - protected function lengthAwarePaginatorInterface(): Transformed + protected function cursorPaginatorInterface(): Transformed { return new Transformed( new TypeScriptAlias( new TypeScriptGeneric( - new TypeScriptIdentifier('LengthAwarePaginator'), + new TypeScriptIdentifier('CursorPaginatorInterface'), [new TypeScriptIdentifier('T')], ), new TypeScriptGeneric( - new TypeReference(new ClassStringReference(LengthAwarePaginator::class)), + new TypeReference(new ClassStringReference(CursorPaginator::class)), [new TypeScriptIdentifier('T')], ), ), - new ClassStringReference(LengthAwarePaginatorInterface::class), - ['Illuminate', 'Contracts', 'Pagination'], + new ClassStringReference(CursorPaginatorInterface::class), + ['Illuminate'], true, ); } diff --git a/src/Laravel/Transformers/DataClassTransformer.php b/src/Laravel/Transformers/DataClassTransformer.php index b69bf9bc..0c25c028 100644 --- a/src/Laravel/Transformers/DataClassTransformer.php +++ b/src/Laravel/Transformers/DataClassTransformer.php @@ -8,11 +8,11 @@ use Spatie\TypeScriptTransformer\Actions\TranspilePhpStanTypeToTypeScriptNodeAction; use Spatie\TypeScriptTransformer\Actions\TranspileReflectionTypeToTypeScriptNodeAction; use Spatie\TypeScriptTransformer\ClassPropertyProcessors\FixArrayLikeStructuresClassPropertyProcessor; -use Spatie\TypeScriptTransformer\Laravel\ClassPropertyProcessors\AddDataCollectionOfInfoClassPropertyProcessor; -use Spatie\TypeScriptTransformer\Laravel\ClassPropertyProcessors\RemoveDataLazyTypeClassPropertyProcessor; +use Spatie\TypeScriptTransformer\Laravel\ClassPropertyProcessors\DataClassPropertyProcessor; +use Spatie\TypeScriptTransformer\Transformers\ClassTransformer; use Spatie\TypeScriptTransformer\TypeResolvers\DocTypeResolver; -class DataClassTransformer extends LaravelClassTransformer +class DataClassTransformer extends ClassTransformer { protected DataConfig $dataConfig; @@ -35,20 +35,20 @@ protected function shouldTransform(ReflectionClass $reflection): bool protected function classPropertyProcessors(): array { - $processors = parent::classPropertyProcessors(); - - foreach ($processors as $processor) { - if ($processor instanceof FixArrayLikeStructuresClassPropertyProcessor) { - $processor->replaceArrayLikeClass( + return [ + new DataClassPropertyProcessor( + $this->dataConfig, + $this->customLazyTypes, + ), + new FixArrayLikeStructuresClassPropertyProcessor( + replaceArrays: true, + arrayLikeClassesToReplace: [ + \Illuminate\Support\Collection::class, + \Illuminate\Database\Eloquent\Collection::class, \Spatie\LaravelData\DataCollection::class, - ...$this->customDataCollections - ); - } - } - - $processors[] = new AddDataCollectionOfInfoClassPropertyProcessor(); - $processors[] = new RemoveDataLazyTypeClassPropertyProcessor($this->customLazyTypes); - - return $processors; + ...$this->customDataCollections, + ] + ), + ]; } } diff --git a/src/Laravel/Transformers/LaravelClassTransformer.php b/src/Laravel/Transformers/LaravelAttributedClassTransformer.php similarity index 82% rename from src/Laravel/Transformers/LaravelClassTransformer.php rename to src/Laravel/Transformers/LaravelAttributedClassTransformer.php index 89119db2..9d44a631 100644 --- a/src/Laravel/Transformers/LaravelClassTransformer.php +++ b/src/Laravel/Transformers/LaravelAttributedClassTransformer.php @@ -3,9 +3,9 @@ namespace Spatie\TypeScriptTransformer\Laravel\Transformers; use Spatie\TypeScriptTransformer\ClassPropertyProcessors\FixArrayLikeStructuresClassPropertyProcessor; -use Spatie\TypeScriptTransformer\Transformers\ClassTransformer; +use Spatie\TypeScriptTransformer\Transformers\AttributedClassTransformer; -abstract class LaravelClassTransformer extends ClassTransformer +class LaravelAttributedClassTransformer extends AttributedClassTransformer { protected function classPropertyProcessors(): array { diff --git a/src/Laravel/TypeScriptTransformerApplicationServiceProvider.php b/src/Laravel/TypeScriptTransformerApplicationServiceProvider.php index 396737b6..5849064e 100644 --- a/src/Laravel/TypeScriptTransformerApplicationServiceProvider.php +++ b/src/Laravel/TypeScriptTransformerApplicationServiceProvider.php @@ -12,12 +12,12 @@ abstract protected function configure(TypeScriptTransformerConfigFactory $config public function register(): void { - $builder = new TypeScriptTransformerConfigFactory(); + $this->app->singleton(TypeScriptTransformerConfig::class, function () { + $builder = new TypeScriptTransformerConfigFactory(); - $this->configure($builder); + $this->configure($builder); - $config = $builder->get(); - - $this->app->singleton(TypeScriptTransformerConfig::class, fn () => $config); + return $builder->get(); + }); } } diff --git a/src/TypeScriptTransformerConfigFactory.php b/src/TypeScriptTransformerConfigFactory.php index ed88555c..1900fa2e 100644 --- a/src/TypeScriptTransformerConfigFactory.php +++ b/src/TypeScriptTransformerConfigFactory.php @@ -27,7 +27,7 @@ class TypeScriptTransformerConfigFactory * @param array $transformers * @param array $directoriesToWatch * @param array $typeReplacements - * @param array $extensions + * @param array, TypeScriptTransformerExtension> $extensions * @param array $providedVisitorClosures * @param array $connectedVisitorClosures */ @@ -69,6 +69,13 @@ public function transformer(string|Transformer ...$transformer): self return $this; } + public function prependTransformer(string|Transformer ...$transformer): self + { + array_unshift($this->transformers, ...$transformer); + + return $this; + } + public function replaceTransformer( string|Transformer $search, string|Transformer $replacement @@ -177,7 +184,15 @@ public function replaceType( public function extension( TypeScriptTransformerExtension ...$extensions ): self { - array_push($this->extensions, ...$extensions); + foreach ($extensions as $extension) { + if (array_key_exists($extension::class, $this->extensions)) { + continue; + } + + $this->extensions[$extension::class] = $extension; + + $extension->enrich($this); + } return $this; } @@ -191,15 +206,6 @@ public function get(): TypeScriptTransformerConfig $this->typeProviders ); - if (! empty($this->transformers)) { - $transformers = array_map( - fn (Transformer|string $transformer) => is_string($transformer) ? new $transformer() : $transformer, - $this->transformers - ); - - $typeProviders[] = new TransformerTypesProvider($transformers, $this->directoriesToWatch); - } - $writer = $this->writer ?? new NamespaceWriter(__DIR__.'/js/typed.ts'); if (is_string($writer)) { @@ -212,8 +218,13 @@ public function get(): TypeScriptTransformerConfig array_unshift($this->providedVisitorClosures, new ReplaceTypesVisitorClosure($this->typeReplacements)); } - foreach ($this->extensions as $extension) { - $extension->enrich($this); + if (! empty($this->transformers)) { + $transformers = array_map( + fn (Transformer|string $transformer) => is_string($transformer) ? new $transformer() : $transformer, + $this->transformers + ); + + $typeProviders[] = new TransformerTypesProvider($transformers, $this->directoriesToWatch); } return new TypeScriptTransformerConfig( diff --git a/tests/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessorTest.php b/tests/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessorTest.php deleted file mode 100644 index da8267f7..00000000 --- a/tests/Laravel/ClassPropertyProcessors/ReplaceLaravelCollectionByArrayClassPropertyProcessorTest.php +++ /dev/null @@ -1,137 +0,0 @@ - */ - public Collection $int_key_collection; - - /** @var Collection */ - public Collection $string_key_collection; - - /** @var Collection */ - public Collection $array_key_collection; - - /** @var Collection */ - public Collection $union_key_collection; - - /** @var Collection */ - public Collection $missing_key_collection; - - /** @var Collection */ - public Collection $missing_types_collection; - - /** @var Collection */ - public Collection $too_much_types_collection; - - public Collection $no_annotation_collection; - }; - - $object = transformSingle($class)->typeScriptNode->type; - - [$propertyNode] = array_values(array_filter( - $object->properties, - fn (TypeScriptProperty $propertyNode) => $propertyNode->name instanceof TypeScriptIdentifier && $propertyNode->name->name === $property - )); - - $propertyNode = (new ReplaceLaravelCollectionByArrayClassPropertyProcessor())->execute( - reflection: new ReflectionProperty($class, $property), - annotation: null, - property: $propertyNode - ); - - expect($propertyNode->type)->toEqual( - $expected - ); -})->with(function () { - yield 'int key collection' => [ - 'int_key_collection', - new TypeScriptArray([ - new TypeScriptBoolean(), - ]), - ]; - - yield 'string key collection' => [ - 'string_key_collection', - new TypeScriptGeneric( - new TypeScriptIdentifier('Record'), - [ - new TypeScriptString(), - new TypeScriptBoolean(), - ], - ), - ]; - - yield 'array key collection' => [ - 'array_key_collection', - new TypeScriptGeneric( - new TypeScriptIdentifier('Record'), - [ - new TypeScriptUnion([ - new TypeScriptString(), - new TypeScriptNumber(), - ]), - new TypeScriptBoolean(), - ], - ), - ]; - - yield 'union key collection' => [ - 'union_key_collection', - new TypeScriptGeneric( - new TypeScriptIdentifier('Record'), - [ - new TypeScriptUnion([ - new TypeScriptString(), - new TypeScriptNumber(), - ]), - new TypeScriptBoolean(), - ], - ), - ]; - - yield 'missing key collection' => [ - 'missing_key_collection', - new TypeScriptArray([ - new TypeScriptBoolean(), - ]), - ]; - - yield 'missing types collection' => [ - 'missing_types_collection', - new TypeReference(new ClassStringReference(Collection::class)), - ]; - - yield 'too much types collection' => [ - 'too_much_types_collection', - new TypeScriptGeneric( - new TypeReference(new ClassStringReference(Collection::class)), - [ - new TypeScriptString(), - new TypeScriptNumber(), - new TypeScriptBoolean(), - ], - ), - ]; - - yield 'no annotation collection' => [ - 'no_annotation_collection', - new TypeReference(new ClassStringReference(Collection::class)), - ]; -}); From 1c578866fad8b1d9e1a5100d0991378ff46e30c3 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 21 Jun 2024 16:39:38 +0200 Subject: [PATCH 42/51] wip --- README.md | 8 +- UPGRADE.md | 55 +++++ ...leReflectionTypeToTypeScriptNodeAction.php | 5 + src/Transformers/ClassTransformer.php | 2 +- src/Transformers/InterfaceTransformer.php | 219 ++++++------------ src/TypeResolvers/Data/ParsedMethod.php | 2 +- src/TypeScriptTransformer.php | 6 +- tests/Actions/DiscoverTypesActionTest.php | 2 + ...flectionTypeToTypeScriptNodeActionTest.php | 12 + tests/Fakes/PropertyTypes/PhpTypesStub.php | 5 + .../Fakes/TypesToProvide/SimpleInterface.php | 25 ++ tests/Support/AllInterfaceTransformer.php | 14 ++ .../Transformers/InterfaceTransformerTest.php | 12 + ...it_transforms_methods_in_interfaces__1.txt | 8 + 14 files changed, 220 insertions(+), 155 deletions(-) create mode 100644 UPGRADE.md create mode 100644 tests/Fakes/TypesToProvide/SimpleInterface.php create mode 100644 tests/Support/AllInterfaceTransformer.php create mode 100644 tests/Transformers/InterfaceTransformerTest.php create mode 100644 tests/__snapshots__/InterfaceTransformerTest__it_transforms_methods_in_interfaces__1.txt diff --git a/README.md b/README.md index a3b99863..51025937 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This package allows you to convert PHP classes to TypeScript. This class... ```php -/** @typescript */ +#[TypeScript] class User { public int $id; @@ -31,10 +31,10 @@ export type User = { Here's another example. ```php -class Languages extends Enum +enum Languages: string { - const TYPESCRIPT = 'typescript'; - const PHP = 'php'; + case TYPESCRIPT = 'typescript'; + case PHP = 'php'; } ``` diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 00000000..89aefb10 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,55 @@ +# Upgrading + +Because there are many breaking changes an upgrade is not that easy. There are many edge cases this guide does not +cover. We accept PRs to improve this guide. + +## Upgrading to v3 + +Version 3 is a complete rewrite of the package. That's why writing an upgrade guide is not that easy. The best way to +upgrade is to start reading the new docs and try to implement the new features. + +A few noticeable changes are: + +- Laravel installs now need to configure the package in a service provider instead of config file +- The package requires PHP 8.2 +- If you're using Laravel, v10 is minimally required +- Collectors were removed in favour of Transformers which decide whether a type should be transformed or not +- The transformer should now return a `Transformed` object when it can transform a type +- The transformer interface now should return `Untransformable` when it cannot transform the type +- The `DtoTransformer` was removed in favour of a more flexible transformer system where you can create your own transformers +- The `EnumTransformer` was rewritten to allow multiple types of enums to be transformed and multiple output structures +- All other enum transformers were removed +- The concept of `TypeProcessors` was removed, `ClassPropertyProcessor` is a kinda replacement for this +- The TypeReflectors were removed +- Support for inline types was removed +- If you were implementing your own attributes, you should now implement the `TypeScriptTypeAttributeContract` interface instead of `TypeScriptTransformableAttribute` +- The `RecordTypeScriptType` attribute was removed since deduction of these kinds of types is now done by the transformer +- The `TypeScriptTransformer` attribute was removed +- If you were implementing your own `Formatter`, please update the `format` method to now work on an array of files + +And so much more. Please read the docs for more information. + +## Upgrading to v2 + +- The package is now PHP 8 only +- The `ClassPropertyProcessor` interface was renamed to `TypeProcessor` and now takes a union of reflection objects +- In the config: + - `searchingPath` was renamed to `autoDiscoverTypes` + - `classPropertyReplacements` was renamed to `defaultTypeReplacements` +- Collectors now only have one method: `getTransformedType` which should + - return `null` when the collector cannot find a transformer + - return a `TransformedType` from a suitable transformer +- Transformers now only have one method: `transform` which should + - return `null` when the transformer cannot transform the class + - return a `TransformedType` if it can transform the class +- In Writers the `replaceMissingSymbols` method was removed and a `replacesSymbolsWithFullyQualifiedIdentifiers` with `bool` as return type was added +- The DTO transformer was completely rewritten, please take a look at the docs how to create you own +- The step classes are now renamed to actions + +Laravel +- In the Laravel config: + - `searching_path` is renamed to `auto_discover_types` + - `class_property_replacements` is renamed to `default_type_relacements` + - `writer` and `formatter` were added +- You should replace the `DefaultCollector::class` with the `DefaultCollector::class` +- It is not possible anymore to convert one file to TypeScript via command diff --git a/src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php b/src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php index 97655149..200921fc 100644 --- a/src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php +++ b/src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php @@ -21,6 +21,7 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUndefined; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnknown; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptVoid; class TranspileReflectionTypeToTypeScriptNodeAction { @@ -98,6 +99,10 @@ protected function reflectionNamedType( return new TypeScriptObject([]); } + if ($type->getName() === 'void') { + return new TypeScriptVoid(); + } + if (class_exists($type->getName()) || interface_exists($type->getName())) { return new TypeReference(new ClassStringReference($type->getName())); } diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index 283412b8..03563ed3 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -38,7 +38,7 @@ public function __construct( public function transform(ReflectionClass $reflectionClass, TransformationContext $context): Transformed|Untransformable { - if ($reflectionClass->isEnum()) { + if ($reflectionClass->isEnum() || $reflectionClass->isInterface()) { return Untransformable::create(); } diff --git a/src/Transformers/InterfaceTransformer.php b/src/Transformers/InterfaceTransformer.php index 7ee7f21f..d64baafd 100644 --- a/src/Transformers/InterfaceTransformer.php +++ b/src/Transformers/InterfaceTransformer.php @@ -2,25 +2,26 @@ namespace Spatie\TypeScriptTransformer\Transformers; -use PHPStan\PhpDocParser\Ast\Type\TypeNode; use ReflectionClass; use ReflectionMethod; -use ReflectionProperty; +use ReflectionParameter; use Spatie\TypeScriptTransformer\Actions\TranspilePhpStanTypeToTypeScriptNodeAction; use Spatie\TypeScriptTransformer\Actions\TranspileReflectionTypeToTypeScriptNodeAction; -use Spatie\TypeScriptTransformer\Attributes\Hidden; -use Spatie\TypeScriptTransformer\Attributes\Optional; -use Spatie\TypeScriptTransformer\Attributes\TypeScriptTypeAttributeContract; use Spatie\TypeScriptTransformer\References\ReflectionClassReference; use Spatie\TypeScriptTransformer\Support\TransformationContext; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\Transformed\Untransformable; +use Spatie\TypeScriptTransformer\TypeResolvers\Data\ParsedMethod; +use Spatie\TypeScriptTransformer\TypeResolvers\Data\ParsedNameAndType; use Spatie\TypeScriptTransformer\TypeResolvers\DocTypeResolver; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptInterface; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptInterfaceMethod; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptParameter; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnknown; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptVoid; abstract class InterfaceTransformer implements Transformer { @@ -33,7 +34,7 @@ public function __construct( public function transform(ReflectionClass $reflectionClass, TransformationContext $context): Transformed|Untransformable { - if ($reflectionClass->isEnum()) { + if (! $reflectionClass->isInterface()) { return Untransformable::create(); } @@ -41,11 +42,14 @@ public function transform(ReflectionClass $reflectionClass, TransformationContex return Untransformable::create(); } + $node = new TypeScriptInterface( + new TypeScriptIdentifier($context->name), + $this->getProperties($reflectionClass, $context), + $this->getMethods($reflectionClass, $context) + ); + return new Transformed( - new TypeScriptAlias( - new TypeScriptIdentifier($context->name), - $this->getTypeScriptNode($reflectionClass, $context) - ), + $node, new ReflectionClassReference($reflectionClass), $context->nameSpaceSegments, true, @@ -54,173 +58,96 @@ public function transform(ReflectionClass $reflectionClass, TransformationContex abstract protected function shouldTransform(ReflectionClass $reflection): bool; - protected function getTypeScriptNode( + /** @return TypeScriptInterfaceMethod[] */ + protected function getMethods( ReflectionClass $reflectionClass, TransformationContext $context, - ): TypeScriptNode { - if ($resolvedAttributeType = $this->resolveTypeByAttribute($reflectionClass)) { - return $resolvedAttributeType; - } - - $constructorAnnotations = $reflectionClass->hasMethod('__construct') - ? $this->docTypeResolver->method($reflectionClass->getMethod('__construct'))?->parameters ?? [] - : []; - - $properties = []; - - foreach ($this->getMethods($reflectionClass) as $reflectionMethod) { - $property = $this->createProperty( - $reflectionClass, - $reflectionMethod, - $annotation?->type, - $context - ); - - if ($property === null) { - continue; - } - - $property = $this->runClassPropertyProcessors( - $reflectionMethod, - $annotation?->type, - $property - ); + ): array { + $methods = []; - if ($property !== null) { - $properties[] = $property; - } + foreach ($reflectionClass->getMethods() as $reflectionMethod) { + $methods[] = $this->getTypeScriptMethod($reflectionClass, $reflectionMethod, $context); } - return new Type($properties); + return $methods; } - protected function resolveTypeByAttribute( + /** @return TypeScriptProperty[] */ + protected function getProperties( ReflectionClass $reflectionClass, - ?ReflectionProperty $property = null, - ): ?TypeScriptNode { - $subject = $property ?? $reflectionClass; - - foreach ($subject->getAttributes() as $attribute) { - if (is_a($attribute->getName(), TypeScriptTypeAttributeContract::class, true)) { - /** @var TypeScriptTypeAttributeContract $attributeInstance */ - $attributeInstance = $attribute->newInstance(); - - return $attributeInstance->getType($reflectionClass); - } - } - - return null; - } - - protected function getMethods(ReflectionClass $reflection): array - { - return array_filter( - $reflection->getMethods(), - fn (ReflectionMethod $method) => ! $method->isStatic() - ); + TransformationContext $context, + ): array { + return []; } - protected function createProperty( + protected function getTypeScriptMethod( ReflectionClass $reflectionClass, - ReflectionProperty $reflectionProperty, - ?TypeNode $annotation, + ReflectionMethod $reflectionMethod, TransformationContext $context, - ): ?TypeScriptProperty { - $type = $this->resolveTypeForProperty( - $reflectionClass, - $reflectionProperty, - $annotation - ); + ): TypeScriptInterfaceMethod { + $annotation = $this->docTypeResolver->method($reflectionMethod); - $property = new TypeScriptProperty( - $reflectionProperty->getName(), - $type, - $this->isPropertyOptional( - $reflectionProperty, - $reflectionClass, - $type, - $context - ), - $this->isPropertyReadonly( - $reflectionProperty, + return new TypeScriptInterfaceMethod( + $reflectionMethod->getName(), + array_map(fn (ReflectionParameter $parameter) => $this->resolveMethodParameterType( $reflectionClass, - $type, - ) + $reflectionMethod, + $parameter, + $context, + $annotation->parameters[$parameter->getName()] ?? null + ), $reflectionMethod->getParameters()), + $this->resolveMethodReturnType($reflectionClass, $reflectionMethod, $context, $annotation) ); - - if ($this->isPropertyHidden($reflectionProperty, $reflectionClass, $property)) { - return null; - } - - return $property; } - protected function resolveTypeForProperty( + protected function resolveMethodReturnType( ReflectionClass $reflectionClass, - ReflectionProperty $reflectionProperty, - ?TypeNode $annotation, + ReflectionMethod $reflectionMethod, + TransformationContext $context, + ?ParsedMethod $annotation ): TypeScriptNode { - if ($resolvedAttributeType = $this->resolveTypeByAttribute($reflectionClass, $reflectionProperty)) { - return $resolvedAttributeType; - } - - if ($annotation) { + if ($annotation->returnType) { return $this->transpilePhpStanTypeToTypeScriptTypeAction->execute( - $annotation, - $reflectionClass, + $annotation->returnType, + $reflectionClass ); } - if ($reflectionProperty->hasType()) { + $reflectionType = $reflectionMethod->getReturnType(); + + if ($reflectionType) { return $this->transpileReflectionTypeToTypeScriptTypeAction->execute( - $reflectionProperty->getType(), + $reflectionType, $reflectionClass ); } - return new TypeScriptUnknown(); + return new TypeScriptVoid(); } - protected function isPropertyOptional( - ReflectionProperty $reflectionProperty, + protected function resolveMethodParameterType( ReflectionClass $reflectionClass, - TypeScriptNode $type, + ReflectionMethod $reflectionMethod, + ReflectionParameter $reflectionParameter, TransformationContext $context, - ): bool { - return $context->optional || count($reflectionProperty->getAttributes(Optional::class)) > 0; - } - - protected function isPropertyReadonly( - ReflectionProperty $reflectionProperty, - ReflectionClass $reflectionClass, - TypeScriptNode $type, - ): bool { - return $reflectionProperty->isReadOnly() || $reflectionClass->isReadOnly(); - } - - protected function isPropertyHidden( - ReflectionProperty $reflectionProperty, - ReflectionClass $reflectionClass, - TypeScriptProperty $property, - ): bool { - return count($reflectionProperty->getAttributes(Hidden::class)) > 0; - } - - protected function runClassPropertyProcessors( - ReflectionProperty $reflectionProperty, - ?TypeNode $annotation, - TypeScriptProperty $property, - ): ?TypeScriptProperty { - $processors = $this->classPropertyProcessors; - - foreach ($processors as $processor) { - $property = $processor->execute($reflectionProperty, $annotation, $property); - - if ($property === null) { - return null; - } - } + ?ParsedNameAndType $annotation, + ): TypeScriptParameter { + $type = match (true) { + $annotation !== null => $this->transpilePhpStanTypeToTypeScriptTypeAction->execute( + $annotation->type, + $reflectionClass + ), + $reflectionParameter->hasType() => $this->transpileReflectionTypeToTypeScriptTypeAction->execute( + $reflectionParameter->getType(), + $reflectionClass + ), + default => new TypeScriptUnknown(), + }; - return $property; + return new TypeScriptParameter( + $reflectionParameter->getName(), + $type, + $reflectionParameter->isOptional() + ); } } diff --git a/src/TypeResolvers/Data/ParsedMethod.php b/src/TypeResolvers/Data/ParsedMethod.php index 122bda20..f321f1e0 100644 --- a/src/TypeResolvers/Data/ParsedMethod.php +++ b/src/TypeResolvers/Data/ParsedMethod.php @@ -7,7 +7,7 @@ class ParsedMethod { /** - * @param array $parameters + * @param array $parameters */ public function __construct( public array $parameters, diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index d52c4956..f8836c4a 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -40,13 +40,13 @@ public function execute(bool $watch = false): void { /** * TODO: - * - Add interface implementation + tests + * - Add interface implementation + tests -> OK * - Split off Laravel specific code and test * - Split off data specific code and test * - Add support for watching files - * - Further write docs + check them + * - Further write docs + check them -> only Laravel specific stuff * - Check old Laravel tests if we missed something - * - Check in Flare whether everything is working as expected + * - Check in Flare whether everything is working as expected -> PR ready, needs fixing TS * - Release */ diff --git a/tests/Actions/DiscoverTypesActionTest.php b/tests/Actions/DiscoverTypesActionTest.php index c4a50d86..582627c8 100644 --- a/tests/Actions/DiscoverTypesActionTest.php +++ b/tests/Actions/DiscoverTypesActionTest.php @@ -6,6 +6,7 @@ use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\OptionalAttributedClass; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\ReadonlyClass; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\SimpleClass; +use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\SimpleInterface; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\StringBackedEnum; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\TypeScriptAttributedClass; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\TypeScriptLocationAttributedClass; @@ -20,6 +21,7 @@ StringBackedEnum::class, HiddenAttributedClass::class, TypeScriptAttributedClass::class, + SimpleInterface::class, TypeScriptLocationAttributedClass::class, OptionalAttributedClass::class, ReadonlyClass::class, diff --git a/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php b/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php index 922601d4..d90d5007 100644 --- a/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php +++ b/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php @@ -17,6 +17,7 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnknown; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptVoid; it('can transpile php types', function ( string $property, @@ -138,3 +139,14 @@ new TypeReference(new ClassStringReference(Collection::class)), ]; }); + +it('can transpile a void return type', function () { + $transpiler = new TranspileReflectionTypeToTypeScriptNodeAction(); + + $typeScriptNode = $transpiler->execute( + (new ReflectionMethod(PhpTypesStub::class, 'voidReturn'))->getReturnType(), + new ReflectionClass(PhpTypesStub::class) + ); + + expect($typeScriptNode)->toBeInstanceOf(TypeScriptVoid::class); +}); diff --git a/tests/Fakes/PropertyTypes/PhpTypesStub.php b/tests/Fakes/PropertyTypes/PhpTypesStub.php index 5c4294a1..b15d115c 100644 --- a/tests/Fakes/PropertyTypes/PhpTypesStub.php +++ b/tests/Fakes/PropertyTypes/PhpTypesStub.php @@ -43,4 +43,9 @@ class PhpTypesStub extends stdClass public array $array; public Collection $reference; + + public function voidReturn(): void + { + + } } diff --git a/tests/Fakes/TypesToProvide/SimpleInterface.php b/tests/Fakes/TypesToProvide/SimpleInterface.php new file mode 100644 index 00000000..0c0aba74 --- /dev/null +++ b/tests/Fakes/TypesToProvide/SimpleInterface.php @@ -0,0 +1,25 @@ + + */ + public function withAnnotatedReturnType(): array; + + public function withParameters(string $param1, int $param2): void; + + public function withOptionalParameters(string $param1, int $param2 = 5): void; + + /** + * @param array $param1 + * @param array $param2 + */ + public function withAnnotatedParameters(array $param1, array $param2): void; +} diff --git a/tests/Support/AllInterfaceTransformer.php b/tests/Support/AllInterfaceTransformer.php new file mode 100644 index 00000000..99fccf9c --- /dev/null +++ b/tests/Support/AllInterfaceTransformer.php @@ -0,0 +1,14 @@ + Date: Thu, 29 Aug 2024 12:55:37 +0200 Subject: [PATCH 43/51] wip --- README.md | 8 ++-- composer.json | 2 +- src/Actions/ConnectReferencesAction.php | 2 +- src/Actions/DiscoverTypesAction.php | 2 - src/Actions/FindClassNameFqcnAction.php | 8 +++- src/Actions/ParseUseDefinitionsAction.php | 45 ------------------- src/Actions/ParseUserDefinedTypeAction.php | 2 +- ...spilePhpStanTypeToTypeScriptNodeAction.php | 34 +++++++------- ...leReflectionTypeToTypeScriptNodeAction.php | 28 ++++++------ src/Attributes/LiteralTypeScriptType.php | 8 ++-- src/Attributes/TypeScriptType.php | 6 +-- .../TypeScriptTypeAttributeContract.php | 2 +- ...ayLikeStructuresClassPropertyProcessor.php | 14 +++--- src/Collections/ImportsCollection.php | 2 +- src/Data/ImportLocation.php | 2 +- .../DataClassPropertyProcessor.php | 8 ++-- src/Laravel/LaravelDataTypesProvider.php | 8 ++-- .../LaravelNamedRouteTypesProvider.php | 34 +++++++------- .../LaravelRouteActionTypesProvider.php | 38 ++++++++-------- .../LaravelTypeScriptTransformerExtension.php | 2 +- src/Laravel/LaravelTypesProvider.php | 22 ++++----- src/Laravel/SpatieLaravelTypesProvider.php | 12 ++--- src/Transformed/Transformed.php | 8 ++-- .../ClassPropertyProcessor.php | 2 +- src/Transformers/ClassTransformer.php | 12 ++--- src/Transformers/EnumTransformer.php | 12 ++--- src/Transformers/InterfaceTransformer.php | 16 +++---- .../TypeReference.php | 2 +- .../TypeScriptAlias.php | 2 +- .../TypeScriptAny.php | 2 +- .../TypeScriptArray.php | 2 +- .../TypeScriptBoolean.php | 2 +- .../TypeScriptConditional.php | 2 +- .../TypeScriptEnum.php | 2 +- .../TypeScriptExport.php | 2 +- .../TypeScriptForwardingNamedNode.php | 2 +- .../TypeScriptFunction.php | 2 +- .../TypeScriptFunctionDefinition.php | 2 +- .../TypeScriptGeneric.php | 2 +- .../TypeScriptGenericTypeVariable.php | 2 +- .../TypeScriptIdentifier.php | 2 +- .../TypeScriptImport.php | 2 +- .../TypeScriptIndexSignature.php | 2 +- .../TypeScriptIndexedAccess.php | 2 +- .../TypeScriptInterface.php | 2 +- .../TypeScriptInterfaceMethod.php | 2 +- .../TypeScriptIntersection.php | 2 +- .../TypeScriptLiteral.php | 2 +- .../TypeScriptNamedNode.php | 2 +- .../TypeScriptNamespace.php | 2 +- .../TypeScriptNode.php | 3 +- .../TypeScriptNull.php | 2 +- .../TypeScriptNumber.php | 2 +- .../TypeScriptObject.php | 2 +- .../TypeScriptOperator.php | 2 +- .../TypeScriptParameter.php | 2 +- .../TypeScriptProperty.php | 2 +- .../TypeScriptRaw.php | 2 +- .../TypeScriptString.php | 2 +- .../TypeScriptUndefined.php | 2 +- .../TypeScriptUnion.php | 2 +- .../TypeScriptUnknown.php | 2 +- .../TypeScriptVisitableNode.php | 2 +- .../TypeScriptVoid.php | 2 +- src/TypeScriptTransformerConfigFactory.php | 6 +-- .../Common/ReplaceTypesVisitorClosure.php | 4 +- src/Visitor/Visitor.php | 4 +- src/Visitor/VisitorClosure.php | 2 +- src/Visitor/VisitorOperation.php | 2 +- src/Writers/NamespaceWriter.php | 2 +- tests/Actions/ConnectReferencesActionTest.php | 2 +- .../ParseUserDefinedTypeActionTest.php | 6 +-- tests/Actions/ProvideTypesActionTest.php | 2 +- .../ResolveModuleImportsActionTest.php | 6 +-- .../SplitTransformedPerLocationActionTest.php | 2 +- ...ePhpStanTypeToTypeScriptNodeActionTest.php | 34 +++++++------- ...flectionTypeToTypeScriptNodeActionTest.php | 26 +++++------ ...keStructuresClassPropertyProcessorTest.php | 20 ++++----- tests/Factories/TransformedFactory.php | 6 +-- .../LaravelRouteActionTypesProviderTest.php | 14 ++++++ tests/Support/MemoryWriter.php | 2 +- tests/Transformers/ClassTransformerTest.php | 16 +++---- .../TransformerTypesProviderTest.php | 10 ++--- tests/TypeReplacements.php | 18 ++++---- tests/TypeScript/TypeScriptEnumTest.php | 2 +- tests/Visitor/VisitorTest.php | 10 ++--- tests/VisitorClosures.php | 8 ++-- tests/Writers/FlatWriterTest.php | 8 ++-- tests/Writers/ModuleWriterTest.php | 8 ++-- tests/Writers/NamespaceWriterTest.php | 8 ++-- 90 files changed, 307 insertions(+), 335 deletions(-) delete mode 100644 src/Actions/ParseUseDefinitionsAction.php rename src/{TypeScript => TypeScriptNodes}/TypeReference.php (94%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptAlias.php (93%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptAny.php (78%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptArray.php (92%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptBoolean.php (79%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptConditional.php (92%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptEnum.php (94%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptExport.php (91%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptForwardingNamedNode.php (72%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptFunction.php (80%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptFunctionDefinition.php (95%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptGeneric.php (94%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptGenericTypeVariable.php (94%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptIdentifier.php (89%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptImport.php (90%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptIndexSignature.php (91%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptIndexedAccess.php (93%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptInterface.php (95%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptInterfaceMethod.php (94%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptIntersection.php (92%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptLiteral.php (84%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptNamedNode.php (59%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptNamespace.php (91%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptNode.php (63%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptNull.php (79%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptNumber.php (79%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptObject.php (93%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptOperator.php (96%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptParameter.php (93%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptProperty.php (95%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptRaw.php (84%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptString.php (79%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptUndefined.php (79%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptUnion.php (94%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptUnknown.php (79%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptVisitableNode.php (73%) rename src/{TypeScript => TypeScriptNodes}/TypeScriptVoid.php (79%) create mode 100644 tests/Laravel/LaravelRouteActionTypesProviderTest.php diff --git a/README.md b/README.md index 51025937..78a24701 100644 --- a/README.md +++ b/README.md @@ -567,7 +567,7 @@ $config->replaceType(DateTimeInterface::class, new TypeScriptString()); Or use a closure to define the replacement: ```php -use Spatie\TypeScriptTransformer\TypeScript\TypeReference; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; $config->replaceType(DateTimeInterface::class, function (TypeReference $reference) { return new TypeScriptString(); @@ -582,7 +582,7 @@ types and it is possible to create your own nodes. For example, a TypeScript alias is representing a User object looks like this: ```php -use Spatie\TypeScriptTransformer\TypeScript; +use Spatie\TypeScriptTransformer\TypeScriptNodes; new TypeScriptAlias( new TypeScriptIdentifier('User'), @@ -1012,8 +1012,8 @@ A TypeScript node is a regular PHP class that implements the `TypeScriptNode` in ```php use Spatie\TypeScriptTransformer\Support\WritingContext; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNamedNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNamedNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; class PickNode implements TypeScriptNode, TypeScriptNamedNode { diff --git a/composer.json b/composer.json index 177905ca..323cf278 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "phpstan/phpdoc-parser": "^1.13", "spatie/file-system-watcher": "^1.1", "spatie/laravel-package-tools": "^1.14.0", - "spatie/php-structure-discoverer": "^2.1", + "spatie/php-structure-discoverer": "^2.2", "spatie/temporary-directory": "^2.1" }, "require-dev": { diff --git a/src/Actions/ConnectReferencesAction.php b/src/Actions/ConnectReferencesAction.php index dc7283c0..ba11f1d5 100644 --- a/src/Actions/ConnectReferencesAction.php +++ b/src/Actions/ConnectReferencesAction.php @@ -6,7 +6,7 @@ use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; -use Spatie\TypeScriptTransformer\TypeScript\TypeReference; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; use Spatie\TypeScriptTransformer\Visitor\Visitor; class ConnectReferencesAction diff --git a/src/Actions/DiscoverTypesAction.php b/src/Actions/DiscoverTypesAction.php index 80f2310e..21d24a5c 100644 --- a/src/Actions/DiscoverTypesAction.php +++ b/src/Actions/DiscoverTypesAction.php @@ -14,8 +14,6 @@ class DiscoverTypesAction public function execute( array $directories = [], ): array { - // Idea / TODO : make it possible for other packages to hook in to find types in other directories, like their vendor dir - return Discover::in(...$directories) ->types( DiscoveredStructureType::ClassDefinition, diff --git a/src/Actions/FindClassNameFqcnAction.php b/src/Actions/FindClassNameFqcnAction.php index 7d2a4c37..411ad35c 100644 --- a/src/Actions/FindClassNameFqcnAction.php +++ b/src/Actions/FindClassNameFqcnAction.php @@ -4,12 +4,18 @@ use ReflectionClass; use Spatie\StructureDiscoverer\Collections\UsageCollection; +use Spatie\StructureDiscoverer\Support\UseDefinitionsResolver; class FindClassNameFqcnAction { /** @var array */ protected static array $cache = []; + public function __construct( + protected UseDefinitionsResolver $useDefinitionsResolver = new UseDefinitionsResolver() + ) { + } + public function execute(ReflectionClass $reflectionClass, string $className): ?string { $usages = $this->loadUsages($reflectionClass); @@ -38,7 +44,7 @@ protected function loadUsages(ReflectionClass $reflectionClass): UsageCollection $filename = $reflectionClass->getFileName(); if (! array_key_exists($filename, static::$cache)) { - static::$cache[$filename] = (new ParseUseDefinitionsAction())->execute($filename); + static::$cache[$filename] = $this->useDefinitionsResolver->execute($filename); } return static::$cache[$filename]; diff --git a/src/Actions/ParseUseDefinitionsAction.php b/src/Actions/ParseUseDefinitionsAction.php deleted file mode 100644 index 8ec6a17a..00000000 --- a/src/Actions/ParseUseDefinitionsAction.php +++ /dev/null @@ -1,45 +0,0 @@ -reject(fn (PhpToken $token) => $token->is([T_COMMENT, T_DOC_COMMENT, T_WHITESPACE])) - ->values() - ->pipe(fn (Collection $collection): TokenCollection => new TokenCollection($collection->all())); - } catch (ParseError) { - return new UsageCollection(); - } - - $usages = new UsageCollection(); - - for ($index = 0; $index < $tokens->count(); $index++) { - if ($tokens->get($index)->is(T_USE)) { - $usages->add(...$this->useResolver->execute($index + 1, $tokens)); - } - } - - return $usages; - } -} diff --git a/src/Actions/ParseUserDefinedTypeAction.php b/src/Actions/ParseUserDefinedTypeAction.php index a1f1b318..5499a633 100644 --- a/src/Actions/ParseUserDefinedTypeAction.php +++ b/src/Actions/ParseUserDefinedTypeAction.php @@ -8,7 +8,7 @@ use PHPStan\PhpDocParser\Parser\TypeParser; use ReflectionClass; use Spatie\TypeScriptTransformer\Support\Concerns\Instanceable; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; class ParseUserDefinedTypeAction { diff --git a/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php b/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php index f387248a..3d2db073 100644 --- a/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php +++ b/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php @@ -16,23 +16,23 @@ use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use ReflectionClass; use Spatie\TypeScriptTransformer\References\ClassStringReference; -use Spatie\TypeScriptTransformer\TypeScript\TypeReference; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAny; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptArray; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptBoolean; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptFunction; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGeneric; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIntersection; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNull; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNumber; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnknown; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptVoid; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAny; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptArray; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptBoolean; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptFunction; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptGeneric; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIntersection; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNull; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNumber; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptObject; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptString; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnion; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnknown; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptVoid; class TranspilePhpStanTypeToTypeScriptNodeAction { diff --git a/src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php b/src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php index 200921fc..0ab938c5 100644 --- a/src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php +++ b/src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php @@ -8,20 +8,20 @@ use ReflectionType; use ReflectionUnionType; use Spatie\TypeScriptTransformer\References\ClassStringReference; -use Spatie\TypeScriptTransformer\TypeScript\TypeReference; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAny; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptArray; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptBoolean; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIntersection; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNull; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNumber; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUndefined; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnknown; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptVoid; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAny; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptArray; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptBoolean; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIntersection; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNull; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNumber; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptObject; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptString; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUndefined; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnion; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnknown; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptVoid; class TranspileReflectionTypeToTypeScriptNodeAction { diff --git a/src/Attributes/LiteralTypeScriptType.php b/src/Attributes/LiteralTypeScriptType.php index c217977a..acb9b30b 100644 --- a/src/Attributes/LiteralTypeScriptType.php +++ b/src/Attributes/LiteralTypeScriptType.php @@ -4,10 +4,10 @@ use Attribute; use ReflectionClass; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptRaw; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptObject; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptRaw; #[Attribute] class LiteralTypeScriptType implements TypeScriptTypeAttributeContract diff --git a/src/Attributes/TypeScriptType.php b/src/Attributes/TypeScriptType.php index db533846..435b167a 100644 --- a/src/Attributes/TypeScriptType.php +++ b/src/Attributes/TypeScriptType.php @@ -6,9 +6,9 @@ use ReflectionClass; use Spatie\TypeScriptTransformer\Actions\TranspilePhpStanTypeToTypeScriptNodeAction; use Spatie\TypeScriptTransformer\TypeResolvers\DocTypeResolver; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptObject; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; #[Attribute] class TypeScriptType implements TypeScriptTypeAttributeContract diff --git a/src/Attributes/TypeScriptTypeAttributeContract.php b/src/Attributes/TypeScriptTypeAttributeContract.php index 098d55ba..c205f7f9 100644 --- a/src/Attributes/TypeScriptTypeAttributeContract.php +++ b/src/Attributes/TypeScriptTypeAttributeContract.php @@ -3,7 +3,7 @@ namespace Spatie\TypeScriptTransformer\Attributes; use ReflectionClass; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; interface TypeScriptTypeAttributeContract { diff --git a/src/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessor.php b/src/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessor.php index c7bf051f..997308cb 100644 --- a/src/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessor.php +++ b/src/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessor.php @@ -6,13 +6,13 @@ use ReflectionProperty; use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\Transformers\ClassPropertyProcessors\ClassPropertyProcessor; -use Spatie\TypeScriptTransformer\TypeScript\TypeReference; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptArray; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGeneric; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptArray; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptGeneric; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptString; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnion; use Spatie\TypeScriptTransformer\Visitor\Visitor; use Spatie\TypeScriptTransformer\Visitor\VisitorOperation; diff --git a/src/Collections/ImportsCollection.php b/src/Collections/ImportsCollection.php index 5db7903c..60d74865 100644 --- a/src/Collections/ImportsCollection.php +++ b/src/Collections/ImportsCollection.php @@ -6,7 +6,7 @@ use Spatie\TypeScriptTransformer\Data\ImportLocation; use Spatie\TypeScriptTransformer\References\Reference; use Spatie\TypeScriptTransformer\Support\ImportName; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptImport; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptImport; use Traversable; class ImportsCollection implements IteratorAggregate diff --git a/src/Data/ImportLocation.php b/src/Data/ImportLocation.php index a53ffd75..31a945ec 100644 --- a/src/Data/ImportLocation.php +++ b/src/Data/ImportLocation.php @@ -4,7 +4,7 @@ use Spatie\TypeScriptTransformer\References\Reference; use Spatie\TypeScriptTransformer\Support\ImportName; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptImport; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptImport; class ImportLocation { diff --git a/src/Laravel/ClassPropertyProcessors/DataClassPropertyProcessor.php b/src/Laravel/ClassPropertyProcessors/DataClassPropertyProcessor.php index 5743f442..257e3473 100644 --- a/src/Laravel/ClassPropertyProcessors/DataClassPropertyProcessor.php +++ b/src/Laravel/ClassPropertyProcessors/DataClassPropertyProcessor.php @@ -8,10 +8,10 @@ use Spatie\LaravelData\Support\DataConfig; use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\Transformers\ClassPropertyProcessors\ClassPropertyProcessor; -use Spatie\TypeScriptTransformer\TypeScript\TypeReference; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnion; class DataClassPropertyProcessor implements ClassPropertyProcessor { diff --git a/src/Laravel/LaravelDataTypesProvider.php b/src/Laravel/LaravelDataTypesProvider.php index d8a2dec4..71d1b4fa 100644 --- a/src/Laravel/LaravelDataTypesProvider.php +++ b/src/Laravel/LaravelDataTypesProvider.php @@ -10,10 +10,10 @@ use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; -use Spatie\TypeScriptTransformer\TypeScript\TypeReference; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGeneric; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAlias; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptGeneric; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class LaravelDataTypesProvider implements TypesProvider diff --git a/src/Laravel/LaravelNamedRouteTypesProvider.php b/src/Laravel/LaravelNamedRouteTypesProvider.php index a962c594..3c94bb6a 100644 --- a/src/Laravel/LaravelNamedRouteTypesProvider.php +++ b/src/Laravel/LaravelNamedRouteTypesProvider.php @@ -15,22 +15,22 @@ use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; -use Spatie\TypeScriptTransformer\TypeScript\TypeReference; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptFunctionDefinition; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGeneric; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGenericTypeVariable; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIndexedAccess; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNumber; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptOperator; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptParameter; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptRaw; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAlias; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptFunctionDefinition; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptGeneric; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptGenericTypeVariable; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIndexedAccess; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNumber; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptObject; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptOperator; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptParameter; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptRaw; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptString; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnion; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class LaravelNamedRouteTypesProvider implements TypesProvider @@ -41,7 +41,7 @@ class LaravelNamedRouteTypesProvider implements TypesProvider */ public function __construct( protected ResolveLaravelRoutControllerCollectionsAction $resolveLaravelRoutControllerCollectionsAction = new ResolveLaravelRoutControllerCollectionsAction(), - protected array $location = [], + protected array $location = ['App'], protected array $filters = [], ) { } diff --git a/src/Laravel/LaravelRouteActionTypesProvider.php b/src/Laravel/LaravelRouteActionTypesProvider.php index 00cfea51..5011cd6e 100644 --- a/src/Laravel/LaravelRouteActionTypesProvider.php +++ b/src/Laravel/LaravelRouteActionTypesProvider.php @@ -14,24 +14,24 @@ use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; -use Spatie\TypeScriptTransformer\TypeScript\TypeReference; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptArray; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptConditional; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptFunctionDefinition; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGeneric; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGenericTypeVariable; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIndexedAccess; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNumber; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptOperator; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptParameter; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptRaw; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAlias; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptArray; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptConditional; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptFunctionDefinition; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptGeneric; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptGenericTypeVariable; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIndexedAccess; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNumber; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptObject; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptOperator; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptParameter; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptRaw; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptString; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnion; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class LaravelRouteActionTypesProvider implements TypesProvider @@ -43,7 +43,7 @@ class LaravelRouteActionTypesProvider implements TypesProvider public function __construct( protected ResolveLaravelRoutControllerCollectionsAction $resolveLaravelRoutControllerCollectionsAction = new ResolveLaravelRoutControllerCollectionsAction(), protected ?string $defaultNamespace = null, - protected array $location = [], + protected array $location = ['App'], protected array $filters = [], ) { } diff --git a/src/Laravel/LaravelTypeScriptTransformerExtension.php b/src/Laravel/LaravelTypeScriptTransformerExtension.php index 4eaf22fe..2dde856e 100644 --- a/src/Laravel/LaravelTypeScriptTransformerExtension.php +++ b/src/Laravel/LaravelTypeScriptTransformerExtension.php @@ -6,7 +6,7 @@ use Spatie\TypeScriptTransformer\Laravel\Transformers\LaravelAttributedClassTransformer; use Spatie\TypeScriptTransformer\Support\Extensions\TypeScriptTransformerExtension; use Spatie\TypeScriptTransformer\Transformers\AttributedClassTransformer; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptString; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfigFactory; class LaravelTypeScriptTransformerExtension implements TypeScriptTransformerExtension diff --git a/src/Laravel/LaravelTypesProvider.php b/src/Laravel/LaravelTypesProvider.php index 5b8d419c..68791a1b 100644 --- a/src/Laravel/LaravelTypesProvider.php +++ b/src/Laravel/LaravelTypesProvider.php @@ -10,17 +10,17 @@ use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; -use Spatie\TypeScriptTransformer\TypeScript\TypeReference; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptBoolean; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGeneric; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNull; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNumber; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAlias; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptBoolean; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptGeneric; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNull; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNumber; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptObject; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptString; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnion; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class LaravelTypesProvider implements TypesProvider diff --git a/src/Laravel/SpatieLaravelTypesProvider.php b/src/Laravel/SpatieLaravelTypesProvider.php index 6fe82492..7f092105 100644 --- a/src/Laravel/SpatieLaravelTypesProvider.php +++ b/src/Laravel/SpatieLaravelTypesProvider.php @@ -6,12 +6,12 @@ use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGeneric; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGenericTypeVariable; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAlias; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptGeneric; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptGenericTypeVariable; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptObject; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class SpatieLaravelTypesProvider implements TypesProvider diff --git a/src/Transformed/Transformed.php b/src/Transformed/Transformed.php index e94d9dec..20d80c55 100644 --- a/src/Transformed/Transformed.php +++ b/src/Transformed/Transformed.php @@ -4,10 +4,10 @@ use Spatie\TypeScriptTransformer\References\Reference; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptExport; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptForwardingNamedNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNamedNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptExport; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptForwardingNamedNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNamedNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; class Transformed { diff --git a/src/Transformers/ClassPropertyProcessors/ClassPropertyProcessor.php b/src/Transformers/ClassPropertyProcessors/ClassPropertyProcessor.php index 729e3a16..46f6f094 100644 --- a/src/Transformers/ClassPropertyProcessors/ClassPropertyProcessor.php +++ b/src/Transformers/ClassPropertyProcessors/ClassPropertyProcessor.php @@ -4,7 +4,7 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use ReflectionProperty; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; interface ClassPropertyProcessor { diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index 03563ed3..121db7a6 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -17,12 +17,12 @@ use Spatie\TypeScriptTransformer\Transformed\Untransformable; use Spatie\TypeScriptTransformer\Transformers\ClassPropertyProcessors\ClassPropertyProcessor; use Spatie\TypeScriptTransformer\TypeResolvers\DocTypeResolver; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnknown; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAlias; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptObject; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnknown; abstract class ClassTransformer implements Transformer { diff --git a/src/Transformers/EnumTransformer.php b/src/Transformers/EnumTransformer.php index 0e8768e3..c50c65d7 100644 --- a/src/Transformers/EnumTransformer.php +++ b/src/Transformers/EnumTransformer.php @@ -9,12 +9,12 @@ use Spatie\TypeScriptTransformer\Transformed\Untransformable; use Spatie\TypeScriptTransformer\Transformers\EnumProviders\EnumProvider; use Spatie\TypeScriptTransformer\Transformers\EnumProviders\PhpEnumProvider; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptEnum; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptLiteral; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAlias; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptEnum; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptLiteral; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnion; class EnumTransformer implements Transformer { diff --git a/src/Transformers/InterfaceTransformer.php b/src/Transformers/InterfaceTransformer.php index d64baafd..1da7209a 100644 --- a/src/Transformers/InterfaceTransformer.php +++ b/src/Transformers/InterfaceTransformer.php @@ -14,14 +14,14 @@ use Spatie\TypeScriptTransformer\TypeResolvers\Data\ParsedMethod; use Spatie\TypeScriptTransformer\TypeResolvers\Data\ParsedNameAndType; use Spatie\TypeScriptTransformer\TypeResolvers\DocTypeResolver; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptInterface; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptInterfaceMethod; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptParameter; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnknown; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptVoid; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptInterface; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptInterfaceMethod; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptParameter; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnknown; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptVoid; abstract class InterfaceTransformer implements Transformer { diff --git a/src/TypeScript/TypeReference.php b/src/TypeScriptNodes/TypeReference.php similarity index 94% rename from src/TypeScript/TypeReference.php rename to src/TypeScriptNodes/TypeReference.php index b7c817ca..24fd89bf 100644 --- a/src/TypeScript/TypeReference.php +++ b/src/TypeScriptNodes/TypeReference.php @@ -1,6 +1,6 @@ action = new ResolveModuleImportsAction(); diff --git a/tests/Actions/SplitTransformedPerLocationActionTest.php b/tests/Actions/SplitTransformedPerLocationActionTest.php index a4bab933..ffa23e6a 100644 --- a/tests/Actions/SplitTransformedPerLocationActionTest.php +++ b/tests/Actions/SplitTransformedPerLocationActionTest.php @@ -4,7 +4,7 @@ use Spatie\TypeScriptTransformer\Support\Location; use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Tests\Factories\TransformedFactory; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptString; it('can split per location', function () { $transformedCollection = new TransformedCollection([ diff --git a/tests/Actions/TranspilePhpStanTypeToTypeScriptNodeActionTest.php b/tests/Actions/TranspilePhpStanTypeToTypeScriptNodeActionTest.php index 273163c1..d30849fa 100644 --- a/tests/Actions/TranspilePhpStanTypeToTypeScriptNodeActionTest.php +++ b/tests/Actions/TranspilePhpStanTypeToTypeScriptNodeActionTest.php @@ -5,23 +5,23 @@ use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\Tests\Fakes\PropertyTypes\PhpDocTypesStub; use Spatie\TypeScriptTransformer\TypeResolvers\DocTypeResolver; -use Spatie\TypeScriptTransformer\TypeScript\TypeReference; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAny; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptArray; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptBoolean; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptFunction; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGeneric; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIntersection; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNull; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNumber; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnknown; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptVoid; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAny; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptArray; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptBoolean; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptFunction; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptGeneric; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIntersection; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNull; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNumber; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptObject; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptString; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnion; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnknown; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptVoid; it('can transpile PHPStan doc types', function ( string $property, diff --git a/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php b/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php index d90d5007..922844bf 100644 --- a/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php +++ b/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php @@ -5,19 +5,19 @@ use Spatie\TypeScriptTransformer\Actions\TranspileReflectionTypeToTypeScriptNodeAction; use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\Tests\Fakes\PropertyTypes\PhpTypesStub; -use Spatie\TypeScriptTransformer\TypeScript\TypeReference; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAny; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptArray; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptBoolean; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIntersection; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNull; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNumber; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnknown; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptVoid; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAny; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptArray; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptBoolean; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIntersection; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNull; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNumber; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptObject; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptString; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnion; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnknown; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptVoid; it('can transpile php types', function ( string $property, diff --git a/tests/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessorTest.php b/tests/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessorTest.php index 092315f9..bca6289a 100644 --- a/tests/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessorTest.php +++ b/tests/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessorTest.php @@ -3,16 +3,16 @@ use Illuminate\Support\Collection; use Spatie\TypeScriptTransformer\ClassPropertyProcessors\FixArrayLikeStructuresClassPropertyProcessor; use Spatie\TypeScriptTransformer\References\ClassStringReference; -use Spatie\TypeScriptTransformer\TypeScript\TypeReference; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptArray; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptBoolean; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptGeneric; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNumber; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptArray; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptBoolean; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptGeneric; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNumber; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptString; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnion; it('replaces array like structures', function ( string $property, diff --git a/tests/Factories/TransformedFactory.php b/tests/Factories/TransformedFactory.php index 3b3776be..ad8519f2 100644 --- a/tests/Factories/TransformedFactory.php +++ b/tests/Factories/TransformedFactory.php @@ -6,9 +6,9 @@ use Spatie\TypeScriptTransformer\References\CustomReference; use Spatie\TypeScriptTransformer\References\Reference; use Spatie\TypeScriptTransformer\Transformed\Transformed; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAlias; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; class TransformedFactory { diff --git a/tests/Laravel/LaravelRouteActionTypesProviderTest.php b/tests/Laravel/LaravelRouteActionTypesProviderTest.php new file mode 100644 index 00000000..55d0c601 --- /dev/null +++ b/tests/Laravel/LaravelRouteActionTypesProviderTest.php @@ -0,0 +1,14 @@ +getActionTypes($route); + + expect($actionTypes)->toBe([ + 'controller' => 'TestController', + 'method' => 'test', + ]); +}); diff --git a/tests/Support/MemoryWriter.php b/tests/Support/MemoryWriter.php index 7c2293b0..3e9f8302 100644 --- a/tests/Support/MemoryWriter.php +++ b/tests/Support/MemoryWriter.php @@ -4,7 +4,7 @@ use Spatie\TypeScriptTransformer\Collections\ReferenceMap; use Spatie\TypeScriptTransformer\Support\TransformedCollection; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; use Spatie\TypeScriptTransformer\Writers\FlatWriter; use Spatie\TypeScriptTransformer\Writers\Writer; diff --git a/tests/Transformers/ClassTransformerTest.php b/tests/Transformers/ClassTransformerTest.php index 9a33f655..8376d622 100644 --- a/tests/Transformers/ClassTransformerTest.php +++ b/tests/Transformers/ClassTransformerTest.php @@ -15,14 +15,14 @@ use Spatie\TypeScriptTransformer\Tests\Support\AllClassTransformer; use Spatie\TypeScriptTransformer\Transformers\ClassPropertyProcessors\ClassPropertyProcessor; use Spatie\TypeScriptTransformer\Transformers\ClassTransformer; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNumber; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptRaw; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnknown; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAlias; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNumber; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptObject; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptRaw; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptString; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnknown; it('can transform a class', function () { $transformed = transformSingle(SimpleClass::class); diff --git a/tests/TypeProviders/TransformerTypesProviderTest.php b/tests/TypeProviders/TransformerTypesProviderTest.php index 78348636..44951615 100644 --- a/tests/TypeProviders/TransformerTypesProviderTest.php +++ b/tests/TypeProviders/TransformerTypesProviderTest.php @@ -14,11 +14,11 @@ use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\Transformers\EnumTransformer; use Spatie\TypeScriptTransformer\TypeProviders\TransformerTypesProvider; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAlias; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptObject; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptString; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfigFactory; function getTestProvidedTypes( diff --git a/tests/TypeReplacements.php b/tests/TypeReplacements.php index 68594b79..8fc53845 100644 --- a/tests/TypeReplacements.php +++ b/tests/TypeReplacements.php @@ -5,15 +5,15 @@ use Spatie\TypeScriptTransformer\Tests\Factories\TransformedFactory; use Spatie\TypeScriptTransformer\Tests\Support\InlineTypesProvider; use Spatie\TypeScriptTransformer\Tests\Support\MemoryWriter; -use Spatie\TypeScriptTransformer\TypeScript\TypeReference; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNumber; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptObject; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptRaw; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAlias; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNumber; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptObject; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptRaw; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptString; use Spatie\TypeScriptTransformer\TypeScriptTransformer; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfigFactory; diff --git a/tests/TypeScript/TypeScriptEnumTest.php b/tests/TypeScript/TypeScriptEnumTest.php index dcee8a6b..1201381f 100644 --- a/tests/TypeScript/TypeScriptEnumTest.php +++ b/tests/TypeScript/TypeScriptEnumTest.php @@ -1,7 +1,7 @@ Date: Fri, 20 Sep 2024 11:16:48 +0200 Subject: [PATCH 44/51] wip --- composer.json | 1 + src/Actions/ConnectReferencesAction.php | 57 +++++-- src/Actions/DiscoverTypesAction.php | 17 +- .../ExecuteConnectedClosuresAction.php | 36 +++++ src/Actions/ExecuteProvidedClosuresAction.php | 36 +++++ src/Actions/FindClassNameFqcnAction.php | 14 +- src/Actions/ParseUserDefinedTypeAction.php | 6 +- src/Actions/ResolveModuleImportsAction.php | 2 +- src/Actions/TransformTypesAction.php | 31 +--- ...spilePhpStanTypeToTypeScriptNodeAction.php | 64 ++++---- ...pilePhpTypeNodeToTypeScriptNodeAction.php} | 56 +++---- src/Actions/WatchFileSystemAction.php | 25 --- src/Attributes/LiteralTypeScriptType.php | 4 +- src/Attributes/TypeScriptType.php | 4 +- .../TypeScriptTypeAttributeContract.php | 4 +- ...ayLikeStructuresClassPropertyProcessor.php | 4 +- src/Collections/ReferenceMap.php | 11 ++ .../Watch/DirectoryCreatedWatchEvent.php | 7 - src/FileSystemWatcher.php | 148 ++++++++++++++++++ .../DirectoryDeletedWatchEventHandler.php | 11 ++ .../Watch/FileCreatedWatchEventHandler.php | 49 ++++++ .../Watch/FileDeletedWatchEventHandler.php | 40 +++++ .../Watch/FileUpdatedWatchEventHandler.php | 79 ++++++++++ src/Handlers/Watch/WatchEventHandler.php | 8 + .../DataClassPropertyProcessor.php | 21 +-- .../Commands/TransformTypeScriptCommand.php | 21 +-- .../Commands/WatchTypeScriptCommand.php | 18 ++- src/Laravel/Support/WrappedLaravelConsole.php | 34 ++++ .../Transformers/DataClassTransformer.php | 12 +- src/PhpNodes/PhpAttributeNode.php | 36 +++++ src/PhpNodes/PhpClassNode.php | 145 +++++++++++++++++ src/PhpNodes/PhpEnumCaseNode.php | 33 ++++ src/PhpNodes/PhpEnumNode.php | 36 +++++ src/PhpNodes/PhpIntersectionTypeNode.php | 24 +++ src/PhpNodes/PhpMethodNode.php | 45 ++++++ src/PhpNodes/PhpNamedTypeNode.php | 22 +++ src/PhpNodes/PhpParameterNode.php | 40 +++++ src/PhpNodes/PhpPropertyNode.php | 74 +++++++++ src/PhpNodes/PhpTypeNode.php | 35 +++++ src/PhpNodes/PhpUnionTypeNode.php | 24 +++ src/References/FilesystemReference.php | 8 + src/References/PhpClassReference.php | 19 +++ src/References/ReflectionClassReference.php | 14 -- src/Support/Console/WrappedArrayConsole.php | 29 ++++ src/Support/Console/WrappedConsole.php | 14 ++ src/Support/Console/WrappedNullConsole.php | 26 +++ src/Support/LoadPhpClassNodeAction.php | 53 +++++++ src/Support/Location.php | 11 ++ src/Support/TransformationContext.php | 14 +- src/Support/TransformedCollection.php | 97 +++++++++++- src/Support/TypeScriptTransformerLog.php | 33 ++-- src/Transformed/Transformed.php | 79 +++++++++- .../AttributedClassTransformer.php | 20 +-- .../ClassPropertyProcessor.php | 4 +- src/Transformers/ClassTransformer.php | 118 +++++++------- .../EnumProviders/EnumProvider.php | 8 +- .../EnumProviders/PhpEnumProvider.php | 28 ++-- src/Transformers/EnumTransformer.php | 14 +- src/Transformers/InterfaceTransformer.php | 90 +++++------ src/Transformers/Transformer.php | 4 +- .../TransformerTypesProvider.php | 6 +- src/TypeResolvers/DocTypeResolver.php | 26 +-- src/TypeScriptNodes/TypeReference.php | 5 + src/TypeScriptTransformer.php | 91 +++++++---- src/TypeScriptTransformerConfig.php | 3 + src/TypeScriptTransformerConfigFactory.php | 13 +- src/Visitor/VisitorClosure.php | 4 +- src/Writers/FlatWriter.php | 2 +- src/Writers/ModuleWriter.php | 8 +- tests/Actions/ConnectReferencesActionTest.php | 44 ++++-- tests/Actions/DiscoverTypesActionTest.php | 24 +-- .../ParseUserDefinedTypeActionTest.php | 3 +- tests/Actions/ProvideTypesActionTest.php | 7 +- tests/Actions/TransformTypesActionTest.php | 11 +- ...ePhpStanTypeToTypeScriptNodeActionTest.php | 6 +- ...flectionTypeToTypeScriptNodeActionTest.php | 17 +- ...keStructuresClassPropertyProcessorTest.php | 5 +- tests/Factories/TransformedFactory.php | 19 ++- tests/Integration.php | 6 +- .../LaravelRouteActionTypesProviderTest.php | 2 + tests/Pest.php | 6 +- tests/Support/AllClassTransformer.php | 4 +- tests/Support/AllInterfaceTransformer.php | 4 +- tests/Support/TransformationContextTest.php | 17 +- tests/Transformers/ClassTransformerTest.php | 16 +- .../TransformerTypesProviderTest.php | 12 +- tests/Writers/FlatWriterTest.php | 3 +- tests/Writers/ModuleWriterTest.php | 11 +- tests/Writers/NamespaceWriterTest.php | 3 +- 89 files changed, 1871 insertions(+), 524 deletions(-) create mode 100644 src/Actions/ExecuteConnectedClosuresAction.php create mode 100644 src/Actions/ExecuteProvidedClosuresAction.php rename src/Actions/{TranspileReflectionTypeToTypeScriptNodeAction.php => TranspilePhpTypeNodeToTypeScriptNodeAction.php} (68%) delete mode 100644 src/Actions/WatchFileSystemAction.php delete mode 100644 src/Events/Watch/DirectoryCreatedWatchEvent.php create mode 100644 src/FileSystemWatcher.php create mode 100644 src/Handlers/Watch/DirectoryDeletedWatchEventHandler.php create mode 100644 src/Handlers/Watch/FileCreatedWatchEventHandler.php create mode 100644 src/Handlers/Watch/FileDeletedWatchEventHandler.php create mode 100644 src/Handlers/Watch/FileUpdatedWatchEventHandler.php create mode 100644 src/Handlers/Watch/WatchEventHandler.php create mode 100644 src/Laravel/Support/WrappedLaravelConsole.php create mode 100644 src/PhpNodes/PhpAttributeNode.php create mode 100644 src/PhpNodes/PhpClassNode.php create mode 100644 src/PhpNodes/PhpEnumCaseNode.php create mode 100644 src/PhpNodes/PhpEnumNode.php create mode 100644 src/PhpNodes/PhpIntersectionTypeNode.php create mode 100644 src/PhpNodes/PhpMethodNode.php create mode 100644 src/PhpNodes/PhpNamedTypeNode.php create mode 100644 src/PhpNodes/PhpParameterNode.php create mode 100644 src/PhpNodes/PhpPropertyNode.php create mode 100644 src/PhpNodes/PhpTypeNode.php create mode 100644 src/PhpNodes/PhpUnionTypeNode.php create mode 100644 src/References/FilesystemReference.php create mode 100644 src/References/PhpClassReference.php delete mode 100644 src/References/ReflectionClassReference.php create mode 100644 src/Support/Console/WrappedArrayConsole.php create mode 100644 src/Support/Console/WrappedConsole.php create mode 100644 src/Support/Console/WrappedNullConsole.php create mode 100644 src/Support/LoadPhpClassNodeAction.php diff --git a/composer.json b/composer.json index 323cf278..6bec3bf3 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "php": "^8.2", "illuminate/contracts": "^10.0|^11.0", "phpstan/phpdoc-parser": "^1.13", + "roave/better-reflection": "^6.41", "spatie/file-system-watcher": "^1.1", "spatie/laravel-package-tools": "^1.14.0", "spatie/php-structure-discoverer": "^2.2", diff --git a/src/Actions/ConnectReferencesAction.php b/src/Actions/ConnectReferencesAction.php index ba11f1d5..f4c5948b 100644 --- a/src/Actions/ConnectReferencesAction.php +++ b/src/Actions/ConnectReferencesAction.php @@ -11,11 +11,18 @@ class ConnectReferencesAction { - public function __construct() - { + protected Visitor $visitor; + + public function __construct( + protected TypeScriptTransformerLog $log, + ) { + $this->visitor = $this->resolveVisitor(); } - public function execute(TransformedCollection $collection): ReferenceMap + /** + * @param TransformedCollection|array $collection + */ + public function execute(TransformedCollection|array $collection): ReferenceMap { $referenceMap = new ReferenceMap(); @@ -23,31 +30,49 @@ public function execute(TransformedCollection $collection): ReferenceMap $referenceMap->add($transformed); } - $visitor = Visitor::create()->before(function (TypeReference $typeReference, array &$metadata) use ($referenceMap) { + foreach ($collection as $transformed) { + $metadata = [ + 'transformed' => $transformed, + 'referenceMap' => $referenceMap, + ]; + + $this->visitor->execute($transformed->typeScriptNode, $metadata); + } + + return $referenceMap; + } + + protected function resolveVisitor(): Visitor + { + return Visitor::create()->before(function (TypeReference $typeReference, array &$metadata) { /** @var Transformed $transformed */ $transformed = $metadata['transformed']; + /** @var ReferenceMap $referenceMap */ + $referenceMap = $metadata['referenceMap']; + if (! $referenceMap->has($typeReference->reference)) { - TypeScriptTransformerLog::resolve()->warning("Tried replacing reference to `{$typeReference->reference->humanFriendlyName()}` in `{$transformed->reference->humanFriendlyName()}` but it was not found in the transformed types"); + $transformed->addMissingReference($typeReference->reference, $typeReference); + + $this->log->warning("Tried replacing reference to `{$typeReference->reference->humanFriendlyName()}` in `{$transformed->reference->humanFriendlyName()}` but it was not found in the transformed types"); return; } $transformedReference = $referenceMap->get($typeReference->reference); - $transformed->references[] = $transformedReference; - - $typeReference->connect($transformedReference); - }, [TypeReference::class]); + if(! $transformed->references->offsetExists($transformedReference)) { + $transformed->references[$transformedReference] = []; + } - foreach ($collection as $transformed) { - $metadata = [ - 'transformed' => $transformed, - ]; + $transformed->references[$transformedReference][] = $typeReference; + $transformedReference->referencedBy[$transformed] = $transformed->reference->getKey(); - $visitor->execute($transformed->typeScriptNode, $metadata); - } + $typeReference->connect($transformedReference); - return $referenceMap; + if (array_key_exists($typeReference->reference->getKey(), $transformed->missingReferences)) { + unset($transformed->missingReferences[$typeReference->reference->getKey()]); + } + }, [TypeReference::class]); } } diff --git a/src/Actions/DiscoverTypesAction.php b/src/Actions/DiscoverTypesAction.php index 21d24a5c..0d87f53b 100644 --- a/src/Actions/DiscoverTypesAction.php +++ b/src/Actions/DiscoverTypesAction.php @@ -2,24 +2,35 @@ namespace Spatie\TypeScriptTransformer\Actions; +use ReflectionClass; use Spatie\StructureDiscoverer\Discover; use Spatie\StructureDiscoverer\Enums\DiscoveredStructureType; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; class DiscoverTypesAction { /** - * @param array $directories - * @return array + * @param array $directories + * + * @return array */ public function execute( array $directories = [], ): array { - return Discover::in(...$directories) + $discovered = Discover::in(...$directories) ->types( DiscoveredStructureType::ClassDefinition, DiscoveredStructureType::Enum, DiscoveredStructureType::Interface ) ->get(); + + return array_values(array_filter(array_map(function (string $discovered) { + try { + return PhpClassNode::fromReflection(new ReflectionClass($discovered)); + } catch (\ReflectionException) { + return null; + } + }, $discovered))); } } diff --git a/src/Actions/ExecuteConnectedClosuresAction.php b/src/Actions/ExecuteConnectedClosuresAction.php new file mode 100644 index 00000000..bf3c0cbd --- /dev/null +++ b/src/Actions/ExecuteConnectedClosuresAction.php @@ -0,0 +1,36 @@ +visitor = Visitor::create()->closures(...$this->config->connectedVisitorClosures); + } + + /** + * @param TransformedCollection|array $nodes + */ + public function execute( + TransformedCollection|array $nodes, + ): void { + if (empty($this->config->providedVisitorClosures)) { + return; + } + + $isTransformedCollection = $nodes instanceof TransformedCollection; + + foreach ($nodes as $node) { + $this->visitor->execute($isTransformedCollection ? $node->typeScriptNode : $node); + } + } +} diff --git a/src/Actions/ExecuteProvidedClosuresAction.php b/src/Actions/ExecuteProvidedClosuresAction.php new file mode 100644 index 00000000..8f37aea8 --- /dev/null +++ b/src/Actions/ExecuteProvidedClosuresAction.php @@ -0,0 +1,36 @@ +visitor = Visitor::create()->closures(...$this->config->providedVisitorClosures); + } + + /** + * @param TransformedCollection|array $nodes + */ + public function execute( + TransformedCollection|array $nodes, + ): void { + if (empty($this->config->providedVisitorClosures)) { + return; + } + + $isTransformedCollection = $nodes instanceof TransformedCollection; + + foreach ($nodes as $node) { + $this->visitor->execute($isTransformedCollection ? $node->typeScriptNode : $node); + } + } +} diff --git a/src/Actions/FindClassNameFqcnAction.php b/src/Actions/FindClassNameFqcnAction.php index 411ad35c..bb1b320c 100644 --- a/src/Actions/FindClassNameFqcnAction.php +++ b/src/Actions/FindClassNameFqcnAction.php @@ -2,9 +2,9 @@ namespace Spatie\TypeScriptTransformer\Actions; -use ReflectionClass; use Spatie\StructureDiscoverer\Collections\UsageCollection; use Spatie\StructureDiscoverer\Support\UseDefinitionsResolver; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; class FindClassNameFqcnAction { @@ -16,9 +16,9 @@ public function __construct( ) { } - public function execute(ReflectionClass $reflectionClass, string $className): ?string + public function execute(PhpClassNode $node, string $className): ?string { - $usages = $this->loadUsages($reflectionClass); + $usages = $this->loadUsages($node); $className = $this->cleanupClassname($className); @@ -26,11 +26,11 @@ public function execute(ReflectionClass $reflectionClass, string $className): ?s return $this->cleanupClassname($usage->fcqn); } - if (! $reflectionClass->inNamespace() && class_exists($className)) { + if (! $node->inNamespace() && class_exists($className)) { return $this->cleanupClassname($className); } - $guessedFqcn = "{$reflectionClass->getNamespaceName()}\\{$className}"; + $guessedFqcn = "{$node->getNamespaceName()}\\{$className}"; if (class_exists($guessedFqcn)) { return $this->cleanupClassname($guessedFqcn); @@ -39,9 +39,9 @@ public function execute(ReflectionClass $reflectionClass, string $className): ?s return $className; } - protected function loadUsages(ReflectionClass $reflectionClass): UsageCollection + protected function loadUsages(PhpClassNode $node): UsageCollection { - $filename = $reflectionClass->getFileName(); + $filename = $node->getFileName(); if (! array_key_exists($filename, static::$cache)) { static::$cache[$filename] = $this->useDefinitionsResolver->execute($filename); diff --git a/src/Actions/ParseUserDefinedTypeAction.php b/src/Actions/ParseUserDefinedTypeAction.php index 5499a633..577f040a 100644 --- a/src/Actions/ParseUserDefinedTypeAction.php +++ b/src/Actions/ParseUserDefinedTypeAction.php @@ -6,7 +6,7 @@ use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; -use ReflectionClass; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; use Spatie\TypeScriptTransformer\Support\Concerns\Instanceable; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; @@ -24,11 +24,11 @@ public function __construct( $this->typeParser = new TypeParser($constExprParser); } - public function execute(string $type, ?ReflectionClass $reflectionClass = null): TypeScriptNode + public function execute(string $type, ?PhpClassNode $node = null): TypeScriptNode { return $this->transpilePhpStanTypeToTypeScriptNodeAction->execute( $this->typeParser->parse(new TokenIterator($this->lexer->tokenize($type))), - $reflectionClass, + $node, ); } } diff --git a/src/Actions/ResolveModuleImportsAction.php b/src/Actions/ResolveModuleImportsAction.php index d8bc3b38..ccf9b9c2 100644 --- a/src/Actions/ResolveModuleImportsAction.php +++ b/src/Actions/ResolveModuleImportsAction.php @@ -29,7 +29,7 @@ public function execute( ); foreach ($location->transformed as $transformedItem) { - foreach ($transformedItem->references as $referencedTransformed) { + foreach ($transformedItem->references as $referencedTransformed => $typeReferences) { if ($referencedTransformed->location === $location->segments) { continue; } diff --git a/src/Actions/TransformTypesAction.php b/src/Actions/TransformTypesAction.php index 64a602a8..7364e4e1 100644 --- a/src/Actions/TransformTypesAction.php +++ b/src/Actions/TransformTypesAction.php @@ -2,11 +2,9 @@ namespace Spatie\TypeScriptTransformer\Actions; -use ReflectionClass; -use ReflectionException; use Spatie\TypeScriptTransformer\Attributes\Hidden; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; use Spatie\TypeScriptTransformer\Support\TransformationContext; -use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\Transformers\Transformer; @@ -14,7 +12,7 @@ class TransformTypesAction { /** * @param array $transformers - * @param array $discoveredClasses + * @param array $discoveredClasses * * @return array */ @@ -25,7 +23,7 @@ public function execute( $types = []; foreach ($discoveredClasses as $discoveredClass) { - $transformed = $this->transformType( + $transformed = $this->transformClassNode( $transformers, $discoveredClass ); @@ -38,31 +36,18 @@ public function execute( return $types; } - /** - * @param class-string $type - */ - protected function transformType( + public function transformClassNode( array $transformers, - string $type + PhpClassNode $node ): ?Transformed { - try { - $reflection = new ReflectionClass($type); - } catch (ReflectionException) { - TypeScriptTransformerLog::resolve()->error( - "Failed to reflect class `{$type}`" - ); - - return null; - } - - if (count($reflection->getAttributes(Hidden::class)) > 0) { + if (count($node->getAttributes(Hidden::class)) > 0) { return null; } foreach ($transformers as $transformer) { $transformed = $transformer->transform( - $reflection, - TransformationContext::createFromReflection($reflection), + $node, + TransformationContext::createFromPhpClass($node), ); if ($transformed instanceof Transformed) { diff --git a/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php b/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php index 3d2db073..8ef703de 100644 --- a/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php +++ b/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php @@ -14,7 +14,7 @@ use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; -use ReflectionClass; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAny; @@ -43,23 +43,23 @@ public function __construct( public function execute( TypeNode $type, - ?ReflectionClass $reflectionClass, + ?PhpClassNode $phpClassNode, ): TypeScriptNode { return match ($type::class) { - IdentifierTypeNode::class => $this->identifierNode($type, $reflectionClass), - ArrayTypeNode::class => $this->arrayTypeNode($type, $reflectionClass), - GenericTypeNode::class => $this->genericNode($type, $reflectionClass), - ArrayShapeNode::class, ObjectShapeNode::class => $this->arrayShapeNode($type, $reflectionClass), - NullableTypeNode::class => $this->nullableNode($type, $reflectionClass), - UnionTypeNode::class => $this->unionNode($type, $reflectionClass), - IntersectionTypeNode::class => $this->intersectionNode($type, $reflectionClass), + IdentifierTypeNode::class => $this->identifierNode($type, $phpClassNode), + ArrayTypeNode::class => $this->arrayTypeNode($type, $phpClassNode), + GenericTypeNode::class => $this->genericNode($type, $phpClassNode), + ArrayShapeNode::class, ObjectShapeNode::class => $this->arrayShapeNode($type, $phpClassNode), + NullableTypeNode::class => $this->nullableNode($type, $phpClassNode), + UnionTypeNode::class => $this->unionNode($type, $phpClassNode), + IntersectionTypeNode::class => $this->intersectionNode($type, $phpClassNode), default => new TypeScriptUnknown(), }; } protected function identifierNode( IdentifierTypeNode $node, - ?ReflectionClass $reflectionClass + ?PhpClassNode $phpClassNode ): TypeScriptNode { if ($node->name === 'mixed') { return new TypeScriptAny(); @@ -94,7 +94,7 @@ protected function identifierNode( } if ($node->name === 'self' || $node->name === 'static') { - return new TypeReference(new ClassStringReference($reflectionClass->getName())); + return new TypeReference(new ClassStringReference($phpClassNode->getName())); } if ($node->name === 'object') { @@ -112,12 +112,12 @@ protected function identifierNode( return new TypeReference(new ClassStringReference($node->name)); } - if ($reflectionClass === null) { + if ($phpClassNode === null) { return new TypeScriptUnknown(); } $referenced = $this->findClassNameFqcnAction->execute( - $reflectionClass, + $phpClassNode, $node->name ); @@ -130,22 +130,22 @@ protected function identifierNode( protected function arrayTypeNode( ArrayTypeNode $node, - ?ReflectionClass $reflectionClass + ?PhpClassNode $phpClassNode ): TypeScriptNode { return new TypeScriptArray( - [$this->execute($node->type, $reflectionClass)] + [$this->execute($node->type, $phpClassNode)] ); } protected function arrayShapeNode( ArrayShapeNode|ObjectShapeNode $node, - ?ReflectionClass $reflectionClass + ?PhpClassNode $phpClassNode ): TypeScriptObject { return new TypeScriptObject(array_map( - function (ArrayShapeItemNode|ObjectShapeItemNode $item) use ($reflectionClass) { + function (ArrayShapeItemNode|ObjectShapeItemNode $item) use ($phpClassNode) { return new TypeScriptProperty( (string) $item->keyName, - $this->execute($item->valueType, $reflectionClass), + $this->execute($item->valueType, $phpClassNode), isOptional: $item->optional ); }, @@ -155,9 +155,9 @@ function (ArrayShapeItemNode|ObjectShapeItemNode $item) use ($reflectionClass) { protected function nullableNode( NullableTypeNode $node, - ?ReflectionClass $reflectionClass + ?PhpClassNode $phpClassNode ): TypeScriptNode { - $type = $this->execute($node->type, $reflectionClass); + $type = $this->execute($node->type, $phpClassNode); if (! $type instanceof TypeScriptUnion) { return new TypeScriptUnion([$type, new TypeScriptNull()]); @@ -172,33 +172,33 @@ protected function nullableNode( protected function unionNode( UnionTypeNode $node, - ?ReflectionClass $reflectionClass + ?PhpClassNode $phpClassNode ): TypeScriptUnion { return new TypeScriptUnion(array_map( - fn (TypeNode $type) => $this->execute($type, $reflectionClass), + fn (TypeNode $type) => $this->execute($type, $phpClassNode), $node->types )); } protected function intersectionNode( IntersectionTypeNode $node, - ?ReflectionClass $reflectionClass + ?PhpClassNode $phpClassNode ): TypeScriptIntersection { return new TypeScriptIntersection(array_map( - fn (TypeNode $type) => $this->execute($type, $reflectionClass), + fn (TypeNode $type) => $this->execute($type, $phpClassNode), $node->types )); } protected function genericNode( GenericTypeNode $node, - ?ReflectionClass $reflectionClass + ?PhpClassNode $phpClassNode ): TypeScriptNode { if ($node->type->name === 'array' || $node->type->name === 'Array') { - return $this->genericArrayNode($node, $reflectionClass); + return $this->genericArrayNode($node, $phpClassNode); } - $type = $this->execute($node->type, $reflectionClass); + $type = $this->execute($node->type, $phpClassNode); if ($type instanceof TypeScriptString) { return $type; // class-string case @@ -207,13 +207,13 @@ protected function genericNode( return new TypeScriptGeneric( $type, array_map( - fn (TypeNode $type) => $this->execute($type, $reflectionClass), + fn (TypeNode $type) => $this->execute($type, $phpClassNode), $node->genericTypes ) ); } - private function genericArrayNode(GenericTypeNode $node, ?ReflectionClass $reflectionClass): TypeScriptGeneric|TypeScriptArray + private function genericArrayNode(GenericTypeNode $node, ?PhpClassNode $phpClassNode): TypeScriptGeneric|TypeScriptArray { $genericTypes = count($node->genericTypes); @@ -222,15 +222,15 @@ private function genericArrayNode(GenericTypeNode $node, ?ReflectionClass $refle } if ($genericTypes === 1) { - return new TypeScriptArray([$this->execute($node->genericTypes[0], $reflectionClass)]); + return new TypeScriptArray([$this->execute($node->genericTypes[0], $phpClassNode)]); } if ($genericTypes > 2) { throw new Exception('Invalid number of generic types for array'); } - $key = $this->execute($node->genericTypes[0], $reflectionClass); - $value = $this->execute($node->genericTypes[1], $reflectionClass); + $key = $this->execute($node->genericTypes[0], $phpClassNode); + $value = $this->execute($node->genericTypes[1], $phpClassNode); if ($key instanceof TypeScriptNumber) { return new TypeScriptArray([$value]); diff --git a/src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php b/src/Actions/TranspilePhpTypeNodeToTypeScriptNodeAction.php similarity index 68% rename from src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php rename to src/Actions/TranspilePhpTypeNodeToTypeScriptNodeAction.php index 0ab938c5..139be81d 100644 --- a/src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php +++ b/src/Actions/TranspilePhpTypeNodeToTypeScriptNodeAction.php @@ -2,11 +2,11 @@ namespace Spatie\TypeScriptTransformer\Actions; -use ReflectionClass; -use ReflectionIntersectionType; -use ReflectionNamedType; -use ReflectionType; -use ReflectionUnionType; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; +use Spatie\TypeScriptTransformer\PhpNodes\PhpIntersectionTypeNode; +use Spatie\TypeScriptTransformer\PhpNodes\PhpNamedTypeNode; +use Spatie\TypeScriptTransformer\PhpNodes\PhpTypeNode; +use Spatie\TypeScriptTransformer\PhpNodes\PhpUnionTypeNode; use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAny; @@ -23,16 +23,16 @@ use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnknown; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptVoid; -class TranspileReflectionTypeToTypeScriptNodeAction +class TranspilePhpTypeNodeToTypeScriptNodeAction { public function execute( - ReflectionType $reflectionType, - ReflectionClass $reflectionClass, + PhpTypeNode $phpTypeNode, + PhpClassNode $phpClassNode, ): TypeScriptNode { - $type = $this->resolveType($reflectionType, $reflectionClass); + $type = $this->resolveType($phpTypeNode, $phpClassNode); if ( - ! $reflectionType->allowsNull() + ! $phpTypeNode->allowsNull() || $type instanceof TypeScriptAny || $type instanceof TypeScriptNull) { return $type; @@ -52,20 +52,20 @@ public function execute( } protected function resolveType( - ReflectionType $reflectionType, - ReflectionClass $reflectionClass, + PhpTypeNode $phpTypeNode, + PhpClassNode $phpClassNode, ): TypeScriptNode { - return match ($reflectionType::class) { - ReflectionNamedType::class => $this->reflectionNamedType($reflectionType, $reflectionClass), - ReflectionUnionType::class => $this->reflectionUnionType($reflectionType, $reflectionClass), - ReflectionIntersectionType::class => $this->reflectionIntersectionType($reflectionType, $reflectionClass), + return match ($phpTypeNode::class) { + PhpNamedTypeNode::class => $this->namedType($phpTypeNode, $phpClassNode), + PhpUnionTypeNode::class => $this->unionType($phpTypeNode, $phpClassNode), + PhpIntersectionTypeNode::class => $this->intersectionType($phpTypeNode, $phpClassNode), default => new TypeScriptUndefined(), }; } - protected function reflectionNamedType( - ReflectionNamedType $type, - ReflectionClass $reflectionClass, + protected function namedType( + PhpNamedTypeNode $type, + PhpClassNode $phpClassNode, ): TypeScriptNode { if ($type->getName() === 'string') { return new TypeScriptString(); @@ -92,7 +92,7 @@ protected function reflectionNamedType( } if ($type->getName() === 'self' || $type->getName() === 'static') { - return new TypeReference(new ClassStringReference($reflectionClass->getName())); + return new TypeReference(new ClassStringReference($phpClassNode->getName())); } if ($type->getName() === 'object') { @@ -110,22 +110,22 @@ protected function reflectionNamedType( return new TypeScriptUnknown(); } - protected function reflectionUnionType( - ReflectionUnionType $type, - ReflectionClass $reflectionClass, + protected function unionType( + PhpUnionTypeNode $type, + PhpClassNode $phpClassNode, ): TypeScriptNode { return new TypeScriptUnion(array_map( - fn (ReflectionType $type) => $this->resolveType($type, $reflectionClass), + fn (PhpTypeNode $type) => $this->resolveType($type, $phpClassNode), $type->getTypes() )); } - protected function reflectionIntersectionType( - ReflectionIntersectionType $type, - ReflectionClass $reflectionClass, + protected function intersectionType( + PhpIntersectionTypeNode $type, + PhpClassNode $classNode, ): TypeScriptNode { return new TypeScriptIntersection(array_map( - fn (ReflectionType $type) => $this->resolveType($type, $reflectionClass), + fn (PhpTypeNode $type) => $this->resolveType($type, $classNode), $type->getTypes() )); } diff --git a/src/Actions/WatchFileSystemAction.php b/src/Actions/WatchFileSystemAction.php deleted file mode 100644 index 38243428..00000000 --- a/src/Actions/WatchFileSystemAction.php +++ /dev/null @@ -1,25 +0,0 @@ -config->directoriesToWatch) - ->onAnyChange(function (string $type, string $path) { - echo $type.'|'.$path; - }) - ->start(); - } -} diff --git a/src/Attributes/LiteralTypeScriptType.php b/src/Attributes/LiteralTypeScriptType.php index acb9b30b..e73178bb 100644 --- a/src/Attributes/LiteralTypeScriptType.php +++ b/src/Attributes/LiteralTypeScriptType.php @@ -3,7 +3,7 @@ namespace Spatie\TypeScriptTransformer\Attributes; use Attribute; -use ReflectionClass; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptObject; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; @@ -19,7 +19,7 @@ public function __construct(string|array $typeScript) $this->typeScript = $typeScript; } - public function getType(ReflectionClass $class): TypeScriptNode + public function getType(PhpClassNode $class): TypeScriptNode { if (is_string($this->typeScript)) { return new TypeScriptRaw($this->typeScript); diff --git a/src/Attributes/TypeScriptType.php b/src/Attributes/TypeScriptType.php index 435b167a..e417a922 100644 --- a/src/Attributes/TypeScriptType.php +++ b/src/Attributes/TypeScriptType.php @@ -3,8 +3,8 @@ namespace Spatie\TypeScriptTransformer\Attributes; use Attribute; -use ReflectionClass; use Spatie\TypeScriptTransformer\Actions\TranspilePhpStanTypeToTypeScriptNodeAction; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; use Spatie\TypeScriptTransformer\TypeResolvers\DocTypeResolver; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptObject; @@ -20,7 +20,7 @@ public function __construct(string|array $type) $this->type = $type; } - public function getType(ReflectionClass $class): TypeScriptNode + public function getType(PhpClassNode $class): TypeScriptNode { $docResolver = new DocTypeResolver(); $transpiler = new TranspilePhpStanTypeToTypeScriptNodeAction(); diff --git a/src/Attributes/TypeScriptTypeAttributeContract.php b/src/Attributes/TypeScriptTypeAttributeContract.php index c205f7f9..4b8e109b 100644 --- a/src/Attributes/TypeScriptTypeAttributeContract.php +++ b/src/Attributes/TypeScriptTypeAttributeContract.php @@ -2,10 +2,10 @@ namespace Spatie\TypeScriptTransformer\Attributes; -use ReflectionClass; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; interface TypeScriptTypeAttributeContract { - public function getType(ReflectionClass $class): TypeScriptNode; + public function getType(PhpClassNode $class): TypeScriptNode; } diff --git a/src/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessor.php b/src/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessor.php index 997308cb..0512cef5 100644 --- a/src/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessor.php +++ b/src/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessor.php @@ -3,7 +3,7 @@ namespace Spatie\TypeScriptTransformer\ClassPropertyProcessors; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -use ReflectionProperty; +use Spatie\TypeScriptTransformer\PhpNodes\PhpPropertyNode; use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\Transformers\ClassPropertyProcessors\ClassPropertyProcessor; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; @@ -77,7 +77,7 @@ public function replaceArrayLikeClass(string ...$class): self } public function execute( - ReflectionProperty $reflection, + PhpPropertyNode $phpPropertyNode, ?TypeNode $annotation, TypeScriptProperty $property ): ?TypeScriptProperty { diff --git a/src/Collections/ReferenceMap.php b/src/Collections/ReferenceMap.php index 212c5d5f..ecd9b0c3 100644 --- a/src/Collections/ReferenceMap.php +++ b/src/Collections/ReferenceMap.php @@ -42,8 +42,19 @@ public function get( return $this->references[$reference->getKey()]; } + public function getByReferenceKey(string $key): ?Transformed + { + return $this->references[$key] ?? null; + } + public function all(): array { return $this->references; } + + public function remove( + Transformed $transformed + ): void { + unset($this->references[$transformed->reference->getKey()]); + } } diff --git a/src/Events/Watch/DirectoryCreatedWatchEvent.php b/src/Events/Watch/DirectoryCreatedWatchEvent.php deleted file mode 100644 index 9f0eb364..00000000 --- a/src/Events/Watch/DirectoryCreatedWatchEvent.php +++ /dev/null @@ -1,7 +0,0 @@ -, WatchEventHandler> */ + protected array $handlers = []; + + public function __construct( + protected TypeScriptTransformer $typeScriptTransformer, + protected TransformedCollection $transformedCollection, + protected ReferenceMap $referenceMap, + ) { + $this->initializeHandlers(); + } + + public function run(): void + { + $watcher = Watch::paths($this->typeScriptTransformer->config->directoriesToWatch) + ->onFileCreated(function (string $path) { + if (! str_ends_with($path, '.php')) { + return; + } + + $this->eventsBuffer[] = new FileCreatedWatchEvent($path); + }) + ->onfileUpdated(function (string $path) { + if (! str_ends_with($path, '.php')) { + return; + } + + $this->eventsBuffer[] = new FileUpdatedWatchEvent($path); + }) + ->onFileDeleted(function (string $path) { + if (! str_ends_with($path, '.php')) { + return; + } + + $this->eventsBuffer[] = new FileDeletedWatchEvent($path); + }) + ->onDirectoryDeleted(function (string $path) { + $this->eventsBuffer[] = new DirectoryDeletedWatchEvent($path); + }) + ->shouldContinue(function () { + // TODO: we probably want a better implementation than this but it works + if (count($this->eventsBuffer) > 0 && $this->processing === false) { + $this->processing = true; + $this->processBuffer(); + $this->processing = false; + } + + return true; + }); + + try { + $this->typeScriptTransformer->log->info('Starting watcher'); + + $watcher->start(); + } catch (CouldNotStartWatcher $e) { + throw new Exception( + 'Could not start watcher. Make sure you have required chokidar. (https://github.com/spatie/file-system-watcher?tab=readme-ov-file#installation)' + ); + } + } + + protected function initializeHandlers(): void + { + // TODO: handle directory deleted + + $this->handlers[FileCreatedWatchEvent::class] = new FileCreatedWatchEventHandler( + $this->typeScriptTransformer, + $this->transformedCollection, + $this->referenceMap + ); + + $this->handlers[FileUpdatedWatchEvent::class] = new FileUpdatedWatchEventHandler( + $this->typeScriptTransformer, + $this->transformedCollection, + $this->referenceMap + ); + + $this->handlers[FileDeletedWatchEvent::class] = new FileDeletedWatchEventHandler( + $this->typeScriptTransformer, + $this->transformedCollection, + $this->referenceMap + ); + } + + protected function processBuffer(): void + { + $this->typeScriptTransformer->log->info('Processing events'); + + [$events, $this->eventsBuffer] = [$this->eventsBuffer, []]; + + foreach ($events as $event) { + $this->handlers[$event::class]->handle($event); + } + + $this->tryToConnectMissingReferencesWithNewTransformed(); + + $this->typeScriptTransformer->outputTransformed( + $this->transformedCollection, + $this->referenceMap + ); + + $this->typeScriptTransformer->log->info('Processed events'); + } + + protected function tryToConnectMissingReferencesWithNewTransformed(): void + { + foreach ($this->transformedCollection as $transformed) { + foreach ($transformed->missingReferences as $missingReference => $typeReferences) { + $referenced = $this->referenceMap->getByReferenceKey($missingReference); + + if ($referenced === null) { + continue; + } + + $referenced->markMissingReferenceFound($transformed); + $referenced->markAsChanged(); + + $transformed->referencedBy[$referenced] = $referenced->reference->getKey(); + + break; + } + } + } +} diff --git a/src/Handlers/Watch/DirectoryDeletedWatchEventHandler.php b/src/Handlers/Watch/DirectoryDeletedWatchEventHandler.php new file mode 100644 index 00000000..4483c1f3 --- /dev/null +++ b/src/Handlers/Watch/DirectoryDeletedWatchEventHandler.php @@ -0,0 +1,11 @@ +typeScriptTransformer->loadPhpClassNodeAction->execute($event->path); + + if($classNode === null) { + $this->typeScriptTransformer->log->warning("Multiple class nodes found in {$event->path}"); + + return; + } + + $transformed = $this->typeScriptTransformer->transformTypesAction->transformClassNode( + $this->typeScriptTransformer->config->transformers, + $classNode + ); + + if ($transformed === null) { + $this->typeScriptTransformer->log->warning("Could not transform {$event->path}"); + + return; + } + + $this->transformedCollection->add($transformed); + + $this->typeScriptTransformer->executeProvidedClosuresAction->execute([$transformed]); + $this->typeScriptTransformer->connectReferencesAction->execute([$transformed]); + $this->typeScriptTransformer->executeConnectedClosuresAction->execute([$transformed]); + } +} diff --git a/src/Handlers/Watch/FileDeletedWatchEventHandler.php b/src/Handlers/Watch/FileDeletedWatchEventHandler.php new file mode 100644 index 00000000..8920c761 --- /dev/null +++ b/src/Handlers/Watch/FileDeletedWatchEventHandler.php @@ -0,0 +1,40 @@ +transformedCollection->findTransformedByPath($event->path); + + if ($transformed === null) { + return; + } + + foreach ($transformed->referencedBy as $referencedBy => $key) { + /** @var Transformed $referencedBy */ + $referencedBy->markReferenceRemoved($transformed); + $referencedBy->markAsChanged(); + } + + $this->referenceMap->remove($transformed); + $this->transformedCollection->removeTransformedByPath($event->path); + } +} diff --git a/src/Handlers/Watch/FileUpdatedWatchEventHandler.php b/src/Handlers/Watch/FileUpdatedWatchEventHandler.php new file mode 100644 index 00000000..ce2fa867 --- /dev/null +++ b/src/Handlers/Watch/FileUpdatedWatchEventHandler.php @@ -0,0 +1,79 @@ +typeScriptTransformer->loadPhpClassNodeAction->execute($event->path); + + if ($classNode === null) { + $this->typeScriptTransformer->log->warning("Multiple class nodes found in {$event->path}"); + + return; + } + + $newlyTransformed = $this->typeScriptTransformer->transformTypesAction->transformClassNode( + $this->typeScriptTransformer->config->transformers, + $classNode + ); + + if ($newlyTransformed === null) { + $this->typeScriptTransformer->log->warning("Could not transform {$event->path}"); + + return; + } + + // TODO: at the moment we replace the node when we see an update + // it could be that no changes are actually made + // and such a case nothing should be updated + $originalTransformed = $this->transformedCollection->findTransformedByPath( + $event->path + ); + + if ($originalTransformed === null) { + $this->addNewlyTransformed($newlyTransformed); + + return; + } + + foreach ($originalTransformed->referencedBy as $referencedBy => $key) { + /** @var Transformed $referencedBy */ + $referencedBy->markReferenceRemoved($originalTransformed); + $referencedBy->markAsChanged(); + } + + $this->referenceMap->remove($originalTransformed); + $this->transformedCollection->removeTransformedByPath($event->path); + + $this->addNewlyTransformed($newlyTransformed); + } + + protected function addNewlyTransformed(Transformed $transformed): void + { + $this->transformedCollection->add($transformed); + + $this->typeScriptTransformer->executeProvidedClosuresAction->execute([$transformed]); + $this->typeScriptTransformer->connectReferencesAction->execute([$transformed]); + $this->typeScriptTransformer->executeConnectedClosuresAction->execute([$transformed]); + } +} diff --git a/src/Handlers/Watch/WatchEventHandler.php b/src/Handlers/Watch/WatchEventHandler.php new file mode 100644 index 00000000..b6f71a88 --- /dev/null +++ b/src/Handlers/Watch/WatchEventHandler.php @@ -0,0 +1,8 @@ +dataConfig->getDataClass($reflection->getDeclaringClass()->getName()); - $dataProperty = $dataClass->properties->get($reflection->getName()); - - if ($dataProperty->hidden) { + if (! empty($phpPropertyNode->getAttributes(Hidden::class)) && ! empty($phpPropertyNode->getAttributes(DataHidden::class))) { return null; } - if ($dataProperty->outputMappedName) { - $property->name = new TypeScriptIdentifier($dataProperty->outputMappedName); - } + // TODO: somehow get mapping working here without dataconfig and dataproperty + // $phpAttributeNodes = $phpPropertyNode->getAttributes(MapOutputName::class); + // + // if ($phpAttributeNodes) { + // $property->name = new TypeScriptIdentifier($dataProperty->outputMappedName); + // } if (! $property->type instanceof TypeScriptUnion) { return $property; diff --git a/src/Laravel/Commands/TransformTypeScriptCommand.php b/src/Laravel/Commands/TransformTypeScriptCommand.php index 45efe6f9..755dcd82 100644 --- a/src/Laravel/Commands/TransformTypeScriptCommand.php +++ b/src/Laravel/Commands/TransformTypeScriptCommand.php @@ -3,7 +3,7 @@ namespace Spatie\TypeScriptTransformer\Laravel\Commands; use Illuminate\Console\Command; -use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; +use Spatie\TypeScriptTransformer\Laravel\Support\WrappedLaravelConsole; use Spatie\TypeScriptTransformer\TypeScriptTransformer; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; @@ -21,21 +21,10 @@ public function handle(): int return self::FAILURE; } - app(TypeScriptTransformer::class)->execute(); - - $log = TypeScriptTransformerLog::resolve(); - - if (! empty($log->infoMessages)) { - foreach ($log->infoMessages as $infoMessage) { - $this->info($infoMessage); - } - } - - if (! empty($log->warningMessages)) { - foreach ($log->warningMessages as $warningMessage) { - $this->warn($warningMessage); - } - } + TypeScriptTransformer::create( + config: app(TypeScriptTransformerConfig::class), + console: new WrappedLaravelConsole($this) + )->execute(); $this->comment('All done'); diff --git a/src/Laravel/Commands/WatchTypeScriptCommand.php b/src/Laravel/Commands/WatchTypeScriptCommand.php index 832a3c61..26f4da3c 100644 --- a/src/Laravel/Commands/WatchTypeScriptCommand.php +++ b/src/Laravel/Commands/WatchTypeScriptCommand.php @@ -3,6 +3,8 @@ namespace Spatie\TypeScriptTransformer\Laravel\Commands; use Illuminate\Console\Command; +use Spatie\TypeScriptTransformer\Laravel\Support\WrappedLaravelConsole; +use Spatie\TypeScriptTransformer\TypeScriptTransformer; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class WatchTypeScriptCommand extends Command @@ -11,9 +13,19 @@ class WatchTypeScriptCommand extends Command public $description = 'Keeps track of changes in your PHP files and automatically re-generates your TypeScript types'; - public function handle( - TypeScriptTransformerConfig $config, - ): int { + public function handle(): int + { + if (! app()->has(TypeScriptTransformerConfig::class)) { + $this->error('Please, first publish the TypeScriptTransformerServiceProvider and configure it.'); + + return self::FAILURE; + } + + TypeScriptTransformer::create( + config: app(TypeScriptTransformerConfig::class), + console: new WrappedLaravelConsole($this), + watch: true + )->execute(); $this->comment('Watching for changes...'); diff --git a/src/Laravel/Support/WrappedLaravelConsole.php b/src/Laravel/Support/WrappedLaravelConsole.php new file mode 100644 index 00000000..e70351e0 --- /dev/null +++ b/src/Laravel/Support/WrappedLaravelConsole.php @@ -0,0 +1,34 @@ +command->error($message); + } + + public function info(string $message): void + { + $this->command->info($message); + } + + public function warn(string $message): void + { + $this->command->warn($message); + } + + public function exit(int $code = 0): void + { + exit($code); + } +} diff --git a/src/Laravel/Transformers/DataClassTransformer.php b/src/Laravel/Transformers/DataClassTransformer.php index 0c25c028..95d34141 100644 --- a/src/Laravel/Transformers/DataClassTransformer.php +++ b/src/Laravel/Transformers/DataClassTransformer.php @@ -2,13 +2,13 @@ namespace Spatie\TypeScriptTransformer\Laravel\Transformers; -use ReflectionClass; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Support\DataConfig; use Spatie\TypeScriptTransformer\Actions\TranspilePhpStanTypeToTypeScriptNodeAction; -use Spatie\TypeScriptTransformer\Actions\TranspileReflectionTypeToTypeScriptNodeAction; +use Spatie\TypeScriptTransformer\Actions\TranspilePhpTypeNodeToTypeScriptNodeAction; use Spatie\TypeScriptTransformer\ClassPropertyProcessors\FixArrayLikeStructuresClassPropertyProcessor; use Spatie\TypeScriptTransformer\Laravel\ClassPropertyProcessors\DataClassPropertyProcessor; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; use Spatie\TypeScriptTransformer\Transformers\ClassTransformer; use Spatie\TypeScriptTransformer\TypeResolvers\DocTypeResolver; @@ -21,16 +21,16 @@ public function __construct( protected array $customDataCollections = [], DocTypeResolver $docTypeResolver = new DocTypeResolver(), TranspilePhpStanTypeToTypeScriptNodeAction $transpilePhpStanTypeToTypeScriptTypeAction = new TranspilePhpStanTypeToTypeScriptNodeAction(), - TranspileReflectionTypeToTypeScriptNodeAction $transpileReflectionTypeToTypeScriptTypeAction = new TranspileReflectionTypeToTypeScriptNodeAction(), + TranspilePhpTypeNodeToTypeScriptNodeAction $transpilePhpTypeNodeToTypeScriptTypeAction = new TranspilePhpTypeNodeToTypeScriptNodeAction(), ) { $this->dataConfig = app(DataConfig::class); - parent::__construct($docTypeResolver, $transpilePhpStanTypeToTypeScriptTypeAction, $transpileReflectionTypeToTypeScriptTypeAction); + parent::__construct($docTypeResolver, $transpilePhpStanTypeToTypeScriptTypeAction, $transpilePhpTypeNodeToTypeScriptTypeAction); } - protected function shouldTransform(ReflectionClass $reflection): bool + protected function shouldTransform(PhpClassNode $phpClassNode): bool { - return $reflection->implementsInterface(BaseData::class); + return $phpClassNode->implementsInterface(BaseData::class); } protected function classPropertyProcessors(): array diff --git a/src/PhpNodes/PhpAttributeNode.php b/src/PhpNodes/PhpAttributeNode.php new file mode 100644 index 00000000..e8b24b62 --- /dev/null +++ b/src/PhpNodes/PhpAttributeNode.php @@ -0,0 +1,36 @@ +reflection->getName(); + } + + public function getArguments(): array + { + return $this->reflection->getArguments(); + } + + public function newInstance(): object + { + if($this->reflection instanceof ReflectionAttribute) { + return $this->reflection->newInstance(); + } + + $className = $this->reflection->getName(); + + // TODO: maybe we can do a little better here + return (new $className())($this->reflection->getArguments()); + } +} diff --git a/src/PhpNodes/PhpClassNode.php b/src/PhpNodes/PhpClassNode.php new file mode 100644 index 00000000..7f3c5440 --- /dev/null +++ b/src/PhpNodes/PhpClassNode.php @@ -0,0 +1,145 @@ +isEnum()) { + return new PhpEnumNode(new ReflectionEnum($reflection->name)); + } + + return new self($reflection); + } + + /** + * @return array + */ + public function getAttributes(?string $name = null): array + { + $attributes = match (true) { + $this->reflection instanceof ReflectionClass => $this->reflection->getAttributes($name), + $name === null => $this->reflection->getAttributes(), + default => $this->reflection->getAttributesByInstance($name), + }; + + return array_map( + fn (ReflectionAttribute|RoaveReflectionAttribute $attribute) => new PhpAttributeNode($attribute), + $attributes, + ); + } + + public function getProperties(?int $filter = null): array + { + return array_map( + fn (ReflectionProperty|RoaveReflectionProperty $property) => new PhpPropertyNode($property), + $this->reflection->getProperties($filter), + ); + } + + public function getMethods(?int $filter = null): array + { + return array_map( + fn (ReflectionMethod|RoaveReflectionMethod $method) => new PhpMethodNode($method), + $this->reflection->getMethods($filter), + ); + } + + public function getShortName(): string + { + return $this->reflection->getShortName(); + } + + public function getName(): string + { + return $this->reflection->getName(); + } + + public function getNamespaceName(): string + { + return $this->reflection->getNamespaceName(); + } + + public function getFileName(): string + { + return $this->reflection->getFileName(); + } + + public function inNamespace(): bool + { + return $this->reflection->inNamespace(); + } + + public function implementsInterface(string $interface): bool + { + return $this->reflection->implementsInterface($interface); + } + + public function isEnum(): bool + { + return $this->reflection->isEnum(); + } + + public function isAbstract(): bool + { + return $this->reflection->isAbstract(); + } + + public function isFinal(): bool + { + return $this->reflection->isFinal(); + } + + public function isInterface(): bool + { + return $this->reflection->isInterface(); + } + + public function isReadonly(): bool + { + return $this->reflection->isReadonly(); + } + + public function getDocComment(): ?string + { + return $this->reflection->getDocComment() ?: null; + } + + public function hasMethod(string $name): bool + { + return $this->reflection->hasMethod($name); + } + + public function getMethod(string $name): ?PhpMethodNode + { + $method = $this->reflection->getMethod($name); + + return $method ? new PhpMethodNode($method) : null; + } +} diff --git a/src/PhpNodes/PhpEnumCaseNode.php b/src/PhpNodes/PhpEnumCaseNode.php new file mode 100644 index 00000000..3ad23d92 --- /dev/null +++ b/src/PhpNodes/PhpEnumCaseNode.php @@ -0,0 +1,33 @@ +reflection->getName(); + } + + public function getValue(): string|int|null + { + if($this->reflection instanceof ReflectionEnumCase) { + return $this->reflection->getValue(); + } + + if(! method_exists($this->reflection, 'getBackingValue')) { + return null; + } + + return $this->reflection->getBackingValue(); + } +} diff --git a/src/PhpNodes/PhpEnumNode.php b/src/PhpNodes/PhpEnumNode.php new file mode 100644 index 00000000..fc19a972 --- /dev/null +++ b/src/PhpNodes/PhpEnumNode.php @@ -0,0 +1,36 @@ +reflection->isBacked(); + } + + /** + * @return PhpEnumCaseNode[] + */ + public function getCases(): array + { + return array_map( + fn (ReflectionEnumCase|ReflectionEnumUnitCase|ReflectionEnumBackedCase $case) => new PhpEnumCaseNode($case), + $this->reflection->getCases(), + ); + } +} diff --git a/src/PhpNodes/PhpIntersectionTypeNode.php b/src/PhpNodes/PhpIntersectionTypeNode.php new file mode 100644 index 00000000..4065e475 --- /dev/null +++ b/src/PhpNodes/PhpIntersectionTypeNode.php @@ -0,0 +1,24 @@ + PhpTypeNode::fromReflection($type), $this->reflection->getTypes()); + } +} diff --git a/src/PhpNodes/PhpMethodNode.php b/src/PhpNodes/PhpMethodNode.php new file mode 100644 index 00000000..bb0a2a20 --- /dev/null +++ b/src/PhpNodes/PhpMethodNode.php @@ -0,0 +1,45 @@ +reflection->getDocComment() ?: null; + } + + public function getName(): string + { + return $this->reflection->getName(); + } + + public function getReturnType(): ?PhpTypeNode + { + $type = $this->reflection->getReturnType(); + + if ($type === null) { + return null; + } + + return PhpTypeNode::fromReflection($type); + } + + public function getParameters(): array + { + return array_map( + fn (ReflectionParameter|RoaveReflectionParameter $parameter) => new PhpParameterNode($parameter), + $this->reflection->getParameters(), + ); + } +} diff --git a/src/PhpNodes/PhpNamedTypeNode.php b/src/PhpNodes/PhpNamedTypeNode.php new file mode 100644 index 00000000..0764790f --- /dev/null +++ b/src/PhpNodes/PhpNamedTypeNode.php @@ -0,0 +1,22 @@ +reflection->getName(); + } +} diff --git a/src/PhpNodes/PhpParameterNode.php b/src/PhpNodes/PhpParameterNode.php new file mode 100644 index 00000000..ffcbe467 --- /dev/null +++ b/src/PhpNodes/PhpParameterNode.php @@ -0,0 +1,40 @@ +reflection->getName(); + } + + public function hasType(): bool + { + return $this->reflection->hasType(); + } + + public function getType(): ?PhpTypeNode + { + $type = $this->reflection->getType(); + + if ($type === null) { + return null; + } + + return PhpTypeNode::fromReflection($type); + } + + public function isOptional(): bool + { + return $this->reflection->isOptional(); + } +} diff --git a/src/PhpNodes/PhpPropertyNode.php b/src/PhpNodes/PhpPropertyNode.php new file mode 100644 index 00000000..04f04e1c --- /dev/null +++ b/src/PhpNodes/PhpPropertyNode.php @@ -0,0 +1,74 @@ +reflection->getName(); + } + + public function getDeclaringClass(): PhpClassNode + { + return new PhpClassNode($this->reflection->getDeclaringClass()); + } + + /** + * @return array + */ + public function getAttributes(?string $name = null): array + { + $attributes = match (true) { + $this->reflection instanceof ReflectionProperty => $this->reflection->getAttributes($name), + $name === null => $this->reflection->getAttributes(), + default => $this->reflection->getAttributesByInstance($name), + }; + + return array_map( + fn (ReflectionAttribute|RoaveReflectionAttribute $attribute) => new PhpAttributeNode($attribute), + $attributes, + ); + } + + public function isStatic(): bool + { + return $this->reflection->isStatic(); + } + + public function hasType(): bool + { + return $this->reflection->hasType(); + } + + public function getType(): ?PhpTypeNode + { + $type = $this->reflection->getType(); + + if ($type === null) { + return null; + } + + return PhpTypeNode::fromReflection($type); + } + + public function isReadonly(): bool + { + return $this->reflection->isReadonly(); + } + + public function getDocComment(): ?string + { + return $this->reflection->getDocComment() ?: null; + } +} diff --git a/src/PhpNodes/PhpTypeNode.php b/src/PhpNodes/PhpTypeNode.php new file mode 100644 index 00000000..c7055685 --- /dev/null +++ b/src/PhpNodes/PhpTypeNode.php @@ -0,0 +1,35 @@ + new PhpNamedTypeNode($reflection), + ReflectionUnionType::class, RoaveReflectionUnionType::class => new PhpUnionTypeNode($reflection), + ReflectionIntersectionType::class, RoaveReflectionIntersectionType::class => new PhpIntersectionTypeNode($reflection), + }; + } + + public function allowsNull(): bool + { + return $this->reflection->allowsNull(); + } +} diff --git a/src/PhpNodes/PhpUnionTypeNode.php b/src/PhpNodes/PhpUnionTypeNode.php new file mode 100644 index 00000000..a570e561 --- /dev/null +++ b/src/PhpNodes/PhpUnionTypeNode.php @@ -0,0 +1,24 @@ + PhpTypeNode::fromReflection($type), $this->reflection->getTypes()); + } +} diff --git a/src/References/FilesystemReference.php b/src/References/FilesystemReference.php new file mode 100644 index 00000000..7e7b13a8 --- /dev/null +++ b/src/References/FilesystemReference.php @@ -0,0 +1,8 @@ +getName()); + } + + public function getFilesystemOriginPath(): string + { + return $this->phpClassNode->getFileName(); + } +} diff --git a/src/References/ReflectionClassReference.php b/src/References/ReflectionClassReference.php deleted file mode 100644 index ce7b219b..00000000 --- a/src/References/ReflectionClassReference.php +++ /dev/null @@ -1,14 +0,0 @@ -getName()); - } -} diff --git a/src/Support/Console/WrappedArrayConsole.php b/src/Support/Console/WrappedArrayConsole.php new file mode 100644 index 00000000..550fbdda --- /dev/null +++ b/src/Support/Console/WrappedArrayConsole.php @@ -0,0 +1,29 @@ + */ + public array $messages = []; + + public function error(string $message): void + { + $this->messages[] = ['message' => $message, 'level' => 'error']; + } + + public function info(string $message): void + { + $this->messages[] = ['message' => $message, 'level' => 'info']; + } + + public function warn(string $message): void + { + $this->messages[] = ['message' => $message, 'level' => 'warning']; + } + + public function exit(int $code = 0): void + { + die($code); + } +} diff --git a/src/Support/Console/WrappedConsole.php b/src/Support/Console/WrappedConsole.php new file mode 100644 index 00000000..e38bf7d0 --- /dev/null +++ b/src/Support/Console/WrappedConsole.php @@ -0,0 +1,14 @@ +astLocator = (new BetterReflection())->astLocator(); + $this->autoSourceLocator = new AutoloadSourceLocator($this->astLocator); + $this->phpInternalSourceLocator = new PhpInternalSourceLocator($this->astLocator, new ReflectionSourceStubber()); + } + + public function execute( + string $path + ): ?PhpClassNode { + $reflector = $this->resolveReflector($path); + + $classes = $reflector->reflectAllClasses(); + + if (count($classes) === 1) { + return PhpClassNode::fromReflection($classes[0]); + } + + return null; + } + + + protected function resolveReflector(string $path): DefaultReflector + { + return new DefaultReflector(new AggregateSourceLocator([ + new SingleFileSourceLocator($path, $this->astLocator), + $this->autoSourceLocator, + $this->phpInternalSourceLocator, + ])); + } +} diff --git a/src/Support/Location.php b/src/Support/Location.php index e28626cd..95392221 100644 --- a/src/Support/Location.php +++ b/src/Support/Location.php @@ -28,6 +28,17 @@ public function getTransformedByReference(Reference $reference): ?Transformed return null; } + public function hasChanges(): bool + { + foreach ($this->transformed as $transformed) { + if ($transformed->changed) { + return true; + } + } + + return false; + } + public function hasReference(Reference $reference): bool { return $this->getTransformedByReference($reference) !== null; diff --git a/src/Support/TransformationContext.php b/src/Support/TransformationContext.php index 13e23854..b7890649 100644 --- a/src/Support/TransformationContext.php +++ b/src/Support/TransformationContext.php @@ -2,9 +2,9 @@ namespace Spatie\TypeScriptTransformer\Support; -use ReflectionClass; use Spatie\TypeScriptTransformer\Attributes\Optional; use Spatie\TypeScriptTransformer\Attributes\TypeScript; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; class TransformationContext { @@ -15,19 +15,19 @@ public function __construct( ) { } - public static function createFromReflection( - ReflectionClass $reflection + public static function createFromPhpClass( + PhpClassNode $node ): TransformationContext { - $attribute = ($reflection->getAttributes(TypeScript::class)[0] ?? null)?->newInstance(); + $attributeArguments = ($node->getAttributes(TypeScript::class)[0] ?? null)?->getArguments() ?? []; - $name = $attribute?->name ?? $reflection->getShortName(); + $name = $attributeArguments['name'] ?? $node->getShortName(); - $nameSpaceSegments = $attribute?->location ?? explode('\\', $reflection->getNamespaceName()); + $nameSpaceSegments = $attributeArguments['location'] ?? explode('\\', $node->getNamespaceName()); return new TransformationContext( $name, $nameSpaceSegments, - count($reflection->getAttributes(Optional::class)) > 0, + count($node->getAttributes(Optional::class)) > 0, ); } } diff --git a/src/Support/TransformedCollection.php b/src/Support/TransformedCollection.php index 2846a0bd..a64ad034 100644 --- a/src/Support/TransformedCollection.php +++ b/src/Support/TransformedCollection.php @@ -5,6 +5,7 @@ use ArrayAccess; use ArrayIterator; use IteratorAggregate; +use Spatie\TypeScriptTransformer\References\FilesystemReference; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Traversable; @@ -14,16 +15,24 @@ class TransformedCollection implements IteratorAggregate, ArrayAccess { /** - * @param array $items + * @param array $items + * @param array $fileMapping */ public function __construct( protected array $items = [], + protected array $fileMapping = [], ) { } public function add(Transformed ...$transformed): self { - array_push($this->items, ...$transformed); + foreach ($transformed as $item) { + $this->items[$item->reference->getKey()] = $item; + + if ($item->reference instanceof FilesystemReference) { + $this->addTransformedFileReference($item, $item->reference); + } + } return $this; } @@ -57,4 +66,88 @@ public function all(): array { return $this->items; } + + public function findTransformedByPath(string $path): ?Transformed + { + $segments = explode('/', ltrim(realpath($path), DIRECTORY_SEPARATOR)); + + $pointer = $this->files; + + foreach ($segments as $segment) { + if (! isset($pointer[$segment])) { + return null; + } + + $pointer = $pointer[$segment]; + } + + return $pointer; + } + + public function removeTransformedByPath(string $path): void + { + $segments = explode('/', ltrim(realpath($path), DIRECTORY_SEPARATOR)); + + $pointer = &$this->files; + + foreach ($segments as $i => $segment) { + if (! isset($pointer[$segment])) { + return; + } + + if ($i === count($segments) - 1) { + /** @var Transformed $transformed */ + $transformed = $pointer[$segment]; + + unset($this->items[$this->resolveIdForTransformed($transformed)]); + unset($pointer[$segment]); + + break; + } + + $pointer = &$pointer[$segment]; + } + } + + public function hasChanges(): bool + { + foreach ($this->items as $item) { + if ($item->changed) { + return true; + } + } + + return false; + } + + protected function addTransformedFileReference(Transformed $transformed, FilesystemReference $reference): void + { + $segments = explode('/', ltrim(realpath($reference->getFilesystemOriginPath()), DIRECTORY_SEPARATOR)); + + $pointer = &$this->files; + + foreach ($segments as $i => $segment) { + if (! isset($pointer[$segment])) { + $pointer[$segment] = []; + } + + if ($i === count($segments) - 1) { + $pointer[$segment] = $transformed; + $this->items[$this->resolveIdForTransformed($transformed)] = $transformed; + + break; + } + + $pointer = &$pointer[$segment]; + } + } + + protected function resolveIdForTransformed(Transformed $transformed): string + { + if ($transformed->reference instanceof FilesystemReference) { + return $transformed->reference->getFilesystemOriginPath(); + } + + return spl_object_id($transformed); + } } diff --git a/src/Support/TypeScriptTransformerLog.php b/src/Support/TypeScriptTransformerLog.php index d30c09e3..081af5b8 100644 --- a/src/Support/TypeScriptTransformerLog.php +++ b/src/Support/TypeScriptTransformerLog.php @@ -2,42 +2,49 @@ namespace Spatie\TypeScriptTransformer\Support; +use Spatie\TypeScriptTransformer\Support\Console\WrappedConsole; +use Spatie\TypeScriptTransformer\Support\Console\WrappedNullConsole; + class TypeScriptTransformerLog { - public array $infoMessages = []; - - public array $warningMessages = []; - - public array $errorMessages = []; - - protected static self $instance; + public function __construct( + protected WrappedConsole $wrappedConsole, + ) { + } - private function __construct() + public static function createNullLog(): self { + return new self(new WrappedNullConsole()); } - public static function resolve(): self + public static function boot(WrappedConsole $console): self { - return self::$instance ??= new self(); + var_dump(debug_backtrace()); + + if (self::$instance !== null) { + throw new \Exception('TypeScriptTransformerLog already booted'); + } + + return self::$instance = new self($console); } public function info(string $message): self { - $this->infoMessages[] = $message; + $this->wrappedConsole->info($message); return $this; } public function warning(string $message): self { - $this->warningMessages[] = $message; + $this->wrappedConsole->warn($message); return $this; } public function error(string $message): self { - $this->errorMessages[] = $message; + $this->wrappedConsole->error($message); return $this; } diff --git a/src/Transformed/Transformed.php b/src/Transformed/Transformed.php index 20d80c55..ae263300 100644 --- a/src/Transformed/Transformed.php +++ b/src/Transformed/Transformed.php @@ -3,27 +3,39 @@ namespace Spatie\TypeScriptTransformer\Transformed; use Spatie\TypeScriptTransformer\References\Reference; -use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptExport; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptForwardingNamedNode; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNamedNode; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; +use WeakMap; class Transformed { protected ?string $name; + public bool $changed = true; + + /** @var WeakMap */ + public WeakMap $references; + + /** @var WeakMap */ + public WeakMap $referencedBy; + + /** @var array */ + public array $missingReferences = []; + /** - * @param array $location - * @param array $references + * @param array $location */ public function __construct( public TypeScriptNode $typeScriptNode, public Reference $reference, public array $location, public bool $export = true, - public array $references = [], ) { + $this->references = new WeakMap(); + $this->referencedBy = new WeakMap(); } public function getName(): ?string @@ -58,16 +70,71 @@ public function nameAs(string $name): self public function prepareForWrite(): TypeScriptNode { + $this->changed = false; + if ($this->export === false) { return $this->typeScriptNode; } if (! $this->typeScriptNode instanceof TypeScriptNamedNode && ! $this->typeScriptNode instanceof TypeScriptForwardingNamedNode) { - TypeScriptTransformerLog::resolve()->warning("Could not export `{$this->reference->humanFriendlyName()}` because it is not exportable"); - return $this->typeScriptNode; } return new TypeScriptExport($this->typeScriptNode); } + + public function addMissingReference( + string|Reference $key, + TypeReference $typeReference + ): void { + if ($key instanceof Reference) { + $key = $key->getKey(); + } + + if(! array_key_exists($key, $this->missingReferences)) { + $this->missingReferences[$key] = []; + } + + $this->missingReferences[$key][] = $typeReference; + } + + public function isMissingReference(string $key) + { + return array_key_exists($key, $this->missingReferences); + } + + public function markMissingReferenceFound( + Transformed $transformed + ): void { + $key = $transformed->reference->getKey(); + + $typeReferences = $this->missingReferences[$key]; + + foreach ($typeReferences as $typeReference) { + $typeReference->connect($transformed); + } + + $this->references[$transformed] = $typeReferences; + + unset($this->missingReferences[$key]); + } + + public function markReferenceRemoved( + Transformed $transformed + ) { + $typeReferences = $this->references[$transformed]; + + foreach ($typeReferences as $typeReference) { + $typeReference->unconnect(); + } + + unset($this->references[$transformed]); + + $this->missingReferences = $typeReferences; + } + + public function markAsChanged(): void + { + $this->changed = true; + } } diff --git a/src/Transformers/AttributedClassTransformer.php b/src/Transformers/AttributedClassTransformer.php index b44ff021..8f2c3d85 100644 --- a/src/Transformers/AttributedClassTransformer.php +++ b/src/Transformers/AttributedClassTransformer.php @@ -2,36 +2,36 @@ namespace Spatie\TypeScriptTransformer\Transformers; -use ReflectionClass; use Spatie\TypeScriptTransformer\Attributes\TypeScript; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; use Spatie\TypeScriptTransformer\Support\TransformationContext; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\Transformed\Untransformable; class AttributedClassTransformer extends ClassTransformer { - protected function shouldTransform(ReflectionClass $reflection): bool + protected function shouldTransform(PhpClassNode $phpClassNode): bool { - return count($reflection->getAttributes(TypeScript::class)) > 0; + return count($phpClassNode->getAttributes(TypeScript::class)) > 0; } - public function transform(ReflectionClass $reflectionClass, TransformationContext $context): Transformed|Untransformable + public function transform(PhpClassNode $phpClassNode, TransformationContext $context): Transformed|Untransformable { - $transformed = parent::transform($reflectionClass, $context); + $transformed = parent::transform($phpClassNode, $context); if ($transformed instanceof Untransformable) { return $transformed; } /** @var TypeScript $attribute */ - $attribute = $reflectionClass->getAttributes(TypeScript::class)[0]->newInstance(); + $attribute = $phpClassNode->getAttributes(TypeScript::class)[0]->getArguments(); - if ($attribute->name !== null) { - $transformed->nameAs($attribute->name); + if (($attribute['name'] ?? null) !== null) { + $transformed->nameAs($attribute['name']); } - if ($attribute->location !== null) { - $transformed->location = $attribute->location; + if (($attribute['location'] ?? null) !== null) { + $transformed->location = $attribute['location']; } return $transformed; diff --git a/src/Transformers/ClassPropertyProcessors/ClassPropertyProcessor.php b/src/Transformers/ClassPropertyProcessors/ClassPropertyProcessor.php index 46f6f094..65150932 100644 --- a/src/Transformers/ClassPropertyProcessors/ClassPropertyProcessor.php +++ b/src/Transformers/ClassPropertyProcessors/ClassPropertyProcessor.php @@ -3,13 +3,13 @@ namespace Spatie\TypeScriptTransformer\Transformers\ClassPropertyProcessors; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -use ReflectionProperty; +use Spatie\TypeScriptTransformer\PhpNodes\PhpPropertyNode; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; interface ClassPropertyProcessor { public function execute( - ReflectionProperty $reflection, + PhpPropertyNode $phpPropertyNode, ?TypeNode $annotation, TypeScriptProperty $property ): ?TypeScriptProperty; diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index 121db7a6..2076298d 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -3,15 +3,15 @@ namespace Spatie\TypeScriptTransformer\Transformers; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -use ReflectionClass; -use ReflectionProperty; use Spatie\TypeScriptTransformer\Actions\TranspilePhpStanTypeToTypeScriptNodeAction; -use Spatie\TypeScriptTransformer\Actions\TranspileReflectionTypeToTypeScriptNodeAction; +use Spatie\TypeScriptTransformer\Actions\TranspilePhpTypeNodeToTypeScriptNodeAction; use Spatie\TypeScriptTransformer\Attributes\Hidden; use Spatie\TypeScriptTransformer\Attributes\Optional; use Spatie\TypeScriptTransformer\Attributes\TypeScriptTypeAttributeContract; use Spatie\TypeScriptTransformer\ClassPropertyProcessors\FixArrayLikeStructuresClassPropertyProcessor; -use Spatie\TypeScriptTransformer\References\ReflectionClassReference; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; +use Spatie\TypeScriptTransformer\PhpNodes\PhpPropertyNode; +use Spatie\TypeScriptTransformer\References\PhpClassReference; use Spatie\TypeScriptTransformer\Support\TransformationContext; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\Transformed\Untransformable; @@ -31,33 +31,33 @@ abstract class ClassTransformer implements Transformer public function __construct( protected DocTypeResolver $docTypeResolver = new DocTypeResolver(), protected TranspilePhpStanTypeToTypeScriptNodeAction $transpilePhpStanTypeToTypeScriptTypeAction = new TranspilePhpStanTypeToTypeScriptNodeAction(), - protected TranspileReflectionTypeToTypeScriptNodeAction $transpileReflectionTypeToTypeScriptTypeAction = new TranspileReflectionTypeToTypeScriptNodeAction(), + protected TranspilePhpTypeNodeToTypeScriptNodeAction $transpilePhpTypeNodeToTypeScriptTypeAction = new TranspilePhpTypeNodeToTypeScriptNodeAction(), ) { $this->classPropertyProcessors = $this->classPropertyProcessors(); } - public function transform(ReflectionClass $reflectionClass, TransformationContext $context): Transformed|Untransformable + public function transform(PhpClassNode $phpClassNode, TransformationContext $context): Transformed|Untransformable { - if ($reflectionClass->isEnum() || $reflectionClass->isInterface()) { + if ($phpClassNode->isEnum() || $phpClassNode->isInterface()) { return Untransformable::create(); } - if (! $this->shouldTransform($reflectionClass)) { + if (! $this->shouldTransform($phpClassNode)) { return Untransformable::create(); } return new Transformed( new TypeScriptAlias( new TypeScriptIdentifier($context->name), - $this->getTypeScriptNode($reflectionClass, $context) + $this->getTypeScriptNode($phpClassNode, $context) ), - new ReflectionClassReference($reflectionClass), + new PhpClassReference($phpClassNode), $context->nameSpaceSegments, true, ); } - abstract protected function shouldTransform(ReflectionClass $reflection): bool; + abstract protected function shouldTransform(PhpClassNode $phpClassNode): bool; /** @return array */ protected function classPropertyProcessors(): array @@ -68,30 +68,30 @@ protected function classPropertyProcessors(): array } protected function getTypeScriptNode( - ReflectionClass $reflectionClass, + PhpClassNode $phpClassNode, TransformationContext $context, ): TypeScriptNode { - if ($resolvedAttributeType = $this->resolveTypeByAttribute($reflectionClass)) { + if ($resolvedAttributeType = $this->resolveTypeByAttribute($phpClassNode)) { return $resolvedAttributeType; } - $classAnnotations = $this->docTypeResolver->class($reflectionClass)?->properties ?? []; + $classAnnotations = $this->docTypeResolver->class($phpClassNode)?->properties ?? []; - $constructorAnnotations = $reflectionClass->hasMethod('__construct') - ? $this->docTypeResolver->method($reflectionClass->getMethod('__construct'))?->parameters ?? [] + $constructorAnnotations = $phpClassNode->hasMethod('__construct') + ? $this->docTypeResolver->method($phpClassNode->getMethod('__construct'))?->parameters ?? [] : []; $properties = []; - foreach ($this->getProperties($reflectionClass) as $reflectionProperty) { - $annotation = $classAnnotations[$reflectionProperty->getName()] - ?? $constructorAnnotations[$reflectionProperty->getName()] - ?? $this->docTypeResolver->property($reflectionProperty) + foreach ($this->getProperties($phpClassNode) as $phpPropertyNode) { + $annotation = $classAnnotations[$phpPropertyNode->getName()] + ?? $constructorAnnotations[$phpPropertyNode->getName()] + ?? $this->docTypeResolver->property($phpPropertyNode) ?? null; $property = $this->createProperty( - $reflectionClass, - $reflectionProperty, + $phpClassNode, + $phpPropertyNode, $annotation?->type, $context ); @@ -101,7 +101,7 @@ protected function getTypeScriptNode( } $property = $this->runClassPropertyProcessors( - $reflectionProperty, + $phpPropertyNode, $annotation?->type, $property ); @@ -115,60 +115,60 @@ protected function getTypeScriptNode( } protected function resolveTypeByAttribute( - ReflectionClass $reflectionClass, - ?ReflectionProperty $property = null, + PhpClassNode $phpClassNode, + ?PhpPropertyNode $property = null, ): ?TypeScriptNode { - $subject = $property ?? $reflectionClass; + $subject = $property ?? $phpClassNode; foreach ($subject->getAttributes() as $attribute) { if (is_a($attribute->getName(), TypeScriptTypeAttributeContract::class, true)) { /** @var TypeScriptTypeAttributeContract $attributeInstance */ $attributeInstance = $attribute->newInstance(); - return $attributeInstance->getType($reflectionClass); + return $attributeInstance->getType($phpClassNode); } } return null; } - protected function getProperties(ReflectionClass $reflection): array + protected function getProperties(PhpClassNode $phpClassNode): array { return array_filter( - $reflection->getProperties(ReflectionProperty::IS_PUBLIC), - fn (ReflectionProperty $property) => ! $property->isStatic() + $phpClassNode->getProperties(\ReflectionProperty::IS_PUBLIC), + fn (PhpPropertyNode $property) => ! $property->isStatic() ); } protected function createProperty( - ReflectionClass $reflectionClass, - ReflectionProperty $reflectionProperty, + PhpClassNode $phpClassNode, + PhpPropertyNode $phpPropertyNode, ?TypeNode $annotation, TransformationContext $context, ): ?TypeScriptProperty { $type = $this->resolveTypeForProperty( - $reflectionClass, - $reflectionProperty, + $phpClassNode, + $phpPropertyNode, $annotation ); $property = new TypeScriptProperty( - $reflectionProperty->getName(), + $phpPropertyNode->getName(), $type, $this->isPropertyOptional( - $reflectionProperty, - $reflectionClass, + $phpPropertyNode, + $phpClassNode, $type, $context ), $this->isPropertyReadonly( - $reflectionProperty, - $reflectionClass, + $phpPropertyNode, + $phpClassNode, $type, ) ); - if ($this->isPropertyHidden($reflectionProperty, $reflectionClass, $property)) { + if ($this->isPropertyHidden($phpPropertyNode, $phpClassNode, $property)) { return null; } @@ -176,25 +176,25 @@ protected function createProperty( } protected function resolveTypeForProperty( - ReflectionClass $reflectionClass, - ReflectionProperty $reflectionProperty, + PhpClassNode $phpClassNode, + PhpPropertyNode $phpPropertyNode, ?TypeNode $annotation, ): TypeScriptNode { - if ($resolvedAttributeType = $this->resolveTypeByAttribute($reflectionClass, $reflectionProperty)) { + if ($resolvedAttributeType = $this->resolveTypeByAttribute($phpClassNode, $phpPropertyNode)) { return $resolvedAttributeType; } if ($annotation) { return $this->transpilePhpStanTypeToTypeScriptTypeAction->execute( $annotation, - $reflectionClass, + $phpClassNode, ); } - if ($reflectionProperty->hasType()) { - return $this->transpileReflectionTypeToTypeScriptTypeAction->execute( - $reflectionProperty->getType(), - $reflectionClass + if ($phpPropertyNode->hasType()) { + return $this->transpilePhpTypeNodeToTypeScriptTypeAction->execute( + $phpPropertyNode->getType(), + $phpClassNode ); } @@ -202,39 +202,39 @@ protected function resolveTypeForProperty( } protected function isPropertyOptional( - ReflectionProperty $reflectionProperty, - ReflectionClass $reflectionClass, + PhpPropertyNode $phpPropertyNode, + PhpClassNode $phpClassNode, TypeScriptNode $type, TransformationContext $context, ): bool { - return $context->optional || count($reflectionProperty->getAttributes(Optional::class)) > 0; + return $context->optional || count($phpPropertyNode->getAttributes(Optional::class)) > 0; } protected function isPropertyReadonly( - ReflectionProperty $reflectionProperty, - ReflectionClass $reflectionClass, + PhpPropertyNode $phpPropertyNode, + PhpClassNode $phpClassNode, TypeScriptNode $type, ): bool { - return $reflectionProperty->isReadOnly() || $reflectionClass->isReadOnly(); + return $phpPropertyNode->isReadOnly() || $phpClassNode->isReadOnly(); } protected function isPropertyHidden( - ReflectionProperty $reflectionProperty, - ReflectionClass $reflectionClass, + PhpPropertyNode $phpPropertyNode, + PhpClassNode $phpClassNode, TypeScriptProperty $property, ): bool { - return count($reflectionProperty->getAttributes(Hidden::class)) > 0; + return count($phpPropertyNode->getAttributes(Hidden::class)) > 0; } protected function runClassPropertyProcessors( - ReflectionProperty $reflectionProperty, + PhpPropertyNode $phpPropertyNode, ?TypeNode $annotation, TypeScriptProperty $property, ): ?TypeScriptProperty { $processors = $this->classPropertyProcessors; foreach ($processors as $processor) { - $property = $processor->execute($reflectionProperty, $annotation, $property); + $property = $processor->execute($phpPropertyNode, $annotation, $property); if ($property === null) { return null; diff --git a/src/Transformers/EnumProviders/EnumProvider.php b/src/Transformers/EnumProviders/EnumProvider.php index b3bad589..1fe5cabd 100644 --- a/src/Transformers/EnumProviders/EnumProvider.php +++ b/src/Transformers/EnumProviders/EnumProvider.php @@ -2,13 +2,13 @@ namespace Spatie\TypeScriptTransformer\Transformers\EnumProviders; -use ReflectionClass; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; interface EnumProvider { - public function isEnum(ReflectionClass $reflection): bool; + public function isEnum(PhpClassNode $phpClassNode): bool; - public function isValidUnion(ReflectionClass $reflection): bool; + public function isValidUnion(PhpClassNode $phpClassNode): bool; - public function resolveCases(ReflectionClass $reflection): array; + public function resolveCases(PhpClassNode $phpClassNode): array; } diff --git a/src/Transformers/EnumProviders/PhpEnumProvider.php b/src/Transformers/EnumProviders/PhpEnumProvider.php index 17987b31..454510d0 100644 --- a/src/Transformers/EnumProviders/PhpEnumProvider.php +++ b/src/Transformers/EnumProviders/PhpEnumProvider.php @@ -2,37 +2,33 @@ namespace Spatie\TypeScriptTransformer\Transformers\EnumProviders; -use BackedEnum; -use ReflectionClass; -use ReflectionEnum; -use UnitEnum; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; +use Spatie\TypeScriptTransformer\PhpNodes\PhpEnumCaseNode; +use Spatie\TypeScriptTransformer\PhpNodes\PhpEnumNode; class PhpEnumProvider implements EnumProvider { - public function isEnum(ReflectionClass $reflection): bool + public function isEnum(PhpClassNode $phpClassNode): bool { - return $reflection->isEnum(); + return $phpClassNode->isEnum(); } - public function isValidUnion(ReflectionClass $reflection): bool + public function isValidUnion(PhpClassNode $phpClassNode): bool { - return (new ReflectionEnum($reflection->getName()))->isBacked(); + return $phpClassNode instanceof PhpEnumNode && $phpClassNode->isBacked(); } /** * @return array */ - public function resolveCases(ReflectionClass $reflection): array + public function resolveCases(PhpClassNode|PhpEnumNode $phpClassNode): array { - /** @var class-string $enumClass */ - $enumClass = $reflection->getName(); - return array_map( - fn ($case) => [ - 'name' => $case->name, - 'value' => $case instanceof BackedEnum ? $case->value : null, + fn (PhpEnumCaseNode $case) => [ + 'name' => $case->getName(), + 'value' => $case->getValue(), ], - $enumClass::cases() + array_values($phpClassNode->getCases()) ); } } diff --git a/src/Transformers/EnumTransformer.php b/src/Transformers/EnumTransformer.php index c50c65d7..daabfdc9 100644 --- a/src/Transformers/EnumTransformer.php +++ b/src/Transformers/EnumTransformer.php @@ -2,8 +2,8 @@ namespace Spatie\TypeScriptTransformer\Transformers; -use ReflectionClass; -use Spatie\TypeScriptTransformer\References\ReflectionClassReference; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; +use Spatie\TypeScriptTransformer\References\PhpClassReference; use Spatie\TypeScriptTransformer\Support\TransformationContext; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\Transformed\Untransformable; @@ -25,24 +25,24 @@ public function __construct( } public function transform( - ReflectionClass $reflectionClass, + PhpClassNode $phpClassNode, TransformationContext $context ): Transformed|Untransformable { - if (! $this->enumProvider->isEnum($reflectionClass)) { + if (! $this->enumProvider->isEnum($phpClassNode)) { return Untransformable::create(); } - if ($this->useUnionEnums === true && ! $this->enumProvider->isValidUnion($reflectionClass)) { + if ($this->useUnionEnums === true && ! $this->enumProvider->isValidUnion($phpClassNode)) { return Untransformable::create(); } - $cases = $this->enumProvider->resolveCases($reflectionClass); + $cases = $this->enumProvider->resolveCases($phpClassNode); return new Transformed( $this->useUnionEnums ? $this->transformAsUnion($context->name, $cases) : $this->transformAsNativeEnum($context->name, $cases), - new ReflectionClassReference($reflectionClass), + new PhpClassReference($phpClassNode), $context->nameSpaceSegments, true, ); diff --git a/src/Transformers/InterfaceTransformer.php b/src/Transformers/InterfaceTransformer.php index 1da7209a..6f5a6835 100644 --- a/src/Transformers/InterfaceTransformer.php +++ b/src/Transformers/InterfaceTransformer.php @@ -2,12 +2,12 @@ namespace Spatie\TypeScriptTransformer\Transformers; -use ReflectionClass; -use ReflectionMethod; -use ReflectionParameter; use Spatie\TypeScriptTransformer\Actions\TranspilePhpStanTypeToTypeScriptNodeAction; -use Spatie\TypeScriptTransformer\Actions\TranspileReflectionTypeToTypeScriptNodeAction; -use Spatie\TypeScriptTransformer\References\ReflectionClassReference; +use Spatie\TypeScriptTransformer\Actions\TranspilePhpTypeNodeToTypeScriptNodeAction; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; +use Spatie\TypeScriptTransformer\PhpNodes\PhpMethodNode; +use Spatie\TypeScriptTransformer\PhpNodes\PhpParameterNode; +use Spatie\TypeScriptTransformer\References\PhpClassReference; use Spatie\TypeScriptTransformer\Support\TransformationContext; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\Transformed\Untransformable; @@ -28,45 +28,45 @@ abstract class InterfaceTransformer implements Transformer public function __construct( protected DocTypeResolver $docTypeResolver = new DocTypeResolver(), protected TranspilePhpStanTypeToTypeScriptNodeAction $transpilePhpStanTypeToTypeScriptTypeAction = new TranspilePhpStanTypeToTypeScriptNodeAction(), - protected TranspileReflectionTypeToTypeScriptNodeAction $transpileReflectionTypeToTypeScriptTypeAction = new TranspileReflectionTypeToTypeScriptNodeAction(), + protected TranspilePhpTypeNodeToTypeScriptNodeAction $transpilePhpTypeNodeToTypeScriptNodeAction = new TranspilePhpTypeNodeToTypeScriptNodeAction(), ) { } - public function transform(ReflectionClass $reflectionClass, TransformationContext $context): Transformed|Untransformable + public function transform(PhpClassNode $phpClassNode, TransformationContext $context): Transformed|Untransformable { - if (! $reflectionClass->isInterface()) { + if (! $phpClassNode->isInterface()) { return Untransformable::create(); } - if (! $this->shouldTransform($reflectionClass)) { + if (! $this->shouldTransform($phpClassNode)) { return Untransformable::create(); } $node = new TypeScriptInterface( new TypeScriptIdentifier($context->name), - $this->getProperties($reflectionClass, $context), - $this->getMethods($reflectionClass, $context) + $this->getProperties($phpClassNode, $context), + $this->getMethods($phpClassNode, $context) ); return new Transformed( $node, - new ReflectionClassReference($reflectionClass), + new PhpClassReference($phpClassNode), $context->nameSpaceSegments, true, ); } - abstract protected function shouldTransform(ReflectionClass $reflection): bool; + abstract protected function shouldTransform(PhpClassNode $phpClassNode): bool; /** @return TypeScriptInterfaceMethod[] */ protected function getMethods( - ReflectionClass $reflectionClass, + PhpClassNode $phpClassNode, TransformationContext $context, ): array { $methods = []; - foreach ($reflectionClass->getMethods() as $reflectionMethod) { - $methods[] = $this->getTypeScriptMethod($reflectionClass, $reflectionMethod, $context); + foreach ($phpClassNode->getMethods() as $phpMethodNode) { + $methods[] = $this->getTypeScriptMethod($phpClassNode, $phpMethodNode, $context); } return $methods; @@ -74,51 +74,51 @@ protected function getMethods( /** @return TypeScriptProperty[] */ protected function getProperties( - ReflectionClass $reflectionClass, + PhpClassNode $phpClassNode, TransformationContext $context, ): array { return []; } protected function getTypeScriptMethod( - ReflectionClass $reflectionClass, - ReflectionMethod $reflectionMethod, + PhpClassNode $phpClassNode, + PhpMethodNode $phpMethodNode, TransformationContext $context, ): TypeScriptInterfaceMethod { - $annotation = $this->docTypeResolver->method($reflectionMethod); + $annotation = $this->docTypeResolver->method($phpMethodNode); return new TypeScriptInterfaceMethod( - $reflectionMethod->getName(), - array_map(fn (ReflectionParameter $parameter) => $this->resolveMethodParameterType( - $reflectionClass, - $reflectionMethod, - $parameter, + $phpMethodNode->getName(), + array_map(fn (PhpParameterNode $parameterNode) => $this->resolveMethodParameterType( + $phpClassNode, + $phpMethodNode, + $parameterNode, $context, - $annotation->parameters[$parameter->getName()] ?? null - ), $reflectionMethod->getParameters()), - $this->resolveMethodReturnType($reflectionClass, $reflectionMethod, $context, $annotation) + $annotation->parameters[$parameterNode->getName()] ?? null + ), $phpMethodNode->getParameters()), + $this->resolveMethodReturnType($phpClassNode, $phpMethodNode, $context, $annotation) ); } protected function resolveMethodReturnType( - ReflectionClass $reflectionClass, - ReflectionMethod $reflectionMethod, + PhpClassNode $classNode, + PhpMethodNode $methodNode, TransformationContext $context, ?ParsedMethod $annotation ): TypeScriptNode { if ($annotation->returnType) { return $this->transpilePhpStanTypeToTypeScriptTypeAction->execute( $annotation->returnType, - $reflectionClass + $classNode ); } - $reflectionType = $reflectionMethod->getReturnType(); + $returnType = $methodNode->getReturnType(); - if ($reflectionType) { - return $this->transpileReflectionTypeToTypeScriptTypeAction->execute( - $reflectionType, - $reflectionClass + if ($returnType) { + return $this->transpilePhpTypeNodeToTypeScriptNodeAction->execute( + $returnType, + $classNode ); } @@ -126,28 +126,28 @@ protected function resolveMethodReturnType( } protected function resolveMethodParameterType( - ReflectionClass $reflectionClass, - ReflectionMethod $reflectionMethod, - ReflectionParameter $reflectionParameter, + PhpClassNode $classNode, + PhpMethodNode $methodNode, + PhpParameterNode $parameterNode, TransformationContext $context, ?ParsedNameAndType $annotation, ): TypeScriptParameter { $type = match (true) { $annotation !== null => $this->transpilePhpStanTypeToTypeScriptTypeAction->execute( $annotation->type, - $reflectionClass + $classNode ), - $reflectionParameter->hasType() => $this->transpileReflectionTypeToTypeScriptTypeAction->execute( - $reflectionParameter->getType(), - $reflectionClass + $parameterNode->hasType() => $this->transpilePhpTypeNodeToTypeScriptNodeAction->execute( + $parameterNode->getType(), + $classNode ), default => new TypeScriptUnknown(), }; return new TypeScriptParameter( - $reflectionParameter->getName(), + $parameterNode->getName(), $type, - $reflectionParameter->isOptional() + $parameterNode->isOptional() ); } } diff --git a/src/Transformers/Transformer.php b/src/Transformers/Transformer.php index ddbf4b57..041cb594 100644 --- a/src/Transformers/Transformer.php +++ b/src/Transformers/Transformer.php @@ -2,12 +2,12 @@ namespace Spatie\TypeScriptTransformer\Transformers; -use ReflectionClass; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; use Spatie\TypeScriptTransformer\Support\TransformationContext; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\Transformed\Untransformable; interface Transformer { - public function transform(ReflectionClass $reflectionClass, TransformationContext $context): Transformed|Untransformable; + public function transform(PhpClassNode $phpClassNode, TransformationContext $context): Transformed|Untransformable; } diff --git a/src/TypeProviders/TransformerTypesProvider.php b/src/TypeProviders/TransformerTypesProvider.php index bd2703ac..c9751bc0 100644 --- a/src/TypeProviders/TransformerTypesProvider.php +++ b/src/TypeProviders/TransformerTypesProvider.php @@ -24,14 +24,12 @@ public function provide( TypeScriptTransformerConfig $config, TransformedCollection $types ): void { - $discoverTypesAction = new DiscoverTypesAction(); $transformTypesAction = new TransformTypesAction(); - - $discoveredClasses = $discoverTypesAction->execute($this->directories); + $discoverTypesAction = new DiscoverTypesAction(); $types->add(...$transformTypesAction->execute( $this->transformers, - $discoveredClasses, + $discoverTypesAction->execute($this->directories), )); } } diff --git a/src/TypeResolvers/DocTypeResolver.php b/src/TypeResolvers/DocTypeResolver.php index 18c2e9f9..832145ab 100644 --- a/src/TypeResolvers/DocTypeResolver.php +++ b/src/TypeResolvers/DocTypeResolver.php @@ -9,9 +9,9 @@ use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; -use ReflectionClass; -use ReflectionMethod; -use ReflectionProperty; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; +use Spatie\TypeScriptTransformer\PhpNodes\PhpMethodNode; +use Spatie\TypeScriptTransformer\PhpNodes\PhpPropertyNode; use Spatie\TypeScriptTransformer\TypeResolvers\Data\ParsedClass; use Spatie\TypeScriptTransformer\TypeResolvers\Data\ParsedMethod; use Spatie\TypeScriptTransformer\TypeResolvers\Data\ParsedNameAndType; @@ -33,9 +33,9 @@ public function __construct() $this->lexer = new Lexer(); } - public function class(ReflectionClass $class): ?ParsedClass + public function class(PhpClassNode $phpClassNode): ?ParsedClass { - $parsed = $this->parseDocComment($class); + $parsed = $this->parseDocComment($phpClassNode); if ($parsed === null) { return null; @@ -56,9 +56,9 @@ public function class(ReflectionClass $class): ?ParsedClass return new ParsedClass($properties); } - public function method(ReflectionMethod $method): ?ParsedMethod + public function method(PhpMethodNode $phpMethodNode): ?ParsedMethod { - $parsed = $this->parseDocComment($method); + $parsed = $this->parseDocComment($phpMethodNode); if ($parsed === null) { return null; @@ -85,9 +85,9 @@ public function method(ReflectionMethod $method): ?ParsedMethod return new ParsedMethod($parameters, $return); } - public function property(ReflectionProperty $property): ?ParsedNameAndType + public function property(PhpPropertyNode $phpPropertyNode): ?ParsedNameAndType { - $parsed = $this->parseDocComment($property); + $parsed = $this->parseDocComment($phpPropertyNode); if ($parsed === null) { return null; @@ -103,7 +103,7 @@ public function property(ReflectionProperty $property): ?ParsedNameAndType return null; } - return new ParsedNameAndType($property->name, $var); + return new ParsedNameAndType($phpPropertyNode->getName(), $var); } public function type(string $type): TypeNode @@ -114,14 +114,14 @@ public function type(string $type): TypeNode } protected function parseDocComment( - ReflectionClass|ReflectionMethod|ReflectionProperty $reflection + PhpClassNode|PhpMethodNode|PhpPropertyNode $phpNode ): ?PhpDocNode { - if ($reflection->getDocComment() === false) { + if ($phpNode->getDocComment() === false || $phpNode->getDocComment() === null) { return null; } return $this->docParser->parse( - new TokenIterator($this->lexer->tokenize($reflection->getDocComment())) + new TokenIterator($this->lexer->tokenize($phpNode->getDocComment())) ); } } diff --git a/src/TypeScriptNodes/TypeReference.php b/src/TypeScriptNodes/TypeReference.php index 24fd89bf..96c4358a 100644 --- a/src/TypeScriptNodes/TypeReference.php +++ b/src/TypeScriptNodes/TypeReference.php @@ -25,6 +25,11 @@ public function connect(Transformed $transformed): void $this->referenced = $transformed; } + public function unconnect(): void + { + $this->referenced = null; + } + public function write(WritingContext $context): string { if($this->referenced === null) { diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index f8836c4a..2c660151 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -4,39 +4,64 @@ use Spatie\TypeScriptTransformer\Actions\ConnectReferencesAction; use Spatie\TypeScriptTransformer\Actions\DiscoverTypesAction; +use Spatie\TypeScriptTransformer\Actions\ExecuteConnectedClosuresAction; +use Spatie\TypeScriptTransformer\Actions\ExecuteProvidedClosuresAction; use Spatie\TypeScriptTransformer\Actions\FormatFilesAction; use Spatie\TypeScriptTransformer\Actions\ProvideTypesAction; +use Spatie\TypeScriptTransformer\Actions\TransformTypesAction; use Spatie\TypeScriptTransformer\Actions\WriteFilesAction; -use Spatie\TypeScriptTransformer\Visitor\Visitor; +use Spatie\TypeScriptTransformer\Collections\ReferenceMap; +use Spatie\TypeScriptTransformer\Support\Console\WrappedConsole; +use Spatie\TypeScriptTransformer\Support\Console\WrappedNullConsole; +use Spatie\TypeScriptTransformer\Support\LoadPhpClassNodeAction; +use Spatie\TypeScriptTransformer\Support\TransformedCollection; +use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; class TypeScriptTransformer { public function __construct( - protected TypeScriptTransformerConfig $config, - protected DiscoverTypesAction $discoverTypesAction, - protected ProvideTypesAction $provideTypesAction, - protected ConnectReferencesAction $connectReferencesAction, - protected WriteFilesAction $writeFilesAction, - protected FormatFilesAction $formatFilesAction, + public readonly TypeScriptTransformerConfig $config, + public readonly TypeScriptTransformerLog $log, + public readonly DiscoverTypesAction $discoverTypesAction, + public readonly ProvideTypesAction $provideTypesAction, + public readonly ExecuteProvidedClosuresAction $executeProvidedClosuresAction, + public readonly ConnectReferencesAction $connectReferencesAction, + public readonly ExecuteConnectedClosuresAction $executeConnectedClosuresAction, + public readonly WriteFilesAction $writeFilesAction, + public readonly FormatFilesAction $formatFilesAction, + public readonly TransformTypesAction $transformTypesAction, + public readonly LoadPhpClassNodeAction $loadPhpClassNodeAction, + public readonly bool $watch = false, ) { } - public static function create(TypeScriptTransformerConfig|TypeScriptTransformerConfigFactory $config): self - { + public static function create( + TypeScriptTransformerConfig|TypeScriptTransformerConfigFactory $config, + WrappedConsole $console = new WrappedNullConsole(), + bool $watch = false, + ): self { $config = $config instanceof TypeScriptTransformerConfigFactory ? $config->get() : $config; + $log = new TypeScriptTransformerLog($console); + return new self( $config, + $log, new DiscoverTypesAction(), new ProvideTypesAction($config), - new ConnectReferencesAction(), + new ExecuteProvidedClosuresAction($config), + new ConnectReferencesAction($log), + new ExecuteConnectedClosuresAction($config), new WriteFilesAction($config), new FormatFilesAction($config), + new TransformTypesAction(), + new LoadPhpClassNodeAction(), + $watch ); } - public function execute(bool $watch = false): void + public function execute(): void { /** * TODO: @@ -50,35 +75,33 @@ public function execute(bool $watch = false): void * - Release */ - /** - * Watch implementation - * - We care about file create, update and delete - * - Directory changes are basically combined operations of file changes - * - File create - * - Run the file though `TransformerTypesProvider` and check if a ReflectionClass can be created - * - If so, add it to the types collection - * - Add it to the reference map - * - Rewrite the file (partially) - */ - $transformedCollection = $this->provideTypesAction->execute(); - if (! empty($this->config->providedVisitorClosures)) { - $visitor = Visitor::create()->closures(...$this->config->providedVisitorClosures); - - foreach ($transformedCollection as $transformed) { - $visitor->execute($transformed->typeScriptNode); - } - } + $this->executeProvidedClosuresAction->execute($transformedCollection); $referenceMap = $this->connectReferencesAction->execute($transformedCollection); - if (! empty($this->config->connectedVisitorClosures)) { - $visitor = Visitor::create()->closures(...$this->config->connectedVisitorClosures); + $this->executeConnectedClosuresAction->execute($transformedCollection); + + $this->outputTransformed($transformedCollection, $referenceMap); + + if ($this->watch) { + $watcher = new FileSystemWatcher( + $this, + $transformedCollection, + $referenceMap + ); + + $watcher->run(); + } + } - foreach ($transformedCollection as $transformed) { - $visitor->execute($transformed->typeScriptNode); - } + public function outputTransformed( + TransformedCollection $transformedCollection, + ReferenceMap $referenceMap + ): void { + if (! $transformedCollection->hasChanges()) { + return; } $writeableFiles = $this->config->writer->output($transformedCollection, $referenceMap); diff --git a/src/TypeScriptTransformerConfig.php b/src/TypeScriptTransformerConfig.php index 0ef71365..9b51f11d 100755 --- a/src/TypeScriptTransformerConfig.php +++ b/src/TypeScriptTransformerConfig.php @@ -3,6 +3,7 @@ namespace Spatie\TypeScriptTransformer; use Spatie\TypeScriptTransformer\Formatters\Formatter; +use Spatie\TypeScriptTransformer\Transformers\Transformer; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\Visitor\VisitorClosure; use Spatie\TypeScriptTransformer\Writers\Writer; @@ -14,6 +15,7 @@ class TypeScriptTransformerConfig * @param array $directoriesToWatch * @param array $providedVisitorClosures * @param array $connectedVisitorClosures + * @param array $transformers */ public function __construct( readonly public array $typeProviders, @@ -22,6 +24,7 @@ public function __construct( readonly public array $directoriesToWatch = [], readonly public array $providedVisitorClosures = [], readonly public array $connectedVisitorClosures = [], + readonly public array $transformers = [], ) { } } diff --git a/src/TypeScriptTransformerConfigFactory.php b/src/TypeScriptTransformerConfigFactory.php index 3f6d019b..055d4d95 100644 --- a/src/TypeScriptTransformerConfigFactory.php +++ b/src/TypeScriptTransformerConfigFactory.php @@ -218,12 +218,12 @@ public function get(): TypeScriptTransformerConfig array_unshift($this->providedVisitorClosures, new ReplaceTypesVisitorClosure($this->typeReplacements)); } - if (! empty($this->transformers)) { - $transformers = array_map( - fn (Transformer|string $transformer) => is_string($transformer) ? new $transformer() : $transformer, - $this->transformers - ); + $transformers = array_map( + fn (Transformer|string $transformer) => is_string($transformer) ? new $transformer() : $transformer, + $this->transformers + ); + if (! empty($transformers)) { $typeProviders[] = new TransformerTypesProvider($transformers, $this->directoriesToWatch); } @@ -233,7 +233,8 @@ public function get(): TypeScriptTransformerConfig $formatter, $this->directoriesToWatch, $this->providedVisitorClosures, - $this->connectedVisitorClosures + $this->connectedVisitorClosures, + $transformers, ); } diff --git a/src/Visitor/VisitorClosure.php b/src/Visitor/VisitorClosure.php index 2cb97b91..c57cc949 100644 --- a/src/Visitor/VisitorClosure.php +++ b/src/Visitor/VisitorClosure.php @@ -3,7 +3,7 @@ namespace Spatie\TypeScriptTransformer\Visitor; use Closure; -use ReflectionFunction; +use Roave\BetterReflection\Reflection\ReflectionFunction; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; class VisitorClosure @@ -15,7 +15,7 @@ public function __construct( protected ?array $allowedNodes, protected VisitorClosureType $type, ) { - $this->requiresMetadata = (new ReflectionFunction($this->closure))->getNumberOfParameters() === 2; + $this->requiresMetadata = ReflectionFunction::createFromClosure($this->closure)->getNumberOfParameters() === 2; } public function isBefore(): bool diff --git a/src/Writers/FlatWriter.php b/src/Writers/FlatWriter.php index 9cc81f26..89ddf365 100644 --- a/src/Writers/FlatWriter.php +++ b/src/Writers/FlatWriter.php @@ -32,7 +32,7 @@ public function output( }); foreach ($collection as $transformed) { - $output .= $transformed->prepareForWrite()->write($writingContext) . PHP_EOL; + $output .= $transformed->prepareForWrite()->write($writingContext).PHP_EOL; } return [new WriteableFile($this->filename, $output)]; diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index ff8e563e..ae76d5c4 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -29,6 +29,10 @@ public function output(TransformedCollection $collection, ReferenceMap $referenc $writtenFiles = []; foreach ($locations as $location) { + if ($location->hasChanges() === false) { + continue; + } + $writtenFiles[] = $this->writeLocation($location, $referenceMap); } @@ -53,7 +57,7 @@ protected function writeLocation( }); foreach ($imports->getTypeScriptNodes() as $import) { - $output .= $import->write($writingContext) . PHP_EOL; + $output .= $import->write($writingContext).PHP_EOL; } if ($imports->isEmpty() === false) { @@ -61,7 +65,7 @@ protected function writeLocation( } foreach ($location->transformed as $transformedItem) { - $output .= $transformedItem->prepareForWrite()->write($writingContext) . PHP_EOL; + $output .= $transformedItem->prepareForWrite()->write($writingContext).PHP_EOL; } return new WriteableFile("{$this->resolvePath($location)}/index.ts", $output); diff --git a/tests/Actions/ConnectReferencesActionTest.php b/tests/Actions/ConnectReferencesActionTest.php index 6c492040..49235118 100644 --- a/tests/Actions/ConnectReferencesActionTest.php +++ b/tests/Actions/ConnectReferencesActionTest.php @@ -1,6 +1,7 @@ execute($collection)->all(); + $action = new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()); + + $referenceMap = $action->execute($collection)->all(); expect($referenceMap) ->toHaveCount(2) @@ -30,10 +33,12 @@ ]); expect($transformedEnum->references)->toHaveCount(0); + expect($transformedEnum->referencedBy)->toHaveCount(1); + expect($transformedEnum->referencedBy->offsetExists($transformedClass)); - expect($transformedClass->references) - ->toHaveCount(1) - ->toBe([$transformedEnum]); + expect($transformedClass->references)->toHaveCount(1); + expect($transformedClass->references->offsetExists($transformedEnum)); + expect($transformedClass->referencedBy)->toHaveCount(0); expect($transformedClass->typeScriptNode->type->properties[0]->type) ->toBeInstanceOf(TypeReference::class) @@ -46,7 +51,9 @@ $circularB = transformSingle(CircularB::class, new AllClassTransformer()), ]); - $referenceMap = app(ConnectReferencesAction::class)->execute($collection)->all(); + $action = new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()); + + $referenceMap = $action->execute($collection)->all(); expect($referenceMap) ->toHaveCount(2) @@ -55,13 +62,15 @@ $circularB->reference->getKey() => $circularB, ]); - expect($circularA->references) - ->toHaveCount(1) - ->toBe([$circularB]); + expect($circularA->references)->toHaveCount(1); + expect($circularA->references->offsetExists($circularB))->toBeTrue(); + expect($circularA->referencedBy)->toHaveCount(1); + expect($circularA->referencedBy->offsetExists($circularB))->toBeTrue(); - expect($circularB->references) - ->toHaveCount(1) - ->toBe([$circularA]); + expect($circularB->references)->toHaveCount(1); + expect($circularB->references->offsetExists($circularA))->toBeTrue(); + expect($circularB->referencedBy)->toHaveCount(1); + expect($circularB->referencedBy->offsetExists($circularA))->toBeTrue(); expect($circularA->typeScriptNode->type->properties[0]->type) ->toBeInstanceOf(TypeReference::class) @@ -81,7 +90,12 @@ $transformedClass = transformSingle($class, new AllClassTransformer()), ]); - $referenceMap = app(ConnectReferencesAction::class)->execute($collection)->all(); + + $action = new ConnectReferencesAction( + new TypeScriptTransformerLog($console = new WrappedArrayConsole()) + ); + + $referenceMap = $action->execute($collection)->all(); expect($referenceMap) ->toHaveCount(1) @@ -89,12 +103,12 @@ $transformedClass->reference->getKey() => $transformedClass, ]); - expect($transformedClass->references) - ->toHaveCount(0); + expect($transformedClass->references)->toHaveCount(0); + expect($transformedClass->referencedBy)->toHaveCount(0); expect($transformedClass->typeScriptNode->type->properties[0]->type) ->toBeInstanceOf(TypeReference::class) ->referenced->toBeNull(); - expect(TypeScriptTransformerLog::resolve()->warningMessages)->not()->toBeEmpty(); + expect($console->messages)->not()->toBeEmpty(); }); diff --git a/tests/Actions/DiscoverTypesActionTest.php b/tests/Actions/DiscoverTypesActionTest.php index 582627c8..eddf04b8 100644 --- a/tests/Actions/DiscoverTypesActionTest.php +++ b/tests/Actions/DiscoverTypesActionTest.php @@ -1,6 +1,8 @@ toBe([ - StringBackedEnum::class, - HiddenAttributedClass::class, - TypeScriptAttributedClass::class, - SimpleInterface::class, - TypeScriptLocationAttributedClass::class, - OptionalAttributedClass::class, - ReadonlyClass::class, - SimpleClass::class, - UnitEnum::class, - IntBackedEnum::class, + expect($types)->toEqual([ + new PhpEnumNode(new ReflectionEnum(StringBackedEnum::class)), + new PhpClassNode(new ReflectionClass(HiddenAttributedClass::class)), + new PhpClassNode(new ReflectionClass(TypeScriptAttributedClass::class)), + new PhpClassNode(new ReflectionClass(SimpleInterface::class)), + new PhpClassNode(new ReflectionClass(TypeScriptLocationAttributedClass::class)), + new PhpClassNode(new ReflectionClass(OptionalAttributedClass::class)), + new PhpClassNode(new ReflectionClass(ReadonlyClass::class)), + new PhpClassNode(new ReflectionClass(SimpleClass::class)), + new PhpEnumNode(new ReflectionEnum(UnitEnum::class)), + new PhpEnumNode(new ReflectionEnum(IntBackedEnum::class)), ]); }); diff --git a/tests/Actions/ParseUserDefinedTypeActionTest.php b/tests/Actions/ParseUserDefinedTypeActionTest.php index c47eafe6..a59698c2 100644 --- a/tests/Actions/ParseUserDefinedTypeActionTest.php +++ b/tests/Actions/ParseUserDefinedTypeActionTest.php @@ -1,6 +1,7 @@ execute('string'))->toBeInstanceOf(TypeScriptString::class); expect($parser->execute('array'))->toEqual(new TypeScriptArray([new TypeScriptString()])); - expect($parser->execute('self', new ReflectionClass(DateTime::class)))->toEqual(new TypeReference(new ClassStringReference(DateTime::class))); + expect($parser->execute('self', PhpClassNode::fromReflection(new ReflectionClass(DateTime::class))))->toEqual(new TypeReference(new ClassStringReference(DateTime::class))); }); diff --git a/tests/Actions/ProvideTypesActionTest.php b/tests/Actions/ProvideTypesActionTest.php index 8637e27d..b50fb044 100644 --- a/tests/Actions/ProvideTypesActionTest.php +++ b/tests/Actions/ProvideTypesActionTest.php @@ -33,6 +33,9 @@ public function provide(TypeScriptTransformerConfig $config, TransformedCollecti $types = (new ProvideTypesAction($config))->execute(); expect($types)->toHaveCount(2); - expect($types[0]->getName())->toBe('Bar'); - expect($types[1]->getName())->toBe('Foo'); + + $typesArray = array_values($types->all()); + + expect($typesArray[0]->getName())->toBe('Bar'); + expect($typesArray[1]->getName())->toBe('Foo'); }); diff --git a/tests/Actions/TransformTypesActionTest.php b/tests/Actions/TransformTypesActionTest.php index ad12c2a8..e1e6a05f 100644 --- a/tests/Actions/TransformTypesActionTest.php +++ b/tests/Actions/TransformTypesActionTest.php @@ -1,6 +1,7 @@ execute( - $docTypeResolver->property(new ReflectionProperty(PhpDocTypesStub::class, $property))->type, - new ReflectionClass(PhpDocTypesStub::class) + $docTypeResolver->property(new PhpPropertyNode(new ReflectionProperty(PhpDocTypesStub::class, $property)))->type, + new PhpClassNode(new ReflectionClass(PhpDocTypesStub::class)) ); expect($typeScriptNode)->toBeInstanceOf($expectedTypeScriptNode::class); diff --git a/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php b/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php index 922844bf..1a93ca0f 100644 --- a/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php +++ b/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php @@ -2,7 +2,10 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Collection; -use Spatie\TypeScriptTransformer\Actions\TranspileReflectionTypeToTypeScriptNodeAction; +use Spatie\TypeScriptTransformer\Actions\TranspilePhpTypeNodeToTypeScriptNodeAction; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; +use Spatie\TypeScriptTransformer\PhpNodes\PhpMethodNode; +use Spatie\TypeScriptTransformer\PhpNodes\PhpPropertyNode; use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\Tests\Fakes\PropertyTypes\PhpTypesStub; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; @@ -23,11 +26,11 @@ string $property, TypeScriptNode $expectedTypeScriptNode, ) { - $transpiler = new TranspileReflectionTypeToTypeScriptNodeAction(); + $transpiler = new TranspilePhpTypeNodeToTypeScriptNodeAction(); $typeScriptNode = $transpiler->execute( - (new ReflectionProperty(PhpTypesStub::class, $property))->getType(), - new ReflectionClass(PhpTypesStub::class) + (new PhpPropertyNode(new ReflectionProperty(PhpTypesStub::class, $property)))->getType(), + new PhpClassNode(new ReflectionClass(PhpTypesStub::class)) ); expect($typeScriptNode)->toBeInstanceOf($expectedTypeScriptNode::class); @@ -141,11 +144,11 @@ }); it('can transpile a void return type', function () { - $transpiler = new TranspileReflectionTypeToTypeScriptNodeAction(); + $transpiler = new TranspilePhpTypeNodeToTypeScriptNodeAction(); $typeScriptNode = $transpiler->execute( - (new ReflectionMethod(PhpTypesStub::class, 'voidReturn'))->getReturnType(), - new ReflectionClass(PhpTypesStub::class) + (new PhpMethodNode(new ReflectionMethod(PhpTypesStub::class, 'voidReturn')))->getReturnType(), + new PhpClassNode(new ReflectionClass(PhpTypesStub::class)) ); expect($typeScriptNode)->toBeInstanceOf(TypeScriptVoid::class); diff --git a/tests/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessorTest.php b/tests/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessorTest.php index bca6289a..c6ff898c 100644 --- a/tests/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessorTest.php +++ b/tests/ClassPropertyProcessors/FixArrayLikeStructuresClassPropertyProcessorTest.php @@ -2,6 +2,7 @@ use Illuminate\Support\Collection; use Spatie\TypeScriptTransformer\ClassPropertyProcessors\FixArrayLikeStructuresClassPropertyProcessor; +use Spatie\TypeScriptTransformer\PhpNodes\PhpPropertyNode; use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptArray; @@ -51,7 +52,7 @@ )); $propertyNode = (new FixArrayLikeStructuresClassPropertyProcessor())->execute( - reflection: new ReflectionProperty($class, $property), + phpPropertyNode: new PhpPropertyNode(new ReflectionProperty($class, $property)), annotation: null, property: $propertyNode ); @@ -170,7 +171,7 @@ $propertyNode = (new FixArrayLikeStructuresClassPropertyProcessor( arrayLikeClassesToReplace: [Collection::class], ))->execute( - reflection: new ReflectionProperty($class, $property), + phpPropertyNode: new PhpPropertyNode(new ReflectionProperty($class, $property)), annotation: null, property: $propertyNode ); diff --git a/tests/Factories/TransformedFactory.php b/tests/Factories/TransformedFactory.php index ad8519f2..1583b7b5 100644 --- a/tests/Factories/TransformedFactory.php +++ b/tests/Factories/TransformedFactory.php @@ -18,6 +18,7 @@ public function __construct( public ?array $location = null, public ?bool $export = null, public ?array $references = null, + public ?array $referencedBy = null, ) { } @@ -28,6 +29,7 @@ public static function alias( ?array $location = null, bool $export = true, ?array $references = null, + ?array $referencedBy = null, ): TransformedFactory { $reference = $reference ?? new CustomReference( 'factory_alias', @@ -39,7 +41,8 @@ public static function alias( reference: $reference, location: $location, export: $export, - references: $references + references: $references, + referencedBy: $referencedBy ); } @@ -48,14 +51,22 @@ public function build(): Transformed $reference = $this->reference ?? new CustomReference('factory', Str::random(6)); $location = $this->location ?? []; $export = $this->export ?? true; - $references = $this->references ?? []; - return new Transformed( + $transformed = new Transformed( typeScriptNode: $this->typeScriptNode, reference: $reference, location: $location, export: $export, - references: $references, ); + + foreach ($this->references ?? [] as $reference) { + $transformed->references[$reference] = null; + } + + foreach ($this->referencedBy ?? [] as $reference) { + $transformed->referencedBy[$reference] = null; + } + + return $transformed; } } diff --git a/tests/Integration.php b/tests/Integration.php index b6eed0e0..140f697a 100644 --- a/tests/Integration.php +++ b/tests/Integration.php @@ -23,7 +23,7 @@ ->replaceType(DateTime::class, 'string') ->writer(new FlatWriter($this->temporaryDirectory->path('flat.d.ts'))); - TypeScriptTransformer::create($config)->execute(watch: false); + TypeScriptTransformer::create($config)->execute(); assertMatchesFileSnapshot($this->temporaryDirectory->path('flat.d.ts')); }); @@ -36,7 +36,7 @@ ->replaceType(DateTime::class, 'string') ->writer(new NamespaceWriter($this->temporaryDirectory->path('flat.d.ts'))); - TypeScriptTransformer::create($config)->execute(watch: false); + TypeScriptTransformer::create($config)->execute(); assertMatchesFileSnapshot($this->temporaryDirectory->path('flat.d.ts')); }); @@ -49,7 +49,7 @@ ->replaceType(DateTime::class, 'string') ->writer(new ModuleWriter($this->temporaryDirectory->path())); - TypeScriptTransformer::create($config)->execute(watch: false); + TypeScriptTransformer::create($config)->execute(); assertMatchesFileSnapshot($this->temporaryDirectory->path('Spatie/TypeScriptTransformer/Tests/Fakes/Integration/index.ts')); assertMatchesFileSnapshot($this->temporaryDirectory->path('Spatie/TypeScriptTransformer/Tests/Fakes/Integration/Level/index.ts')); diff --git a/tests/Laravel/LaravelRouteActionTypesProviderTest.php b/tests/Laravel/LaravelRouteActionTypesProviderTest.php index 55d0c601..229a6d8f 100644 --- a/tests/Laravel/LaravelRouteActionTypesProviderTest.php +++ b/tests/Laravel/LaravelRouteActionTypesProviderTest.php @@ -1,5 +1,7 @@ add(transformSingle($class, $transformer)); } - $referenceMap = (new ConnectReferencesAction())->execute($collection); + $referenceMap = (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($collection); $writer = new MemoryWriter(); @@ -38,7 +40,7 @@ function transformSingle( [$transformed] = $transformTypesAction->execute( [$transformer], - [is_string($class) ? $class : $class::class], + [PhpClassNode::fromClassString(is_string($class) ? $class : $class::class)], ); return $transformed ?? Untransformable::create(); diff --git a/tests/Support/AllClassTransformer.php b/tests/Support/AllClassTransformer.php index 3ef63393..3bbb1e87 100644 --- a/tests/Support/AllClassTransformer.php +++ b/tests/Support/AllClassTransformer.php @@ -2,12 +2,12 @@ namespace Spatie\TypeScriptTransformer\Tests\Support; -use ReflectionClass; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; use Spatie\TypeScriptTransformer\Transformers\ClassTransformer; class AllClassTransformer extends ClassTransformer { - protected function shouldTransform(ReflectionClass $reflection): bool + protected function shouldTransform(PhpClassNode $phpClassNode): bool { return true; } diff --git a/tests/Support/AllInterfaceTransformer.php b/tests/Support/AllInterfaceTransformer.php index 99fccf9c..faaa08d8 100644 --- a/tests/Support/AllInterfaceTransformer.php +++ b/tests/Support/AllInterfaceTransformer.php @@ -2,12 +2,12 @@ namespace Spatie\TypeScriptTransformer\Tests\Support; -use ReflectionClass; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; use Spatie\TypeScriptTransformer\Transformers\InterfaceTransformer; class AllInterfaceTransformer extends InterfaceTransformer { - protected function shouldTransform(ReflectionClass $reflection): bool + protected function shouldTransform(PhpClassNode $phpClassNode): bool { return true; } diff --git a/tests/Support/TransformationContextTest.php b/tests/Support/TransformationContextTest.php index 61d756b5..21697468 100644 --- a/tests/Support/TransformationContextTest.php +++ b/tests/Support/TransformationContextTest.php @@ -1,5 +1,6 @@ name)->toBe('SimpleClass'); expect($context->nameSpaceSegments)->toBe(['Spatie', 'TypeScriptTransformer', 'Tests', 'Fakes', 'TypesToProvide']); @@ -17,25 +18,25 @@ }); it('can make a class optional by attribute in its context', function () { - $reflection = new ReflectionClass(OptionalAttributedClass::class); + $reflection = PhpClassNode::fromClassString(OptionalAttributedClass::class); - $context = TransformationContext::createFromReflection($reflection); + $context = TransformationContext::createFromPhpClass($reflection); expect($context->optional)->toBeTrue(); }); it('can set the name by attribute', function () { - $reflection = new ReflectionClass(TypeScriptAttributedClass::class); + $reflection = PhpClassNode::fromClassString(TypeScriptAttributedClass::class); - $context = TransformationContext::createFromReflection($reflection); + $context = TransformationContext::createFromPhpClass($reflection); expect($context->name)->toBe('JustAnotherName'); }); it('can set the location by attribute', function () { - $reflection = new ReflectionClass(TypeScriptLocationAttributedClass::class); + $reflection = PhpClassNode::fromClassString(TypeScriptLocationAttributedClass::class); - $context = TransformationContext::createFromReflection($reflection); + $context = TransformationContext::createFromPhpClass($reflection); expect($context->nameSpaceSegments)->toBe(['App', 'Here']); }); diff --git a/tests/Transformers/ClassTransformerTest.php b/tests/Transformers/ClassTransformerTest.php index 8376d622..bb52d41c 100644 --- a/tests/Transformers/ClassTransformerTest.php +++ b/tests/Transformers/ClassTransformerTest.php @@ -3,13 +3,13 @@ namespace Spatie\TypeScriptTransformer\Tests\Transformers; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -use ReflectionClass; -use ReflectionProperty; use Spatie\TypeScriptTransformer\Attributes\Hidden; use Spatie\TypeScriptTransformer\Attributes\LiteralTypeScriptType; use Spatie\TypeScriptTransformer\Attributes\Optional; use Spatie\TypeScriptTransformer\Attributes\TypeScriptType; -use Spatie\TypeScriptTransformer\References\ReflectionClassReference; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; +use Spatie\TypeScriptTransformer\PhpNodes\PhpPropertyNode; +use Spatie\TypeScriptTransformer\References\PhpClassReference; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\ReadonlyClass; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\SimpleClass; use Spatie\TypeScriptTransformer\Tests\Support\AllClassTransformer; @@ -44,11 +44,11 @@ ) ); expect($transformed->reference)->toEqual( - new ReflectionClassReference(new ReflectionClass(SimpleClass::class)) + new PhpClassReference(PhpClassNode::fromClassString(SimpleClass::class)) ); expect($transformed->location)->toEqual(['Spatie', 'TypeScriptTransformer', 'Tests', 'Fakes', 'TypesToProvide']); expect($transformed->export)->toBeTrue(); - expect($transformed->references)->toEqual([]); + expect($transformed->references)->toHaveCount(0); }); it('can transform a class by depending on a TypeScriptTypeAttributeContract attribute type', function () { @@ -286,7 +286,7 @@ protected function classPropertyProcessors(): array { return [ new class () implements ClassPropertyProcessor { - public function execute(ReflectionProperty $reflection, ?TypeNode $annotation, TypeScriptProperty $property): ?TypeScriptProperty + public function execute(PhpPropertyNode $phpPropertyNode, ?TypeNode $annotation, TypeScriptProperty $property): ?TypeScriptProperty { $property->name = new TypeScriptIdentifier('newName'); $property->type = new TypeScriptNumber(); @@ -318,7 +318,7 @@ public function execute(ReflectionProperty $reflection, ?TypeNode $annotation, T }; $object = transformSingle($class, transformer: new class () extends ClassTransformer { - protected function shouldTransform(ReflectionClass $reflection): bool + protected function shouldTransform(PhpClassNode $phpClassNode): bool { return true; } @@ -327,7 +327,7 @@ protected function classPropertyProcessors(): array { return [ new class () implements ClassPropertyProcessor { - public function execute(ReflectionProperty $reflection, ?TypeNode $annotation, TypeScriptProperty $property): ?TypeScriptProperty + public function execute(PhpPropertyNode $phpPropertyNode, ?TypeNode $annotation, TypeScriptProperty $property): ?TypeScriptProperty { return null; } diff --git a/tests/TypeProviders/TransformerTypesProviderTest.php b/tests/TypeProviders/TransformerTypesProviderTest.php index 44951615..e1a24d96 100644 --- a/tests/TypeProviders/TransformerTypesProviderTest.php +++ b/tests/TypeProviders/TransformerTypesProviderTest.php @@ -1,7 +1,7 @@ reference->toBeInstanceOf(ReflectionClassReference::class) + ->reference->toBeInstanceOf(PhpClassReference::class) ->reference->classString->toBe(TypeScriptAttributedClass::class) ->location->toBe(['Spatie', 'TypeScriptTransformer', 'Tests', 'Fakes', 'TypesToProvide']), fn (Expectation $transformed) => $transformed @@ -66,7 +66,7 @@ function getTestProvidedTypes( new TypeScriptProperty('property', new TypeScriptString()), ]) )) - ->reference->toBeInstanceOf(ReflectionClassReference::class) + ->reference->toBeInstanceOf(PhpClassReference::class) ->reference->classString->toBe(TypeScriptLocationAttributedClass::class) ->location->toBe(['App', 'Here']), fn (Expectation $transformed) => $transformed @@ -78,7 +78,7 @@ function getTestProvidedTypes( new TypeScriptProperty('property', new TypeScriptString(), isOptional: true), ]) )) - ->reference->toBeInstanceOf(ReflectionClassReference::class) + ->reference->toBeInstanceOf(PhpClassReference::class) ->reference->classString->toBe(OptionalAttributedClass::class) ->location->toBe(['Spatie', 'TypeScriptTransformer', 'Tests', 'Fakes', 'TypesToProvide']), fn (Expectation $transformed) => $transformed @@ -90,7 +90,7 @@ function getTestProvidedTypes( new TypeScriptProperty('property', new TypeScriptString(), isReadonly: true), ]) )) - ->reference->toBeInstanceOf(ReflectionClassReference::class) + ->reference->toBeInstanceOf(PhpClassReference::class) ->reference->classString->toBe(ReadonlyClass::class) ->location->toBe(['Spatie', 'TypeScriptTransformer', 'Tests', 'Fakes', 'TypesToProvide']), fn (Expectation $transformed) => $transformed @@ -103,7 +103,7 @@ function getTestProvidedTypes( new TypeScriptProperty('constructorPromotedStringProperty', new TypeScriptString()), ]) )) - ->reference->toBeInstanceOf(ReflectionClassReference::class) + ->reference->toBeInstanceOf(PhpClassReference::class) ->reference->classString->toBe(SimpleClass::class) ->location->toBe(['Spatie', 'TypeScriptTransformer', 'Tests', 'Fakes', 'TypesToProvide']), ); diff --git a/tests/Writers/FlatWriterTest.php b/tests/Writers/FlatWriterTest.php index 087e02ec..db746514 100644 --- a/tests/Writers/FlatWriterTest.php +++ b/tests/Writers/FlatWriterTest.php @@ -4,6 +4,7 @@ use Spatie\TypeScriptTransformer\Collections\ReferenceMap; use Spatie\TypeScriptTransformer\References\CustomReference; use Spatie\TypeScriptTransformer\Support\TransformedCollection; +use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Support\WriteableFile; use Spatie\TypeScriptTransformer\Tests\Factories\TransformedFactory; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; @@ -61,7 +62,7 @@ [$file] = $this->writer->output( $transformedCollection, - (new ConnectReferencesAction())->execute($transformedCollection), + (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection), ); expect($file) diff --git a/tests/Writers/ModuleWriterTest.php b/tests/Writers/ModuleWriterTest.php index b18a40aa..18301ce1 100644 --- a/tests/Writers/ModuleWriterTest.php +++ b/tests/Writers/ModuleWriterTest.php @@ -4,6 +4,7 @@ use Spatie\TypeScriptTransformer\Collections\ReferenceMap; use Spatie\TypeScriptTransformer\References\CustomReference; use Spatie\TypeScriptTransformer\Support\TransformedCollection; +use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Support\WriteableFile; use Spatie\TypeScriptTransformer\Tests\Factories\TransformedFactory; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; @@ -75,7 +76,7 @@ $files = $this->writer->output( $transformedCollection, - (new ConnectReferencesAction())->execute($transformedCollection), + (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection), ); expect($files) @@ -102,7 +103,7 @@ $files = $this->writer->output( $transformedCollection, - (new ConnectReferencesAction())->execute($transformedCollection), + (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection), ); expect($files) @@ -148,7 +149,7 @@ $files = $this->writer->output( $transformedCollection, - (new ConnectReferencesAction())->execute($transformedCollection), + (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection), ); expect($files) @@ -184,7 +185,7 @@ $files = $this->writer->output( $transformedCollection, - (new ConnectReferencesAction())->execute($transformedCollection), + (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection), ); expect($files) @@ -215,7 +216,7 @@ $files = $this->writer->output( $transformedCollection, - (new ConnectReferencesAction())->execute($transformedCollection), + (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection), ); expect($files) diff --git a/tests/Writers/NamespaceWriterTest.php b/tests/Writers/NamespaceWriterTest.php index 7dd92dc8..ea2c1235 100644 --- a/tests/Writers/NamespaceWriterTest.php +++ b/tests/Writers/NamespaceWriterTest.php @@ -4,6 +4,7 @@ use Spatie\TypeScriptTransformer\Collections\ReferenceMap; use Spatie\TypeScriptTransformer\References\CustomReference; use Spatie\TypeScriptTransformer\Support\TransformedCollection; +use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Support\WriteableFile; use Spatie\TypeScriptTransformer\Tests\Factories\TransformedFactory; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; @@ -69,7 +70,7 @@ $files = (new NamespaceWriter($filename))->output( $transformedCollection, - (new ConnectReferencesAction())->execute($transformedCollection), + (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection), ); expect($files) From d363866acf461d6b50092bbdb34ee28b600229b4 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 27 Sep 2024 09:27:17 +0200 Subject: [PATCH 45/51] wip --- README.md | 3 +- src/Actions/ConnectReferencesAction.php | 51 +++--- .../ExecuteConnectedClosuresAction.php | 13 +- src/Actions/ExecuteProvidedClosuresAction.php | 13 +- src/Actions/ProvideTypesAction.php | 2 +- src/Actions/ResolveModuleImportsAction.php | 6 +- .../SplitTransformedPerLocationAction.php | 2 +- src/Collections/ReferenceMap.php | 60 ------- src/Collections/TransformedCollection.php | 118 ++++++++++++++ src/FileSystemWatcher.php | 40 ++--- .../Watch/FileCreatedWatchEventHandler.php | 49 ------ .../Watch/FileDeletedWatchEventHandler.php | 14 +- .../FileUpdatedOrCreatedWatchEventHandler.php | 74 +++++++++ .../Watch/FileUpdatedWatchEventHandler.php | 79 --------- src/Laravel/LaravelDataTypesProvider.php | 2 +- .../LaravelNamedRouteTypesProvider.php | 2 +- .../LaravelRouteActionTypesProvider.php | 2 +- src/Laravel/LaravelTypesProvider.php | 2 +- src/Laravel/SpatieLaravelTypesProvider.php | 2 +- src/Support/TransformedCollection.php | 153 ------------------ src/Transformed/Transformed.php | 34 ++-- src/Transformers/EnumTransformer.php | 4 + .../TransformerTypesProvider.php | 2 +- src/TypeProviders/TypesProvider.php | 2 +- src/TypeScriptTransformer.php | 11 +- src/Writers/FlatWriter.php | 8 +- src/Writers/ModuleWriter.php | 15 +- src/Writers/NamespaceWriter.php | 8 +- src/Writers/Writer.php | 4 +- tests/Actions/ConnectReferencesActionTest.php | 40 ++--- tests/Actions/DiscoverTypesActionTest.php | 2 + tests/Actions/ProvideTypesActionTest.php | 2 +- .../ResolveModuleImportsActionTest.php | 39 +++-- .../SplitTransformedPerLocationActionTest.php | 2 +- tests/Factories/TransformedFactory.php | 8 +- tests/Fakes/TypesToProvide/EmptyEnum.php | 7 + tests/Pest.php | 2 +- tests/Support/InlineTypesProvider.php | 2 +- tests/Support/MemoryWriter.php | 10 +- tests/Transformers/EnumTransformerTest.php | 14 ++ .../TransformerTypesProviderTest.php | 3 +- tests/Writers/FlatWriterTest.php | 7 +- tests/Writers/ModuleWriterTest.php | 24 +-- tests/Writers/NamespaceWriterTest.php | 7 +- 44 files changed, 393 insertions(+), 551 deletions(-) delete mode 100644 src/Collections/ReferenceMap.php create mode 100644 src/Collections/TransformedCollection.php delete mode 100644 src/Handlers/Watch/FileCreatedWatchEventHandler.php create mode 100644 src/Handlers/Watch/FileUpdatedOrCreatedWatchEventHandler.php delete mode 100644 src/Handlers/Watch/FileUpdatedWatchEventHandler.php delete mode 100644 src/Support/TransformedCollection.php create mode 100644 tests/Fakes/TypesToProvide/EmptyEnum.php diff --git a/README.md b/README.md index 78a24701..2a497d8b 100644 --- a/README.md +++ b/README.md @@ -817,8 +817,7 @@ A `TypesProvider` implements the `TypeProvider` interface: ```php namespace Spatie\TypeScriptTransformer\TypeProviders; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; -use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection;use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; interface TypesProvider { diff --git a/src/Actions/ConnectReferencesAction.php b/src/Actions/ConnectReferencesAction.php index f4c5948b..253a34d1 100644 --- a/src/Actions/ConnectReferencesAction.php +++ b/src/Actions/ConnectReferencesAction.php @@ -2,8 +2,7 @@ namespace Spatie\TypeScriptTransformer\Actions; -use Spatie\TypeScriptTransformer\Collections\ReferenceMap; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; @@ -20,58 +19,50 @@ public function __construct( } /** - * @param TransformedCollection|array $collection + * @param TransformedCollection $collection */ - public function execute(TransformedCollection|array $collection): ReferenceMap + public function execute(TransformedCollection $collection): void { - $referenceMap = new ReferenceMap(); - - foreach ($collection as $transformed) { - $referenceMap->add($transformed); - } - - foreach ($collection as $transformed) { + foreach ($collection->onlyChanged() as $transformed) { $metadata = [ 'transformed' => $transformed, - 'referenceMap' => $referenceMap, + 'collection' => $collection, ]; $this->visitor->execute($transformed->typeScriptNode, $metadata); } - - return $referenceMap; } protected function resolveVisitor(): Visitor { return Visitor::create()->before(function (TypeReference $typeReference, array &$metadata) { - /** @var Transformed $transformed */ - $transformed = $metadata['transformed']; + /** @var Transformed $currentTransformed */ + $currentTransformed = $metadata['transformed']; + + /** @var TransformedCollection $collection */ + $collection = $metadata['collection']; - /** @var ReferenceMap $referenceMap */ - $referenceMap = $metadata['referenceMap']; + $foundTransformed = $collection->get($typeReference->reference); - if (! $referenceMap->has($typeReference->reference)) { - $transformed->addMissingReference($typeReference->reference, $typeReference); + if ($foundTransformed === null) { + $currentTransformed->addMissingReference($typeReference->reference, $typeReference); - $this->log->warning("Tried replacing reference to `{$typeReference->reference->humanFriendlyName()}` in `{$transformed->reference->humanFriendlyName()}` but it was not found in the transformed types"); + $this->log->warning("Tried replacing reference to `{$typeReference->reference->humanFriendlyName()}` in `{$currentTransformed->reference->humanFriendlyName()}` but it was not found in the transformed types"); return; } - $transformedReference = $referenceMap->get($typeReference->reference); - - if(! $transformed->references->offsetExists($transformedReference)) { - $transformed->references[$transformedReference] = []; + if (! array_key_exists($foundTransformed->reference->getKey(), $currentTransformed->references)) { + $currentTransformed->references[$foundTransformed->reference->getKey()] = []; } - $transformed->references[$transformedReference][] = $typeReference; - $transformedReference->referencedBy[$transformed] = $transformed->reference->getKey(); + $currentTransformed->references[$foundTransformed->reference->getKey()][] = $typeReference; + $foundTransformed->referencedBy[] = $currentTransformed->reference->getKey(); - $typeReference->connect($transformedReference); + $typeReference->connect($foundTransformed); - if (array_key_exists($typeReference->reference->getKey(), $transformed->missingReferences)) { - unset($transformed->missingReferences[$typeReference->reference->getKey()]); + if (array_key_exists($foundTransformed->reference->getKey(), $currentTransformed->missingReferences)) { + unset($currentTransformed->missingReferences[$foundTransformed->reference->getKey()]); } }, [TypeReference::class]); } diff --git a/src/Actions/ExecuteConnectedClosuresAction.php b/src/Actions/ExecuteConnectedClosuresAction.php index bf3c0cbd..b219957f 100644 --- a/src/Actions/ExecuteConnectedClosuresAction.php +++ b/src/Actions/ExecuteConnectedClosuresAction.php @@ -2,10 +2,11 @@ namespace Spatie\TypeScriptTransformer\Actions; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; -use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; +use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; use Spatie\TypeScriptTransformer\Visitor\Visitor; +use Traversable; class ExecuteConnectedClosuresAction { @@ -18,19 +19,17 @@ public function __construct( } /** - * @param TransformedCollection|array $nodes + * @param TransformedCollection|Traversable $nodes */ public function execute( - TransformedCollection|array $nodes, + TransformedCollection|Traversable $nodes, ): void { if (empty($this->config->providedVisitorClosures)) { return; } - $isTransformedCollection = $nodes instanceof TransformedCollection; - foreach ($nodes as $node) { - $this->visitor->execute($isTransformedCollection ? $node->typeScriptNode : $node); + $this->visitor->execute($node->typeScriptNode); } } } diff --git a/src/Actions/ExecuteProvidedClosuresAction.php b/src/Actions/ExecuteProvidedClosuresAction.php index 8f37aea8..a6662fcf 100644 --- a/src/Actions/ExecuteProvidedClosuresAction.php +++ b/src/Actions/ExecuteProvidedClosuresAction.php @@ -2,10 +2,11 @@ namespace Spatie\TypeScriptTransformer\Actions; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; -use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; +use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; use Spatie\TypeScriptTransformer\Visitor\Visitor; +use Traversable; class ExecuteProvidedClosuresAction { @@ -18,19 +19,17 @@ public function __construct( } /** - * @param TransformedCollection|array $nodes + * @param TransformedCollection|Traversable $nodes */ public function execute( - TransformedCollection|array $nodes, + TransformedCollection|Traversable $nodes, ): void { if (empty($this->config->providedVisitorClosures)) { return; } - $isTransformedCollection = $nodes instanceof TransformedCollection; - foreach ($nodes as $node) { - $this->visitor->execute($isTransformedCollection ? $node->typeScriptNode : $node); + $this->visitor->execute($node->typeScriptNode); } } } diff --git a/src/Actions/ProvideTypesAction.php b/src/Actions/ProvideTypesAction.php index 05ec092e..8aa24621 100644 --- a/src/Actions/ProvideTypesAction.php +++ b/src/Actions/ProvideTypesAction.php @@ -2,7 +2,7 @@ namespace Spatie\TypeScriptTransformer\Actions; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; diff --git a/src/Actions/ResolveModuleImportsAction.php b/src/Actions/ResolveModuleImportsAction.php index ccf9b9c2..c7e8e883 100644 --- a/src/Actions/ResolveModuleImportsAction.php +++ b/src/Actions/ResolveModuleImportsAction.php @@ -4,6 +4,7 @@ use Closure; use Spatie\TypeScriptTransformer\Collections\ImportsCollection; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\Support\ImportName; use Spatie\TypeScriptTransformer\Support\Location; use Spatie\TypeScriptTransformer\Transformed\Transformed; @@ -21,6 +22,7 @@ public function __construct( public function execute( Location $location, + TransformedCollection $transformedCollection, ): ImportsCollection { $collection = new ImportsCollection(); @@ -29,7 +31,9 @@ public function execute( ); foreach ($location->transformed as $transformedItem) { - foreach ($transformedItem->references as $referencedTransformed => $typeReferences) { + foreach ($transformedItem->references as $referencedTransformedKey => $typeReferences) { + $referencedTransformed = $transformedCollection->get($referencedTransformedKey); + if ($referencedTransformed->location === $location->segments) { continue; } diff --git a/src/Actions/SplitTransformedPerLocationAction.php b/src/Actions/SplitTransformedPerLocationAction.php index 158c39c6..26809415 100644 --- a/src/Actions/SplitTransformedPerLocationAction.php +++ b/src/Actions/SplitTransformedPerLocationAction.php @@ -2,8 +2,8 @@ namespace Spatie\TypeScriptTransformer\Actions; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\Support\Location; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Transformed\Transformed; class SplitTransformedPerLocationAction diff --git a/src/Collections/ReferenceMap.php b/src/Collections/ReferenceMap.php deleted file mode 100644 index ecd9b0c3..00000000 --- a/src/Collections/ReferenceMap.php +++ /dev/null @@ -1,60 +0,0 @@ - */ - protected array $references = []; - - /** - * @param array $references - */ - public function __construct(array $references = []) - { - foreach ($references as $reference) { - $this->add($reference); - } - } - - public function add( - Transformed $transformed - ): void { - if ($transformed->reference === null) { - throw new Exception('Can only add transformed items with a reference'); - } - - $this->references[$transformed->reference->getKey()] = $transformed; - } - - public function has(Reference $reference): bool - { - return array_key_exists($reference->getKey(), $this->references); - } - - public function get( - Reference $reference - ): Transformed { - return $this->references[$reference->getKey()]; - } - - public function getByReferenceKey(string $key): ?Transformed - { - return $this->references[$key] ?? null; - } - - public function all(): array - { - return $this->references; - } - - public function remove( - Transformed $transformed - ): void { - unset($this->references[$transformed->reference->getKey()]); - } -} diff --git a/src/Collections/TransformedCollection.php b/src/Collections/TransformedCollection.php new file mode 100644 index 00000000..7590d6fa --- /dev/null +++ b/src/Collections/TransformedCollection.php @@ -0,0 +1,118 @@ + + */ +class TransformedCollection implements IteratorAggregate +{ + /** @var array */ + protected array $items = []; + + /** @var array */ + protected array $fileMapping = []; + + public function __construct( + array $items = [], + ) { + $this->add(...$items); + } + + public function add(Transformed ...$transformed): self + { + foreach ($transformed as $item) { + $this->items[$item->reference->getKey()] = $item; + + if ($item->reference instanceof FilesystemReference) { + $this->fileMapping[$this->cleanupFilePath($item->reference->getFilesystemOriginPath())] = $item; + } + } + + return $this; + } + + public function has(Reference|string $reference): bool + { + return array_key_exists(is_string($reference) ? $reference : $reference->getKey(), $this->items); + } + + public function get(Reference|string $reference): ?Transformed + { + return $this->items[is_string($reference) ? $reference : $reference->getKey()] ?? null; + } + + public function remove(Reference|string $reference): void + { + $transformed = $this->get($reference); + + if ($transformed === null) { + return; + } + + foreach (array_unique($transformed->referencedBy) as $referencedBy) { + $referencedBy = $this->get($referencedBy); + + $referencedBy->markReferenceMissing($transformed); + $referencedBy->markAsChanged(); + } + + unset($this->items[$transformed->reference->getKey()]); + + if($transformed->reference instanceof FilesystemReference) { + $path = $this->cleanupFilePath($transformed->reference->getFilesystemOriginPath()); + + unset($this->fileMapping[$path]); + } + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->items); + } + + public function all(): array + { + return $this->items; + } + + public function onlyChanged(): Generator + { + foreach ($this->items as $item) { + if ($item->changed) { + yield $item; + } + } + } + + public function findTransformedByPath(string $path): ?Transformed + { + $path = $this->cleanupFilePath($path); + + return $this->fileMapping[$path] ?? null; + } + + public function hasChanges(): bool + { + foreach ($this->items as $item) { + if ($item->changed) { + return true; + } + } + + return false; + } + + protected function cleanupFilePath(string $path): string + { + return realpath($path); + } +} diff --git a/src/FileSystemWatcher.php b/src/FileSystemWatcher.php index 89e005c0..b84bad73 100644 --- a/src/FileSystemWatcher.php +++ b/src/FileSystemWatcher.php @@ -3,17 +3,15 @@ namespace Spatie\TypeScriptTransformer; use Exception; -use Spatie\TypeScriptTransformer\Collections\ReferenceMap; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\Events\Watch\DirectoryDeletedWatchEvent; use Spatie\TypeScriptTransformer\Events\Watch\FileCreatedWatchEvent; use Spatie\TypeScriptTransformer\Events\Watch\FileDeletedWatchEvent; use Spatie\TypeScriptTransformer\Events\Watch\FileUpdatedWatchEvent; use Spatie\TypeScriptTransformer\Events\Watch\WatchEvent; -use Spatie\TypeScriptTransformer\Handlers\Watch\FileCreatedWatchEventHandler; use Spatie\TypeScriptTransformer\Handlers\Watch\FileDeletedWatchEventHandler; -use Spatie\TypeScriptTransformer\Handlers\Watch\FileUpdatedWatchEventHandler; +use Spatie\TypeScriptTransformer\Handlers\Watch\FileUpdatedOrCreatedWatchEventHandler; use Spatie\TypeScriptTransformer\Handlers\Watch\WatchEventHandler; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\Watcher\Exceptions\CouldNotStartWatcher; use Spatie\Watcher\Watch; @@ -29,7 +27,6 @@ class FileSystemWatcher public function __construct( protected TypeScriptTransformer $typeScriptTransformer, protected TransformedCollection $transformedCollection, - protected ReferenceMap $referenceMap, ) { $this->initializeHandlers(); } @@ -87,22 +84,19 @@ protected function initializeHandlers(): void { // TODO: handle directory deleted - $this->handlers[FileCreatedWatchEvent::class] = new FileCreatedWatchEventHandler( + $this->handlers[FileCreatedWatchEvent::class] = new FileUpdatedOrCreatedWatchEventHandler( $this->typeScriptTransformer, $this->transformedCollection, - $this->referenceMap ); - $this->handlers[FileUpdatedWatchEvent::class] = new FileUpdatedWatchEventHandler( + $this->handlers[FileUpdatedWatchEvent::class] = new FileUpdatedOrCreatedWatchEventHandler( $this->typeScriptTransformer, $this->transformedCollection, - $this->referenceMap ); $this->handlers[FileDeletedWatchEvent::class] = new FileDeletedWatchEventHandler( $this->typeScriptTransformer, $this->transformedCollection, - $this->referenceMap ); } @@ -116,11 +110,22 @@ protected function processBuffer(): void $this->handlers[$event::class]->handle($event); } + $this->typeScriptTransformer->executeProvidedClosuresAction->execute( + $this->transformedCollection->onlyChanged() + ); + + $this->typeScriptTransformer->connectReferencesAction->execute( + $this->transformedCollection + ); + $this->tryToConnectMissingReferencesWithNewTransformed(); + $this->typeScriptTransformer->executeConnectedClosuresAction->execute( + $this->transformedCollection->onlyChanged() + ); + $this->typeScriptTransformer->outputTransformed( $this->transformedCollection, - $this->referenceMap ); $this->typeScriptTransformer->log->info('Processed events'); @@ -128,18 +133,15 @@ protected function processBuffer(): void protected function tryToConnectMissingReferencesWithNewTransformed(): void { - foreach ($this->transformedCollection as $transformed) { - foreach ($transformed->missingReferences as $missingReference => $typeReferences) { - $referenced = $this->referenceMap->getByReferenceKey($missingReference); + foreach ($this->transformedCollection as $currentTransformed) { + foreach ($currentTransformed->missingReferences as $missingReference => $typeReferences) { + $foundTransformed = $this->transformedCollection->get($missingReference); - if ($referenced === null) { + if ($foundTransformed === null) { continue; } - $referenced->markMissingReferenceFound($transformed); - $referenced->markAsChanged(); - - $transformed->referencedBy[$referenced] = $referenced->reference->getKey(); + $currentTransformed->markMissingReferenceFound($foundTransformed); break; } diff --git a/src/Handlers/Watch/FileCreatedWatchEventHandler.php b/src/Handlers/Watch/FileCreatedWatchEventHandler.php deleted file mode 100644 index 3c531dfe..00000000 --- a/src/Handlers/Watch/FileCreatedWatchEventHandler.php +++ /dev/null @@ -1,49 +0,0 @@ -typeScriptTransformer->loadPhpClassNodeAction->execute($event->path); - - if($classNode === null) { - $this->typeScriptTransformer->log->warning("Multiple class nodes found in {$event->path}"); - - return; - } - - $transformed = $this->typeScriptTransformer->transformTypesAction->transformClassNode( - $this->typeScriptTransformer->config->transformers, - $classNode - ); - - if ($transformed === null) { - $this->typeScriptTransformer->log->warning("Could not transform {$event->path}"); - - return; - } - - $this->transformedCollection->add($transformed); - - $this->typeScriptTransformer->executeProvidedClosuresAction->execute([$transformed]); - $this->typeScriptTransformer->connectReferencesAction->execute([$transformed]); - $this->typeScriptTransformer->executeConnectedClosuresAction->execute([$transformed]); - } -} diff --git a/src/Handlers/Watch/FileDeletedWatchEventHandler.php b/src/Handlers/Watch/FileDeletedWatchEventHandler.php index 8920c761..b3a3c56a 100644 --- a/src/Handlers/Watch/FileDeletedWatchEventHandler.php +++ b/src/Handlers/Watch/FileDeletedWatchEventHandler.php @@ -2,10 +2,8 @@ namespace Spatie\TypeScriptTransformer\Handlers\Watch; -use Spatie\TypeScriptTransformer\Collections\ReferenceMap; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\Events\Watch\FileDeletedWatchEvent; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; -use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeScriptTransformer; class FileDeletedWatchEventHandler implements WatchEventHandler @@ -13,7 +11,6 @@ class FileDeletedWatchEventHandler implements WatchEventHandler public function __construct( protected TypeScriptTransformer $typeScriptTransformer, protected TransformedCollection $transformedCollection, - protected ReferenceMap $referenceMap ) { } @@ -28,13 +25,6 @@ public function handle($event): void return; } - foreach ($transformed->referencedBy as $referencedBy => $key) { - /** @var Transformed $referencedBy */ - $referencedBy->markReferenceRemoved($transformed); - $referencedBy->markAsChanged(); - } - - $this->referenceMap->remove($transformed); - $this->transformedCollection->removeTransformedByPath($event->path); + $this->transformedCollection->remove($transformed->reference); } } diff --git a/src/Handlers/Watch/FileUpdatedOrCreatedWatchEventHandler.php b/src/Handlers/Watch/FileUpdatedOrCreatedWatchEventHandler.php new file mode 100644 index 00000000..eb12682a --- /dev/null +++ b/src/Handlers/Watch/FileUpdatedOrCreatedWatchEventHandler.php @@ -0,0 +1,74 @@ +typeScriptTransformer->loadPhpClassNodeAction->execute($event->path); + + if ($classNode === null) { + $this->typeScriptTransformer->log->warning("Multiple class nodes found in {$event->path}"); + + return; + } + + $newlyTransformed = $this->typeScriptTransformer->transformTypesAction->transformClassNode( + $this->typeScriptTransformer->config->transformers, + $classNode + ); + } catch (Throwable $throwable) { + if (str_starts_with($throwable::class, 'Roave\BetterReflection')) { + return; + } + + throw $throwable; + } + + $originalTransformed = $this->transformedCollection->findTransformedByPath( + $event->path + ); + + if ($originalTransformed && $newlyTransformed === null) { + $this->transformedCollection->remove($originalTransformed->reference); + // TODO: when removing a ts transformed structure (e.g. remove the TypeScript Attributes) + // everything is correctly removed from the collection + // but since there are no changes, no new rewrite is triggered + // somehow we should be able to trigger rewrites based upon namespaces + } + + if ($newlyTransformed === null) { + $this->typeScriptTransformer->log->warning("Could not transform {$event->path}"); + + return; + } + + // TODO: at the moment we replace the node when we see an update + // it could be that no changes are actually made + // and such a case nothing should be updated + + if ($originalTransformed !== null) { + $this->transformedCollection->remove($originalTransformed->reference); + } + + $this->transformedCollection->add($newlyTransformed); + } +} diff --git a/src/Handlers/Watch/FileUpdatedWatchEventHandler.php b/src/Handlers/Watch/FileUpdatedWatchEventHandler.php deleted file mode 100644 index ce2fa867..00000000 --- a/src/Handlers/Watch/FileUpdatedWatchEventHandler.php +++ /dev/null @@ -1,79 +0,0 @@ -typeScriptTransformer->loadPhpClassNodeAction->execute($event->path); - - if ($classNode === null) { - $this->typeScriptTransformer->log->warning("Multiple class nodes found in {$event->path}"); - - return; - } - - $newlyTransformed = $this->typeScriptTransformer->transformTypesAction->transformClassNode( - $this->typeScriptTransformer->config->transformers, - $classNode - ); - - if ($newlyTransformed === null) { - $this->typeScriptTransformer->log->warning("Could not transform {$event->path}"); - - return; - } - - // TODO: at the moment we replace the node when we see an update - // it could be that no changes are actually made - // and such a case nothing should be updated - $originalTransformed = $this->transformedCollection->findTransformedByPath( - $event->path - ); - - if ($originalTransformed === null) { - $this->addNewlyTransformed($newlyTransformed); - - return; - } - - foreach ($originalTransformed->referencedBy as $referencedBy => $key) { - /** @var Transformed $referencedBy */ - $referencedBy->markReferenceRemoved($originalTransformed); - $referencedBy->markAsChanged(); - } - - $this->referenceMap->remove($originalTransformed); - $this->transformedCollection->removeTransformedByPath($event->path); - - $this->addNewlyTransformed($newlyTransformed); - } - - protected function addNewlyTransformed(Transformed $transformed): void - { - $this->transformedCollection->add($transformed); - - $this->typeScriptTransformer->executeProvidedClosuresAction->execute([$transformed]); - $this->typeScriptTransformer->connectReferencesAction->execute([$transformed]); - $this->typeScriptTransformer->executeConnectedClosuresAction->execute([$transformed]); - } -} diff --git a/src/Laravel/LaravelDataTypesProvider.php b/src/Laravel/LaravelDataTypesProvider.php index 71d1b4fa..aacf0f3d 100644 --- a/src/Laravel/LaravelDataTypesProvider.php +++ b/src/Laravel/LaravelDataTypesProvider.php @@ -6,8 +6,8 @@ use Illuminate\Pagination\LengthAwarePaginator; use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\PaginatedDataCollection; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\References\ClassStringReference; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; diff --git a/src/Laravel/LaravelNamedRouteTypesProvider.php b/src/Laravel/LaravelNamedRouteTypesProvider.php index 3c94bb6a..723d4f8f 100644 --- a/src/Laravel/LaravelNamedRouteTypesProvider.php +++ b/src/Laravel/LaravelNamedRouteTypesProvider.php @@ -2,6 +2,7 @@ namespace Spatie\TypeScriptTransformer\Laravel; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\Laravel\Actions\ResolveLaravelRoutControllerCollectionsAction; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteClosure; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteCollection; @@ -12,7 +13,6 @@ use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameterCollection; use Spatie\TypeScriptTransformer\Laravel\Support\WithoutRoutes; use Spatie\TypeScriptTransformer\References\CustomReference; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; diff --git a/src/Laravel/LaravelRouteActionTypesProvider.php b/src/Laravel/LaravelRouteActionTypesProvider.php index 5011cd6e..6f8bd79e 100644 --- a/src/Laravel/LaravelRouteActionTypesProvider.php +++ b/src/Laravel/LaravelRouteActionTypesProvider.php @@ -2,6 +2,7 @@ namespace Spatie\TypeScriptTransformer\Laravel; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\Laravel\Actions\ResolveLaravelRoutControllerCollectionsAction; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteCollection; use Spatie\TypeScriptTransformer\Laravel\Routes\RouteController; @@ -11,7 +12,6 @@ use Spatie\TypeScriptTransformer\Laravel\Routes\RouteParameterCollection; use Spatie\TypeScriptTransformer\Laravel\Support\WithoutRoutes; use Spatie\TypeScriptTransformer\References\CustomReference; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; diff --git a/src/Laravel/LaravelTypesProvider.php b/src/Laravel/LaravelTypesProvider.php index 68791a1b..bfc52c99 100644 --- a/src/Laravel/LaravelTypesProvider.php +++ b/src/Laravel/LaravelTypesProvider.php @@ -6,8 +6,8 @@ use Illuminate\Contracts\Pagination\LengthAwarePaginator as LengthAwarePaginatorInterface; use Illuminate\Pagination\CursorPaginator; use Illuminate\Pagination\LengthAwarePaginator; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\References\ClassStringReference; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; diff --git a/src/Laravel/SpatieLaravelTypesProvider.php b/src/Laravel/SpatieLaravelTypesProvider.php index 7f092105..617d2b0f 100644 --- a/src/Laravel/SpatieLaravelTypesProvider.php +++ b/src/Laravel/SpatieLaravelTypesProvider.php @@ -2,8 +2,8 @@ namespace Spatie\TypeScriptTransformer\Laravel; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\References\ClassStringReference; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptAlias; diff --git a/src/Support/TransformedCollection.php b/src/Support/TransformedCollection.php deleted file mode 100644 index a64ad034..00000000 --- a/src/Support/TransformedCollection.php +++ /dev/null @@ -1,153 +0,0 @@ - - */ -class TransformedCollection implements IteratorAggregate, ArrayAccess -{ - /** - * @param array $items - * @param array $fileMapping - */ - public function __construct( - protected array $items = [], - protected array $fileMapping = [], - ) { - } - - public function add(Transformed ...$transformed): self - { - foreach ($transformed as $item) { - $this->items[$item->reference->getKey()] = $item; - - if ($item->reference instanceof FilesystemReference) { - $this->addTransformedFileReference($item, $item->reference); - } - } - - return $this; - } - - public function getIterator(): Traversable - { - return new ArrayIterator($this->items); - } - - public function offsetExists(mixed $offset): bool - { - return isset($this->items[$offset]); - } - - public function offsetGet(mixed $offset): Transformed - { - return $this->items[$offset]; - } - - public function offsetSet(mixed $offset, mixed $value): void - { - $this->items[$offset] = $value; - } - - public function offsetUnset(mixed $offset): void - { - unset($this->items[$offset]); - } - - public function all(): array - { - return $this->items; - } - - public function findTransformedByPath(string $path): ?Transformed - { - $segments = explode('/', ltrim(realpath($path), DIRECTORY_SEPARATOR)); - - $pointer = $this->files; - - foreach ($segments as $segment) { - if (! isset($pointer[$segment])) { - return null; - } - - $pointer = $pointer[$segment]; - } - - return $pointer; - } - - public function removeTransformedByPath(string $path): void - { - $segments = explode('/', ltrim(realpath($path), DIRECTORY_SEPARATOR)); - - $pointer = &$this->files; - - foreach ($segments as $i => $segment) { - if (! isset($pointer[$segment])) { - return; - } - - if ($i === count($segments) - 1) { - /** @var Transformed $transformed */ - $transformed = $pointer[$segment]; - - unset($this->items[$this->resolveIdForTransformed($transformed)]); - unset($pointer[$segment]); - - break; - } - - $pointer = &$pointer[$segment]; - } - } - - public function hasChanges(): bool - { - foreach ($this->items as $item) { - if ($item->changed) { - return true; - } - } - - return false; - } - - protected function addTransformedFileReference(Transformed $transformed, FilesystemReference $reference): void - { - $segments = explode('/', ltrim(realpath($reference->getFilesystemOriginPath()), DIRECTORY_SEPARATOR)); - - $pointer = &$this->files; - - foreach ($segments as $i => $segment) { - if (! isset($pointer[$segment])) { - $pointer[$segment] = []; - } - - if ($i === count($segments) - 1) { - $pointer[$segment] = $transformed; - $this->items[$this->resolveIdForTransformed($transformed)] = $transformed; - - break; - } - - $pointer = &$pointer[$segment]; - } - } - - protected function resolveIdForTransformed(Transformed $transformed): string - { - if ($transformed->reference instanceof FilesystemReference) { - return $transformed->reference->getFilesystemOriginPath(); - } - - return spl_object_id($transformed); - } -} diff --git a/src/Transformed/Transformed.php b/src/Transformed/Transformed.php index ae263300..c01bf084 100644 --- a/src/Transformed/Transformed.php +++ b/src/Transformed/Transformed.php @@ -8,7 +8,6 @@ use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptForwardingNamedNode; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNamedNode; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; -use WeakMap; class Transformed { @@ -16,11 +15,11 @@ class Transformed public bool $changed = true; - /** @var WeakMap */ - public WeakMap $references; + /** @var array */ + public array $references = []; - /** @var WeakMap */ - public WeakMap $referencedBy; + /** @var array */ + public array $referencedBy = []; /** @var array */ public array $missingReferences = []; @@ -34,8 +33,6 @@ public function __construct( public array $location, public bool $export = true, ) { - $this->references = new WeakMap(); - $this->referencedBy = new WeakMap(); } public function getName(): ?string @@ -98,11 +95,6 @@ public function addMissingReference( $this->missingReferences[$key][] = $typeReference; } - public function isMissingReference(string $key) - { - return array_key_exists($key, $this->missingReferences); - } - public function markMissingReferenceFound( Transformed $transformed ): void { @@ -114,23 +106,29 @@ public function markMissingReferenceFound( $typeReference->connect($transformed); } - $this->references[$transformed] = $typeReferences; + $this->references[$key] = $typeReferences; unset($this->missingReferences[$key]); + + $this->markAsChanged(); + + $transformed->referencedBy[] = $this->reference->getKey(); } - public function markReferenceRemoved( + public function markReferenceMissing( Transformed $transformed - ) { - $typeReferences = $this->references[$transformed]; + ): void { + $key = $transformed->reference->getKey(); + + $typeReferences = $this->references[$key]; foreach ($typeReferences as $typeReference) { $typeReference->unconnect(); } - unset($this->references[$transformed]); + unset($this->references[$key]); - $this->missingReferences = $typeReferences; + $this->missingReferences[$key] = $typeReferences; } public function markAsChanged(): void diff --git a/src/Transformers/EnumTransformer.php b/src/Transformers/EnumTransformer.php index daabfdc9..50163757 100644 --- a/src/Transformers/EnumTransformer.php +++ b/src/Transformers/EnumTransformer.php @@ -38,6 +38,10 @@ public function transform( $cases = $this->enumProvider->resolveCases($phpClassNode); + if(count($cases) === 0) { + return Untransformable::create(); + } + return new Transformed( $this->useUnionEnums ? $this->transformAsUnion($context->name, $cases) diff --git a/src/TypeProviders/TransformerTypesProvider.php b/src/TypeProviders/TransformerTypesProvider.php index c9751bc0..94e366d7 100644 --- a/src/TypeProviders/TransformerTypesProvider.php +++ b/src/TypeProviders/TransformerTypesProvider.php @@ -4,7 +4,7 @@ use Spatie\TypeScriptTransformer\Actions\DiscoverTypesAction; use Spatie\TypeScriptTransformer\Actions\TransformTypesAction; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\Transformers\Transformer; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; diff --git a/src/TypeProviders/TypesProvider.php b/src/TypeProviders/TypesProvider.php index f4721c16..97edf1d7 100644 --- a/src/TypeProviders/TypesProvider.php +++ b/src/TypeProviders/TypesProvider.php @@ -2,7 +2,7 @@ namespace Spatie\TypeScriptTransformer\TypeProviders; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; interface TypesProvider diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index 2c660151..dabc5068 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -10,11 +10,10 @@ use Spatie\TypeScriptTransformer\Actions\ProvideTypesAction; use Spatie\TypeScriptTransformer\Actions\TransformTypesAction; use Spatie\TypeScriptTransformer\Actions\WriteFilesAction; -use Spatie\TypeScriptTransformer\Collections\ReferenceMap; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\Support\Console\WrappedConsole; use Spatie\TypeScriptTransformer\Support\Console\WrappedNullConsole; use Spatie\TypeScriptTransformer\Support\LoadPhpClassNodeAction; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; class TypeScriptTransformer @@ -79,17 +78,16 @@ public function execute(): void $this->executeProvidedClosuresAction->execute($transformedCollection); - $referenceMap = $this->connectReferencesAction->execute($transformedCollection); + $this->connectReferencesAction->execute($transformedCollection); $this->executeConnectedClosuresAction->execute($transformedCollection); - $this->outputTransformed($transformedCollection, $referenceMap); + $this->outputTransformed($transformedCollection); if ($this->watch) { $watcher = new FileSystemWatcher( $this, $transformedCollection, - $referenceMap ); $watcher->run(); @@ -98,13 +96,12 @@ public function execute(): void public function outputTransformed( TransformedCollection $transformedCollection, - ReferenceMap $referenceMap ): void { if (! $transformedCollection->hasChanges()) { return; } - $writeableFiles = $this->config->writer->output($transformedCollection, $referenceMap); + $writeableFiles = $this->config->writer->output($transformedCollection); $this->writeFilesAction->execute($writeableFiles); diff --git a/src/Writers/FlatWriter.php b/src/Writers/FlatWriter.php index 89ddf365..920e8343 100644 --- a/src/Writers/FlatWriter.php +++ b/src/Writers/FlatWriter.php @@ -2,9 +2,8 @@ namespace Spatie\TypeScriptTransformer\Writers; -use Spatie\TypeScriptTransformer\Collections\ReferenceMap; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\References\Reference; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\WriteableFile; use Spatie\TypeScriptTransformer\Support\WritingContext; @@ -17,12 +16,11 @@ public function __construct( public function output( TransformedCollection $collection, - ReferenceMap $referenceMap ): array { $output = ''; - $writingContext = new WritingContext(function (Reference $reference) use ($referenceMap) { - $transformable = $referenceMap->get($reference); + $writingContext = new WritingContext(function (Reference $reference) use ($collection) { + $transformable = $collection->get($reference); if (empty($transformable->location)) { return $transformable->getName(); diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index ae76d5c4..1f67acad 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -4,10 +4,9 @@ use Spatie\TypeScriptTransformer\Actions\ResolveModuleImportsAction; use Spatie\TypeScriptTransformer\Actions\SplitTransformedPerLocationAction; -use Spatie\TypeScriptTransformer\Collections\ReferenceMap; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\References\Reference; use Spatie\TypeScriptTransformer\Support\Location; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\WriteableFile; use Spatie\TypeScriptTransformer\Support\WritingContext; @@ -20,7 +19,7 @@ public function __construct( ) { } - public function output(TransformedCollection $collection, ReferenceMap $referenceMap): array + public function output(TransformedCollection $collection): array { $locations = $this->transformedPerLocationAction->execute( $collection @@ -33,7 +32,7 @@ public function output(TransformedCollection $collection, ReferenceMap $referenc continue; } - $writtenFiles[] = $this->writeLocation($location, $referenceMap); + $writtenFiles[] = $this->writeLocation($location, $collection); } return $writtenFiles; @@ -41,19 +40,19 @@ public function output(TransformedCollection $collection, ReferenceMap $referenc protected function writeLocation( Location $location, - ReferenceMap $referenceMap, + TransformedCollection $collection, ): WriteableFile { - $imports = $this->resolveModuleImportsAction->execute($location); + $imports = $this->resolveModuleImportsAction->execute($location, $collection); $output = ''; - $writingContext = new WritingContext(function (Reference $reference) use ($referenceMap, $imports) { + $writingContext = new WritingContext(function (Reference $reference) use ($collection, $imports) { if ($name = $imports->getAliasOrNameForReference($reference)) { return $name; } // Type declared somewhere else in the module - return $referenceMap->get($reference)->getName(); + return $collection->get($reference)->getName(); }); foreach ($imports->getTypeScriptNodes() as $import) { diff --git a/src/Writers/NamespaceWriter.php b/src/Writers/NamespaceWriter.php index 8ecfdfda..a4f22358 100644 --- a/src/Writers/NamespaceWriter.php +++ b/src/Writers/NamespaceWriter.php @@ -3,9 +3,8 @@ namespace Spatie\TypeScriptTransformer\Writers; use Spatie\TypeScriptTransformer\Actions\SplitTransformedPerLocationAction; -use Spatie\TypeScriptTransformer\Collections\ReferenceMap; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\References\Reference; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; use Spatie\TypeScriptTransformer\Support\WriteableFile; use Spatie\TypeScriptTransformer\Support\WritingContext; use Spatie\TypeScriptTransformer\Transformed\Transformed; @@ -23,7 +22,6 @@ public function __construct( public function output( TransformedCollection $collection, - ReferenceMap $referenceMap ): array { $split = $this->splitTransformedPerLocationAction->execute( $collection @@ -31,8 +29,8 @@ public function output( $output = ''; - $writingContext = new WritingContext(function (Reference $reference) use ($referenceMap) { - $transformable = $referenceMap->get($reference); + $writingContext = new WritingContext(function (Reference $reference) use ($collection) { + $transformable = $collection->get($reference); if (empty($transformable->location)) { return $transformable->getName(); diff --git a/src/Writers/Writer.php b/src/Writers/Writer.php index 5b02f67f..f65a6324 100644 --- a/src/Writers/Writer.php +++ b/src/Writers/Writer.php @@ -2,8 +2,7 @@ namespace Spatie\TypeScriptTransformer\Writers; -use Spatie\TypeScriptTransformer\Collections\ReferenceMap; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\Support\WriteableFile; interface Writer @@ -11,6 +10,5 @@ interface Writer /** @return array */ public function output( TransformedCollection $collection, - ReferenceMap $referenceMap, ): array; } diff --git a/tests/Actions/ConnectReferencesActionTest.php b/tests/Actions/ConnectReferencesActionTest.php index 49235118..ceefb08d 100644 --- a/tests/Actions/ConnectReferencesActionTest.php +++ b/tests/Actions/ConnectReferencesActionTest.php @@ -1,8 +1,8 @@ execute($collection)->all(); + $action->execute($collection); - expect($referenceMap) - ->toHaveCount(2) - ->toBe([ - $transformedEnum->reference->getKey() => $transformedEnum, - $transformedClass->reference->getKey() => $transformedClass, - ]); + ray($transformedClass, $transformedEnum); expect($transformedEnum->references)->toHaveCount(0); expect($transformedEnum->referencedBy)->toHaveCount(1); - expect($transformedEnum->referencedBy->offsetExists($transformedClass)); + expect($transformedEnum->referencedBy)->toContain($transformedClass->reference->getKey()); expect($transformedClass->references)->toHaveCount(1); - expect($transformedClass->references->offsetExists($transformedEnum)); + expect($transformedClass->references)->toHaveKey($transformedEnum->reference->getKey()); expect($transformedClass->referencedBy)->toHaveCount(0); expect($transformedClass->typeScriptNode->type->properties[0]->type) @@ -53,24 +48,17 @@ $action = new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()); - $referenceMap = $action->execute($collection)->all(); - - expect($referenceMap) - ->toHaveCount(2) - ->toBe([ - $circularA->reference->getKey() => $circularA, - $circularB->reference->getKey() => $circularB, - ]); + $action->execute($collection); expect($circularA->references)->toHaveCount(1); - expect($circularA->references->offsetExists($circularB))->toBeTrue(); + expect($circularA->references)->toHaveKey($circularB->reference->getKey()); expect($circularA->referencedBy)->toHaveCount(1); - expect($circularA->referencedBy->offsetExists($circularB))->toBeTrue(); + expect($circularA->referencedBy)->toContain($circularB->reference->getKey()); expect($circularB->references)->toHaveCount(1); - expect($circularB->references->offsetExists($circularA))->toBeTrue(); + expect($circularB->references)->toHaveKey($circularA->reference->getKey()); expect($circularB->referencedBy)->toHaveCount(1); - expect($circularB->referencedBy->offsetExists($circularA))->toBeTrue(); + expect($circularB->referencedBy)->toContain($circularA->reference->getKey()); expect($circularA->typeScriptNode->type->properties[0]->type) ->toBeInstanceOf(TypeReference::class) @@ -95,13 +83,7 @@ new TypeScriptTransformerLog($console = new WrappedArrayConsole()) ); - $referenceMap = $action->execute($collection)->all(); - - expect($referenceMap) - ->toHaveCount(1) - ->toBe([ - $transformedClass->reference->getKey() => $transformedClass, - ]); + $action->execute($collection); expect($transformedClass->references)->toHaveCount(0); expect($transformedClass->referencedBy)->toHaveCount(0); diff --git a/tests/Actions/DiscoverTypesActionTest.php b/tests/Actions/DiscoverTypesActionTest.php index eddf04b8..6a142c51 100644 --- a/tests/Actions/DiscoverTypesActionTest.php +++ b/tests/Actions/DiscoverTypesActionTest.php @@ -3,6 +3,7 @@ use Spatie\TypeScriptTransformer\Actions\DiscoverTypesAction; use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; use Spatie\TypeScriptTransformer\PhpNodes\PhpEnumNode; +use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\EmptyEnum; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\HiddenAttributedClass; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\IntBackedEnum; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\OptionalAttributedClass; @@ -26,6 +27,7 @@ new PhpClassNode(new ReflectionClass(SimpleInterface::class)), new PhpClassNode(new ReflectionClass(TypeScriptLocationAttributedClass::class)), new PhpClassNode(new ReflectionClass(OptionalAttributedClass::class)), + new PhpEnumNode(new ReflectionEnum(EmptyEnum::class)), new PhpClassNode(new ReflectionClass(ReadonlyClass::class)), new PhpClassNode(new ReflectionClass(SimpleClass::class)), new PhpEnumNode(new ReflectionEnum(UnitEnum::class)), diff --git a/tests/Actions/ProvideTypesActionTest.php b/tests/Actions/ProvideTypesActionTest.php index b50fb044..5c498309 100644 --- a/tests/Actions/ProvideTypesActionTest.php +++ b/tests/Actions/ProvideTypesActionTest.php @@ -3,7 +3,7 @@ namespace Spatie\TypeScriptTransformer\Tests\Actions; use Spatie\TypeScriptTransformer\Actions\ProvideTypesAction; -use Spatie\TypeScriptTransformer\Support\TransformedCollection; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\Tests\Factories\TransformedFactory; use Spatie\TypeScriptTransformer\Tests\Support\InlineTypesProvider; use Spatie\TypeScriptTransformer\TypeProviders\TypesProvider; diff --git a/tests/Actions/ResolveModuleImportsActionTest.php b/tests/Actions/ResolveModuleImportsActionTest.php index 4efc6b0e..ee633796 100644 --- a/tests/Actions/ResolveModuleImportsActionTest.php +++ b/tests/Actions/ResolveModuleImportsActionTest.php @@ -1,6 +1,7 @@ build(), TransformedFactory::alias('B', new TypeReference($reference->reference), references: [ $reference, ])->build(), ]); - expect($this->action->execute($location)->isEmpty())->toBe(true); + $location = new Location([], [$reference]); + + expect($this->action->execute($location, $transformedCollection)->isEmpty())->toBe(true); }); it('will import a type from another module', function () { - $nestedReference = TransformedFactory::alias('Nested', new TypeScriptString(), location: ['parent', 'level', 'nested'])->build(); - $parentReference = TransformedFactory::alias('Parent', new TypeScriptString(), location: ['parent'])->build(); - $deeperParent = TransformedFactory::alias('DeeperParent', new TypeScriptString(), location: ['parent', 'deeper'])->build(); - $rootReference = TransformedFactory::alias('Root', new TypeScriptString(), location: [])->build(); + $transformedCollection = new TransformedCollection([ + $nestedReference = TransformedFactory::alias('Nested', new TypeScriptString(), location: ['parent', 'level', 'nested'])->build(), + $parentReference = TransformedFactory::alias('Parent', new TypeScriptString(), location: ['parent'])->build(), + $deeperParent = TransformedFactory::alias('DeeperParent', new TypeScriptString(), location: ['parent', 'deeper'])->build(), + $rootReference = TransformedFactory::alias('Root', new TypeScriptString(), location: [])->build(), + ]); $location = new Location(['parent', 'level'], [ TransformedFactory::alias('Type', new TypeScriptString(), references: [ @@ -39,7 +44,7 @@ ])->build(), ]); - $imports = $this->action->execute($location); + $imports = $this->action->execute($location, $transformedCollection); expect($imports->toArray()) ->toHaveCount(4) @@ -54,7 +59,9 @@ }); it('wont import the same type twice', function () { - $nestedReference = TransformedFactory::alias('Nested', new TypeScriptString(), location: ['nested'])->build(); + $transformedCollection = new TransformedCollection([ + $nestedReference = TransformedFactory::alias('Nested', new TypeScriptString(), location: ['nested'])->build(), + ]); $location = new Location([], [ TransformedFactory::alias('TypeA', new TypeScriptString(), references: [ @@ -65,7 +72,7 @@ ])->build(), ]); - $imports = $this->action->execute($location); + $imports = $this->action->execute($location, $transformedCollection); expect($imports->toArray()) ->toHaveCount(1) @@ -77,7 +84,9 @@ }); it('will alias a reference if it is already in the module', function () { - $nestedCollection = TransformedFactory::alias('Collection', new TypeScriptString(), location: ['nested'])->build(); + $transformedCollection = new TransformedCollection([ + $nestedCollection = TransformedFactory::alias('Collection', new TypeScriptString(), location: ['nested'])->build(), + ]); $location = new Location([], [ TransformedFactory::alias('Collection', new TypeScriptString(), references: [ @@ -85,7 +94,7 @@ ])->build(), ]); - $imports = $this->action->execute($location); + $imports = $this->action->execute($location, $transformedCollection); expect($imports->toArray()) ->toHaveCount(1) @@ -97,8 +106,10 @@ }); it('will alias a reference if it is already in the module and already aliased by another import', function () { - $nestedCollection = TransformedFactory::alias('Collection', new TypeScriptString(), location: ['nested'])->build(); - $otherNestedCollection = TransformedFactory::alias('Collection', new TypeScriptString(), location: ['otherNested'])->build(); + $transformedCollection = new TransformedCollection([ + $nestedCollection = TransformedFactory::alias('Collection', new TypeScriptString(), location: ['nested'])->build(), + $otherNestedCollection = TransformedFactory::alias('Collection', new TypeScriptString(), location: ['otherNested'])->build(), + ]); $location = new Location([], [ TransformedFactory::alias('Collection', new TypeScriptString(), references: [ @@ -107,7 +118,7 @@ ])->build(), ]); - $imports = $this->action->execute($location); + $imports = $this->action->execute($location, $transformedCollection); expect($imports->toArray()) ->toHaveCount(2) diff --git a/tests/Actions/SplitTransformedPerLocationActionTest.php b/tests/Actions/SplitTransformedPerLocationActionTest.php index ffa23e6a..b85ad749 100644 --- a/tests/Actions/SplitTransformedPerLocationActionTest.php +++ b/tests/Actions/SplitTransformedPerLocationActionTest.php @@ -1,8 +1,8 @@ $references + * @param array $referencedBy + */ public function __construct( public TypeScriptNode $typeScriptNode, public ?Reference $reference = null, @@ -60,11 +64,11 @@ public function build(): Transformed ); foreach ($this->references ?? [] as $reference) { - $transformed->references[$reference] = null; + $transformed->references[$reference->reference->getKey()] = []; } foreach ($this->referencedBy ?? [] as $reference) { - $transformed->referencedBy[$reference] = null; + $transformed->referencedBy[] = $reference->reference->getKey(); } return $transformed; diff --git a/tests/Fakes/TypesToProvide/EmptyEnum.php b/tests/Fakes/TypesToProvide/EmptyEnum.php new file mode 100644 index 00000000..6adb3095 --- /dev/null +++ b/tests/Fakes/TypesToProvide/EmptyEnum.php @@ -0,0 +1,7 @@ +output(static::$collection, static::$referenceMap); + [$writeableFile] = $writer->output(static::$collection); return $writeableFile->contents; } diff --git a/tests/Transformers/EnumTransformerTest.php b/tests/Transformers/EnumTransformerTest.php index 97846921..6d270889 100644 --- a/tests/Transformers/EnumTransformerTest.php +++ b/tests/Transformers/EnumTransformerTest.php @@ -1,5 +1,8 @@ transform( + $enum = PhpClassNode::fromClassString(EmptyEnum::class), + TransformationContext::createFromPhpClass($enum), + ); + + expect($transformed)->toBeInstanceOf(Untransformable::class); +}); diff --git a/tests/TypeProviders/TransformerTypesProviderTest.php b/tests/TypeProviders/TransformerTypesProviderTest.php index e1a24d96..8e7ccc5c 100644 --- a/tests/TypeProviders/TransformerTypesProviderTest.php +++ b/tests/TypeProviders/TransformerTypesProviderTest.php @@ -1,8 +1,8 @@ toHaveCount(5); - ray(iterator_to_array($collection)); expect(iterator_to_array($collection))->sequence( fn (Expectation $transformed) => $transformed ->toBeInstanceOf(Transformed::class) diff --git a/tests/Writers/FlatWriterTest.php b/tests/Writers/FlatWriterTest.php index db746514..848e267b 100644 --- a/tests/Writers/FlatWriterTest.php +++ b/tests/Writers/FlatWriterTest.php @@ -1,9 +1,8 @@ writer->output( $transformedCollection, - new ReferenceMap(), ); expect($file) @@ -60,9 +58,10 @@ ]))->build(), ]); + (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection); + [$file] = $this->writer->output( $transformedCollection, - (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection), ); expect($file) diff --git a/tests/Writers/ModuleWriterTest.php b/tests/Writers/ModuleWriterTest.php index 18301ce1..8918d5b0 100644 --- a/tests/Writers/ModuleWriterTest.php +++ b/tests/Writers/ModuleWriterTest.php @@ -1,9 +1,8 @@ writer->output( $transformedCollection, - new ReferenceMap(), ); expect($files) @@ -58,10 +56,9 @@ $withoutEndWriter = new ModuleWriter('/some-path'); $transformedCollection = new TransformedCollection([$rootTransformed, $nestedTransformed]); - $referenceMap = new ReferenceMap(); - $withEndFiles = $withEndWriter->output($transformedCollection, $referenceMap); - $withoutEndFiles = $withoutEndWriter->output($transformedCollection, $referenceMap); + $withEndFiles = $withEndWriter->output($transformedCollection); + $withoutEndFiles = $withoutEndWriter->output($transformedCollection); expect($withEndFiles)->toEqual($withoutEndFiles); }); @@ -74,9 +71,10 @@ TransformedFactory::alias('B', new TypeReference($reference))->build(), ]); + (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection); + $files = $this->writer->output( $transformedCollection, - (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection), ); expect($files) @@ -101,9 +99,10 @@ ]))->build(), ]); + (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection); + $files = $this->writer->output( $transformedCollection, - (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection), ); expect($files) @@ -147,9 +146,10 @@ ]))->build(), ]); + (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection); + $files = $this->writer->output( $transformedCollection, - (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection), ); expect($files) @@ -183,9 +183,10 @@ TransformedFactory::alias('B', new TypeReference($reference), location: ['nested'])->build(), ]); + (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection); + $files = $this->writer->output( $transformedCollection, - (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection), ); expect($files) @@ -214,9 +215,10 @@ TransformedFactory::alias('A', new TypeReference($reference), location: ['nested'])->build(), ]); + (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection); + $files = $this->writer->output( $transformedCollection, - (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection), ); expect($files) diff --git a/tests/Writers/NamespaceWriterTest.php b/tests/Writers/NamespaceWriterTest.php index ea2c1235..98e41b60 100644 --- a/tests/Writers/NamespaceWriterTest.php +++ b/tests/Writers/NamespaceWriterTest.php @@ -1,9 +1,8 @@ output( $transformedCollection, - new ReferenceMap(), ); expect($files) @@ -68,9 +66,10 @@ $filename = 'types.ts'; + (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection); + $files = (new NamespaceWriter($filename))->output( $transformedCollection, - (new ConnectReferencesAction(TypeScriptTransformerLog::createNullLog()))->execute($transformedCollection), ); expect($files) From b34ac5949694f84305536da98a35f5a2f8b059a7 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 4 Oct 2024 15:12:01 +0200 Subject: [PATCH 46/51] wip --- src/Actions/TransformTypesAction.php | 3 +- .../DirectoryDeletedWatchEventHandler.php | 2 +- src/PhpNodes/PhpAttributeNode.php | 63 ++++++++++++++++++- src/Support/TransformationContext.php | 10 ++- src/Writers/ModuleWriter.php | 9 +-- tests/Actions/TransformTypesActionTest.php | 17 ----- 6 files changed, 77 insertions(+), 27 deletions(-) diff --git a/src/Actions/TransformTypesAction.php b/src/Actions/TransformTypesAction.php index 7364e4e1..6cb3a2d3 100644 --- a/src/Actions/TransformTypesAction.php +++ b/src/Actions/TransformTypesAction.php @@ -43,11 +43,12 @@ public function transformClassNode( if (count($node->getAttributes(Hidden::class)) > 0) { return null; } + $transformationContext = TransformationContext::createFromPhpClass($node); foreach ($transformers as $transformer) { $transformed = $transformer->transform( $node, - TransformationContext::createFromPhpClass($node), + $transformationContext, ); if ($transformed instanceof Transformed) { diff --git a/src/Handlers/Watch/DirectoryDeletedWatchEventHandler.php b/src/Handlers/Watch/DirectoryDeletedWatchEventHandler.php index 4483c1f3..5304a0b5 100644 --- a/src/Handlers/Watch/DirectoryDeletedWatchEventHandler.php +++ b/src/Handlers/Watch/DirectoryDeletedWatchEventHandler.php @@ -6,6 +6,6 @@ class DirectoryDeletedWatchEventHandler implements WatchEventHandler { public function handle($event): void { - + // TODO: Implement handle() method. } } diff --git a/src/PhpNodes/PhpAttributeNode.php b/src/PhpNodes/PhpAttributeNode.php index e8b24b62..dd729b3b 100644 --- a/src/PhpNodes/PhpAttributeNode.php +++ b/src/PhpNodes/PhpAttributeNode.php @@ -3,10 +3,13 @@ namespace Spatie\TypeScriptTransformer\PhpNodes; use ReflectionAttribute; +use ReflectionMethod; use Roave\BetterReflection\Reflection\ReflectionAttribute as RoaveReflectionAttribute; class PhpAttributeNode { + protected ?array $arguments = null; + public function __construct( public readonly ReflectionAttribute|RoaveReflectionAttribute $reflection ) { @@ -22,9 +25,27 @@ public function getArguments(): array return $this->reflection->getArguments(); } + public function hasArgument(string $name): bool + { + if ($this->arguments === null) { + $this->initializeArguments(); + } + + return array_key_exists($name, $this->arguments); + } + + public function getArgument(string $name): mixed + { + if ($this->arguments === null) { + $this->initializeArguments(); + } + + return $this->arguments[$name] ?? null; + } + public function newInstance(): object { - if($this->reflection instanceof ReflectionAttribute) { + if ($this->reflection instanceof ReflectionAttribute) { return $this->reflection->newInstance(); } @@ -33,4 +54,44 @@ public function newInstance(): object // TODO: maybe we can do a little better here return (new $className())($this->reflection->getArguments()); } + + /** @return array */ + protected function initializeArguments(): array + { + // TODO: this is a quickly written thing, test it to be sure it works + if ($this->arguments !== null) { + return $this->arguments; + } + + $this->arguments = []; + + $values = $this->getArguments(); + + foreach ($values as $name => $value) { + if (is_string($name)) { + $this->arguments[$name] = $value; + unset($values[$name]); + } + } + + if (count($values) === 0) { + return $this->arguments; + } + + $constructor = new ReflectionMethod($this->reflection->getName(), '__construct'); + + foreach ($constructor->getParameters() as $index => $param) { + if(array_key_exists($param->getName(), $this->arguments)) { + continue; + } + + if(! array_key_exists($index, $values)) { + continue; + } + + $this->arguments[$param->getName()] = $values[$index]; + } + + return $this->arguments; + } } diff --git a/src/Support/TransformationContext.php b/src/Support/TransformationContext.php index b7890649..8543f2eb 100644 --- a/src/Support/TransformationContext.php +++ b/src/Support/TransformationContext.php @@ -18,11 +18,15 @@ public function __construct( public static function createFromPhpClass( PhpClassNode $node ): TransformationContext { - $attributeArguments = ($node->getAttributes(TypeScript::class)[0] ?? null)?->getArguments() ?? []; + $attribute = $node->getAttributes(TypeScript::class)[0] ?? null; - $name = $attributeArguments['name'] ?? $node->getShortName(); + $name = $attribute && $attribute->hasArgument('name') + ? $attribute->getArgument('name') + : $node->getShortName(); - $nameSpaceSegments = $attributeArguments['location'] ?? explode('\\', $node->getNamespaceName()); + $nameSpaceSegments = $attribute && $attribute->hasArgument('location') + ? $attribute->getArgument('location') + : explode('\\', $node->getNamespaceName()); return new TransformationContext( $name, diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index 1f67acad..827d323b 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -27,14 +27,15 @@ public function output(TransformedCollection $collection): array $writtenFiles = []; - foreach ($locations as $location) { - if ($location->hasChanges() === false) { - continue; - } + // TODO: remove files which still exists due to previous run + foreach ($locations as $location) { $writtenFiles[] = $this->writeLocation($location, $collection); } + // TODO: we probably can be a bit smarter about this + // -> only write files which have changed + return $writtenFiles; } diff --git a/tests/Actions/TransformTypesActionTest.php b/tests/Actions/TransformTypesActionTest.php index e1e6a05f..afb4ebd4 100644 --- a/tests/Actions/TransformTypesActionTest.php +++ b/tests/Actions/TransformTypesActionTest.php @@ -2,7 +2,6 @@ use Spatie\TypeScriptTransformer\Actions\TransformTypesAction; use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; -use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\HiddenAttributedClass; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\SimpleClass; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\StringBackedEnum; @@ -52,19 +51,3 @@ expect($types)->toBeEmpty(); }); - -it('will log errors when a type cannot be reflected', function () { - $types = (new TransformTypesAction())->execute( - [ - new AllClassTransformer(), - ], - [ - PhpClassNode::fromClassString('NonExistentClass'), - ] - ); - - expect($types)->toBeEmpty(); - - expect(TypeScriptTransformerLog::resolve()->errorMessages) - ->toHaveCount(1); -}); From 8f4b2bc2dd0a41377d5dfd28053db5d07dc26cd2 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Fri, 4 Oct 2024 13:12:25 +0000 Subject: [PATCH 47/51] Fix styling --- src/Collections/TransformedCollection.php | 2 +- src/PhpNodes/PhpAttributeNode.php | 4 ++-- src/PhpNodes/PhpEnumCaseNode.php | 4 ++-- src/Transformed/Transformed.php | 2 +- src/Transformers/EnumTransformer.php | 2 +- src/TypeScriptNodes/TypeReference.php | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Collections/TransformedCollection.php b/src/Collections/TransformedCollection.php index 7590d6fa..00c92b2a 100644 --- a/src/Collections/TransformedCollection.php +++ b/src/Collections/TransformedCollection.php @@ -67,7 +67,7 @@ public function remove(Reference|string $reference): void unset($this->items[$transformed->reference->getKey()]); - if($transformed->reference instanceof FilesystemReference) { + if ($transformed->reference instanceof FilesystemReference) { $path = $this->cleanupFilePath($transformed->reference->getFilesystemOriginPath()); unset($this->fileMapping[$path]); diff --git a/src/PhpNodes/PhpAttributeNode.php b/src/PhpNodes/PhpAttributeNode.php index dd729b3b..cc50ae92 100644 --- a/src/PhpNodes/PhpAttributeNode.php +++ b/src/PhpNodes/PhpAttributeNode.php @@ -81,11 +81,11 @@ protected function initializeArguments(): array $constructor = new ReflectionMethod($this->reflection->getName(), '__construct'); foreach ($constructor->getParameters() as $index => $param) { - if(array_key_exists($param->getName(), $this->arguments)) { + if (array_key_exists($param->getName(), $this->arguments)) { continue; } - if(! array_key_exists($index, $values)) { + if (! array_key_exists($index, $values)) { continue; } diff --git a/src/PhpNodes/PhpEnumCaseNode.php b/src/PhpNodes/PhpEnumCaseNode.php index 3ad23d92..1f1fa3f0 100644 --- a/src/PhpNodes/PhpEnumCaseNode.php +++ b/src/PhpNodes/PhpEnumCaseNode.php @@ -20,11 +20,11 @@ public function getName(): string public function getValue(): string|int|null { - if($this->reflection instanceof ReflectionEnumCase) { + if ($this->reflection instanceof ReflectionEnumCase) { return $this->reflection->getValue(); } - if(! method_exists($this->reflection, 'getBackingValue')) { + if (! method_exists($this->reflection, 'getBackingValue')) { return null; } diff --git a/src/Transformed/Transformed.php b/src/Transformed/Transformed.php index c01bf084..97bfd174 100644 --- a/src/Transformed/Transformed.php +++ b/src/Transformed/Transformed.php @@ -88,7 +88,7 @@ public function addMissingReference( $key = $key->getKey(); } - if(! array_key_exists($key, $this->missingReferences)) { + if (! array_key_exists($key, $this->missingReferences)) { $this->missingReferences[$key] = []; } diff --git a/src/Transformers/EnumTransformer.php b/src/Transformers/EnumTransformer.php index 50163757..be795a3d 100644 --- a/src/Transformers/EnumTransformer.php +++ b/src/Transformers/EnumTransformer.php @@ -38,7 +38,7 @@ public function transform( $cases = $this->enumProvider->resolveCases($phpClassNode); - if(count($cases) === 0) { + if (count($cases) === 0) { return Untransformable::create(); } diff --git a/src/TypeScriptNodes/TypeReference.php b/src/TypeScriptNodes/TypeReference.php index 96c4358a..4ac726e5 100644 --- a/src/TypeScriptNodes/TypeReference.php +++ b/src/TypeScriptNodes/TypeReference.php @@ -32,7 +32,7 @@ public function unconnect(): void public function write(WritingContext $context): string { - if($this->referenced === null) { + if ($this->referenced === null) { return 'undefined'; } From a0f02f3d318aedc425645d5fe9e522079479e76f Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 4 Oct 2024 17:27:19 +0200 Subject: [PATCH 48/51] Extra tests --- src/Collections/TransformedCollection.php | 13 +- .../DirectoryDeletedWatchEventHandler.php | 19 +- .../Watch/FileDeletedWatchEventHandler.php | 2 +- .../FileUpdatedOrCreatedWatchEventHandler.php | 2 +- src/Transformed/Transformed.php | 2 + src/TypeScriptTransformer.php | 22 ++- tests/Actions/ConnectReferencesActionTest.php | 2 - .../Collections/TransformedCollectionTest.php | 172 ++++++++++++++++++ tests/Fakes/Integration/Enum.php | 9 - tests/Fakes/Integration/IntegrationClass.php | 82 --------- tests/Fakes/Integration/IntegrationItem.php | 8 - .../Fakes/Integration/Level/LevelUpClass.php | 8 - tests/Transformed/TransformedTest.php | 155 ++++++++++++++++ 13 files changed, 376 insertions(+), 120 deletions(-) create mode 100644 tests/Collections/TransformedCollectionTest.php delete mode 100644 tests/Fakes/Integration/Enum.php delete mode 100644 tests/Fakes/Integration/IntegrationClass.php delete mode 100644 tests/Fakes/Integration/IntegrationItem.php delete mode 100644 tests/Fakes/Integration/Level/LevelUpClass.php create mode 100644 tests/Transformed/TransformedTest.php diff --git a/src/Collections/TransformedCollection.php b/src/Collections/TransformedCollection.php index 00c92b2a..8a3231c8 100644 --- a/src/Collections/TransformedCollection.php +++ b/src/Collections/TransformedCollection.php @@ -93,13 +93,24 @@ public function onlyChanged(): Generator } } - public function findTransformedByPath(string $path): ?Transformed + public function findTransformedByFile(string $path): ?Transformed { $path = $this->cleanupFilePath($path); return $this->fileMapping[$path] ?? null; } + public function findTransformedByDirectory(string $path): Generator + { + $path = $this->cleanupFilePath($path); + + foreach ($this->fileMapping as $transformedPath => $transformed) { + if (str_starts_with($transformedPath, $path)) { + yield $transformed; + } + } + } + public function hasChanges(): bool { foreach ($this->items as $item) { diff --git a/src/Handlers/Watch/DirectoryDeletedWatchEventHandler.php b/src/Handlers/Watch/DirectoryDeletedWatchEventHandler.php index 5304a0b5..0f7c9524 100644 --- a/src/Handlers/Watch/DirectoryDeletedWatchEventHandler.php +++ b/src/Handlers/Watch/DirectoryDeletedWatchEventHandler.php @@ -2,10 +2,27 @@ namespace Spatie\TypeScriptTransformer\Handlers\Watch; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; +use Spatie\TypeScriptTransformer\Events\Watch\DirectoryDeletedWatchEvent; +use Spatie\TypeScriptTransformer\TypeScriptTransformer; + class DirectoryDeletedWatchEventHandler implements WatchEventHandler { + public function __construct( + protected TypeScriptTransformer $typeScriptTransformer, + protected TransformedCollection $transformedCollection, + ) { + } + + /** + * @param DirectoryDeletedWatchEvent $event + */ public function handle($event): void { - // TODO: Implement handle() method. + $transformedItems = $this->transformedCollection->findTransformedByDirectory($event->path); + + foreach ($transformedItems as $transformed) { + $this->transformedCollection->remove($transformed->reference); + } } } diff --git a/src/Handlers/Watch/FileDeletedWatchEventHandler.php b/src/Handlers/Watch/FileDeletedWatchEventHandler.php index b3a3c56a..1fbd4c99 100644 --- a/src/Handlers/Watch/FileDeletedWatchEventHandler.php +++ b/src/Handlers/Watch/FileDeletedWatchEventHandler.php @@ -19,7 +19,7 @@ public function __construct( */ public function handle($event): void { - $transformed = $this->transformedCollection->findTransformedByPath($event->path); + $transformed = $this->transformedCollection->findTransformedByFile($event->path); if ($transformed === null) { return; diff --git a/src/Handlers/Watch/FileUpdatedOrCreatedWatchEventHandler.php b/src/Handlers/Watch/FileUpdatedOrCreatedWatchEventHandler.php index eb12682a..50cacc51 100644 --- a/src/Handlers/Watch/FileUpdatedOrCreatedWatchEventHandler.php +++ b/src/Handlers/Watch/FileUpdatedOrCreatedWatchEventHandler.php @@ -43,7 +43,7 @@ public function handle($event): void throw $throwable; } - $originalTransformed = $this->transformedCollection->findTransformedByPath( + $originalTransformed = $this->transformedCollection->findTransformedByFile( $event->path ); diff --git a/src/Transformed/Transformed.php b/src/Transformed/Transformed.php index 97bfd174..796e5d41 100644 --- a/src/Transformed/Transformed.php +++ b/src/Transformed/Transformed.php @@ -129,6 +129,8 @@ public function markReferenceMissing( unset($this->references[$key]); $this->missingReferences[$key] = $typeReferences; + + $this->markAsChanged(); } public function markAsChanged(): void diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index dabc5068..5a4d6afb 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -71,16 +71,11 @@ public function execute(): void * - Further write docs + check them -> only Laravel specific stuff * - Check old Laravel tests if we missed something * - Check in Flare whether everything is working as expected -> PR ready, needs fixing TS + * - Make sure nullables can be exported as optional: https://github.com/spatie/typescript-transformer/pull/88/files * - Release */ - $transformedCollection = $this->provideTypesAction->execute(); - - $this->executeProvidedClosuresAction->execute($transformedCollection); - - $this->connectReferencesAction->execute($transformedCollection); - - $this->executeConnectedClosuresAction->execute($transformedCollection); + $transformedCollection = $this->resolveTransformedCollection(); $this->outputTransformed($transformedCollection); @@ -94,6 +89,19 @@ public function execute(): void } } + public function resolveTransformedCollection(): TransformedCollection + { + $transformedCollection = $this->provideTypesAction->execute(); + + $this->executeProvidedClosuresAction->execute($transformedCollection); + + $this->connectReferencesAction->execute($transformedCollection); + + $this->executeConnectedClosuresAction->execute($transformedCollection); + + return $transformedCollection; + } + public function outputTransformed( TransformedCollection $transformedCollection, ): void { diff --git a/tests/Actions/ConnectReferencesActionTest.php b/tests/Actions/ConnectReferencesActionTest.php index ceefb08d..2a30c245 100644 --- a/tests/Actions/ConnectReferencesActionTest.php +++ b/tests/Actions/ConnectReferencesActionTest.php @@ -25,8 +25,6 @@ $action->execute($collection); - ray($transformedClass, $transformedEnum); - expect($transformedEnum->references)->toHaveCount(0); expect($transformedEnum->referencedBy)->toHaveCount(1); expect($transformedEnum->referencedBy)->toContain($transformedClass->reference->getKey()); diff --git a/tests/Collections/TransformedCollectionTest.php b/tests/Collections/TransformedCollectionTest.php new file mode 100644 index 00000000..0aaa7202 --- /dev/null +++ b/tests/Collections/TransformedCollectionTest.php @@ -0,0 +1,172 @@ +toHaveCount(1); +}); + +it('can add transformed items to the collection', function () { + $collection = new TransformedCollection(); + + $collection->add( + $initialTransformed = transformSingle(SimpleClass::class), + ); + + expect($collection)->toHaveCount(1); +}); + +it('can get a transformed item by reference', function () { + $collection = new TransformedCollection([ + $classTransformed = transformSingle(SimpleClass::class), + $manualTransformed = new Transformed( + new TypeScriptString(), + new CustomReference('vendor', 'package'), + [], + ), + ]); + + expect($collection->has(new ClassStringReference(SimpleClass::class)))->toBeTrue(); + expect($collection->get(new ClassStringReference(SimpleClass::class)))->toBe($classTransformed); + expect($collection->has(new CustomReference('vendor', 'package')))->toBeTrue(); + expect($collection->get(new CustomReference('vendor', 'package')))->toBe($manualTransformed); +}); + +it('can loop over items in the collection', function () { + $collection = new TransformedCollection([ + $a = transformSingle(SimpleClass::class), + $b = transformSingle(TypeScriptAttributedClass::class), + ]); + + $found = []; + + foreach ($collection as $transformed) { + $found[] = $transformed; + } + + expect($found)->toBe([$a, $b]); +}); + +it('can loop over only changed items in the collection', function () { + $collection = new TransformedCollection([ + $a = transformSingle(SimpleClass::class), + $b = transformSingle(TypeScriptAttributedClass::class), + ]); + + $a->changed = true; + $b->changed = false; + + $found = []; + + foreach ($collection->onlyChanged() as $transformed) { + $found[] = $transformed; + } + + expect($found)->toBe([$a]); +}); + +it('all items added to the collection are marked as changed', function () { + new TransformedCollection([ + $a = transformSingle(SimpleClass::class), + $b = transformSingle(TypeScriptAttributedClass::class), + ]); + + expect($a->changed)->toBeTrue(); + expect($b->changed)->toBeTrue(); +}); + +it('can find transformed items by file path', function () { + $collection = new TransformedCollection([ + $transformed = transformSingle(SimpleClass::class), + ]); + + $path = __DIR__.'/../Fakes/TypesToProvide/SimpleClass.php'; + + expect($collection->findTransformedByFile($path))->toBe($transformed); +}); + +it('can find transformed items by directory path', function () { + $collection = new TransformedCollection([ + $a = transformSingle(SimpleClass::class), + $b = transformSingle(TypeScriptAttributedClass::class), + $c = transformSingle(CircularA::class), + ]); + + $path = __DIR__.'/../Fakes/TypesToProvide'; + + $found = []; + + foreach ($collection->findTransformedByDirectory($path) as $transformed) { + $found[] = $transformed; + } + + expect($found)->toBe([$a, $b]); +}); + +it('can check if any items in the collection have changed', function () { + $collection = new TransformedCollection([ + $a = transformSingle(SimpleClass::class), + $b = transformSingle(TypeScriptAttributedClass::class), + ]); + + $a->changed = false; + $b->changed = false; + + expect($collection->hasChanges())->toBeFalse(); + + $a->changed = true; + + expect($collection->hasChanges())->toBeTrue(); +}); + +it('can remove a transformed item by reference', function () { + $collection = new TransformedCollection([ + transformSingle(SimpleClass::class), + ]); + + $collection->remove(new ClassStringReference(SimpleClass::class)); + + expect($collection->has(new ClassStringReference(SimpleClass::class)))->toBeFalse(); +}); + +it('can remove a transformed item by reference and update references', function () { + $collection = TypeScriptTransformer::create( + TypeScriptTransformerConfigFactory::create() + ->transformer(new AllClassTransformer()) + ->watchDirectories(__DIR__.'/../Fakes/Circular') + )->resolveTransformedCollection(); + + foreach ($collection as $transformed) { + $transformed->changed = false; + } + + $collection->remove($referenceA = new ClassStringReference(CircularA::class)); + + expect($collection)->toHaveCount(1); + + $transformedB = $collection->get($referenceB = new ClassStringReference(CircularB::class)); + + expect($transformedB->changed)->toBeTrue(); + expect($transformedB->missingReferences)->toHaveCount(1); + expect($transformedB->missingReferences)->toHaveKey($referenceA->getKey()); + expect($transformedB->missingReferences[$referenceA->getKey()]) + ->toBeArray() + ->each() + ->toBeInstanceOf(TypeReference::class); +}); diff --git a/tests/Fakes/Integration/Enum.php b/tests/Fakes/Integration/Enum.php deleted file mode 100644 index 10517116..00000000 --- a/tests/Fakes/Integration/Enum.php +++ /dev/null @@ -1,9 +0,0 @@ - */ - public $complex_union; - - public Enum $enum; - - public Exception $non_typescript_type; - - /** @var IntegrationItem[] */ - public array $array_of_reference; - - public DateTime $replacement_type; - - /** @var \DateTime */ - public $annotated_replacement_type; - - /** @var \DateTime[] */ - public array $array_annotated_replacement_type; - - public LevelUpClass $level_up_class; - - #[Hidden] - public string $hidden; - - public readonly string $readonly; - - #[Optional] - public string $optional; - - /** - * @param array $constructor_annotated_array - */ - public function __construct( - public array $constructor_annotated_array, - /** @var array */ - public array $constructor_inline_annotated_array, - ) { - } -} diff --git a/tests/Fakes/Integration/IntegrationItem.php b/tests/Fakes/Integration/IntegrationItem.php deleted file mode 100644 index 1a7bd1ff..00000000 --- a/tests/Fakes/Integration/IntegrationItem.php +++ /dev/null @@ -1,8 +0,0 @@ -typeScriptNode)->toBeInstanceOf(TypeScriptNamedNode::class); + expect($transformed->getName())->toBe('StringBackedEnum'); +}); + +it('can get the name of a transformed when having a forwarding named node', function () { + $transformed = transformSingle( + \Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\StringBackedEnum::class, + new EnumTransformer(useUnionEnums: true) + ); + + expect($transformed->typeScriptNode)->toBeInstanceOf(TypeScriptForwardingNamedNode::class); + expect($transformed->getName())->toBe('StringBackedEnum'); +}); + +it('can manually set the name of a transformed', function () { + $transformed = transformSingle( + \Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\StringBackedEnum::class, + new EnumTransformer(useUnionEnums: false) + ); + + $transformed->nameAs('MyEnum'); + + expect($transformed->getName())->toBe('MyEnum'); +}); + +it('can add a missing reference', function () { + $missing = transformSingle(SimpleClass::class); + + $transformed = new Transformed( + new TypeScriptObject([ + new TypeScriptProperty('first_name', $typeReferenceA = new TypeReference($missing->reference)), + new TypeScriptProperty('last_name', $typeReferenceB = new TypeReference($missing->reference)), + ]), + new CustomReference('vendor', 'package'), + [], + ); + + $transformed->addMissingReference( + $missing->reference, + $typeReferenceA + ); + + expect($transformed->missingReferences)->toBe([ + $missing->reference->getKey() => [$typeReferenceA], + ]); + + $transformed->addMissingReference( + $missing->reference, + $typeReferenceB + ); + + expect($transformed->missingReferences)->toBe([ + $missing->reference->getKey() => [$typeReferenceA, $typeReferenceB], + ]); +}); + +it('can mark a missing reference as found', function () { + $missing = transformSingle(SimpleClass::class); + + $transformed = new Transformed( + new TypeScriptObject([ + new TypeScriptProperty('first_name', $typeReferenceA = new TypeReference($missing->reference)), + new TypeScriptProperty('last_name', $typeReferenceB = new TypeReference($missing->reference)), + ]), + new CustomReference('vendor', 'package'), + [], + ); + + $transformed->addMissingReference( + $missing->reference, + $typeReferenceA + ); + + $transformed->addMissingReference( + $missing->reference, + $typeReferenceB + ); + + $missing->changed = false; + $transformed->changed = false; + + $transformed->markMissingReferenceFound($missing); + + expect($missing->changed)->toBeFalse(); + expect($transformed->changed)->toBeTrue(); + + expect($transformed->missingReferences)->toBeEmpty(); + expect($transformed->references[$missing->reference->getKey()])->toBe([ + $typeReferenceA, + $typeReferenceB, + ]); + + expect($typeReferenceA->referenced)->toBe($missing); + expect($typeReferenceB->referenced)->toBe($missing); + + expect($missing->referencedBy)->toBe([$transformed->reference->getKey()]); +}); + +it('can mark a reference as missing', function () { + $found = transformSingle(SimpleClass::class); + + $transformed = new Transformed( + new TypeScriptObject([ + new TypeScriptProperty('first_name', $typeReferenceA = new TypeReference($found->reference)), + new TypeScriptProperty('last_name', $typeReferenceB = new TypeReference($found->reference)), + ]), + new CustomReference('vendor', 'package'), + [], + ); + + $connector = new ConnectReferencesAction( + new TypeScriptTransformerLog(new WrappedNullConsole()) + ); + + $connector->execute(new TransformedCollection([$found, $transformed])); + + $transformed->changed = false; + $found->changed = false; + + expect($transformed->missingReferences)->toBeEmpty(); + + $transformed->markReferenceMissing($found); + + expect($transformed->changed)->toBeTrue(); + + expect($transformed->references)->toBeEmpty(); + expect($transformed->missingReferences)->toBe([ + $found->reference->getKey() => [$typeReferenceA, $typeReferenceB], + ]); + + expect($typeReferenceA->referenced)->toBeNull(); + expect($typeReferenceB->referenced)->toBeNull(); +}); From 67b2376f94819bf5536284aa58471a75031acbba Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 7 Aug 2025 09:52:43 +0200 Subject: [PATCH 49/51] wip --- README.md | 25 +- composer.json | 21 +- src/Actions/ParseUserDefinedTypeAction.php | 8 +- ...spilePhpStanTypeToTypeScriptNodeAction.php | 160 +++++++++++-- src/TypeResolvers/DocTypeResolver.php | 11 +- ...ePhpStanTypeToTypeScriptNodeActionTest.php | 214 ++++++++++++++++++ tests/Fakes/PropertyTypes/PhpDocTypesStub.php | 117 ++++++++++ 7 files changed, 507 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 2a497d8b..13b8b7cf 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ $config->transformer(new EnumTransformer(useNativeEnums: true)); // transformers ``` Quick note: transformers are executed in the order they are registered in the configuration, when a transformer cannot -transform a class, the next transformer is executed. +transform a class, the next transformer is checked. Transformers work on PHP classes, we need to tell TypeScript transformer where to look for these classes. This can be done by adding a directory to the configuration: @@ -369,7 +369,7 @@ class Types } ``` -As you an see, when an array value is typed correctly, it will also be typed correctly in TypeScript. +As you can see, when an array value is typed correctly, it will also be typed correctly in TypeScript. It is also possible to use non-typical array key types, like an enum: @@ -381,6 +381,16 @@ class Types } ``` +It is possible to define array shapes like this: + +```php +class Types +{ + /** @var array{age: int, name: string} */ + public array $property; // { age: number, name: string } +} +``` + There are multiple locations where you can add property annotations: ```php @@ -416,7 +426,7 @@ class Types } ``` -If an typed object is not transformed and thus we don't know how it will look like in TypeScript, it will be replaced +If a typed object is not transformed and thus we don't know how it will look like in TypeScript, it will be replaced by `unknown`. It is possible to replace these unknown types with a TypeScript type, without transforming them, keep reading to learn how to do that. @@ -579,7 +589,7 @@ $config->replaceType(DateTimeInterface::class, function (TypeReference $referenc Internally the package uses TypeScript nodes to represent TypeScript types, these nodes can be used to build complex types and it is possible to create your own nodes. -For example, a TypeScript alias is representing a User object looks like this: +For example, a TypeScript alias is representing a User object will look like this: ```php use Spatie\TypeScriptTransformer\TypeScriptNodes; @@ -670,7 +680,7 @@ When a class cannot be transformed, the next transformer in the list will be exe Most of the time, transforming a class comes down to taking all the properties and transforming them to a TypeScript object with properties, the package provides an easy-to-extend class for this called `ClassTransformer`. -You can create your own by extending the `ClassTransformer` and implementing the `shouldTransform` method: +You can create your own version by extending the `ClassTransformer` and implementing the `shouldTransform` method: ```php use Spatie\TypeScriptTransformer\Transformers\ClassTransformer; @@ -817,7 +827,8 @@ A `TypesProvider` implements the `TypeProvider` interface: ```php namespace Spatie\TypeScriptTransformer\TypeProviders; -use Spatie\TypeScriptTransformer\Collections\TransformedCollection;use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; +use Spatie\TypeScriptTransformer\Collections\TransformedCollection; +use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; interface TypesProvider { @@ -1196,7 +1207,7 @@ The steps look as following: 6. Write those files to disk 7. Format the files -The two hooking points above can be used to run a visitor on the collection of Transformed types: +The two hooking points below can be used to run a visitor on the collection of Transformed types: ```php use Spatie\TypeScriptTransformer\Visitor\VisitorClosureType; diff --git a/composer.json b/composer.json index 6bec3bf3..93b0e87d 100644 --- a/composer.json +++ b/composer.json @@ -16,8 +16,8 @@ ], "require": { "php": "^8.2", - "illuminate/contracts": "^10.0|^11.0", - "phpstan/phpdoc-parser": "^1.13", + "illuminate/contracts": "^12.0", + "phpstan/phpdoc-parser": "^2.0", "roave/better-reflection": "^6.41", "spatie/file-system-watcher": "^1.1", "spatie/laravel-package-tools": "^1.14.0", @@ -27,15 +27,16 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.0", "laravel/pint": "^1.0", - "nunomaduro/collision": "^7.9", - "nunomaduro/larastan": "^2.0.1", - "orchestra/testbench": "^8.0|^9.0", - "pestphp/pest": "^2.0", - "pestphp/pest-plugin-arch": "^2.0", - "pestphp/pest-plugin-laravel": "^2.0", + "nunomaduro/collision": "^8.0", + "nunomaduro/larastan": "^3.0", + "orchestra/testbench": "^10.0", + "pestphp/pest": "^3.0", + "pestphp/pest-plugin-arch": "^3.0", + "pestphp/pest-plugin-laravel": "^3.0", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan" : "^2.0", "spatie/laravel-ray": "^1.26", "spatie/pest-plugin-snapshots": "^2.1", "spatie/ray": "^1.41", diff --git a/src/Actions/ParseUserDefinedTypeAction.php b/src/Actions/ParseUserDefinedTypeAction.php index 577f040a..30c338ef 100644 --- a/src/Actions/ParseUserDefinedTypeAction.php +++ b/src/Actions/ParseUserDefinedTypeAction.php @@ -6,6 +6,7 @@ use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; +use PHPStan\PhpDocParser\ParserConfig; use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; use Spatie\TypeScriptTransformer\Support\Concerns\Instanceable; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; @@ -17,11 +18,12 @@ class ParseUserDefinedTypeAction protected TypeParser $typeParser; public function __construct( - protected ConstExprParser $constExprParser = new ConstExprParser(), - protected Lexer $lexer = new Lexer(), + protected ParserConfig $parserConfig = new ParserConfig(usedAttributes: []), + protected ConstExprParser $constExprParser = new ConstExprParser(new ParserConfig(usedAttributes: [])), + protected Lexer $lexer = new Lexer(new ParserConfig(usedAttributes: [])), protected TranspilePhpStanTypeToTypeScriptNodeAction $transpilePhpStanTypeToTypeScriptNodeAction = new TranspilePhpStanTypeToTypeScriptNodeAction(), ) { - $this->typeParser = new TypeParser($constExprParser); + $this->typeParser = new TypeParser($this->parserConfig, $constExprParser); } public function execute(string $type, ?PhpClassNode $node = null): TypeScriptNode diff --git a/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php b/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php index 8ef703de..7119cdff 100644 --- a/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php +++ b/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php @@ -3,9 +3,12 @@ namespace Spatie\TypeScriptTransformer\Actions; use Exception; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; +use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; @@ -24,6 +27,7 @@ use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptGeneric; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIntersection; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptLiteral; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNull; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNumber; @@ -65,11 +69,37 @@ protected function identifierNode( return new TypeScriptAny(); } - if ($node->name === 'string' || $node->name === 'class-string') { + if ($node->name === 'string' + || $node->name === 'class-string' + || $node->name === 'interface-string' + || $node->name === 'trait-string' + || $node->name === 'callable-string' + || $node->name === 'enum-string' + || $node->name === 'lowercase-string' + || $node->name === 'uppercase-string' + || $node->name === 'literal-string' + || $node->name === 'numeric-string' + || $node->name === 'non-empty-string' + || $node->name === 'non-empty-lowercase-string' + || $node->name === 'non-empty-uppercase-string' + || $node->name === 'truthy-string' + || $node->name === 'non-falsy-string' + || $node->name === 'non-empty-literal-string' + ) { return new TypeScriptString(); } - if ($node->name === 'float' || $node->name === 'double' || $node->name === 'int' || $node->name === 'integer') { + if ($node->name === 'float' + || $node->name === 'double' + || $node->name === 'int' + || $node->name === 'integer' + || $node->name === 'positive-int' + || $node->name === 'negative-int' + || $node->name === 'non-positive-int' + || $node->name === 'non-negative-int' + || $node->name === 'non-zero-int' + || $node->name === 'numeric' + ) { return new TypeScriptNumber(); } @@ -77,6 +107,14 @@ protected function identifierNode( return new TypeScriptBoolean(); } + if ($node->name === 'scalar') { + return new TypeScriptUnion([ + new TypeScriptNumber(), + new TypeScriptString(), + new TypeScriptBoolean(), + ]); + } + if ($node->name === 'void') { return new TypeScriptVoid(); } @@ -93,10 +131,6 @@ protected function identifierNode( return new TypeScriptNull(); } - if ($node->name === 'self' || $node->name === 'static') { - return new TypeReference(new ClassStringReference($phpClassNode->getName())); - } - if ($node->name === 'object') { return new TypeScriptObject([]); } @@ -108,24 +142,41 @@ protected function identifierNode( ]); } - if (class_exists($node->name) || interface_exists($node->name)) { - return new TypeReference(new ClassStringReference($node->name)); + $className = $this->resolveClass($node->name, $phpClassNode); + + if ($className) { + return new TypeReference(new ClassStringReference($className)); + } + + return new TypeScriptUnknown(); + } + + protected function resolveClass( + string $className, + ?PhpClassNode $phpClassNode + ): ?string { + if ($className === 'self' || $className === 'static' || $className === '$this') { + return $phpClassNode?->getName(); + } + + if (class_exists($className) || interface_exists($className)) { + return $className; } if ($phpClassNode === null) { - return new TypeScriptUnknown(); + return null; } $referenced = $this->findClassNameFqcnAction->execute( $phpClassNode, - $node->name + $className ); if (class_exists($referenced) || interface_exists($referenced)) { - return new TypeReference(new ClassStringReference($referenced)); + return $referenced; } - return new TypeScriptUnknown(); + return null; } protected function arrayTypeNode( @@ -143,8 +194,13 @@ protected function arrayShapeNode( ): TypeScriptObject { return new TypeScriptObject(array_map( function (ArrayShapeItemNode|ObjectShapeItemNode $item) use ($phpClassNode) { + $name = match ($item->keyName::class) { + IdentifierTypeNode::class => $item->keyName->name, + ConstExprStringNode::class => $item->keyName->value, + }; + return new TypeScriptProperty( - (string) $item->keyName, + $name, $this->execute($item->valueType, $phpClassNode), isOptional: $item->optional ); @@ -194,26 +250,27 @@ protected function genericNode( GenericTypeNode $node, ?PhpClassNode $phpClassNode ): TypeScriptNode { - if ($node->type->name === 'array' || $node->type->name === 'Array') { + if ($node->type->name === 'array' + || $node->type->name === 'Array' + || $node->type->name === 'non-empty-array' + || $node->type->name === 'list' + || $node->type->name === 'non-empty-list' + ) { return $this->genericArrayNode($node, $phpClassNode); } - $type = $this->execute($node->type, $phpClassNode); + if ($node->type->name === 'int') { + return new TypeScriptNumber(); + } - if ($type instanceof TypeScriptString) { - return $type; // class-string case + if ($node->type->name === 'key-of' || $node->type->name === 'value-of') { + return $this->keyOrValueOfGenericNode($node, $phpClassNode); } - return new TypeScriptGeneric( - $type, - array_map( - fn (TypeNode $type) => $this->execute($type, $phpClassNode), - $node->genericTypes - ) - ); + return $this->defaultGenericNode($node, $phpClassNode); } - private function genericArrayNode(GenericTypeNode $node, ?PhpClassNode $phpClassNode): TypeScriptGeneric|TypeScriptArray + protected function genericArrayNode(GenericTypeNode $node, ?PhpClassNode $phpClassNode): TypeScriptGeneric|TypeScriptArray { $genericTypes = count($node->genericTypes); @@ -241,4 +298,57 @@ private function genericArrayNode(GenericTypeNode $node, ?PhpClassNode $phpClass [$key, $value,] ); } + + protected function keyOrValueOfGenericNode(GenericTypeNode $node, ?PhpClassNode $phpClassNode): TypeScriptNode + { + if (count($node->genericTypes) === 1 + && $node->genericTypes[0] instanceof ConstTypeNode + && $node->genericTypes[0]->constExpr instanceof ConstFetchNode + ) { + return $this->keyOrValueOfArrayConstNode($node, $phpClassNode, $node->genericTypes[0]->constExpr); + } + + + return $this->defaultGenericNode($node, $phpClassNode); + } + + protected function keyOrValueOfArrayConstNode( + GenericTypeNode $node, + ?PhpClassNode $phpClassNode, + ConstFetchNode $constFetchNode, + ): TypeScriptNode { + $class = $this->resolveClass($constFetchNode->className, $phpClassNode); + + if ($class === null) { + return $this->defaultGenericNode($node, $phpClassNode); + } + + $array = $class::{$constFetchNode->name}; + + $items = $node->type->name === 'key-of' + ? array_keys($array) + : array_values($array); + + return new TypeScriptUnion(array_map( + fn (mixed $key) => new TypeScriptLiteral($key), + $items + )); + } + + protected function defaultGenericNode(GenericTypeNode $node, ?PhpClassNode $phpClassNode): TypeScriptNode + { + $type = $this->execute($node->type, $phpClassNode); + + if ($type instanceof TypeScriptString) { + return $type; // class-string case + } + + return new TypeScriptGeneric( + $type, + array_map( + fn (TypeNode $type) => $this->execute($type, $phpClassNode), + $node->genericTypes + ) + ); + } } diff --git a/src/TypeResolvers/DocTypeResolver.php b/src/TypeResolvers/DocTypeResolver.php index 832145ab..1f1e52c9 100644 --- a/src/TypeResolvers/DocTypeResolver.php +++ b/src/TypeResolvers/DocTypeResolver.php @@ -9,6 +9,7 @@ use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; +use PHPStan\PhpDocParser\ParserConfig; use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; use Spatie\TypeScriptTransformer\PhpNodes\PhpMethodNode; use Spatie\TypeScriptTransformer\PhpNodes\PhpPropertyNode; @@ -26,11 +27,13 @@ class DocTypeResolver public function __construct() { - $constExprParser = new ConstExprParser(); - $this->typeParser = new TypeParser($constExprParser); + $config = new ParserConfig(usedAttributes: []); - $this->docParser = new PhpDocParser($this->typeParser, $constExprParser); - $this->lexer = new Lexer(); + $constExprParser = new ConstExprParser($config); + $this->typeParser = new TypeParser($config, $constExprParser); + + $this->docParser = new PhpDocParser($config, $this->typeParser, $constExprParser); + $this->lexer = new Lexer($config); } public function class(PhpClassNode $phpClassNode): ?ParsedClass diff --git a/tests/Actions/TranspilePhpStanTypeToTypeScriptNodeActionTest.php b/tests/Actions/TranspilePhpStanTypeToTypeScriptNodeActionTest.php index 28d9c822..ffeba948 100644 --- a/tests/Actions/TranspilePhpStanTypeToTypeScriptNodeActionTest.php +++ b/tests/Actions/TranspilePhpStanTypeToTypeScriptNodeActionTest.php @@ -15,6 +15,7 @@ use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptGeneric; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIntersection; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptLiteral; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNull; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNumber; @@ -64,6 +65,67 @@ 'integer', new TypeScriptNumber(), ]; + yield [ + 'positiveInt', + new TypeScriptNumber(), + ]; + + yield [ + 'negativeInt', + new TypeScriptNumber(), + ]; + + yield [ + 'nonPositiveInt', + new TypeScriptNumber(), + ]; + + yield [ + 'nonNegativeInt', + new TypeScriptNumber(), + ]; + + yield [ + 'nonZeroInt', + new TypeScriptNumber(), + ]; + + yield [ + 'intRange', + new TypeScriptNumber(), + ]; + + yield [ + 'intRangeMin', + new TypeScriptNumber(), + ]; + + yield [ + 'intRangeMax', + new TypeScriptNumber(), + ]; + + yield [ + 'numeric', + new TypeScriptNumber(), + ]; + + yield [ + 'scalar', + new TypeScriptUnion([ + new TypeScriptNumber(), + new TypeScriptString(), + new TypeScriptBoolean(), + ]), + ]; + + yield [ + 'arrayKey', + new TypeScriptUnion([ + new TypeScriptString(), + new TypeScriptNumber(), + ]), + ]; yield [ 'float', @@ -210,6 +272,32 @@ ), ]; + yield [ + 'nonEmptyArrayGeneric', + new TypeScriptArray([new TypeScriptString()]), + ]; + + yield [ + 'nonEmptyArrayGenericWithKey', + new TypeScriptGeneric( + new TypeScriptIdentifier('Record'), + [ + new TypeScriptString(), + new TypeScriptString(), + ] + ), + ]; + + yield [ + 'list', + new TypeScriptArray([new TypeScriptString()]), + ]; + + yield [ + 'nonEmptyList', + new TypeScriptArray([new TypeScriptString()]), + ]; + yield [ 'typeArray', new TypeScriptArray([new TypeScriptString()]), @@ -246,6 +334,96 @@ new TypeScriptString(), ]; + yield [ + 'interfaceString', + new TypeScriptString(), + ]; + + yield [ + 'interfaceStringGeneric', + new TypeScriptString(), + ]; + + yield [ + 'traitString', + new TypeScriptString(), + ]; + + yield [ + 'traitStringGeneric', + new TypeScriptString(), + ]; + + yield [ + 'callableString', + new TypeScriptString(), + ]; + + yield [ + 'callableStringGeneric', + new TypeScriptString(), + ]; + + yield [ + 'enumString', + new TypeScriptString(), + ]; + + yield [ + 'enumStringGeneric', + new TypeScriptString(), + ]; + + yield [ + 'lowercaseString', + new TypeScriptString(), + ]; + + yield [ + 'uppercaseString', + new TypeScriptString(), + ]; + + yield [ + 'literalString', + new TypeScriptString(), + ]; + + yield [ + 'numericString', + new TypeScriptString(), + ]; + + yield [ + 'nonEmptyString', + new TypeScriptString(), + ]; + + yield [ + 'nonEmptyLowercaseString', + new TypeScriptString(), + ]; + + yield [ + 'nonEmptyUppercaseString', + new TypeScriptString(), + ]; + + yield [ + 'truthyString', + new TypeScriptString(), + ]; + + yield [ + 'nonFalsyString', + new TypeScriptString(), + ]; + + yield [ + 'nonEmptyLiteralString', + new TypeScriptString(), + ]; + yield [ 'reference', new TypeReference(new ClassStringReference(Collection::class)), @@ -266,4 +444,40 @@ ] ), ]; + + yield [ + 'keyOfArrayConst', + new TypeScriptUnion([ + new TypeScriptLiteral('script'), + new TypeScriptLiteral('type'), + ]), + ]; + + yield [ + 'valueOfArrayConst', + new TypeScriptUnion([ + new TypeScriptLiteral(2), + new TypeScriptLiteral(1), + ]), + ]; + + // yield [ + // 'keyOfEnum', + // new TypeScriptUnion([ + // new TypeScriptLiteral('john'), + // new TypeScriptLiteral('paul'), + // new TypeScriptLiteral('george'), + // new TypeScriptLiteral('ringo'), + // ]) + // ]; + // + // yield [ + // 'valueOfEnum', + // new TypeScriptUnion([ + // new TypeScriptLiteral('john'), + // new TypeScriptLiteral('paul'), + // new TypeScriptLiteral('george'), + // new TypeScriptLiteral('ringo'), + // ]) + // ]; }); diff --git a/tests/Fakes/PropertyTypes/PhpDocTypesStub.php b/tests/Fakes/PropertyTypes/PhpDocTypesStub.php index 40cf2164..151e3ba0 100644 --- a/tests/Fakes/PropertyTypes/PhpDocTypesStub.php +++ b/tests/Fakes/PropertyTypes/PhpDocTypesStub.php @@ -3,10 +3,16 @@ namespace Spatie\TypeScriptTransformer\Tests\Fakes\PropertyTypes; use Illuminate\Support\Collection; +use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\StringBackedEnum; use stdClass; class PhpDocTypesStub extends stdClass { + public const ARRAYCONST = [ + 'script' => 2, + 'type' => 1, + ]; + /** @var string */ public $string; @@ -22,6 +28,33 @@ class PhpDocTypesStub extends stdClass /** @var int */ public $integer; + /** @var positive-int */ + public $positiveInt; + + /** @var negative-int */ + public $negativeInt; + + /** @var non-positive-int */ + public $nonPositiveInt; + + /** @var non-negative-int */ + public $nonNegativeInt; + + /** @var non-zero-int */ + public $nonZeroInt; + + /** @var int<0, 100> */ + public $intRange; + + /** @var int */ + public $intRangeMin; + + /** @var int<0, max> */ + public $intRangeMax; + + /** @var numeric */ + public $numeric; + /** @var float */ public $float; @@ -31,6 +64,12 @@ class PhpDocTypesStub extends stdClass /** @var mixed */ public $mixed; + /** @var scalar */ + public $scalar; + + /** @var array-key */ + public $arrayKey; + /** @var void */ public $void; @@ -88,6 +127,18 @@ class PhpDocTypesStub extends stdClass /** @var array */ public $arrayGenericWithArrayKey; + /** @var non-empty-array */ + public $nonEmptyArrayGeneric; + + /** @var non-empty-array */ + public $nonEmptyArrayGenericWithKey; + + /** @var list */ + public $list; + + /** @var non-empty-list */ + public $nonEmptyList; + /** @var string[] */ public $typeArray; @@ -103,6 +154,60 @@ class PhpDocTypesStub extends stdClass /** @var class-string */ public $classStringGeneric; + /** @var interface-string */ + public $interfaceString; + + /** @var interface-string */ + public $interfaceStringGeneric; + + /** @var trait-string */ + public $traitString; + + /** @var trait-string */ + public $traitStringGeneric; + + /** @var callable-string */ + public $callableString; + + /** @var callable-string */ + public $callableStringGeneric; + + /** @var enum-string */ + public $enumString; + + /** @var enum-string */ + public $enumStringGeneric; + + /** @var lowercase-string */ + public $lowercaseString; + + /** @var uppercase-string */ + public $uppercaseString; + + /** @var literal-string */ + public $literalString; + + /** @var numeric-string */ + public $numericString; + + /** @var non-empty-string */ + public $nonEmptyString; + + /** @var non-empty-lowercase-string */ + public $nonEmptyLowercaseString; + + /** @var non-empty-uppercase-string */ + public $nonEmptyUppercaseString; + + /** @var truthy-string */ + public $truthyString; + + /** @var non-falsy-string */ + public $nonFalsyString; + + /** @var non-empty-literal-string */ + public $nonEmptyLiteralString; + /** @var \Illuminate\Support\Collection */ public $reference; @@ -111,4 +216,16 @@ class PhpDocTypesStub extends stdClass /** @var Collection */ public $generic; + + /** @var key-of */ + public $keyOfArrayConst; + + /** @var value-of */ + public $valueOfArrayConst; + + /** @var key-of */ + public $keyOfEnum; + + /** @var value-of */ + public $valueOfEnum; } From 6c8e76f040bdf794f5154e354b87a3eeb02b9ed7 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 7 Aug 2025 10:50:53 +0200 Subject: [PATCH 50/51] wip --- .claude/settings.local.json | 9 +++ composer.json | 1 + phpstan-baseline.neon | 7 +++ phpstan.neon | 10 +++ ...spilePhpStanTypeToTypeScriptNodeAction.php | 62 ++++++++++--------- src/Data/ImportLocation.php | 7 +-- .../LaravelNamedRouteTypesProvider.php | 8 +-- .../LaravelRouteActionTypesProvider.php | 2 +- src/Laravel/Support/WithoutRoutes.php | 2 +- src/PhpNodes/PhpClassNode.php | 6 +- src/PhpNodes/PhpTypeNode.php | 2 + src/Support/Concerns/Instanceable.php | 4 +- src/Support/TypeScriptTransformerLog.php | 11 ---- src/Support/WritingContext.php | 2 +- src/Transformed/Untransformable.php | 6 +- src/Transformers/ClassTransformer.php | 4 +- .../EnumProviders/PhpEnumProvider.php | 5 ++ src/Transformers/InterfaceTransformer.php | 2 +- src/TypeResolvers/DocTypeResolver.php | 6 +- src/TypeScriptNodes/TypeScriptExport.php | 2 +- .../TypeScriptGenericTypeVariable.php | 4 +- src/TypeScriptTransformerConfig.php | 16 ++--- ...velRoutControllerCollectionsActionTest.php | 16 +++-- .../LaravelRouteActionTypesProviderTest.php | 16 ----- tests/Pest.php | 4 +- tests/Transformers/EnumTransformerTest.php | 2 +- 26 files changed, 116 insertions(+), 100 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon delete mode 100644 tests/Laravel/LaravelRouteActionTypesProviderTest.php diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..3d9d721c --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(vendor/bin/pest:*)", + "Bash(vendor/bin/phpstan analyse:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index 93b0e87d..597ed0b6 100644 --- a/composer.json +++ b/composer.json @@ -55,6 +55,7 @@ "scripts": { "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", "analyse": "vendor/bin/phpstan analyse", + "baseline": "vendor/bin/phpstan analyse --generate-baseline", "test": "vendor/bin/pest", "test-coverage": "vendor/bin/pest --coverage", "format": "vendor/bin/pint" diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..5d6834da --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,7 @@ +parameters: + ignoreErrors: + - + message: '#^Instanceof between Closure and Closure will always evaluate to true\.$#' + identifier: instanceof.alwaysTrue + count: 1 + path: src/TypeScriptTransformerConfigFactory.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..a864e9bc --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,10 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 5 + paths: + - src + tmpDir: build/phpstan + checkOctaneCompatibility: true + checkModelProperties: true diff --git a/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php b/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php index 7119cdff..7078a67d 100644 --- a/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php +++ b/src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php @@ -3,9 +3,9 @@ namespace Spatie\TypeScriptTransformer\Actions; use Exception; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; -use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; @@ -13,7 +13,6 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; -use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; @@ -192,21 +191,28 @@ protected function arrayShapeNode( ArrayShapeNode|ObjectShapeNode $node, ?PhpClassNode $phpClassNode ): TypeScriptObject { - return new TypeScriptObject(array_map( - function (ArrayShapeItemNode|ObjectShapeItemNode $item) use ($phpClassNode) { - $name = match ($item->keyName::class) { - IdentifierTypeNode::class => $item->keyName->name, - ConstExprStringNode::class => $item->keyName->value, - }; - - return new TypeScriptProperty( - $name, - $this->execute($item->valueType, $phpClassNode), - isOptional: $item->optional - ); - }, - $node->items - )); + $properties = []; + + foreach ($node->items as $item) { + $name = match ($item->keyName::class) { + IdentifierTypeNode::class => $item->keyName->name, + ConstExprStringNode::class => $item->keyName->value, + ConstExprIntegerNode::class => (string) $item->keyName->value, + default => null, + }; + + if ($name === null) { + continue; + } + + $properties[] = new TypeScriptProperty( + $name, + $this->execute($item->valueType, $phpClassNode), + isOptional: $item->optional + ); + } + + return new TypeScriptObject($properties); } protected function nullableNode( @@ -301,22 +307,14 @@ protected function genericArrayNode(GenericTypeNode $node, ?PhpClassNode $phpCla protected function keyOrValueOfGenericNode(GenericTypeNode $node, ?PhpClassNode $phpClassNode): TypeScriptNode { - if (count($node->genericTypes) === 1 - && $node->genericTypes[0] instanceof ConstTypeNode - && $node->genericTypes[0]->constExpr instanceof ConstFetchNode + if (count($node->genericTypes) !== 1 + || ! $node->genericTypes[0] instanceof ConstTypeNode + || ! $node->genericTypes[0]->constExpr instanceof ConstFetchNode ) { - return $this->keyOrValueOfArrayConstNode($node, $phpClassNode, $node->genericTypes[0]->constExpr); + return $this->defaultGenericNode($node, $phpClassNode); } - - return $this->defaultGenericNode($node, $phpClassNode); - } - - protected function keyOrValueOfArrayConstNode( - GenericTypeNode $node, - ?PhpClassNode $phpClassNode, - ConstFetchNode $constFetchNode, - ): TypeScriptNode { + $constFetchNode = $node->genericTypes[0]->constExpr; $class = $this->resolveClass($constFetchNode->className, $phpClassNode); if ($class === null) { @@ -343,6 +341,10 @@ protected function defaultGenericNode(GenericTypeNode $node, ?PhpClassNode $phpC return $type; // class-string case } + if (! ($type instanceof TypeReference || $type instanceof TypeScriptIdentifier)) { + return new TypeScriptUnknown(); + } + return new TypeScriptGeneric( $type, array_map( diff --git a/src/Data/ImportLocation.php b/src/Data/ImportLocation.php index 31a945ec..9a1adf2e 100644 --- a/src/Data/ImportLocation.php +++ b/src/Data/ImportLocation.php @@ -33,13 +33,8 @@ public function getAliasOrNameForReference(Reference $reference): ?string return null; } - public function toTypeScriptNode(): ?TypeScriptImport + public function toTypeScriptNode(): TypeScriptImport { - if ($this->relativePath === null) { - // current path - return null; - } - return new TypeScriptImport($this->relativePath, $this->importNames); } } diff --git a/src/Laravel/LaravelNamedRouteTypesProvider.php b/src/Laravel/LaravelNamedRouteTypesProvider.php index 723d4f8f..79b57df9 100644 --- a/src/Laravel/LaravelNamedRouteTypesProvider.php +++ b/src/Laravel/LaravelNamedRouteTypesProvider.php @@ -139,7 +139,7 @@ protected function parseRouteCollection(RouteCollection $collection): TypeScript if ($entity instanceof RouteController) { return collect($entity->actions) - ->filter(fn (RouteControllerAction $action) => $action->name) + ->filter(fn (RouteControllerAction $action) => $action->name !== null) ->values() ->map($mappingFunction); } @@ -158,7 +158,7 @@ protected function parseRouteParameterCollection(RouteParameterCollection $colle }, $collection->parameters)); } - protected function parseRouteParameter(RouteParameter $parameter): TypeScriptNode + protected function parseRouteParameter(RouteParameter $parameter): TypeScriptProperty { return new TypeScriptProperty( $parameter->name, @@ -183,7 +183,7 @@ protected function routeCollectionToJson(RouteCollection $collection): string if ($controller instanceof RouteController) { return collect($controller->actions) - ->filter(fn (RouteControllerAction $action) => $action->name) + ->filter(fn (RouteControllerAction $action) => $action->name !== null) ->values() ->mapWithKeys($mappingFunction); } @@ -192,7 +192,7 @@ protected function routeCollectionToJson(RouteCollection $collection): string }); $closures = collect($collection->closures) - ->filter(fn (RouteClosure $closure) => $closure->name) + ->filter(fn (RouteClosure $closure) => $closure->name !== null) ->values() ->mapWithKeys(function (RouteClosure $closure) use ($mappingFunction) { return $mappingFunction($closure); diff --git a/src/Laravel/LaravelRouteActionTypesProvider.php b/src/Laravel/LaravelRouteActionTypesProvider.php index 6f8bd79e..eadd44c0 100644 --- a/src/Laravel/LaravelRouteActionTypesProvider.php +++ b/src/Laravel/LaravelRouteActionTypesProvider.php @@ -249,7 +249,7 @@ protected function parseRouteParameterCollection(RouteParameterCollection $colle }, $collection->parameters)); } - protected function parseRouteParameter(RouteParameter $parameter): TypeScriptNode + protected function parseRouteParameter(RouteParameter $parameter): TypeScriptProperty { return new TypeScriptProperty( $parameter->name, diff --git a/src/Laravel/Support/WithoutRoutes.php b/src/Laravel/Support/WithoutRoutes.php index 527f8c25..c059ef48 100644 --- a/src/Laravel/Support/WithoutRoutes.php +++ b/src/Laravel/Support/WithoutRoutes.php @@ -19,7 +19,7 @@ public function shouldHide(Route $route): bool public static function satisfying(Closure $closure): self { - return new static($closure); + return new self($closure); } public static function named(string ...$names): self diff --git a/src/PhpNodes/PhpClassNode.php b/src/PhpNodes/PhpClassNode.php index 7f3c5440..ed60afa1 100644 --- a/src/PhpNodes/PhpClassNode.php +++ b/src/PhpNodes/PhpClassNode.php @@ -20,19 +20,19 @@ public function __construct( ) { } - public static function fromClassString(string $classString): static + public static function fromClassString(string $classString): self { return self::fromReflection(new ReflectionClass($classString)); } - public static function fromReflection(ReflectionClass|RoaveReflectionClass $reflection): static + public static function fromReflection(ReflectionClass|RoaveReflectionClass $reflection): self { if ($reflection instanceof RoaveReflectionEnum) { return new PhpEnumNode($reflection); } if ($reflection->isEnum()) { - return new PhpEnumNode(new ReflectionEnum($reflection->name)); + return new PhpEnumNode(new ReflectionEnum($reflection->getName())); } return new self($reflection); diff --git a/src/PhpNodes/PhpTypeNode.php b/src/PhpNodes/PhpTypeNode.php index c7055685..b113d8d3 100644 --- a/src/PhpNodes/PhpTypeNode.php +++ b/src/PhpNodes/PhpTypeNode.php @@ -2,6 +2,7 @@ namespace Spatie\TypeScriptTransformer\PhpNodes; +use InvalidArgumentException; use ReflectionIntersectionType; use ReflectionNamedType; use ReflectionType; @@ -25,6 +26,7 @@ public static function fromReflection( ReflectionNamedType::class, RoaveReflectionNamedType::class => new PhpNamedTypeNode($reflection), ReflectionUnionType::class, RoaveReflectionUnionType::class => new PhpUnionTypeNode($reflection), ReflectionIntersectionType::class, RoaveReflectionIntersectionType::class => new PhpIntersectionTypeNode($reflection), + default => throw new InvalidArgumentException('Unsupported reflection type'), }; } diff --git a/src/Support/Concerns/Instanceable.php b/src/Support/Concerns/Instanceable.php index abee350f..d0089976 100644 --- a/src/Support/Concerns/Instanceable.php +++ b/src/Support/Concerns/Instanceable.php @@ -6,8 +6,8 @@ trait Instanceable { protected static ?self $instance = null; - public static function instance(): static + public static function instance(): self { - return static::$instance ??= new static(); + return self::$instance ??= new self(); } } diff --git a/src/Support/TypeScriptTransformerLog.php b/src/Support/TypeScriptTransformerLog.php index 081af5b8..a93fc0d5 100644 --- a/src/Support/TypeScriptTransformerLog.php +++ b/src/Support/TypeScriptTransformerLog.php @@ -17,17 +17,6 @@ public static function createNullLog(): self return new self(new WrappedNullConsole()); } - public static function boot(WrappedConsole $console): self - { - var_dump(debug_backtrace()); - - if (self::$instance !== null) { - throw new \Exception('TypeScriptTransformerLog already booted'); - } - - return self::$instance = new self($console); - } - public function info(string $message): self { $this->wrappedConsole->info($message); diff --git a/src/Support/WritingContext.php b/src/Support/WritingContext.php index 9bdfee48..c3f40713 100644 --- a/src/Support/WritingContext.php +++ b/src/Support/WritingContext.php @@ -8,7 +8,7 @@ class WritingContext { /** - * @param callable(Reference):string $referenceWriter + * @param Closure(Reference):string $referenceWriter */ public function __construct( public Closure $referenceWriter, diff --git a/src/Transformed/Untransformable.php b/src/Transformed/Untransformable.php index 1cd372a4..d1efaea7 100644 --- a/src/Transformed/Untransformable.php +++ b/src/Transformed/Untransformable.php @@ -2,13 +2,15 @@ namespace Spatie\TypeScriptTransformer\Transformed; +use Spatie\TypeScriptTransformer\Support\Concerns\Instanceable; + class Untransformable { - protected static ?self $self = null; + use Instanceable; public static function create(): self { - return static::$self ??= new static(); + return self::instance(); } private function __construct() diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index 2076298d..b818fc83 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -75,10 +75,10 @@ protected function getTypeScriptNode( return $resolvedAttributeType; } - $classAnnotations = $this->docTypeResolver->class($phpClassNode)?->properties ?? []; + $classAnnotations = $this->docTypeResolver->class($phpClassNode)->properties ?? []; $constructorAnnotations = $phpClassNode->hasMethod('__construct') - ? $this->docTypeResolver->method($phpClassNode->getMethod('__construct'))?->parameters ?? [] + ? $this->docTypeResolver->method($phpClassNode->getMethod('__construct'))->parameters ?? [] : []; $properties = []; diff --git a/src/Transformers/EnumProviders/PhpEnumProvider.php b/src/Transformers/EnumProviders/PhpEnumProvider.php index 454510d0..a12fecfd 100644 --- a/src/Transformers/EnumProviders/PhpEnumProvider.php +++ b/src/Transformers/EnumProviders/PhpEnumProvider.php @@ -2,6 +2,7 @@ namespace Spatie\TypeScriptTransformer\Transformers\EnumProviders; +use InvalidArgumentException; use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; use Spatie\TypeScriptTransformer\PhpNodes\PhpEnumCaseNode; use Spatie\TypeScriptTransformer\PhpNodes\PhpEnumNode; @@ -23,6 +24,10 @@ public function isValidUnion(PhpClassNode $phpClassNode): bool */ public function resolveCases(PhpClassNode|PhpEnumNode $phpClassNode): array { + if (! $phpClassNode instanceof PhpEnumNode) { + throw new InvalidArgumentException('Expected instance of PhpEnumNode.'); + } + return array_map( fn (PhpEnumCaseNode $case) => [ 'name' => $case->getName(), diff --git a/src/Transformers/InterfaceTransformer.php b/src/Transformers/InterfaceTransformer.php index 6f5a6835..830b11ea 100644 --- a/src/Transformers/InterfaceTransformer.php +++ b/src/Transformers/InterfaceTransformer.php @@ -106,7 +106,7 @@ protected function resolveMethodReturnType( TransformationContext $context, ?ParsedMethod $annotation ): TypeScriptNode { - if ($annotation->returnType) { + if ($annotation?->returnType) { return $this->transpilePhpStanTypeToTypeScriptTypeAction->execute( $annotation->returnType, $classNode diff --git a/src/TypeResolvers/DocTypeResolver.php b/src/TypeResolvers/DocTypeResolver.php index 1f1e52c9..4bfbc5ef 100644 --- a/src/TypeResolvers/DocTypeResolver.php +++ b/src/TypeResolvers/DocTypeResolver.php @@ -119,12 +119,14 @@ public function type(string $type): TypeNode protected function parseDocComment( PhpClassNode|PhpMethodNode|PhpPropertyNode $phpNode ): ?PhpDocNode { - if ($phpNode->getDocComment() === false || $phpNode->getDocComment() === null) { + $docComment = $phpNode->getDocComment(); + + if ($docComment === null) { return null; } return $this->docParser->parse( - new TokenIterator($this->lexer->tokenize($phpNode->getDocComment())) + new TokenIterator($this->lexer->tokenize($docComment)) ); } } diff --git a/src/TypeScriptNodes/TypeScriptExport.php b/src/TypeScriptNodes/TypeScriptExport.php index 3f74cc95..bc7f5a78 100644 --- a/src/TypeScriptNodes/TypeScriptExport.php +++ b/src/TypeScriptNodes/TypeScriptExport.php @@ -8,7 +8,7 @@ class TypeScriptExport implements TypeScriptNode, TypeScriptVisitableNode { public function __construct( - public TypeScriptNamedNode|TypeScriptForwardingNamedNode $node, + public (TypeScriptNamedNode&TypeScriptNode)|TypeScriptForwardingNamedNode $node, ) { } diff --git a/src/TypeScriptNodes/TypeScriptGenericTypeVariable.php b/src/TypeScriptNodes/TypeScriptGenericTypeVariable.php index 70fb963a..d9948826 100644 --- a/src/TypeScriptNodes/TypeScriptGenericTypeVariable.php +++ b/src/TypeScriptNodes/TypeScriptGenericTypeVariable.php @@ -17,8 +17,8 @@ public function __construct( public function write(WritingContext $context): string { return "{$this->identifier->write($context)}". - ($this->extends ? " extends {$this->extends?->write($context)}" : ''). - ($this->default ? " = {$this->default?->write($context)}" : ''); + ($this->extends ? " extends {$this->extends->write($context)}" : ''). + ($this->default ? " = {$this->default->write($context)}" : ''); } public function children(): array diff --git a/src/TypeScriptTransformerConfig.php b/src/TypeScriptTransformerConfig.php index 9b51f11d..6bfa57c2 100755 --- a/src/TypeScriptTransformerConfig.php +++ b/src/TypeScriptTransformerConfig.php @@ -11,20 +11,20 @@ class TypeScriptTransformerConfig { /** - * @param array $typeProviders + * @param array|TypesProvider> $typeProviders * @param array $directoriesToWatch * @param array $providedVisitorClosures * @param array $connectedVisitorClosures * @param array $transformers */ public function __construct( - readonly public array $typeProviders, - readonly public Writer $writer, - readonly public ?Formatter $formatter, - readonly public array $directoriesToWatch = [], - readonly public array $providedVisitorClosures = [], - readonly public array $connectedVisitorClosures = [], - readonly public array $transformers = [], + public readonly array $typeProviders, + public readonly Writer $writer, + public readonly ?Formatter $formatter, + public readonly array $directoriesToWatch = [], + public readonly array $providedVisitorClosures = [], + public readonly array $connectedVisitorClosures = [], + public readonly array $transformers = [], ) { } } diff --git a/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php b/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php index 9ab981a1..6c05d50a 100644 --- a/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php +++ b/tests/Laravel/Actions/ResolveLaravelRoutControllerCollectionsActionTest.php @@ -16,7 +16,11 @@ uses(LaravelTestCase::class)->in(__DIR__.'/../'); it('can resolve all possible routes', function (Closure $route, Closure $expectations) { - $route(app(Router::class)); + $router = app(Router::class); + + $router->setRoutes(new \Illuminate\Routing\RouteCollection()); // Laravel registers a storage.local route by default, which we want to ignore in this test. + + $route($router); $routes = app(ResolveLaravelRoutControllerCollectionsAction::class)->execute(null, true); @@ -212,9 +216,13 @@ function (RouteCollection $routes) { WithoutRoutes $withoutRoutes, Closure $expectations ) { - app(Router::class)->get('simple', fn () => 'simple')->name('simple'); - app(Router::class)->get('invokable', InvokableController::class)->name('invokable'); - app(Router::class)->resource('resource', ResourceController::class); + $router = app(Router::class); + + $router->setRoutes(new \Illuminate\Routing\RouteCollection()); // Laravel registers a storage.local route by default, which we want to ignore in this test. + + $router->get('simple', fn () => 'simple')->name('simple'); + $router->get('invokable', InvokableController::class)->name('invokable'); + $router->resource('resource', ResourceController::class); $routes = app(ResolveLaravelRoutControllerCollectionsAction::class)->execute(null, true, [$withoutRoutes]); diff --git a/tests/Laravel/LaravelRouteActionTypesProviderTest.php b/tests/Laravel/LaravelRouteActionTypesProviderTest.php deleted file mode 100644 index 229a6d8f..00000000 --- a/tests/Laravel/LaravelRouteActionTypesProviderTest.php +++ /dev/null @@ -1,16 +0,0 @@ -getActionTypes($route); - - expect($actionTypes)->toBe([ - 'controller' => 'TestController', - 'method' => 'test', - ]); -}); diff --git a/tests/Pest.php b/tests/Pest.php index a93658ac..f87f3de3 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -38,10 +38,10 @@ function transformSingle( $transformTypesAction = new TransformTypesAction(); - [$transformed] = $transformTypesAction->execute( + $results = $transformTypesAction->execute( [$transformer], [PhpClassNode::fromClassString(is_string($class) ? $class : $class::class)], ); - return $transformed ?? Untransformable::create(); + return $results[0] ?? Untransformable::create(); } diff --git a/tests/Transformers/EnumTransformerTest.php b/tests/Transformers/EnumTransformerTest.php index 6d270889..d88a66c8 100644 --- a/tests/Transformers/EnumTransformerTest.php +++ b/tests/Transformers/EnumTransformerTest.php @@ -15,7 +15,7 @@ expect(transformSingle(DateTime::class, new EnumTransformer()))->toBeInstanceOf(Untransformable::class); }); -it('does not transform a unit enum when using union enums', function () { +it('does not transform a unit enum when using unit enums', function () { expect(transformSingle(UnitEnum::class, new EnumTransformer()))->toBeInstanceOf(Untransformable::class); }); From bbafc678307322941ff467025031c85e7089c0fd Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 13 Aug 2025 12:40:54 +0200 Subject: [PATCH 51/51] wip --- src/Actions/DiscoverTypesAction.php | 2 +- src/Actions/TransformTypesAction.php | 1 + src/Actions/WriteFilesAction.php | 109 +++++++++++++++- src/Collections/TransformedCollection.php | 31 +++++ src/FileSystemWatcher.php | 12 +- .../DirectoryDeletedWatchEventHandler.php | 8 +- .../Watch/FileDeletedWatchEventHandler.php | 8 +- .../FileUpdatedOrCreatedWatchEventHandler.php | 56 ++++++-- src/Handlers/Watch/WatchEventHandler.php | 8 ++ .../DataClassPropertyProcessor.php | 24 +++- .../Commands/TransformTypeScriptCommand.php | 4 +- .../Commands/WatchTypeScriptCommand.php | 9 +- src/Laravel/Support/ConsoleLogger.php | 40 ++++++ src/Laravel/Support/WrappedLaravelConsole.php | 34 ----- src/PhpNodes/PhpAttributeNode.php | 78 ++++++----- src/Support/Console/ConsoleLogger.php | 50 ++++++++ src/Support/Console/Logger.php | 14 ++ src/Support/Console/MultiLogger.php | 42 ++++++ src/Support/Console/NullLogger.php | 26 ++++ src/Support/Console/RayLogger.php | 38 ++++++ src/Support/Console/WrappedArrayConsole.php | 29 ----- src/Support/Console/WrappedConsole.php | 14 -- src/Support/Console/WrappedNullConsole.php | 26 ---- src/Support/TransformationContext.php | 4 +- src/Support/TypeScriptTransformerLog.php | 50 ++++++-- src/Support/WriteableFile.php | 5 +- src/Transformed/Transformed.php | 4 + .../AttributedClassTransformer.php | 2 +- src/TypeScriptTransformer.php | 14 +- src/Writers/ModuleWriter.php | 12 +- src/Writers/MultipleFilesWriter.php | 8 ++ tests/Actions/ConnectReferencesActionTest.php | 4 +- tests/Actions/WriteFilesActionTest.php | 66 ++++++++++ .../Fakes/TypesToProvide/ComplexAttribute.php | 17 +++ .../Fakes/TypesToProvide/SimpleAttribute.php | 15 +++ .../TypesToProvide/VariadicAttribute.php | 15 +++ tests/PhpNodes/PhpAttributeNode.php | 121 ++++++++++++++++++ tests/Transformed/TransformedTest.php | 4 +- 38 files changed, 805 insertions(+), 199 deletions(-) create mode 100644 src/Laravel/Support/ConsoleLogger.php delete mode 100644 src/Laravel/Support/WrappedLaravelConsole.php create mode 100644 src/Support/Console/ConsoleLogger.php create mode 100644 src/Support/Console/Logger.php create mode 100644 src/Support/Console/MultiLogger.php create mode 100644 src/Support/Console/NullLogger.php create mode 100644 src/Support/Console/RayLogger.php delete mode 100644 src/Support/Console/WrappedArrayConsole.php delete mode 100644 src/Support/Console/WrappedConsole.php delete mode 100644 src/Support/Console/WrappedNullConsole.php create mode 100644 src/Writers/MultipleFilesWriter.php create mode 100644 tests/Fakes/TypesToProvide/ComplexAttribute.php create mode 100644 tests/Fakes/TypesToProvide/SimpleAttribute.php create mode 100644 tests/Fakes/TypesToProvide/VariadicAttribute.php create mode 100644 tests/PhpNodes/PhpAttributeNode.php diff --git a/src/Actions/DiscoverTypesAction.php b/src/Actions/DiscoverTypesAction.php index 0d87f53b..94f27e6c 100644 --- a/src/Actions/DiscoverTypesAction.php +++ b/src/Actions/DiscoverTypesAction.php @@ -28,7 +28,7 @@ public function execute( return array_values(array_filter(array_map(function (string $discovered) { try { return PhpClassNode::fromReflection(new ReflectionClass($discovered)); - } catch (\ReflectionException) { + } catch (\Throwable) { return null; } }, $discovered))); diff --git a/src/Actions/TransformTypesAction.php b/src/Actions/TransformTypesAction.php index 6cb3a2d3..3ff22838 100644 --- a/src/Actions/TransformTypesAction.php +++ b/src/Actions/TransformTypesAction.php @@ -43,6 +43,7 @@ public function transformClassNode( if (count($node->getAttributes(Hidden::class)) > 0) { return null; } + $transformationContext = TransformationContext::createFromPhpClass($node); foreach ($transformers as $transformer) { diff --git a/src/Actions/WriteFilesAction.php b/src/Actions/WriteFilesAction.php index 4bc6293b..463fcf73 100644 --- a/src/Actions/WriteFilesAction.php +++ b/src/Actions/WriteFilesAction.php @@ -2,8 +2,10 @@ namespace Spatie\TypeScriptTransformer\Actions; +use JsonException; use Spatie\TypeScriptTransformer\Support\WriteableFile; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; +use Spatie\TypeScriptTransformer\Writers\MultipleFilesWriter; class WriteFilesAction { @@ -12,13 +14,34 @@ public function __construct( ) { } - /** @param array $writeableFiles */ + /** @param array $writeableFiles */ public function execute( array $writeableFiles ): void { + $oldManifest = $this->fetchManifest(); + foreach ($writeableFiles as $writeableFile) { + if ($oldManifest !== null + && array_key_exists($writeableFile->path, $oldManifest) + && $oldManifest[$writeableFile->path] === $writeableFile->hash + ) { + continue; + } + $this->writeFile($writeableFile); } + + $writer = $this->config->writer; + + if (! $writer instanceof MultipleFilesWriter) { + return; + } + + $newManifest = $this->buildManifest($writeableFiles); + + $this->deleteOldFiles($oldManifest, $newManifest); + + $this->storeManifest($writer, $newManifest); } protected function writeFile(WriteableFile $file): void @@ -31,4 +54,88 @@ protected function writeFile(WriteableFile $file): void file_put_contents($file->path, $file->contents); } + + /** @return array|null */ + protected function fetchManifest(): ?array + { + $writer = $this->config->writer; + + if (! $writer instanceof MultipleFilesWriter) { + return null; + } + + $manifestPath = $this->getManifestPath($writer); + + if (! file_exists($manifestPath)) { + return null; + } + + $manifestContent = file_get_contents($manifestPath); + + if ($manifestContent === false) { + return null; + } + + try { + return json_decode($manifestContent, associative: true, flags: JSON_THROW_ON_ERROR); + } catch (JsonException) { + return null; + } + } + + /** + * @param array $writeableFiles + * + * @return array + */ + protected function buildManifest( + array $writeableFiles, + ): array { + $manifest = []; + + foreach ($writeableFiles as $writeableFile) { + $manifest[$writeableFile->path] = $writeableFile->hash; + } + + return $manifest; + } + + /** + * @param array|null $oldManifest + * @param array $newManifest + */ + protected function deleteOldFiles( + ?array $oldManifest, + array $newManifest, + ): void { + if ($oldManifest === null) { + return; + } + + $filesToDelete = array_keys(array_diff_key( + $oldManifest, + $newManifest, + )); + + foreach ($filesToDelete as $fileToDelete) { + if (file_exists($fileToDelete)) { + unlink($fileToDelete); + } + } + } + + protected function storeManifest( + MultipleFilesWriter $writer, + array $manifest, + ): void { + file_put_contents( + $this->getManifestPath($writer), + json_encode($manifest) + ); + } + + protected function getManifestPath(MultipleFilesWriter $writer): string + { + return "{$writer->getPath()}/typescript-transformer-manifest.json"; + } } diff --git a/src/Collections/TransformedCollection.php b/src/Collections/TransformedCollection.php index 8a3231c8..b521e375 100644 --- a/src/Collections/TransformedCollection.php +++ b/src/Collections/TransformedCollection.php @@ -7,6 +7,7 @@ use IteratorAggregate; use Spatie\TypeScriptTransformer\References\FilesystemReference; use Spatie\TypeScriptTransformer\References\Reference; +use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Traversable; @@ -21,15 +22,22 @@ class TransformedCollection implements IteratorAggregate /** @var array */ protected array $fileMapping = []; + protected bool $requireCompleteRewrite = false; + + protected TypeScriptTransformerLog $log; + public function __construct( array $items = [], ) { $this->add(...$items); + $this->log = TypeScriptTransformerLog::instance(); } public function add(Transformed ...$transformed): self { foreach ($transformed as $item) { + $this->log->debug($item, 'Adding transformed'); + $this->items[$item->reference->getKey()] = $item; if ($item->reference instanceof FilesystemReference) { @@ -37,6 +45,8 @@ public function add(Transformed ...$transformed): self } } + ray($this); + return $this; } @@ -54,10 +64,15 @@ public function remove(Reference|string $reference): void { $transformed = $this->get($reference); + $this->log->debug($reference, 'Removing reference'); + $this->log->debug($transformed, 'Removing transformed'); + if ($transformed === null) { return; } + $this->log->debug($transformed->referencedBy, 'Marking references as missing'); + foreach (array_unique($transformed->referencedBy) as $referencedBy) { $referencedBy = $this->get($referencedBy); @@ -72,6 +87,8 @@ public function remove(Reference|string $reference): void unset($this->fileMapping[$path]); } + + ray($this); } public function getIterator(): Traversable @@ -113,6 +130,10 @@ public function findTransformedByDirectory(string $path): Generator public function hasChanges(): bool { + if ($this->requireCompleteRewrite) { + return true; + } + foreach ($this->items as $item) { if ($item->changed) { return true; @@ -122,6 +143,16 @@ public function hasChanges(): bool return false; } + public function requireCompleteRewrite(): void + { + $this->requireCompleteRewrite = true; + } + + public function rewriteExecuted(): void + { + $this->requireCompleteRewrite = false; + } + protected function cleanupFilePath(string $path): string { return realpath($path); diff --git a/src/FileSystemWatcher.php b/src/FileSystemWatcher.php index b84bad73..1a5bac25 100644 --- a/src/FileSystemWatcher.php +++ b/src/FileSystemWatcher.php @@ -9,6 +9,7 @@ use Spatie\TypeScriptTransformer\Events\Watch\FileDeletedWatchEvent; use Spatie\TypeScriptTransformer\Events\Watch\FileUpdatedWatchEvent; use Spatie\TypeScriptTransformer\Events\Watch\WatchEvent; +use Spatie\TypeScriptTransformer\Handlers\Watch\DirectoryDeletedWatchEventHandler; use Spatie\TypeScriptTransformer\Handlers\Watch\FileDeletedWatchEventHandler; use Spatie\TypeScriptTransformer\Handlers\Watch\FileUpdatedOrCreatedWatchEventHandler; use Spatie\TypeScriptTransformer\Handlers\Watch\WatchEventHandler; @@ -70,7 +71,7 @@ public function run(): void }); try { - $this->typeScriptTransformer->log->info('Starting watcher'); + $this->typeScriptTransformer->log->info('Now watching for changes ...'); $watcher->start(); } catch (CouldNotStartWatcher $e) { @@ -82,8 +83,6 @@ public function run(): void protected function initializeHandlers(): void { - // TODO: handle directory deleted - $this->handlers[FileCreatedWatchEvent::class] = new FileUpdatedOrCreatedWatchEventHandler( $this->typeScriptTransformer, $this->transformedCollection, @@ -98,6 +97,11 @@ protected function initializeHandlers(): void $this->typeScriptTransformer, $this->transformedCollection, ); + + $this->handlers[DirectoryDeletedWatchEvent::class] = new DirectoryDeletedWatchEventHandler( + $this->typeScriptTransformer, + $this->transformedCollection, + ); } protected function processBuffer(): void @@ -127,8 +131,6 @@ protected function processBuffer(): void $this->typeScriptTransformer->outputTransformed( $this->transformedCollection, ); - - $this->typeScriptTransformer->log->info('Processed events'); } protected function tryToConnectMissingReferencesWithNewTransformed(): void diff --git a/src/Handlers/Watch/DirectoryDeletedWatchEventHandler.php b/src/Handlers/Watch/DirectoryDeletedWatchEventHandler.php index 0f7c9524..ddf69324 100644 --- a/src/Handlers/Watch/DirectoryDeletedWatchEventHandler.php +++ b/src/Handlers/Watch/DirectoryDeletedWatchEventHandler.php @@ -4,8 +4,12 @@ use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\Events\Watch\DirectoryDeletedWatchEvent; +use Spatie\TypeScriptTransformer\Events\Watch\WatchEvent; use Spatie\TypeScriptTransformer\TypeScriptTransformer; +/** + * @implements WatchEventHandler + */ class DirectoryDeletedWatchEventHandler implements WatchEventHandler { public function __construct( @@ -15,10 +19,12 @@ public function __construct( } /** - * @param DirectoryDeletedWatchEvent $event + * @param WatchEvent $event */ public function handle($event): void { + $this->typeScriptTransformer->log->debug($event->path, 'Directory Deleted'); + $transformedItems = $this->transformedCollection->findTransformedByDirectory($event->path); foreach ($transformedItems as $transformed) { diff --git a/src/Handlers/Watch/FileDeletedWatchEventHandler.php b/src/Handlers/Watch/FileDeletedWatchEventHandler.php index 1fbd4c99..f7ed54d9 100644 --- a/src/Handlers/Watch/FileDeletedWatchEventHandler.php +++ b/src/Handlers/Watch/FileDeletedWatchEventHandler.php @@ -6,6 +6,9 @@ use Spatie\TypeScriptTransformer\Events\Watch\FileDeletedWatchEvent; use Spatie\TypeScriptTransformer\TypeScriptTransformer; +/** + * @implements WatchEventHandler + */ class FileDeletedWatchEventHandler implements WatchEventHandler { public function __construct( @@ -14,11 +17,10 @@ public function __construct( ) { } - /** - * @param FileDeletedWatchEvent $event - */ public function handle($event): void { + $this->typeScriptTransformer->log->debug($event->path, 'File Deleted'); + $transformed = $this->transformedCollection->findTransformedByFile($event->path); if ($transformed === null) { diff --git a/src/Handlers/Watch/FileUpdatedOrCreatedWatchEventHandler.php b/src/Handlers/Watch/FileUpdatedOrCreatedWatchEventHandler.php index 50cacc51..9bb61933 100644 --- a/src/Handlers/Watch/FileUpdatedOrCreatedWatchEventHandler.php +++ b/src/Handlers/Watch/FileUpdatedOrCreatedWatchEventHandler.php @@ -3,10 +3,16 @@ namespace Spatie\TypeScriptTransformer\Handlers\Watch; use Spatie\TypeScriptTransformer\Collections\TransformedCollection; +use Spatie\TypeScriptTransformer\Events\Watch\FileCreatedWatchEvent; use Spatie\TypeScriptTransformer\Events\Watch\FileUpdatedWatchEvent; +use Spatie\TypeScriptTransformer\Events\Watch\WatchEvent; +use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode; use Spatie\TypeScriptTransformer\TypeScriptTransformer; use Throwable; +/** + * @implements WatchEventHandler + */ class FileUpdatedOrCreatedWatchEventHandler implements WatchEventHandler { public function __construct( @@ -16,12 +22,17 @@ public function __construct( } /** - * @param FileUpdatedWatchEvent $event + * @param WatchEvent $event * * @return void */ public function handle($event): void { + $this->typeScriptTransformer->log->debug( + $event->path, + $event instanceof FileCreatedWatchEvent ? 'File Created' : 'File Updated', + ); + try { $classNode = $this->typeScriptTransformer->loadPhpClassNodeAction->execute($event->path); @@ -31,6 +42,18 @@ public function handle($event): void return; } + if ($this->checkIfClassNodeIsInvalid($event->path, $classNode)) { + /** + * PHPStorm and probably other IDEs, will during refactoring when changing the class name + * create a new file with the new class name. Yet the contents will still contain the + * old class name. Later on an update event will be triggered with the correct + * class name. In order to not generate false positives, ignore a file when + * it is not matching the expected class name. + */ + + return; + } + $newlyTransformed = $this->typeScriptTransformer->transformTypesAction->transformClassNode( $this->typeScriptTransformer->config->transformers, $classNode @@ -49,26 +72,35 @@ public function handle($event): void if ($originalTransformed && $newlyTransformed === null) { $this->transformedCollection->remove($originalTransformed->reference); - // TODO: when removing a ts transformed structure (e.g. remove the TypeScript Attributes) - // everything is correctly removed from the collection - // but since there are no changes, no new rewrite is triggered - // somehow we should be able to trigger rewrites based upon namespaces - } - if ($newlyTransformed === null) { - $this->typeScriptTransformer->log->warning("Could not transform {$event->path}"); + $this->transformedCollection->requireCompleteRewrite(); return; } - // TODO: at the moment we replace the node when we see an update - // it could be that no changes are actually made - // and such a case nothing should be updated + if ($originalTransformed && $newlyTransformed) { + // TODO: at the moment we replace the node when we see an update + // it could be that no changes are actually made + // and such a case nothing should be updated + // Ideally we check if two transformed items correspond + } if ($originalTransformed !== null) { $this->transformedCollection->remove($originalTransformed->reference); } - $this->transformedCollection->add($newlyTransformed); + if ($newlyTransformed) { + $this->transformedCollection->add($newlyTransformed); + } + } + + protected function checkIfClassNodeIsInvalid( + string $path, + PhpClassNode $classNode, + ): bool { + $expectedClassName = basename($path, '.php'); + $actualClassName = $classNode->getShortName(); + + return $expectedClassName !== $actualClassName; } } diff --git a/src/Handlers/Watch/WatchEventHandler.php b/src/Handlers/Watch/WatchEventHandler.php index b6f71a88..ae3f1b5e 100644 --- a/src/Handlers/Watch/WatchEventHandler.php +++ b/src/Handlers/Watch/WatchEventHandler.php @@ -2,7 +2,15 @@ namespace Spatie\TypeScriptTransformer\Handlers\Watch; +use Spatie\TypeScriptTransformer\Events\Watch\WatchEvent; + +/** + * @template T of WatchEvent + */ interface WatchEventHandler { + /** + * @param T $event + */ public function handle($event): void; } diff --git a/src/Laravel/ClassPropertyProcessors/DataClassPropertyProcessor.php b/src/Laravel/ClassPropertyProcessors/DataClassPropertyProcessor.php index 66d72781..5460cea5 100644 --- a/src/Laravel/ClassPropertyProcessors/DataClassPropertyProcessor.php +++ b/src/Laravel/ClassPropertyProcessors/DataClassPropertyProcessor.php @@ -4,6 +4,8 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use Spatie\LaravelData\Attributes\Hidden as DataHidden; +use Spatie\LaravelData\Attributes\MapName; +use Spatie\LaravelData\Attributes\MapOutputName; use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\DataConfig; use Spatie\TypeScriptTransformer\Attributes\Hidden; @@ -11,6 +13,7 @@ use Spatie\TypeScriptTransformer\References\ClassStringReference; use Spatie\TypeScriptTransformer\Transformers\ClassPropertyProcessors\ClassPropertyProcessor; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeReference; +use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptProperty; use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptUnion; @@ -41,12 +44,21 @@ public function execute( return null; } - // TODO: somehow get mapping working here without dataconfig and dataproperty - // $phpAttributeNodes = $phpPropertyNode->getAttributes(MapOutputName::class); - // - // if ($phpAttributeNodes) { - // $property->name = new TypeScriptIdentifier($dataProperty->outputMappedName); - // } + $mapOutputNodes = $phpPropertyNode->getAttributes(MapOutputName::class); + + if (! empty($mapOutputNodes)) { + $property->name = new TypeScriptIdentifier($mapOutputNodes[0]->getArgument('output')); + } + + $mapNodes = $phpPropertyNode->getAttributes(MapName::class); + + if (! empty($mapNodes)) { + $name = $mapNodes[0]->getArgument('output') === null + ? $mapNodes[0]->getArgument('input') + : $mapNodes[0]->getArgument('output'); + + $property->name = new TypeScriptIdentifier($$name); + } if (! $property->type instanceof TypeScriptUnion) { return $property; diff --git a/src/Laravel/Commands/TransformTypeScriptCommand.php b/src/Laravel/Commands/TransformTypeScriptCommand.php index 755dcd82..06c18549 100644 --- a/src/Laravel/Commands/TransformTypeScriptCommand.php +++ b/src/Laravel/Commands/TransformTypeScriptCommand.php @@ -3,7 +3,7 @@ namespace Spatie\TypeScriptTransformer\Laravel\Commands; use Illuminate\Console\Command; -use Spatie\TypeScriptTransformer\Laravel\Support\WrappedLaravelConsole; +use Spatie\TypeScriptTransformer\Laravel\Support\Logger; use Spatie\TypeScriptTransformer\TypeScriptTransformer; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; @@ -23,7 +23,7 @@ public function handle(): int TypeScriptTransformer::create( config: app(TypeScriptTransformerConfig::class), - console: new WrappedLaravelConsole($this) + console: new Logger($this) )->execute(); $this->comment('All done'); diff --git a/src/Laravel/Commands/WatchTypeScriptCommand.php b/src/Laravel/Commands/WatchTypeScriptCommand.php index 26f4da3c..06bd1a9f 100644 --- a/src/Laravel/Commands/WatchTypeScriptCommand.php +++ b/src/Laravel/Commands/WatchTypeScriptCommand.php @@ -3,7 +3,9 @@ namespace Spatie\TypeScriptTransformer\Laravel\Commands; use Illuminate\Console\Command; -use Spatie\TypeScriptTransformer\Laravel\Support\WrappedLaravelConsole; +use Spatie\TypeScriptTransformer\Laravel\Support\ConsoleLogger; +use Spatie\TypeScriptTransformer\Support\Console\MultiLogger; +use Spatie\TypeScriptTransformer\Support\Console\RayLogger; use Spatie\TypeScriptTransformer\TypeScriptTransformer; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; @@ -23,7 +25,10 @@ public function handle(): int TypeScriptTransformer::create( config: app(TypeScriptTransformerConfig::class), - console: new WrappedLaravelConsole($this), + console: new MultiLogger([ + new RayLogger(), + new ConsoleLogger($this), + ]), watch: true )->execute(); diff --git a/src/Laravel/Support/ConsoleLogger.php b/src/Laravel/Support/ConsoleLogger.php new file mode 100644 index 00000000..1612326d --- /dev/null +++ b/src/Laravel/Support/ConsoleLogger.php @@ -0,0 +1,40 @@ +command->error($this->formatTitle($title).$this->mixedToString($item)); + } + + public function info(mixed $item, ?string $title = null): void + { + $this->command->info($this->formatTitle($title).$this->mixedToString($item)); + } + + public function warn(mixed $item, ?string $title = null): void + { + $this->command->warn($this->formatTitle($title).$this->mixedToString($item)); + } + + public function debug(mixed $item, ?string $title = null): void + { + $this->command->line($this->formatTitle($title).$this->mixedToString($item)); + } + + protected function formatTitle( + ?string $title = null + ): string { + return $title ? "[$title] " : ''; + } +} diff --git a/src/Laravel/Support/WrappedLaravelConsole.php b/src/Laravel/Support/WrappedLaravelConsole.php deleted file mode 100644 index e70351e0..00000000 --- a/src/Laravel/Support/WrappedLaravelConsole.php +++ /dev/null @@ -1,34 +0,0 @@ -command->error($message); - } - - public function info(string $message): void - { - $this->command->info($message); - } - - public function warn(string $message): void - { - $this->command->warn($message); - } - - public function exit(int $code = 0): void - { - exit($code); - } -} diff --git a/src/PhpNodes/PhpAttributeNode.php b/src/PhpNodes/PhpAttributeNode.php index cc50ae92..f2c2d615 100644 --- a/src/PhpNodes/PhpAttributeNode.php +++ b/src/PhpNodes/PhpAttributeNode.php @@ -8,7 +8,7 @@ class PhpAttributeNode { - protected ?array $arguments = null; + protected ?array $namedArguments = null; public function __construct( public readonly ReflectionAttribute|RoaveReflectionAttribute $reflection @@ -20,27 +20,27 @@ public function getName(): string return $this->reflection->getName(); } - public function getArguments(): array + public function getRawArguments(): array { return $this->reflection->getArguments(); } public function hasArgument(string $name): bool { - if ($this->arguments === null) { - $this->initializeArguments(); + if ($this->namedArguments === null) { + $this->initializeNamedArguments(); } - return array_key_exists($name, $this->arguments); + return array_key_exists($name, $this->namedArguments); } public function getArgument(string $name): mixed { - if ($this->arguments === null) { - $this->initializeArguments(); + if ($this->namedArguments === null) { + $this->initializeNamedArguments(); } - return $this->arguments[$name] ?? null; + return $this->namedArguments[$name] ?? null; } public function newInstance(): object @@ -51,47 +51,59 @@ public function newInstance(): object $className = $this->reflection->getName(); - // TODO: maybe we can do a little better here - return (new $className())($this->reflection->getArguments()); + $this->initializeNamedArguments(); + + return (new $className())(...$this->namedArguments); } /** @return array */ - protected function initializeArguments(): array + protected function initializeNamedArguments(): array { - // TODO: this is a quickly written thing, test it to be sure it works - if ($this->arguments !== null) { - return $this->arguments; + if ($this->namedArguments !== null) { + return $this->namedArguments; } - $this->arguments = []; + $this->namedArguments = []; + + $values = $this->getRawArguments(); + + $constructor = new ReflectionMethod($this->reflection->getName(), '__construct'); - $values = $this->getArguments(); + $parameters = $constructor->getParameters(); + + $namedParametersMap = array_flip(array_map( + fn ($param) => $param->getName(), + $parameters + )); foreach ($values as $name => $value) { - if (is_string($name)) { - $this->arguments[$name] = $value; - unset($values[$name]); - } - } + if (is_int($name)) { + $parameter = $parameters[$name]; - if (count($values) === 0) { - return $this->arguments; - } + if ($parameter->isVariadic()) { + $this->namedArguments[$parameter->name] = array_values(array_slice( + $values, + $name, + null, + true + )); - $constructor = new ReflectionMethod($this->reflection->getName(), '__construct'); + return $this->namedArguments; + } - foreach ($constructor->getParameters() as $index => $param) { - if (array_key_exists($param->getName(), $this->arguments)) { - continue; + $name = $parameters[$name]->getName(); } - if (! array_key_exists($index, $values)) { - continue; - } + $this->namedArguments[$name] = $value; - $this->arguments[$param->getName()] = $values[$index]; + unset($parameters[$namedParametersMap[$name]]); } - return $this->arguments; + foreach ($parameters as $parameter) { + $this->namedArguments[$parameter->getName()] = $parameter->getDefaultValue(); + } + + + return $this->namedArguments; } } diff --git a/src/Support/Console/ConsoleLogger.php b/src/Support/Console/ConsoleLogger.php new file mode 100644 index 00000000..1670ca4b --- /dev/null +++ b/src/Support/Console/ConsoleLogger.php @@ -0,0 +1,50 @@ + */ + public array $messages = []; + + public function error(mixed $item, ?string $title = null): void + { + $this->messages[] = ['message' => $this->mixedToString($item), 'title' => $title, 'level' => 'error']; + } + + public function info(mixed $item, ?string $title = null): void + { + $this->messages[] = ['message' => $this->mixedToString($item), 'title' => $title, 'level' => 'info']; + } + + public function warn(mixed $item, ?string $title = null): void + { + $this->messages[] = ['message' => $this->mixedToString($item), 'title' => $title, 'level' => 'warning']; + } + + public function debug(mixed $item, ?string $title = null): void + { + $this->messages[] = ['message' => $this->mixedToString($item), 'title' => $title, 'level' => 'debug']; + } + + protected function mixedToString(mixed $item): string + { + if ($item === null) { + return 'null'; + } + + if (is_numeric($item) || is_string($item)) { + return (string) $item; + } + + if (is_bool($item)) { + return $item ? 'true' : 'false'; + } + + $type = is_object($item) + ? get_class($item) + : gettype($item); + + return "({$type}) " .json_encode($item, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } +} diff --git a/src/Support/Console/Logger.php b/src/Support/Console/Logger.php new file mode 100644 index 00000000..322d5c88 --- /dev/null +++ b/src/Support/Console/Logger.php @@ -0,0 +1,14 @@ +loggers as $logger) { + $logger->error($item, $title); + } + } + + public function info(mixed $item, ?string $title = null): void + { + foreach ($this->loggers as $logger) { + $logger->info($item, $title); + } + } + + public function warn(mixed $item, ?string $title = null): void + { + foreach ($this->loggers as $logger) { + $logger->warn($item, $title); + } + } + + public function debug(mixed $item, ?string $title = null): void + { + foreach ($this->loggers as $logger) { + $logger->debug($item, $title); + } + } +} diff --git a/src/Support/Console/NullLogger.php b/src/Support/Console/NullLogger.php new file mode 100644 index 00000000..953348c5 --- /dev/null +++ b/src/Support/Console/NullLogger.php @@ -0,0 +1,26 @@ +sendToRay($item, $title, 'red'); + } + + public function info(mixed $item, ?string $title = null): void + { + $this->sendToRay($item, $title, 'blue'); + } + + public function warn(mixed $item, ?string $title = null): void + { + $this->sendToRay($item, $title, 'orange'); + } + + public function debug(mixed $item, ?string $title = null): void + { + $this->sendToRay($item, $title, 'gray'); + } + + protected function sendToRay( + mixed $item, + ?string $title, + string $color, + ) { + $ray = ray($item)->color($color); + + if ($title) { + $ray->label($title); + } + } +} diff --git a/src/Support/Console/WrappedArrayConsole.php b/src/Support/Console/WrappedArrayConsole.php deleted file mode 100644 index 550fbdda..00000000 --- a/src/Support/Console/WrappedArrayConsole.php +++ /dev/null @@ -1,29 +0,0 @@ - */ - public array $messages = []; - - public function error(string $message): void - { - $this->messages[] = ['message' => $message, 'level' => 'error']; - } - - public function info(string $message): void - { - $this->messages[] = ['message' => $message, 'level' => 'info']; - } - - public function warn(string $message): void - { - $this->messages[] = ['message' => $message, 'level' => 'warning']; - } - - public function exit(int $code = 0): void - { - die($code); - } -} diff --git a/src/Support/Console/WrappedConsole.php b/src/Support/Console/WrappedConsole.php deleted file mode 100644 index e38bf7d0..00000000 --- a/src/Support/Console/WrappedConsole.php +++ /dev/null @@ -1,14 +0,0 @@ -getAttributes(TypeScript::class)[0] ?? null; - $name = $attribute && $attribute->hasArgument('name') + $name = $attribute && $attribute->hasArgument('name') && $attribute->getArgument('name') !== null ? $attribute->getArgument('name') : $node->getShortName(); - $nameSpaceSegments = $attribute && $attribute->hasArgument('location') + $nameSpaceSegments = $attribute && $attribute->hasArgument('location') && $attribute->getArgument('location') !== null ? $attribute->getArgument('location') : explode('\\', $node->getNamespaceName()); diff --git a/src/Support/TypeScriptTransformerLog.php b/src/Support/TypeScriptTransformerLog.php index a93fc0d5..da62e918 100644 --- a/src/Support/TypeScriptTransformerLog.php +++ b/src/Support/TypeScriptTransformerLog.php @@ -2,38 +2,66 @@ namespace Spatie\TypeScriptTransformer\Support; -use Spatie\TypeScriptTransformer\Support\Console\WrappedConsole; -use Spatie\TypeScriptTransformer\Support\Console\WrappedNullConsole; +use RuntimeException; +use Spatie\TypeScriptTransformer\Support\Console\Logger; +use Spatie\TypeScriptTransformer\Support\Console\NullLogger; class TypeScriptTransformerLog { - public function __construct( - protected WrappedConsole $wrappedConsole, + protected static $self; + + protected function __construct( + protected Logger $logger, ) { } + public static function create(Logger $logger): self + { + if (isset(static::$self)) { + throw new RuntimeException('TypeScriptTransformerLog instance already created.'); + } + + return static::$self = new static($logger); + } + + public static function instance(): self + { + if (! isset(static::$self)) { + throw new RuntimeException('TypeScriptTransformerLog instance not created.'); + } + + return static::$self; + } + public static function createNullLog(): self { - return new self(new WrappedNullConsole()); + return new self(new NullLogger()); + } + + public function info(mixed $item, ?string $title = null): self + { + $this->logger->info($item, $title); + + return $this; } - public function info(string $message): self + public function warning(mixed $item, ?string $title = null): self { - $this->wrappedConsole->info($message); + $this->logger->warn($item, $title); return $this; } - public function warning(string $message): self + public function error(mixed $item, ?string $title = null): self { - $this->wrappedConsole->warn($message); + $this->logger->error($item, $title); return $this; } - public function error(string $message): self + public function debug(mixed $item, ?string $title = null): self { - $this->wrappedConsole->error($message); + $this->logger->debug($item, $title); return $this; } diff --git a/src/Support/WriteableFile.php b/src/Support/WriteableFile.php index ad5948b2..30ce8e1b 100644 --- a/src/Support/WriteableFile.php +++ b/src/Support/WriteableFile.php @@ -2,11 +2,14 @@ namespace Spatie\TypeScriptTransformer\Support; -class WriteableFile +readonly class WriteableFile { + public string $hash; + public function __construct( public string $path, public string $contents, ) { + $this->hash = md5($contents); } } diff --git a/src/Transformed/Transformed.php b/src/Transformed/Transformed.php index 796e5d41..821ed55f 100644 --- a/src/Transformed/Transformed.php +++ b/src/Transformed/Transformed.php @@ -67,6 +67,10 @@ public function nameAs(string $name): self public function prepareForWrite(): TypeScriptNode { + // TODO: could we when a node is not changed, keep a cached version when writing it? + // that way we don't have to write out the whole node when writing files if + // it hasn't changed. + $this->changed = false; if ($this->export === false) { diff --git a/src/Transformers/AttributedClassTransformer.php b/src/Transformers/AttributedClassTransformer.php index 8f2c3d85..2e7bc014 100644 --- a/src/Transformers/AttributedClassTransformer.php +++ b/src/Transformers/AttributedClassTransformer.php @@ -24,7 +24,7 @@ public function transform(PhpClassNode $phpClassNode, TransformationContext $con } /** @var TypeScript $attribute */ - $attribute = $phpClassNode->getAttributes(TypeScript::class)[0]->getArguments(); + $attribute = $phpClassNode->getAttributes(TypeScript::class)[0]->getRawArguments(); if (($attribute['name'] ?? null) !== null) { $transformed->nameAs($attribute['name']); diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index 5a4d6afb..c6650fcd 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -11,8 +11,8 @@ use Spatie\TypeScriptTransformer\Actions\TransformTypesAction; use Spatie\TypeScriptTransformer\Actions\WriteFilesAction; use Spatie\TypeScriptTransformer\Collections\TransformedCollection; -use Spatie\TypeScriptTransformer\Support\Console\WrappedConsole; -use Spatie\TypeScriptTransformer\Support\Console\WrappedNullConsole; +use Spatie\TypeScriptTransformer\Support\Console\Logger; +use Spatie\TypeScriptTransformer\Support\Console\NullLogger; use Spatie\TypeScriptTransformer\Support\LoadPhpClassNodeAction; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; @@ -37,12 +37,12 @@ public function __construct( public static function create( TypeScriptTransformerConfig|TypeScriptTransformerConfigFactory $config, - WrappedConsole $console = new WrappedNullConsole(), + Logger $console = new NullLogger(), bool $watch = false, ): self { $config = $config instanceof TypeScriptTransformerConfigFactory ? $config->get() : $config; - $log = new TypeScriptTransformerLog($console); + $log = TypeScriptTransformerLog::create($console); return new self( $config, @@ -67,10 +67,12 @@ public function execute(): void * - Add interface implementation + tests -> OK * - Split off Laravel specific code and test * - Split off data specific code and test - * - Add support for watching files + * - Add support for watching files -> ok, maybe add docs and some tests * - Further write docs + check them -> only Laravel specific stuff * - Check old Laravel tests if we missed something * - Check in Flare whether everything is working as expected -> PR ready, needs fixing TS + * - Fix todos + * - Write some text arround refactoring in IDE and watcher, that IDE's useally take some time to write eveyrthing out so it can take up to 10 seconds before the watcher kicks in * - Make sure nullables can be exported as optional: https://github.com/spatie/typescript-transformer/pull/88/files * - Release */ @@ -109,6 +111,8 @@ public function outputTransformed( return; } + $transformedCollection->rewriteExecuted(); + $writeableFiles = $this->config->writer->output($transformedCollection); $this->writeFilesAction->execute($writeableFiles); diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index 827d323b..d556ad40 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -10,7 +10,7 @@ use Spatie\TypeScriptTransformer\Support\WriteableFile; use Spatie\TypeScriptTransformer\Support\WritingContext; -class ModuleWriter implements Writer +class ModuleWriter implements Writer, MultipleFilesWriter { public function __construct( protected string $path, @@ -27,18 +27,18 @@ public function output(TransformedCollection $collection): array $writtenFiles = []; - // TODO: remove files which still exists due to previous run - foreach ($locations as $location) { $writtenFiles[] = $this->writeLocation($location, $collection); } - // TODO: we probably can be a bit smarter about this - // -> only write files which have changed - return $writtenFiles; } + public function getPath(): string + { + return $this->path; + } + protected function writeLocation( Location $location, TransformedCollection $collection, diff --git a/src/Writers/MultipleFilesWriter.php b/src/Writers/MultipleFilesWriter.php new file mode 100644 index 00000000..b046d50f --- /dev/null +++ b/src/Writers/MultipleFilesWriter.php @@ -0,0 +1,8 @@ +execute($collection); diff --git a/tests/Actions/WriteFilesActionTest.php b/tests/Actions/WriteFilesActionTest.php index 9052aa47..10041f09 100644 --- a/tests/Actions/WriteFilesActionTest.php +++ b/tests/Actions/WriteFilesActionTest.php @@ -4,6 +4,7 @@ use Spatie\TypeScriptTransformer\Actions\WriteFilesAction; use Spatie\TypeScriptTransformer\Support\WriteableFile; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfigFactory; +use Spatie\TypeScriptTransformer\Writers\ModuleWriter; beforeEach(function () { $this->temporaryDirectory = TemporaryDirectory::make(); @@ -28,3 +29,68 @@ expect(file_get_contents($fileA->path))->toBe('fileA contents'); expect(file_get_contents($fileB->path))->toBe('fileB contents'); }); + +it('will store a manifest file', function () { + $fileA = new WriteableFile($pathA = $this->temporaryDirectory->path('fileA.ts'), 'fileA contents'); + $fileB = new WriteableFile($pathB = $this->temporaryDirectory->path('fileB.ts'), 'fileB contents'); + + (new WriteFilesAction(TypeScriptTransformerConfigFactory::create()->writer(new ModuleWriter($this->temporaryDirectory->path()))->get()))->execute([$fileA, $fileB]); + + $manifestPath = $this->temporaryDirectory->path('typescript-transformer-manifest.json'); + + expect($manifestPath)->toBeFile(); + expect(json_decode(file_get_contents($manifestPath), true)) + ->toBeArray() + ->toHaveKeys([$pathA, $pathB]); +}); + +it('will not write files that have not changed', function () { + $fileA = new WriteableFile($pathA = $this->temporaryDirectory->path('fileA.ts'), 'fileA contents'); + $fileB = new WriteableFile($pathB = $this->temporaryDirectory->path('fileB.ts'), 'fileB contents'); + + (new WriteFilesAction(TypeScriptTransformerConfigFactory::create()->writer(new ModuleWriter($this->temporaryDirectory->path()))->get()))->execute([$fileA, $fileB]); + + unlink($pathA); + + (new WriteFilesAction(TypeScriptTransformerConfigFactory::create()->writer(new ModuleWriter($this->temporaryDirectory->path()))->get()))->execute([$fileA, $fileB]); + + expect(file_exists($pathA))->toBeFalse(); // Since we deleted it, it should not be written again +}); + +it('will delete older files not present anymore in the manifest', function () { + $fileA = new WriteableFile($pathA = $this->temporaryDirectory->path('fileA.ts'), 'fileA contents'); + $fileB = new WriteableFile($pathB = $this->temporaryDirectory->path('fileB.ts'), 'fileB contents'); + + (new WriteFilesAction(TypeScriptTransformerConfigFactory::create()->writer(new ModuleWriter($this->temporaryDirectory->path()))->get()))->execute([$fileA, $fileB]); + + unlink($pathA); + + (new WriteFilesAction(TypeScriptTransformerConfigFactory::create()->writer(new ModuleWriter($this->temporaryDirectory->path()))->get()))->execute([$fileB]); + + expect(file_exists($pathA))->toBeFalse(); +}); + +it('will update the manifest file', function () { + $fileA = new WriteableFile($pathA = $this->temporaryDirectory->path('fileA.ts'), 'fileA contents'); + $fileB = new WriteableFile($pathB = $this->temporaryDirectory->path('fileB.ts'), 'fileB contents'); + + (new WriteFilesAction(TypeScriptTransformerConfigFactory::create()->writer(new ModuleWriter($this->temporaryDirectory->path()))->get()))->execute([$fileA, $fileB]); + (new WriteFilesAction(TypeScriptTransformerConfigFactory::create()->writer(new ModuleWriter($this->temporaryDirectory->path()))->get()))->execute([$fileA]); + + $manifestPath = $this->temporaryDirectory->path('typescript-transformer-manifest.json'); + + expect($manifestPath)->toBeFile(); + expect(json_decode(file_get_contents($manifestPath), true)) + ->toBeArray() + ->toHaveKeys([$pathA]) + ->not->toHaveKey($pathB); +}); + +it('will not use a manifest file when the writer does not support it', function () { + $fileA = new WriteableFile($pathA = $this->temporaryDirectory->path('fileA.ts'), 'fileA contents'); + $fileB = new WriteableFile($pathB = $this->temporaryDirectory->path('fileB.ts'), 'fileB contents'); + + (new WriteFilesAction(TypeScriptTransformerConfigFactory::create()->get()))->execute([$fileA, $fileB]); + + expect(file_exists($this->temporaryDirectory->path('typescript-transformer-manifest.json')))->toBeFalse(); +}); diff --git a/tests/Fakes/TypesToProvide/ComplexAttribute.php b/tests/Fakes/TypesToProvide/ComplexAttribute.php new file mode 100644 index 00000000..a9e89a75 --- /dev/null +++ b/tests/Fakes/TypesToProvide/ComplexAttribute.php @@ -0,0 +1,17 @@ +getRawArguments())->toBe([69]); + + expect($attributeNode->hasArgument('argument'))->toBeTrue(); + expect($attributeNode->getArgument('argument'))->toBe(69); + + expect($attributeNode->hasArgument('default'))->toBeTrue(); + expect($attributeNode->getArgument('default'))->toBe(42); +}); + +it('can check and get named arguments from a simple attribute with default set', function () { + #[SimpleAttribute(69, 314)] + class TestSimpleAttributeNamedArgumentFetchWithDefaultSet + { + } + + $attributeNode = createAttributeNode(TestSimpleAttributeNamedArgumentFetchWithDefaultSet::class, SimpleAttribute::class); + + expect($attributeNode->getRawArguments())->toBe([69, 314]); + + expect($attributeNode->hasArgument('argument'))->toBeTrue(); + expect($attributeNode->getArgument('argument'))->toBe(69); + + expect($attributeNode->hasArgument('default'))->toBeTrue(); + expect($attributeNode->getArgument('default'))->toBe(314); +}); + +it('can check and get named arguments from a simple attribute with specific naming', function () { + #[SimpleAttribute(argument: 69)] + class TestSimpleAttributeNamedArgumentFetchWithSpecificNaming + { + } + + $attributeNode = createAttributeNode(TestSimpleAttributeNamedArgumentFetchWithSpecificNaming::class, SimpleAttribute::class); + + expect($attributeNode->getRawArguments())->toBe(['argument' => 69]); + + expect($attributeNode->hasArgument('argument'))->toBeTrue(); + expect($attributeNode->getArgument('argument'))->toBe(69); + + expect($attributeNode->hasArgument('default'))->toBeTrue(); + expect($attributeNode->getArgument('default'))->toBe(42); +}); + +it('can check and get named arguments from a simple attribute with named and unnamed attributes', function () { + #[SimpleAttribute(69, default: 314)] + class TestSimpleAttributeNamedAndUnnamedArgumentFetchWithSpecificNaming + { + } + + $attributeNode = createAttributeNode(TestSimpleAttributeNamedAndUnnamedArgumentFetchWithSpecificNaming::class, SimpleAttribute::class); + + expect($attributeNode->getRawArguments())->toBe([0 => 69, 'default' => 314]); + + expect($attributeNode->hasArgument('argument'))->toBeTrue(); + expect($attributeNode->getArgument('argument'))->toBe(69); + + expect($attributeNode->hasArgument('default'))->toBeTrue(); + expect($attributeNode->getArgument('default'))->toBe(314); +}); + +it('can mix and match named parameters in reversed order', function () { + #[ComplexAttribute(argumentD: 'DD', argumentB: 2, argumentA: 1)] + class TestSimpleAttributeNamedAndUnnamedArgumentFetchWithReversedOrder + { + } + + $attributeNode = createAttributeNode(TestSimpleAttributeNamedAndUnnamedArgumentFetchWithReversedOrder::class, ComplexAttribute::class); + + expect($attributeNode->getRawArguments())->toBe(['argumentD' => 'DD', 'argumentB' => 2, 'argumentA' => 1]); + + expect($attributeNode->hasArgument('argumentA'))->toBeTrue(); + expect($attributeNode->getArgument('argumentA'))->toBe(1); + + expect($attributeNode->hasArgument('argumentB'))->toBeTrue(); + expect($attributeNode->getArgument('argumentB'))->toBe(2); + + expect($attributeNode->hasArgument('argumentC'))->toBeTrue(); + expect($attributeNode->getArgument('argumentC'))->toBe('C'); + + expect($attributeNode->hasArgument('argumentD'))->toBeTrue(); + expect($attributeNode->getArgument('argumentD'))->toBe('DD'); +}); + +it('can use variadic parameters', function () { + #[VariadicAttribute(1, 2, 3, 4)] + class TestVariadicAttribute + { + } + + $attributeNode = createAttributeNode(TestVariadicAttribute::class, VariadicAttribute::class); + + expect($attributeNode->getRawArguments())->toBe([1, 2, 3, 4]); + + expect($attributeNode->hasArgument('argument'))->toBeTrue(); + expect($attributeNode->getArgument('argument'))->toBe(1); + + expect($attributeNode->hasArgument('variadic'))->toBeTrue(); + expect($attributeNode->getArgument('variadic'))->toBe([2, 3, 4]); +}); +function createAttributeNode(string $className, string $attributeClass): PhpAttributeNode +{ + $reflection = new ReflectionClass($className); + $attribute = $reflection->getAttributes($attributeClass)[0]; + + return new PhpAttributeNode($attribute); +} diff --git a/tests/Transformed/TransformedTest.php b/tests/Transformed/TransformedTest.php index 3e054e3a..5b0d5455 100644 --- a/tests/Transformed/TransformedTest.php +++ b/tests/Transformed/TransformedTest.php @@ -3,7 +3,7 @@ use Spatie\TypeScriptTransformer\Actions\ConnectReferencesAction; use Spatie\TypeScriptTransformer\Collections\TransformedCollection; use Spatie\TypeScriptTransformer\References\CustomReference; -use Spatie\TypeScriptTransformer\Support\Console\WrappedNullConsole; +use Spatie\TypeScriptTransformer\Support\Console\NullLogger; use Spatie\TypeScriptTransformer\Support\TypeScriptTransformerLog; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\SimpleClass; use Spatie\TypeScriptTransformer\Transformed\Transformed; @@ -131,7 +131,7 @@ ); $connector = new ConnectReferencesAction( - new TypeScriptTransformerLog(new WrappedNullConsole()) + TypeScriptTransformerLog::create(new NullLogger()) ); $connector->execute(new TransformedCollection([$found, $transformed]));