typelizer 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -3
- data/README.md +17 -0
- data/lib/typelizer/config.rb +4 -0
- data/lib/typelizer/interface.rb +16 -1
- data/lib/typelizer/openapi.rb +14 -8
- data/lib/typelizer/serializer_plugins/alba.rb +2 -1
- data/lib/typelizer/templates/index.ts.erb +2 -2
- data/lib/typelizer/version.rb +1 -1
- data/lib/typelizer/writer.rb +19 -12
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6dbce7bf8d558500341875958f875183e18668765f812d1ef8110f51c521cc7d
|
|
4
|
+
data.tar.gz: c297302a9425f27f0491307d7597201430ddf94eff38d80cf0b0760f16186296
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5509e82f421cc1f52411b073431a246134d5bdcfe53eb5f308554492da3f2dbc78c708cf45c89651a749d5ad2aa1e0a59c119287f0c0ce8af19a10e374d4efbb
|
|
7
|
+
data.tar.gz: 37ba94237e3992f96542f33ef4376db48a1c5397907d01fbd7a619102a412db58418e36822d46f522f2ebeb672922591a46cbb112e0be2103bf7119d241e2bbe
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning].
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.11.0] - 2026-03-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Per-serializer `output_dir` override via `typelizer_config`. Interfaces are written to their configured directory while the shared `index.ts` barrel generates correct relative import paths. ([@skryukov])
|
|
15
|
+
|
|
16
|
+
- `filename_mapper` configuration to decouple generated file paths from TypeScript type names. Useful for mirroring Ruby module namespaces as nested directories. ([@skryukov], [@rdavid1099])
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
Typelizer.configure do |config|
|
|
20
|
+
config.filename_mapper = ->(name) { name.gsub("::", "/") }
|
|
21
|
+
end
|
|
22
|
+
# Alba::UserSerializer → types/Alba/User.ts (type name stays AlbaUser)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Walk over inline association properties to determine imports. ([@skryukov])
|
|
28
|
+
- Fix `config.type_mapping` override not applied to OpenAPI schema generation. ([@skryukov])
|
|
29
|
+
- Fix key transformation for Alba traits. ([@skryukov])
|
|
30
|
+
|
|
10
31
|
## [0.10.0] - 2026-03-02
|
|
11
32
|
|
|
12
33
|
### Changed
|
|
@@ -426,20 +447,22 @@ and this project adheres to [Semantic Versioning].
|
|
|
426
447
|
|
|
427
448
|
[@davidrunger]: https://github.com/davidrunger
|
|
428
449
|
[@Envek]: https://github.com/Envek
|
|
429
|
-
[@jonmarkgo]: https://github.com/jonmarkgo
|
|
430
450
|
[@hkamberovic]: https://github.com/hkamberovic
|
|
451
|
+
[@jonmarkgo]: https://github.com/jonmarkgo
|
|
431
452
|
[@kristinemcbride]: https://github.com/kristinemcbride
|
|
432
453
|
[@nkriege]: https://github.com/nkriege
|
|
433
454
|
[@NOX73]: https://github.com/NOX73
|
|
434
455
|
[@okuramasafumi]: https://github.com/okuramasafumi
|
|
435
456
|
[@patvice]: https://github.com/patvice
|
|
436
457
|
[@pgiblock]: https://github.com/pgiblock
|
|
437
|
-
[@prog-supdex]: https://github.com/prog-supdex
|
|
438
458
|
[@PedroAugustoRamalhoDuarte]: https://github.com/PedroAugustoRamalhoDuarte
|
|
459
|
+
[@prog-supdex]: https://github.com/prog-supdex
|
|
460
|
+
[@rdavid1099]: https://github.com/rdavid1099
|
|
439
461
|
[@skryukov]: https://github.com/skryukov
|
|
440
462
|
[@ventsislaf]: https://github.com/ventsislaf
|
|
441
463
|
|
|
442
|
-
[Unreleased]: https://github.com/skryukov/typelizer/compare/v0.
|
|
464
|
+
[Unreleased]: https://github.com/skryukov/typelizer/compare/v0.11.0...HEAD
|
|
465
|
+
[0.11.0]: https://github.com/skryukov/typelizer/compare/v0.10.0...v0.11.0
|
|
443
466
|
[0.10.0]: https://github.com/skryukov/typelizer/compare/v0.9.3...v0.10.0
|
|
444
467
|
[0.9.3]: https://github.com/skryukov/typelizer/compare/v0.9.2...v0.9.3
|
|
445
468
|
[0.9.2]: https://github.com/skryukov/typelizer/compare/v0.9.1...v0.9.2
|
data/README.md
CHANGED
|
@@ -628,6 +628,16 @@ class PostResource < ApplicationResource
|
|
|
628
628
|
end
|
|
629
629
|
```
|
|
630
630
|
|
|
631
|
+
You can also override `output_dir` per serializer to place its generated file in a different directory:
|
|
632
|
+
|
|
633
|
+
```ruby
|
|
634
|
+
class Admin::UserResource < ApplicationResource
|
|
635
|
+
typelizer_config do |c|
|
|
636
|
+
c.output_dir = Rails.root.join("app/javascript/types/admin")
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
```
|
|
640
|
+
|
|
631
641
|
### Option reference
|
|
632
642
|
|
|
633
643
|
```ruby
|
|
@@ -635,6 +645,13 @@ Typelizer.configure do |config|
|
|
|
635
645
|
# Name to type mapping for serializer classes
|
|
636
646
|
config.serializer_name_mapper = ->(serializer) { ... }
|
|
637
647
|
|
|
648
|
+
# Custom file path mapping (decouples filename from type name)
|
|
649
|
+
# Receives the mapped name (output of serializer_name_mapper) and returns a file path.
|
|
650
|
+
# When nil (default), filename is derived from the type name.
|
|
651
|
+
# Example: ->(name) { name.gsub("::", "/") }
|
|
652
|
+
# Alba::UserSerializer → types/Alba/User.ts (type name stays AlbaUser)
|
|
653
|
+
config.filename_mapper = nil
|
|
654
|
+
|
|
638
655
|
# Maps serializers to their corresponding model classes
|
|
639
656
|
config.serializer_model_mapper = ->(serializer) { ... }
|
|
640
657
|
|
data/lib/typelizer/config.rb
CHANGED
|
@@ -41,6 +41,7 @@ module Typelizer
|
|
|
41
41
|
# Config keys that don't affect file content (runtime behavior, or effects captured via properties).
|
|
42
42
|
CONFIGS_NOT_AFFECTING_OUTPUT = %i[
|
|
43
43
|
serializer_name_mapper
|
|
44
|
+
filename_mapper
|
|
44
45
|
serializer_model_mapper
|
|
45
46
|
properties_transformer
|
|
46
47
|
model_plugin
|
|
@@ -56,6 +57,7 @@ module Typelizer
|
|
|
56
57
|
|
|
57
58
|
Config = Struct.new(
|
|
58
59
|
:serializer_name_mapper,
|
|
60
|
+
:filename_mapper,
|
|
59
61
|
:serializer_model_mapper,
|
|
60
62
|
:properties_transformer,
|
|
61
63
|
:properties_sort_order,
|
|
@@ -95,6 +97,8 @@ module Typelizer
|
|
|
95
97
|
name.sub(/(Serializer|Resource)\z/, "")
|
|
96
98
|
end,
|
|
97
99
|
|
|
100
|
+
filename_mapper: nil,
|
|
101
|
+
|
|
98
102
|
serializer_model_mapper: lambda do |serializer|
|
|
99
103
|
base_class = serializer_name_mapper.call(serializer)
|
|
100
104
|
Object.const_get(base_class) if Object.const_defined?(base_class)
|
data/lib/typelizer/interface.rb
CHANGED
|
@@ -36,7 +36,20 @@ module Typelizer
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def filename
|
|
39
|
-
|
|
39
|
+
if config.filename_mapper
|
|
40
|
+
config.filename_mapper.call(config.serializer_name_mapper.call(serializer))
|
|
41
|
+
else
|
|
42
|
+
name.gsub("::", "/")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def index_path(index_dir)
|
|
47
|
+
iface_dir = config.output_dir.to_s
|
|
48
|
+
if iface_dir != index_dir
|
|
49
|
+
Pathname.new(File.join(iface_dir, filename)).relative_path_from(Pathname.new(index_dir)).to_s
|
|
50
|
+
else
|
|
51
|
+
"./#{filename}"
|
|
52
|
+
end
|
|
40
53
|
end
|
|
41
54
|
|
|
42
55
|
def root_key
|
|
@@ -166,6 +179,8 @@ module Typelizer
|
|
|
166
179
|
props.flat_map do |prop|
|
|
167
180
|
if prop.nested_properties&.any?
|
|
168
181
|
[prop] + collect_all_properties(prop.nested_properties)
|
|
182
|
+
elsif prop.type.is_a?(Interface) && prop.type.inline?
|
|
183
|
+
[prop] + collect_all_properties(prop.type.properties)
|
|
169
184
|
else
|
|
170
185
|
[prop]
|
|
171
186
|
end
|
data/lib/typelizer/openapi.rb
CHANGED
|
@@ -31,21 +31,22 @@ module Typelizer
|
|
|
31
31
|
def schema_for(interface, openapi_version: "3.0")
|
|
32
32
|
validate_version!(openapi_version)
|
|
33
33
|
|
|
34
|
+
type_mapping = interface.respond_to?(:config) ? interface.config.type_mapping : Typelizer.configuration.type_mapping
|
|
34
35
|
required_props = interface.properties.reject(&:optional).map(&:name)
|
|
35
36
|
schema = {
|
|
36
37
|
type: :object,
|
|
37
|
-
properties: interface.properties.to_h { |prop| [prop.name, property_schema(prop, openapi_version: openapi_version)] }
|
|
38
|
+
properties: interface.properties.to_h { |prop| [prop.name, property_schema(prop, openapi_version: openapi_version, type_mapping: type_mapping)] }
|
|
38
39
|
}
|
|
39
40
|
schema[:required] = required_props if required_props.any?
|
|
40
41
|
schema
|
|
41
42
|
end
|
|
42
43
|
|
|
43
|
-
def property_schema(property, openapi_version: "3.0")
|
|
44
|
+
def property_schema(property, openapi_version: "3.0", type_mapping: Typelizer.configuration.type_mapping)
|
|
44
45
|
if property.type.is_a?(Array)
|
|
45
46
|
return union_schema(property, openapi_version: openapi_version)
|
|
46
47
|
end
|
|
47
48
|
|
|
48
|
-
definition = base_type(property, openapi_version: openapi_version)
|
|
49
|
+
definition = base_type(property, openapi_version: openapi_version, type_mapping: type_mapping)
|
|
49
50
|
ref = definition.delete("$ref")
|
|
50
51
|
|
|
51
52
|
definition = if ref
|
|
@@ -171,7 +172,7 @@ module Typelizer
|
|
|
171
172
|
definition
|
|
172
173
|
end
|
|
173
174
|
|
|
174
|
-
def base_type(property, openapi_version:)
|
|
175
|
+
def base_type(property, openapi_version:, type_mapping:)
|
|
175
176
|
if property.type.respond_to?(:properties)
|
|
176
177
|
if property.type.respond_to?(:inline?) && property.type.inline?
|
|
177
178
|
schema_for(property.type, openapi_version: openapi_version)
|
|
@@ -179,8 +180,9 @@ module Typelizer
|
|
|
179
180
|
{"$ref" => "#/components/schemas/#{property.type.name}"}
|
|
180
181
|
end
|
|
181
182
|
elsif property.type.nil? && property.respond_to?(:nested_properties) && property.nested_properties&.any?
|
|
182
|
-
nested_schema(property, openapi_version: openapi_version)
|
|
183
|
-
elsif property.column_type && COLUMN_TYPE_MAP.key?(property.column_type)
|
|
183
|
+
nested_schema(property, openapi_version: openapi_version, type_mapping: type_mapping)
|
|
184
|
+
elsif property.column_type && COLUMN_TYPE_MAP.key?(property.column_type) &&
|
|
185
|
+
!type_mapping_overridden?(property, type_mapping)
|
|
184
186
|
result = COLUMN_TYPE_MAP[property.column_type].dup
|
|
185
187
|
result[:type] = :string if property.enum
|
|
186
188
|
result
|
|
@@ -194,16 +196,20 @@ module Typelizer
|
|
|
194
196
|
end
|
|
195
197
|
end
|
|
196
198
|
|
|
197
|
-
def nested_schema(property, openapi_version:)
|
|
199
|
+
def nested_schema(property, openapi_version:, type_mapping:)
|
|
198
200
|
required = property.nested_properties.reject(&:optional).map(&:name)
|
|
199
201
|
schema = {
|
|
200
202
|
type: :object,
|
|
201
|
-
properties: property.nested_properties.to_h { |p| [p.name, property_schema(p, openapi_version: openapi_version)] }
|
|
203
|
+
properties: property.nested_properties.to_h { |p| [p.name, property_schema(p, openapi_version: openapi_version, type_mapping: type_mapping)] }
|
|
202
204
|
}
|
|
203
205
|
schema[:required] = required if required.any?
|
|
204
206
|
schema
|
|
205
207
|
end
|
|
206
208
|
|
|
209
|
+
def type_mapping_overridden?(property, type_mapping)
|
|
210
|
+
type_mapping[property.column_type] != TYPE_MAPPING[property.column_type]
|
|
211
|
+
end
|
|
212
|
+
|
|
207
213
|
def v31?(openapi_version)
|
|
208
214
|
openapi_version.to_s == "3.1"
|
|
209
215
|
end
|
|
@@ -61,11 +61,12 @@ module Typelizer
|
|
|
61
61
|
def build_collected_property(name, attr)
|
|
62
62
|
case attr
|
|
63
63
|
when BlockAttributeCollector::BlockAssociation
|
|
64
|
+
prop_name = has_transform_key?(serializer) ? fetch_key(serializer, name) : name
|
|
64
65
|
with_traits = Array(attr.with_traits) if attr.with_traits
|
|
65
66
|
resource = attr.resource || infer_resource_from_name(name)
|
|
66
67
|
|
|
67
68
|
Property.new(
|
|
68
|
-
name:
|
|
69
|
+
name: prop_name,
|
|
69
70
|
type: resource ? context.interface_for(resource) : nil,
|
|
70
71
|
optional: false,
|
|
71
72
|
nullable: false,
|
|
@@ -5,8 +5,8 @@ export type { <%= ImportSorter.sort(enums.map(&:enum_type_name), imports_sort_or
|
|
|
5
5
|
<%- interfaces.each do |interface| -%>
|
|
6
6
|
<%- sorted_traits = ImportSorter.sort(interface.trait_interfaces.map(&:name), interface.config.imports_sort_order) -%>
|
|
7
7
|
<%- if interface.config.verbatim_module_syntax -%>
|
|
8
|
-
export type { <%= interface.name %><%= ", " + sorted_traits.join(", ") if sorted_traits.any? %> } from <%= interface.quote(
|
|
8
|
+
export type { <%= interface.name %><%= ", " + sorted_traits.join(", ") if sorted_traits.any? %> } from <%= interface.quote(interface.index_path(index_dir)) %>
|
|
9
9
|
<%- else -%>
|
|
10
|
-
export type { default as <%= interface.name %><%= ", " + sorted_traits.join(", ") if sorted_traits.any? %> } from <%= interface.quote(
|
|
10
|
+
export type { default as <%= interface.name %><%= ", " + sorted_traits.join(", ") if sorted_traits.any? %> } from <%= interface.quote(interface.index_path(index_dir)) %>
|
|
11
11
|
<%- end -%>
|
|
12
12
|
<%- end -%>
|
data/lib/typelizer/version.rb
CHANGED
data/lib/typelizer/writer.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Typelizer
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def call(interfaces, force:)
|
|
15
|
-
cleanup_output_dir if force
|
|
15
|
+
cleanup_output_dir(interfaces) if force
|
|
16
16
|
|
|
17
17
|
valid_interfaces = interfaces.reject(&:empty?)
|
|
18
18
|
return [] if valid_interfaces.empty?
|
|
@@ -27,7 +27,7 @@ module Typelizer
|
|
|
27
27
|
|
|
28
28
|
written_files << write_index(valid_interfaces, enums: enums)
|
|
29
29
|
|
|
30
|
-
cleanup_stale_files(written_files) unless force
|
|
30
|
+
cleanup_stale_files(written_files, valid_interfaces) unless force
|
|
31
31
|
|
|
32
32
|
Typelizer.logger.debug("Generated #{written_files.size} TypeScript files in #{config.output_dir}")
|
|
33
33
|
|
|
@@ -43,10 +43,10 @@ module Typelizer
|
|
|
43
43
|
|
|
44
44
|
attr_reader :config, :template_cache
|
|
45
45
|
|
|
46
|
-
def cleanup_stale_files(written_files)
|
|
47
|
-
|
|
46
|
+
def cleanup_stale_files(written_files, interfaces)
|
|
47
|
+
output_dirs = output_dirs_for(interfaces)
|
|
48
48
|
|
|
49
|
-
existing_files = Dir[File.join(
|
|
49
|
+
existing_files = output_dirs.flat_map { |dir| Dir[File.join(dir, "**/*.ts")] }
|
|
50
50
|
stale_files = existing_files - written_files
|
|
51
51
|
|
|
52
52
|
File.delete(*stale_files) unless stale_files.empty?
|
|
@@ -70,22 +70,23 @@ module Typelizer
|
|
|
70
70
|
fingerprint = [
|
|
71
71
|
enums.map(&:enum_type_name),
|
|
72
72
|
interfaces.map { |i|
|
|
73
|
-
[i.name, i.filename, i.trait_interfaces.map(&:name), CONFIGS_AFFECTING_INDEX_OUTPUT.map { |key| i.config.public_send(key) }]
|
|
73
|
+
[i.name, i.filename, i.index_path(config.output_dir.to_s), i.trait_interfaces.map(&:name), CONFIGS_AFFECTING_INDEX_OUTPUT.map { |key| i.config.public_send(key) }]
|
|
74
74
|
}
|
|
75
75
|
].inspect
|
|
76
76
|
write_file("index.ts", fingerprint) do
|
|
77
|
-
render_template("index.ts.erb", interfaces: interfaces, enums: enums, imports_sort_order: config.imports_sort_order, prefer_double_quotes: config.prefer_double_quotes)
|
|
77
|
+
render_template("index.ts.erb", interfaces: interfaces, enums: enums, index_dir: config.output_dir.to_s, imports_sort_order: config.imports_sort_order, prefer_double_quotes: config.prefer_double_quotes)
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
def write_interface(interface)
|
|
82
|
-
|
|
82
|
+
output_dir = interface.config.output_dir
|
|
83
|
+
write_file("#{interface.filename}.ts", interface.fingerprint, output_dir: output_dir) do
|
|
83
84
|
render_template("interface.ts.erb", interface: interface)
|
|
84
85
|
end
|
|
85
86
|
end
|
|
86
87
|
|
|
87
|
-
def write_file(filename, fingerprint)
|
|
88
|
-
output_file = File.join(
|
|
88
|
+
def write_file(filename, fingerprint, output_dir: config.output_dir)
|
|
89
|
+
output_file = File.join(output_dir, filename)
|
|
89
90
|
existing_content = File.exist?(output_file) ? File.read(output_file) : nil
|
|
90
91
|
digest = render_template("fingerprint.ts.erb", fingerprint: fingerprint)
|
|
91
92
|
|
|
@@ -104,8 +105,14 @@ module Typelizer
|
|
|
104
105
|
template_cache[template].call(**context)
|
|
105
106
|
end
|
|
106
107
|
|
|
107
|
-
def cleanup_output_dir
|
|
108
|
-
FileUtils.rm_rf(
|
|
108
|
+
def cleanup_output_dir(interfaces)
|
|
109
|
+
output_dirs_for(interfaces).each { |dir| FileUtils.rm_rf(dir) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def output_dirs_for(interfaces)
|
|
113
|
+
dirs = interfaces.filter_map { |i| i.config.output_dir.to_s unless i.empty? }
|
|
114
|
+
dirs << config.output_dir.to_s
|
|
115
|
+
dirs.uniq
|
|
109
116
|
end
|
|
110
117
|
|
|
111
118
|
def cleanup_partial_writes(partial_files)
|