sorbet-typescript 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +103 -0
- data/Taskfile.yml +41 -0
- data/lib/sorbet/typescript/cli.rb +96 -0
- data/lib/sorbet/typescript/exporter.rb +546 -0
- data/lib/sorbet/typescript/version.rb +8 -0
- data/lib/sorbet/typescript.rb +14 -0
- data/sig/sorbet/typescript.rbs +39 -0
- data/sorbet-typescript.gemspec +45 -0
- metadata +181 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 20b6b038eabc0014a293fcd994c56e1413d217613fa86c0df40157dc3e4399a2
|
4
|
+
data.tar.gz: 331239897ffb2f29a4b52dd09987d1fecdbbb298f9cfd8a42dc7898e0034889c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 28d320cb930debcab937be85c99d676743469c3a223a6d9b3e1ae50d89b14e123a7d81f698126cde66f978d0147fddaadf43422da9d5e941fe1c31e52d6b46be
|
7
|
+
data.tar.gz: b7656d8962f845ed7ff48afa5a507416dfd0accdac1f8a6ff641a2561b58056fb85db80471cf78244e62a3bbc2df7511246bd1adb5189ebc0137df512b990cbf
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Nathan Broadbent
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# Sorbet::Typescript
|
2
|
+
|
3
|
+
Sorbet::Typescript exports Sorbet `T::Enum` and `T::Struct` definitions into JSON metadata and TypeScript declarations so front-end clients can stay in sync with your Ruby types.
|
4
|
+
|
5
|
+
## Background
|
6
|
+
|
7
|
+
DocSpring built this gem for [LogStruct](https://github.com/DocSpring/logstruct) to keep its documented payload types in lockstep with the Ruby source. The generated metadata powers the live reference at [logstruct.com/docs/sorbet-types](https://logstruct.com/docs/sorbet-types/) and all sample logs published across [logstruct.com](https://logstruct.com/), ensuring every example stays current through code generation.
|
8
|
+
|
9
|
+
See how LogStruct wires the exporter into its build pipeline in [`scripts/generate_structs.rb`](https://github.com/DocSpring/logstruct/blob/main/scripts/generate_structs.rb).
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add the gem to your application:
|
14
|
+
|
15
|
+
```bash
|
16
|
+
bundle add sorbet-typescript
|
17
|
+
```
|
18
|
+
|
19
|
+
If you need the latest unreleased changes, point bundler at GitHub:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
bundle add sorbet-typescript --git https://github.com/DocSpring/sorbet-typescript
|
23
|
+
```
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
### Command line
|
28
|
+
|
29
|
+
The executable `sorbet-typescript` inspects the namespaces you specify and writes JSON and/or TypeScript files:
|
30
|
+
|
31
|
+
```bash
|
32
|
+
bundle exec sorbet-typescript export \
|
33
|
+
--struct-namespace MyApp::Types \
|
34
|
+
--enum-namespace MyApp::Enums \
|
35
|
+
--typescript-file app/frontend/types/generated.ts
|
36
|
+
```
|
37
|
+
|
38
|
+
You can also drive the export with a YAML config (default path `sorbet_typescript.yml`):
|
39
|
+
|
40
|
+
```yaml
|
41
|
+
requires:
|
42
|
+
- config/environment
|
43
|
+
struct_namespaces:
|
44
|
+
- MyApp::Types
|
45
|
+
enum_namespaces:
|
46
|
+
- MyApp::Enums
|
47
|
+
outputs:
|
48
|
+
typescript_file: app/frontend/types/generated.ts
|
49
|
+
enums_json: tmp/sorbet/enums.json
|
50
|
+
structs_json: tmp/sorbet/structs.json
|
51
|
+
```
|
52
|
+
|
53
|
+
Run it with `bundle exec sorbet-typescript export --config sorbet_typescript.yml`. Any CLI options override the config file.
|
54
|
+
|
55
|
+
### Ruby API
|
56
|
+
|
57
|
+
If you want to script the export yourself, use the `Exporter` directly:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
exporter = Sorbet::Typescript::Exporter.new(
|
61
|
+
enum_namespaces: ["MyApp::Enums"],
|
62
|
+
struct_namespaces: ["MyApp::Types"]
|
63
|
+
)
|
64
|
+
|
65
|
+
exporter.export(
|
66
|
+
typescript_file: "app/frontend/types/generated.ts",
|
67
|
+
enums_json: "tmp/sorbet/enums.json",
|
68
|
+
structs_json: "tmp/sorbet/structs.json"
|
69
|
+
)
|
70
|
+
```
|
71
|
+
|
72
|
+
The returned `ExportData` object contains `enums` and `structs` hashes that you can leverage for custom tooling.
|
73
|
+
|
74
|
+
## Development
|
75
|
+
|
76
|
+
After cloning the repo, install dependencies and run the task workflow:
|
77
|
+
|
78
|
+
```bash
|
79
|
+
task setup
|
80
|
+
task all
|
81
|
+
```
|
82
|
+
|
83
|
+
Useful individual commands:
|
84
|
+
|
85
|
+
- `task typecheck` – run Sorbet (`srb tc`) with the strict sigils enforced in the repo
|
86
|
+
- `task lint` / `task lint:fix` – Standard/RuboCop lint checks and auto-corrections
|
87
|
+
- `task test` – execute the Minitest suite via `scripts/test.rb`
|
88
|
+
|
89
|
+
Make sure you have [Go Task](https://taskfile.dev/#/installation) installed to run the `task` command.
|
90
|
+
|
91
|
+
## Releasing
|
92
|
+
|
93
|
+
Bump the version in `lib/sorbet/typescript/version.rb`, then run:
|
94
|
+
|
95
|
+
```bash
|
96
|
+
bundle exec rake release
|
97
|
+
```
|
98
|
+
|
99
|
+
This creates a git tag, pushes the commit and tag, and publishes the gem.
|
100
|
+
|
101
|
+
## License
|
102
|
+
|
103
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Taskfile.yml
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
version: '3'
|
2
|
+
|
3
|
+
tasks:
|
4
|
+
setup:
|
5
|
+
desc: Install Ruby dependencies
|
6
|
+
cmds:
|
7
|
+
- bundle install
|
8
|
+
|
9
|
+
typecheck:
|
10
|
+
desc: Run Sorbet type checking
|
11
|
+
cmds:
|
12
|
+
- ./scripts/typecheck.sh
|
13
|
+
|
14
|
+
lint:
|
15
|
+
desc: Run RuboCop lint checks
|
16
|
+
cmds:
|
17
|
+
- bundle exec rubocop
|
18
|
+
|
19
|
+
lint:fix:
|
20
|
+
desc: Auto-fix RuboCop issues
|
21
|
+
cmds:
|
22
|
+
- bundle exec rubocop -A
|
23
|
+
|
24
|
+
test:
|
25
|
+
desc: Run the Ruby test suite
|
26
|
+
cmds:
|
27
|
+
- ./scripts/test.rb
|
28
|
+
|
29
|
+
all:
|
30
|
+
aliases: [ci]
|
31
|
+
desc: Run the full validation workflow
|
32
|
+
cmds:
|
33
|
+
- task: typecheck
|
34
|
+
- task: lint
|
35
|
+
- task: test
|
36
|
+
|
37
|
+
fix:
|
38
|
+
desc: Auto-fix lint, then rerun validation
|
39
|
+
cmds:
|
40
|
+
- task: lint:fix
|
41
|
+
- task: all
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "yaml"
|
5
|
+
require "thor"
|
6
|
+
require "sorbet-runtime"
|
7
|
+
|
8
|
+
class Sorbet; end unless defined?(Sorbet)
|
9
|
+
|
10
|
+
module Sorbet::Typescript
|
11
|
+
class CLI < Thor
|
12
|
+
extend T::Sig
|
13
|
+
|
14
|
+
DEFAULT_CONFIG = "sorbet_typescript.yml"
|
15
|
+
|
16
|
+
class_option :config, type: :string, desc: "Path to configuration file"
|
17
|
+
|
18
|
+
desc "export", "Export Sorbet enums and structs to JSON and TypeScript files"
|
19
|
+
option :require, type: :array, desc: "Files to require before exporting"
|
20
|
+
option :struct_namespace, type: :array, desc: "Namespaces containing T::Struct subclasses"
|
21
|
+
option :enum_namespace, type: :array, desc: "Namespaces containing T::Enum subclasses"
|
22
|
+
option :enums_json, type: :string, desc: "Output path for enums JSON"
|
23
|
+
option :structs_json, type: :string, desc: "Output path for structs JSON"
|
24
|
+
option :typescript_file, type: :string, desc: "Output path for generated TypeScript definitions"
|
25
|
+
option :property_map_json, type: :string, desc: "Output path for property metadata JSON"
|
26
|
+
def export
|
27
|
+
opts = symbolize_keys(options.to_h)
|
28
|
+
configuration = load_configuration(opts[:config])
|
29
|
+
|
30
|
+
Array(configuration.fetch("requires", [])).concat(opts[:require] || []).each do |requirement|
|
31
|
+
require requirement
|
32
|
+
end
|
33
|
+
|
34
|
+
enum_namespaces = resolved_namespace_list(
|
35
|
+
configuration,
|
36
|
+
opts,
|
37
|
+
key: "enum_namespaces",
|
38
|
+
option_key: :enum_namespace
|
39
|
+
)
|
40
|
+
struct_namespaces = resolved_namespace_list(
|
41
|
+
configuration,
|
42
|
+
opts,
|
43
|
+
key: "struct_namespaces",
|
44
|
+
option_key: :struct_namespace
|
45
|
+
)
|
46
|
+
|
47
|
+
outputs = T.cast(configuration.fetch("outputs", {}), T::Hash[String, T.untyped])
|
48
|
+
|
49
|
+
exporter = Exporter.new(
|
50
|
+
enum_namespaces: enum_namespaces,
|
51
|
+
struct_namespaces: struct_namespaces
|
52
|
+
)
|
53
|
+
|
54
|
+
result = exporter.export(
|
55
|
+
enums_json: opts[:enums_json] || outputs["enums_json"],
|
56
|
+
structs_json: opts[:structs_json] || outputs["structs_json"],
|
57
|
+
typescript_file: opts[:typescript_file] || outputs["typescript_file"],
|
58
|
+
property_map_json: opts[:property_map_json] || outputs["property_map_json"]
|
59
|
+
)
|
60
|
+
|
61
|
+
say "Exported #{result.enums.keys.size} enums and #{result.structs.keys.size} structs"
|
62
|
+
rescue Error => e
|
63
|
+
raise Thor::Error, e.message
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
sig { params(config_path: T.nilable(String)).returns(T::Hash[String, T.untyped]) }
|
69
|
+
def load_configuration(config_path)
|
70
|
+
path = config_path || (File.exist?(DEFAULT_CONFIG) ? DEFAULT_CONFIG : nil)
|
71
|
+
return {} unless path
|
72
|
+
|
73
|
+
YAML.load_file(path) || {}
|
74
|
+
rescue Errno::ENOENT
|
75
|
+
raise Error, "Configuration file not found: #{path}"
|
76
|
+
rescue Psych::SyntaxError => e
|
77
|
+
raise Error, "Unable to parse configuration file #{path}: #{e.message}"
|
78
|
+
end
|
79
|
+
|
80
|
+
sig do
|
81
|
+
params(configuration: T::Hash[String, T.untyped], options: T::Hash[Symbol, T.untyped], key: String, option_key: Symbol)
|
82
|
+
.returns(T::Array[T.any(String, Module)])
|
83
|
+
end
|
84
|
+
def resolved_namespace_list(configuration, options, key:, option_key:)
|
85
|
+
cli_value = options[option_key]
|
86
|
+
return Array(cli_value) if cli_value && !cli_value.empty?
|
87
|
+
|
88
|
+
Array(configuration.fetch(key, []))
|
89
|
+
end
|
90
|
+
|
91
|
+
sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
92
|
+
def symbolize_keys(hash)
|
93
|
+
hash.transform_keys { |key| key.to_sym }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,546 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "json"
|
5
|
+
require "date"
|
6
|
+
require "fileutils"
|
7
|
+
require "sorbet-runtime"
|
8
|
+
|
9
|
+
class Sorbet; end unless defined?(Sorbet)
|
10
|
+
|
11
|
+
module Sorbet::Typescript
|
12
|
+
class Exporter
|
13
|
+
extend T::Sig
|
14
|
+
|
15
|
+
class ExportData < T::Struct
|
16
|
+
const :enums, T::Hash[String, T::Hash[Symbol, T.untyped]]
|
17
|
+
const :structs, T::Hash[String, T::Hash[Symbol, T.untyped]]
|
18
|
+
end
|
19
|
+
|
20
|
+
sig do
|
21
|
+
params(
|
22
|
+
enum_namespaces: T::Array[T.any(String, Module)],
|
23
|
+
struct_namespaces: T::Array[T.any(String, Module)]
|
24
|
+
).void
|
25
|
+
end
|
26
|
+
def initialize(enum_namespaces: [], struct_namespaces: [])
|
27
|
+
@enum_namespaces = normalize_namespaces(enum_namespaces)
|
28
|
+
@struct_namespaces = normalize_namespaces(struct_namespaces)
|
29
|
+
end
|
30
|
+
|
31
|
+
sig do
|
32
|
+
params(
|
33
|
+
enums_json: T.nilable(String),
|
34
|
+
structs_json: T.nilable(String),
|
35
|
+
typescript_file: T.nilable(String),
|
36
|
+
property_map_json: T.nilable(String)
|
37
|
+
).returns(ExportData)
|
38
|
+
end
|
39
|
+
def export(enums_json: nil, structs_json: nil, typescript_file: nil, property_map_json: nil)
|
40
|
+
data = generate_data
|
41
|
+
|
42
|
+
write_json(enums_json, data.enums) if enums_json
|
43
|
+
write_json(structs_json, data.structs) if structs_json
|
44
|
+
write_property_map(property_map_json, data.structs) if property_map_json
|
45
|
+
write_typescript(typescript_file, data) if typescript_file
|
46
|
+
|
47
|
+
data
|
48
|
+
end
|
49
|
+
|
50
|
+
sig { returns(ExportData) }
|
51
|
+
def generate_data
|
52
|
+
enums = export_enums
|
53
|
+
structs = export_structs
|
54
|
+
ExportData.new(enums: enums, structs: structs)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
sig { params(enum_namespaces: T::Array[T.any(String, Module)]).returns(T::Array[String]) }
|
60
|
+
def normalize_namespaces(enum_namespaces)
|
61
|
+
enum_namespaces.map do |namespace|
|
62
|
+
case namespace
|
63
|
+
when Module
|
64
|
+
namespace.name || ""
|
65
|
+
else
|
66
|
+
namespace.to_s
|
67
|
+
end
|
68
|
+
end.reject(&:empty?)
|
69
|
+
end
|
70
|
+
|
71
|
+
sig { returns(T::Array[T.class_of(T::Enum)]) }
|
72
|
+
def enum_classes
|
73
|
+
T::Enum.subclasses.to_a.compact.select do |klass|
|
74
|
+
klass_name = klass.name
|
75
|
+
klass_name && in_namespaces?(klass_name, @enum_namespaces)
|
76
|
+
end.sort_by { |klass| T.must(klass.name) }
|
77
|
+
end
|
78
|
+
|
79
|
+
sig { returns(T::Array[T.class_of(T::Struct)]) }
|
80
|
+
def struct_classes
|
81
|
+
T::Struct.subclasses.to_a.compact.select do |klass|
|
82
|
+
klass_name = klass.name
|
83
|
+
klass_name && in_namespaces?(klass_name, @struct_namespaces)
|
84
|
+
end.sort_by { |klass| T.must(klass.name) }
|
85
|
+
end
|
86
|
+
|
87
|
+
sig { params(name: String, namespaces: T::Array[String]).returns(T::Boolean) }
|
88
|
+
def in_namespaces?(name, namespaces)
|
89
|
+
return true if namespaces.empty?
|
90
|
+
|
91
|
+
namespaces.any? do |namespace|
|
92
|
+
name == namespace || name.start_with?("#{namespace}::")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
sig { returns(T::Hash[String, T::Hash[Symbol, T.untyped]]) }
|
97
|
+
def export_enums
|
98
|
+
enum_classes.each_with_object({}) do |enum_class, memo|
|
99
|
+
class_name = T.must(enum_class.name)
|
100
|
+
values = enum_class.values.map do |value|
|
101
|
+
{
|
102
|
+
name: enum_constant_name(value),
|
103
|
+
serialized: value.serialize
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
memo[class_name] = {
|
108
|
+
name: class_name,
|
109
|
+
simple_name: class_name.split("::").last,
|
110
|
+
values: values
|
111
|
+
}
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
sig { returns(T::Hash[String, T::Hash[Symbol, T.untyped]]) }
|
116
|
+
def export_structs
|
117
|
+
struct_classes.each_with_object({}) do |struct_class, memo|
|
118
|
+
class_name = T.must(struct_class.name)
|
119
|
+
fields = struct_class.props.each_with_object({}) do |(field, prop_info), acc|
|
120
|
+
acc[field.to_s] = extract_type_info(prop_info)
|
121
|
+
end
|
122
|
+
|
123
|
+
memo[class_name] = {
|
124
|
+
name: class_name,
|
125
|
+
simple_name: class_name.split("::").last,
|
126
|
+
fields: fields
|
127
|
+
}
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
sig { params(path: String, data: T::Hash[String, T::Hash[Symbol, T.untyped]]).void }
|
132
|
+
def write_json(path, data)
|
133
|
+
ensure_dir(File.dirname(path))
|
134
|
+
File.write(path, JSON.pretty_generate(stringify_keys(data)))
|
135
|
+
end
|
136
|
+
|
137
|
+
sig { params(path: String, structs: T::Hash[String, T::Hash[Symbol, T.untyped]]).void }
|
138
|
+
def write_property_map(path, structs)
|
139
|
+
return unless path
|
140
|
+
|
141
|
+
mapping = structs.each_with_object({}) do |(struct_name, info), acc|
|
142
|
+
fields = T.cast(info[:fields], T::Hash[String, T::Hash[Symbol, T.untyped]])
|
143
|
+
fields.each do |field_name, field_info|
|
144
|
+
key = "#{struct_name}.#{field_name}"
|
145
|
+
acc[key] = field_info
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
write_json(path, mapping)
|
150
|
+
end
|
151
|
+
|
152
|
+
sig { params(path: String, data: ExportData).void }
|
153
|
+
def write_typescript(path, data)
|
154
|
+
return unless path
|
155
|
+
|
156
|
+
ensure_dir(File.dirname(path))
|
157
|
+
|
158
|
+
enums = data.enums
|
159
|
+
structs = data.structs
|
160
|
+
|
161
|
+
lines = []
|
162
|
+
lines << "// @generated by sorbet-typescript"
|
163
|
+
lines << "/* eslint-disable */"
|
164
|
+
lines << ""
|
165
|
+
|
166
|
+
enums.keys.sort.each do |enum_name|
|
167
|
+
enum_info = enums.fetch(enum_name)
|
168
|
+
ts_name = typescript_identifier(enum_name)
|
169
|
+
values = T.cast(enum_info[:values], T::Array[T::Hash[Symbol, T.untyped]])
|
170
|
+
|
171
|
+
union = values.map { |value| %("#{value[:serialized]}") }.join(" | ")
|
172
|
+
literal_values = values.map { |value| %("#{value[:serialized]}") }
|
173
|
+
|
174
|
+
lines << "export type #{ts_name} = #{union};"
|
175
|
+
lines << "export const #{ts_name}Values = [#{literal_values.join(", ")}] as const;"
|
176
|
+
lines << ""
|
177
|
+
end
|
178
|
+
|
179
|
+
structs.keys.sort.each do |struct_name|
|
180
|
+
struct_info = structs.fetch(struct_name)
|
181
|
+
ts_name = typescript_identifier(struct_name)
|
182
|
+
fields = T.cast(struct_info[:fields], T::Hash[String, T::Hash[Symbol, T.untyped]])
|
183
|
+
|
184
|
+
lines << "export interface #{ts_name} {"
|
185
|
+
fields.keys.sort.each do |field_name|
|
186
|
+
field_info = fields.fetch(field_name)
|
187
|
+
optional = field_info[:optional] ? "?" : ""
|
188
|
+
ts_type = typescript_type_for(field_info, enums)
|
189
|
+
lines << " #{field_name}#{optional}: #{ts_type};"
|
190
|
+
end
|
191
|
+
lines << "}"
|
192
|
+
lines << ""
|
193
|
+
end
|
194
|
+
|
195
|
+
contents = lines.join("\n").rstrip + "\n"
|
196
|
+
File.write(path, contents)
|
197
|
+
end
|
198
|
+
|
199
|
+
sig { params(field_info: T::Hash[Symbol, T.untyped], enums: T::Hash[String, T::Hash[Symbol, T.untyped]]).returns(String) }
|
200
|
+
def typescript_type_for(field_info, enums)
|
201
|
+
case field_info[:type]
|
202
|
+
when "enum"
|
203
|
+
ts_name = field_info[:enum_name]
|
204
|
+
return "unknown" unless ts_name
|
205
|
+
|
206
|
+
enum_key = T.cast(ts_name, String)
|
207
|
+
if enums.key?(enum_key)
|
208
|
+
typescript_identifier(enum_key)
|
209
|
+
else
|
210
|
+
typescript_identifier(enum_key.split("::").last)
|
211
|
+
end
|
212
|
+
when "enum_single"
|
213
|
+
details = enum_value_details_for(field_info)
|
214
|
+
detail = details.first
|
215
|
+
literal = detail&.fetch(:serialized, nil) || detail&.fetch(:name, nil) || field_info[:enum_value_serialized] || field_info[:enum_value]
|
216
|
+
literal ? %("#{literal}") : "unknown"
|
217
|
+
when "enum_union"
|
218
|
+
details = enum_value_details_for(field_info)
|
219
|
+
return "unknown" if details.empty?
|
220
|
+
|
221
|
+
base_enum = field_info[:base_enum] || simple_enum_name(field_info[:enum_name])
|
222
|
+
|
223
|
+
details.map do |detail|
|
224
|
+
serialized = detail[:serialized]
|
225
|
+
constant_name = detail[:name]
|
226
|
+
|
227
|
+
if base_enum && serialized
|
228
|
+
"#{base_enum}.#{serialized.upcase}"
|
229
|
+
elsif base_enum && constant_name
|
230
|
+
"#{base_enum}.#{constant_name.upcase}"
|
231
|
+
else
|
232
|
+
%("#{serialized || constant_name}")
|
233
|
+
end
|
234
|
+
end.join(" | ")
|
235
|
+
when "array"
|
236
|
+
item = T.cast(field_info[:item], T::Hash[Symbol, T.untyped])
|
237
|
+
"#{typescript_type_for(item, enums)}[]"
|
238
|
+
when "struct"
|
239
|
+
ts_name = field_info[:struct_name]
|
240
|
+
ts_name ? typescript_identifier(ts_name) : "Record<string, unknown>"
|
241
|
+
when "object", "hash"
|
242
|
+
"Record<string, unknown>"
|
243
|
+
when "string"
|
244
|
+
"string"
|
245
|
+
when "integer", "number"
|
246
|
+
"number"
|
247
|
+
when "boolean"
|
248
|
+
"boolean"
|
249
|
+
when "symbol"
|
250
|
+
"string"
|
251
|
+
else
|
252
|
+
"unknown"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
sig { params(name: String).returns(String) }
|
257
|
+
def typescript_identifier(name)
|
258
|
+
name.split("::").join
|
259
|
+
end
|
260
|
+
|
261
|
+
sig { params(prop_info: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
262
|
+
def extract_type_info(prop_info)
|
263
|
+
type_object = prop_info[:type_object] || prop_info[:type]
|
264
|
+
info = analyse_type_object(type_object)
|
265
|
+
info = augment_enum_metadata(info)
|
266
|
+
info[:optional] = prop_info[:_tnilable] || prop_info[:fully_optional] || false
|
267
|
+
info
|
268
|
+
end
|
269
|
+
|
270
|
+
sig { params(obj: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
271
|
+
def analyse_type_object(obj)
|
272
|
+
case obj
|
273
|
+
when T::Types::TypedArray
|
274
|
+
item_type = array_inner_type(obj)
|
275
|
+
item_meta = analyse_type_object(item_type)
|
276
|
+
{type: "array", item: item_meta}
|
277
|
+
when T::Types::TypedHash
|
278
|
+
{type: "object"}
|
279
|
+
when T::Private::Types::SimplePairUnion
|
280
|
+
inner_types = T.cast(obj.instance_variable_get(:@types), T::Array[T::Types::Base])
|
281
|
+
non_nil = inner_types.find do |inner|
|
282
|
+
!(inner.is_a?(T::Types::Simple) && inner.raw_type == NilClass)
|
283
|
+
end
|
284
|
+
non_nil ? analyse_type_object(resolve_type(inner: non_nil)) : {type: "any"}
|
285
|
+
when T::Types::Union
|
286
|
+
analyse_union(obj)
|
287
|
+
when T::Types::Simple
|
288
|
+
analyse_type_object(obj.raw_type)
|
289
|
+
when T::Types::TEnum
|
290
|
+
enum_value = obj.val
|
291
|
+
{
|
292
|
+
type: "enum_single",
|
293
|
+
enum_name: enum_value.class.name,
|
294
|
+
enum_values: [enum_value_metadata(enum_value)]
|
295
|
+
}
|
296
|
+
when T::Types::Untyped
|
297
|
+
{type: "any"}
|
298
|
+
when Class, Module
|
299
|
+
analyse_class(obj)
|
300
|
+
when Symbol
|
301
|
+
{type: "symbol"}
|
302
|
+
else
|
303
|
+
analyse_by_string(obj.to_s)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
sig { params(inner: T::Types::Base).returns(T.untyped) }
|
308
|
+
def resolve_type(inner:)
|
309
|
+
if inner.is_a?(T::Types::Simple)
|
310
|
+
inner.raw_type
|
311
|
+
elsif inner.is_a?(T::Types::TEnum)
|
312
|
+
inner
|
313
|
+
elsif inner.respond_to?(:inner_type)
|
314
|
+
inner
|
315
|
+
else
|
316
|
+
inner
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
sig { params(array_type: T::Types::TypedArray).returns(T.untyped) }
|
321
|
+
def array_inner_type(array_type)
|
322
|
+
if array_type.respond_to?(:inner_type)
|
323
|
+
array_type.inner_type
|
324
|
+
else
|
325
|
+
array_type.instance_variable_get(:@inner_type)
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
sig { params(obj: T::Types::Union).returns(T::Hash[Symbol, T.untyped]) }
|
330
|
+
def analyse_union(obj)
|
331
|
+
inner_types = T.cast(obj.instance_variable_get(:@types), T::Array[T::Types::Base])
|
332
|
+
|
333
|
+
enum_values = []
|
334
|
+
enum_class = nil
|
335
|
+
other_types = []
|
336
|
+
|
337
|
+
inner_types.each do |inner|
|
338
|
+
case inner
|
339
|
+
when T::Types::TEnum
|
340
|
+
enum_value = inner.val
|
341
|
+
enum_class ||= enum_value.class
|
342
|
+
|
343
|
+
if enum_value.instance_of?(enum_class)
|
344
|
+
enum_values << enum_value_metadata(enum_value)
|
345
|
+
else
|
346
|
+
other_types << inner
|
347
|
+
end
|
348
|
+
when T::Types::Simple
|
349
|
+
raw = inner.raw_type
|
350
|
+
next if raw == NilClass
|
351
|
+
|
352
|
+
other_types << inner
|
353
|
+
else
|
354
|
+
other_types << inner
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
if enum_values.any? && other_types.empty?
|
359
|
+
unique_values = enum_values.uniq { |value| value[:serialized] }
|
360
|
+
enum_name = enum_class&.name
|
361
|
+
|
362
|
+
if unique_values.length == 1
|
363
|
+
return {
|
364
|
+
type: "enum_single",
|
365
|
+
enum_name: enum_name,
|
366
|
+
enum_values: unique_values
|
367
|
+
}
|
368
|
+
end
|
369
|
+
|
370
|
+
return {
|
371
|
+
type: "enum_union",
|
372
|
+
enum_name: enum_name,
|
373
|
+
enum_values: unique_values
|
374
|
+
}
|
375
|
+
end
|
376
|
+
|
377
|
+
resolved_non_nil = inner_types.map { |inner| resolve_type(inner: inner) }.reject { |type| type == NilClass }
|
378
|
+
return {type: "any"} if resolved_non_nil.empty?
|
379
|
+
|
380
|
+
if resolved_non_nil.length == 1
|
381
|
+
return analyse_type_object(resolved_non_nil.first)
|
382
|
+
end
|
383
|
+
|
384
|
+
{type: "any"}
|
385
|
+
end
|
386
|
+
|
387
|
+
sig { params(klass: Module).returns(T::Hash[Symbol, T.untyped]) }
|
388
|
+
def analyse_class(klass)
|
389
|
+
if klass <= T::Enum
|
390
|
+
{
|
391
|
+
type: "enum",
|
392
|
+
enum_name: klass.name
|
393
|
+
}
|
394
|
+
elsif klass <= T::Struct
|
395
|
+
{
|
396
|
+
type: "struct",
|
397
|
+
struct_name: klass.name
|
398
|
+
}
|
399
|
+
elsif klass <= String
|
400
|
+
{type: "string"}
|
401
|
+
elsif klass <= Symbol
|
402
|
+
{type: "symbol"}
|
403
|
+
elsif klass <= Integer
|
404
|
+
{type: "integer"}
|
405
|
+
elsif klass <= Float
|
406
|
+
{type: "number"}
|
407
|
+
elsif klass <= TrueClass || klass <= FalseClass
|
408
|
+
{type: "boolean"}
|
409
|
+
elsif klass <= Time || klass <= Date || klass <= DateTime
|
410
|
+
{type: "string", format: "date-time"}
|
411
|
+
elsif klass <= Hash
|
412
|
+
{type: "hash"}
|
413
|
+
elsif klass <= Array
|
414
|
+
{type: "array", item: {type: "any"}}
|
415
|
+
else
|
416
|
+
{type: "any", ruby_type: klass.name}
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
sig { params(type: String).returns(T::Hash[Symbol, T.untyped]) }
|
421
|
+
def analyse_by_string(type)
|
422
|
+
case type
|
423
|
+
when /String/
|
424
|
+
{type: "string"}
|
425
|
+
when /Integer/
|
426
|
+
{type: "integer"}
|
427
|
+
when /Float|Numeric/
|
428
|
+
{type: "number"}
|
429
|
+
when /Boolean|TrueClass|FalseClass/
|
430
|
+
{type: "boolean"}
|
431
|
+
when /Array/
|
432
|
+
{type: "array", item: {type: "any"}}
|
433
|
+
when /Hash/
|
434
|
+
{type: "hash"}
|
435
|
+
else
|
436
|
+
{type: "any"}
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
sig { params(enum_value: T.untyped).returns(T.nilable(String)) }
|
441
|
+
def enum_constant_name(enum_value)
|
442
|
+
const_name = enum_value.instance_variable_get(:@const_name)
|
443
|
+
return unless const_name
|
444
|
+
|
445
|
+
const_name.to_s.delete_prefix(":")
|
446
|
+
end
|
447
|
+
|
448
|
+
sig { params(enum_value: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
449
|
+
def enum_value_metadata(enum_value)
|
450
|
+
{
|
451
|
+
name: enum_constant_name(enum_value),
|
452
|
+
serialized: enum_value.serialize
|
453
|
+
}
|
454
|
+
end
|
455
|
+
|
456
|
+
sig { params(info: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
457
|
+
def augment_enum_metadata(info)
|
458
|
+
case info[:type]
|
459
|
+
when "enum"
|
460
|
+
enum_name = info[:enum_name]
|
461
|
+
info[:values] ||= simple_enum_name(enum_name)
|
462
|
+
when "enum_single"
|
463
|
+
details = enum_value_details_for(info)
|
464
|
+
info[:enum_value_details] = details if details.any?
|
465
|
+
info[:base_enum] ||= simple_enum_name(info[:enum_name])
|
466
|
+
|
467
|
+
first = details.first
|
468
|
+
if first
|
469
|
+
info[:enum_value] ||= first[:name]
|
470
|
+
info[:enum_value_serialized] ||= first[:serialized]
|
471
|
+
end
|
472
|
+
when "enum_union"
|
473
|
+
details = enum_value_details_for(info)
|
474
|
+
info[:enum_value_details] = details if details.any?
|
475
|
+
info[:base_enum] ||= simple_enum_name(info[:enum_name])
|
476
|
+
|
477
|
+
if details.any?
|
478
|
+
info[:enum_values] = details.map { |detail| detail[:name] }
|
479
|
+
info[:enum_values_serialized] = details.map { |detail| detail[:serialized] }
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
info
|
484
|
+
end
|
485
|
+
|
486
|
+
sig { params(info: T::Hash[Symbol, T.untyped]).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
487
|
+
def enum_value_details_for(info)
|
488
|
+
details = info[:enum_value_details]
|
489
|
+
if details.is_a?(Array) && details.first.is_a?(Hash)
|
490
|
+
return T.cast(details, T::Array[T::Hash[Symbol, T.untyped]])
|
491
|
+
end
|
492
|
+
|
493
|
+
raw_values = info[:enum_values]
|
494
|
+
return [] unless raw_values.is_a?(Array)
|
495
|
+
|
496
|
+
if raw_values.first.is_a?(Hash)
|
497
|
+
return T.cast(raw_values, T::Array[T::Hash[Symbol, T.untyped]])
|
498
|
+
end
|
499
|
+
|
500
|
+
enum_class = enum_class_by_name(info[:enum_name])
|
501
|
+
return [] unless enum_class
|
502
|
+
|
503
|
+
raw_values.filter_map do |const_name|
|
504
|
+
next unless const_name.is_a?(String)
|
505
|
+
|
506
|
+
enum_class.values.find do |value|
|
507
|
+
enum_constant_name(value) == const_name
|
508
|
+
end&.then { |enum_value| enum_value_metadata(enum_value) }
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
sig { params(enum_name: T.untyped).returns(T.nilable(T.class_of(T::Enum))) }
|
513
|
+
def enum_class_by_name(enum_name)
|
514
|
+
return nil unless enum_name.is_a?(String)
|
515
|
+
|
516
|
+
enum_classes.find { |klass| klass.name == enum_name }
|
517
|
+
end
|
518
|
+
|
519
|
+
sig { params(enum_name: T.untyped).returns(T.nilable(String)) }
|
520
|
+
def simple_enum_name(enum_name)
|
521
|
+
return nil unless enum_name.is_a?(String)
|
522
|
+
|
523
|
+
enum_name.split("::").last
|
524
|
+
end
|
525
|
+
|
526
|
+
sig { params(path: String).void }
|
527
|
+
def ensure_dir(path)
|
528
|
+
FileUtils.mkdir_p(path) unless File.directory?(path)
|
529
|
+
end
|
530
|
+
|
531
|
+
sig { params(value: T.untyped).returns(T.untyped) }
|
532
|
+
def stringify_keys(value)
|
533
|
+
case value
|
534
|
+
when Hash
|
535
|
+
value.each_with_object({}) do |(k, v), hash|
|
536
|
+
key = k.is_a?(Symbol) ? k.to_s : k
|
537
|
+
hash[key] = stringify_keys(v)
|
538
|
+
end
|
539
|
+
when Array
|
540
|
+
value.map { |item| stringify_keys(item) }
|
541
|
+
else
|
542
|
+
value
|
543
|
+
end
|
544
|
+
end
|
545
|
+
end
|
546
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
require_relative "typescript/version"
|
7
|
+
require_relative "typescript/exporter"
|
8
|
+
require_relative "typescript/cli"
|
9
|
+
|
10
|
+
class Sorbet; end unless defined?(Sorbet)
|
11
|
+
|
12
|
+
module Sorbet::Typescript
|
13
|
+
class Error < StandardError; end
|
14
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class Sorbet
|
2
|
+
end
|
3
|
+
|
4
|
+
module Sorbet::Typescript
|
5
|
+
VERSION: String
|
6
|
+
|
7
|
+
class Error < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
class Exporter
|
11
|
+
def initialize: (
|
12
|
+
enum_namespaces: Array[untyped] ?,
|
13
|
+
struct_namespaces: Array[untyped] ?
|
14
|
+
) -> void
|
15
|
+
|
16
|
+
def export: (
|
17
|
+
enums_json: String?
|
18
|
+
structs_json: String?
|
19
|
+
typescript_file: String?
|
20
|
+
property_map_json: String?
|
21
|
+
) -> ExportData
|
22
|
+
|
23
|
+
def generate_data: () -> ExportData
|
24
|
+
|
25
|
+
class ExportData
|
26
|
+
attr_reader enums: Hash[String, Hash[Symbol, untyped]]
|
27
|
+
attr_reader structs: Hash[String, Hash[Symbol, untyped]]
|
28
|
+
|
29
|
+
def initialize: (
|
30
|
+
enums: Hash[String, Hash[Symbol, untyped]],
|
31
|
+
structs: Hash[String, Hash[Symbol, untyped]]
|
32
|
+
) -> void
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class CLI < ::Thor
|
37
|
+
def export: (*untyped) -> void
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/sorbet/typescript/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "sorbet-typescript"
|
7
|
+
spec.version = Sorbet::Typescript::VERSION
|
8
|
+
spec.authors = ["DocSpring"]
|
9
|
+
spec.email = ["support@docspring.com"]
|
10
|
+
|
11
|
+
spec.summary = "Export Sorbet enums and structs into TypeScript-ready metadata."
|
12
|
+
spec.description = "Sorbet TypeScript bridges Sorbet RBI definitions to TypeScript by introspecting T::Enum and T::Struct classes and emitting JSON metadata and helper typings."
|
13
|
+
spec.homepage = "https://github.com/DocSpring/sorbet-typescript"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 3.2.0"
|
16
|
+
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
19
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
20
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
21
|
+
|
22
|
+
spec.files = Dir[
|
23
|
+
"lib/**/*",
|
24
|
+
"sig/**/*",
|
25
|
+
"README.md",
|
26
|
+
"CHANGELOG.md",
|
27
|
+
"LICENSE.txt",
|
28
|
+
"Taskfile*",
|
29
|
+
"*.gemspec"
|
30
|
+
]
|
31
|
+
spec.bindir = "exe"
|
32
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
33
|
+
spec.require_paths = ["lib"]
|
34
|
+
|
35
|
+
spec.add_dependency "sorbet-runtime", ">= 0.5"
|
36
|
+
spec.add_dependency "thor", ">= 1.3"
|
37
|
+
|
38
|
+
spec.add_development_dependency "minitest", ">= 5.20"
|
39
|
+
spec.add_development_dependency "rake", ">= 13.0"
|
40
|
+
spec.add_development_dependency "rubocop", ">= 1.59"
|
41
|
+
spec.add_development_dependency "rubocop-performance", ">= 1.20"
|
42
|
+
spec.add_development_dependency "rubocop-sorbet", ">= 0.8"
|
43
|
+
spec.add_development_dependency "sorbet", ">= 0.5"
|
44
|
+
spec.add_development_dependency "tapioca", ">= 0.13"
|
45
|
+
end
|
metadata
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sorbet-typescript
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- DocSpring
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-09-28 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: sorbet-runtime
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0.5'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0.5'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: thor
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.3'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.3'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: minitest
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '5.20'
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '5.20'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: rake
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '13.0'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '13.0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: rubocop
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '1.59'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '1.59'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: rubocop-performance
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '1.20'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '1.20'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: rubocop-sorbet
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0.8'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0.8'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: sorbet
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0.5'
|
117
|
+
type: :development
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0.5'
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: tapioca
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0.13'
|
131
|
+
type: :development
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0.13'
|
138
|
+
description: Sorbet TypeScript bridges Sorbet RBI definitions to TypeScript by introspecting
|
139
|
+
T::Enum and T::Struct classes and emitting JSON metadata and helper typings.
|
140
|
+
email:
|
141
|
+
- support@docspring.com
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- CHANGELOG.md
|
147
|
+
- LICENSE.txt
|
148
|
+
- README.md
|
149
|
+
- Taskfile.yml
|
150
|
+
- lib/sorbet/typescript.rb
|
151
|
+
- lib/sorbet/typescript/cli.rb
|
152
|
+
- lib/sorbet/typescript/exporter.rb
|
153
|
+
- lib/sorbet/typescript/version.rb
|
154
|
+
- sig/sorbet/typescript.rbs
|
155
|
+
- sorbet-typescript.gemspec
|
156
|
+
homepage: https://github.com/DocSpring/sorbet-typescript
|
157
|
+
licenses:
|
158
|
+
- MIT
|
159
|
+
metadata:
|
160
|
+
homepage_uri: https://github.com/DocSpring/sorbet-typescript
|
161
|
+
source_code_uri: https://github.com/DocSpring/sorbet-typescript
|
162
|
+
changelog_uri: https://github.com/DocSpring/sorbet-typescript/blob/main/CHANGELOG.md
|
163
|
+
rubygems_mfa_required: 'true'
|
164
|
+
rdoc_options: []
|
165
|
+
require_paths:
|
166
|
+
- lib
|
167
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
168
|
+
requirements:
|
169
|
+
- - ">="
|
170
|
+
- !ruby/object:Gem::Version
|
171
|
+
version: 3.2.0
|
172
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
173
|
+
requirements:
|
174
|
+
- - ">="
|
175
|
+
- !ruby/object:Gem::Version
|
176
|
+
version: '0'
|
177
|
+
requirements: []
|
178
|
+
rubygems_version: 3.6.3
|
179
|
+
specification_version: 4
|
180
|
+
summary: Export Sorbet enums and structs into TypeScript-ready metadata.
|
181
|
+
test_files: []
|