Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add namespace option to generate .d.ts files #9

Merged
merged 8 commits into from
Jul 19, 2023
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,13 @@ if Rails.env.development?
end
```

### `namespace`

_Default:_ `nil`

Allows to specify a TypeScript namespace and generate `.d.ts` to make types
available globally, avoiding the need to import types explicitly.

### `base_serializers`

_Default:_ `["BaseSerializer"]`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,3 @@ export default interface Composer {
lastName?: string
name: string
}

Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,3 @@ export default interface ComposerWithSongs {
name: string
songs: Model[]
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ export default interface Model {
id: AnyModel['id']
title: AnyModel['title']
}

Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@ export default interface NestedAlbum {
title: AnyModel['title']
tracks: Song[]
}

Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,3 @@ export default interface SnakeComposer {
last_name?: string
name: string
}

Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,3 @@ export default interface Song {
composer: Composer
title?: string
}

Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,3 @@ export default interface SongWithVideos {
title?: string
videos: Video[]
}

Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,3 @@ export default interface Video {
youtubeId?: string
youtubeUrl?: string
}

Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,3 @@ export default interface VideoWithSong {
youtubeId?: string
youtubeUrl?: string
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// TypesFromSerializers CacheKey b379726b6fef2dadbfd614384b868746
//
// DO NOT MODIFY: This file was automatically generated by TypesFromSerializers.
export {}

declare global {
namespace Schema {
interface Composer {
id: number
firstName?: string
lastName?: string
name: string
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// TypesFromSerializers CacheKey 1e3a5dea8847b2dcfe76fec134589cbb
//
// DO NOT MODIFY: This file was automatically generated by TypesFromSerializers.
import type Model from './Model'

declare global {
namespace Schema {
interface ComposerWithSongs {
id: number
firstName?: string
lastName?: string
name: string
songs: Model[]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// TypesFromSerializers CacheKey ed8ff6fbc986e6b666d559749c666dc5
//
// DO NOT MODIFY: This file was automatically generated by TypesFromSerializers.
import type AnyModel from '../AnyModel'

declare global {
namespace Schema {
interface Model {
id: AnyModel['id']
title: AnyModel['title']
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// TypesFromSerializers CacheKey 58897b61d86838c60d1f12700f326896
//
// DO NOT MODIFY: This file was automatically generated by TypesFromSerializers.
import type AnyModel from '../../AnyModel'
import type Song from '../Song'

declare global {
namespace Schema {
interface NestedAlbum {
id: AnyModel['id']
title: AnyModel['title']
tracks: Song[]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// TypesFromSerializers CacheKey 89e5760366d01f7f21244d341e1af2d4
//
// DO NOT MODIFY: This file was automatically generated by TypesFromSerializers.
export {}

declare global {
namespace Schema {
interface SnakeComposer {
id: number
first_name?: string
last_name?: string
name: string
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// TypesFromSerializers CacheKey c3af64a41d21e71dfae56644517994e1
//
// DO NOT MODIFY: This file was automatically generated by TypesFromSerializers.
import type Composer from './Composer'

declare global {
namespace Schema {
interface Song {
id: number
composer: Composer
title?: string
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// TypesFromSerializers CacheKey 6774f7cbf07614cf9b4136fbd0c8b441
//
// DO NOT MODIFY: This file was automatically generated by TypesFromSerializers.
import type Composer from './Composer'
import type Video from './Video'

declare global {
namespace Schema {
interface SongWithVideos {
id: number
composer: Composer
title?: string
videos: Video[]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// TypesFromSerializers CacheKey 45d6b515fb6118eabbd39d586269cdd0
//
// DO NOT MODIFY: This file was automatically generated by TypesFromSerializers.
export {}

declare global {
namespace Schema {
interface Video {
id: number
createdAt: string | Date
title?: string
untypedFieldExample: any
youtubeId?: string
youtubeUrl?: string
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// TypesFromSerializers CacheKey 4042db0fa9ebdf8668c7c5f0ec172e6d
//
// DO NOT MODIFY: This file was automatically generated by TypesFromSerializers.
import type Song from './Song'

declare global {
namespace Schema {
interface VideoWithSong {
id: number
createdAt: string | Date
song: Song
title?: string
untypedFieldExample: any
youtubeId?: string
youtubeUrl?: string
}
}
}
70 changes: 48 additions & 22 deletions spec/types_from_serializers/generator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@
]
}

def file_for(dir, name)
dir.join("#{name.chomp("Serializer").gsub("::", "/")}.ts")
def file_for(dir, name, ext)
dir.join("#{name.chomp("Serializer").gsub("::", "/")}.#{ext}")
end

def app_file_for(name)
file_for(sample_dir, name)
def app_file_for(name, ext = "ts")
file_for(sample_dir, name, ext)
end

def output_file_for(name)
file_for(output_dir, name)
def output_file_for(name, ext = "ts")
file_for(output_dir, name, ext)
end

def expect_generator
Expand All @@ -51,27 +51,53 @@ def generate_serializers
output_dir.rmtree if output_dir.exist?
end

# NOTE: We do a manual snapshot test for now, more tests coming in the future.
it "generates the files as expected" do
expect_generator.to generate_serializers.exactly(serializers.size).times
TypesFromSerializers.generate
context "with default config options" do
# NOTE: We do a manual snapshot test for now, more tests coming in the future.
it "generates the files as expected" do
expect_generator.to generate_serializers.exactly(serializers.size).times
TypesFromSerializers.generate

# It does not generate routes that don't have `export: true`.
expect(output_file_for("BaseSerializer").exist?).to be false

# It does not generate routes that don't have `export: true`.
expect(output_file_for("BaseSerializer").exist?).to be false
# It generates one file per serializer.
serializers.each do |name|
output_file = output_file_for(name)
expect(output_file.read).to match_snapshot("interfaces_#{name.gsub("::", "__")}") # UPDATE_SNAPSHOTS="1" bin/rspec
end

# It generates one file per serializer.
serializers.each do |name|
output_file = output_file_for(name)
expect(output_file.read).to match_snapshot("interfaces_#{name}") # UPDATE_SNAPSHOTS="1" bin/rspec
# It generates an file that exports all interfaces.
index_file = output_dir.join("index.ts")
expect(index_file.exist?).to be true
expect(index_file.read).to match_snapshot("interfaces_index") # UPDATE_SNAPSHOTS="1" bin/rspec

# It does not render if generating again.
TypesFromSerializers.generate
end
end

# It generates an file that exports all interfaces.
index_file = output_dir.join("index.ts")
expect(index_file.exist?).to be true
expect(index_file.read).to match_snapshot("interfaces_index") # UPDATE_SNAPSHOTS="1" bin/rspec
context "with namespace config option" do
it "generates the files as expected" do
TypesFromSerializers.config do |config|
config.namespace = "Schema"
end

# It does not render if generating again.
TypesFromSerializers.generate
expect_generator.to generate_serializers.exactly(serializers.size).times
TypesFromSerializers.generate

# It does not generate routes that don't have `export: true`.
expect(output_file_for("BaseSerializer", "d.ts").exist?).to be false

# It does not generate an index file
index_file = output_dir.join("index.ts")
expect(index_file.exist?).to be false

# It generates one file per serializer.
serializers.each do |name|
output_file = output_file_for(name, "d.ts")
expect(output_file.read).to match_snapshot("namespace_interfaces_#{name.gsub("::", "__")}") # UPDATE_SNAPSHOTS="1" bin/rspec
end
end
end

it "has a rake task available" do
Expand Down
37 changes: 30 additions & 7 deletions types_from_serializers/lib/types_from_serializers/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def ts_interface
:sql_to_typescript_type_mapping,
:skip_serializer_if,
:transform_keys,
:namespace,
keyword_init: true,
) do
def relative_custom_types_dir
Expand Down Expand Up @@ -140,10 +141,11 @@ def used_imports
end

def as_typescript
<<~TS
indent = TypesFromSerializers.config.namespace ? 3 : 1
<<~TS.gsub(/\n$/, "")
interface #{name} {
#{properties.index_by(&:name).values.map(&:as_typescript).join("\n ")}
}
#{" " * indent}#{properties.index_by(&:name).values.map(&:as_typescript).join("\n#{" " * indent}")}
#{" " * (indent - 1)}}
TS
end

Expand Down Expand Up @@ -269,7 +271,8 @@ def config
def generate(force: ENV["SERIALIZER_TYPES_FORCE"])
@force_generation = force
config.output_dir.rmtree if force && config.output_dir.exist?
generate_index_file

generate_index_file unless config.namespace

loaded_serializers.each do |serializer|
generate_interface_for(serializer)
Expand All @@ -289,7 +292,7 @@ def generate_changed
def generate_interface_for(serializer)
interface = serializer.ts_interface

write_if_changed(filename: interface.filename, cache_key: interface.inspect) {
write_if_changed(filename: interface.filename, cache_key: interface.inspect, extension: config.namespace ? "d.ts" : "ts") {
serializer_interface_content(interface)
}
end
Expand Down Expand Up @@ -387,15 +390,18 @@ def default_config(root)

# Allows to transform keys, useful when converting objects client-side.
transform_keys: nil,

# Allows scoping typescript definitions to a namespace
namespace: nil,
)
end

# Internal: Writes if the file does not exist or the cache key has changed.
# The cache strategy consists of a comment on the first line of the file.
#
# Yields to receive the rendered file content when it needs to.
def write_if_changed(filename:, cache_key:)
filename = config.output_dir.join("#{filename}.ts")
def write_if_changed(filename:, cache_key:, extension: "ts")
filename = config.output_dir.join("#{filename}.#{extension}")
FileUtils.mkdir_p(filename.dirname)
cache_key_comment = "// TypesFromSerializers CacheKey #{Digest::MD5.hexdigest(cache_key)}\n"
File.open(filename, "a+") { |file|
Expand All @@ -418,6 +424,10 @@ def serializers_index_content(serializers)
end

def serializer_interface_content(interface)
config.namespace ? declaration_interface_definition(interface) : standard_interface_definition(interface)
end

def standard_interface_definition(interface)
<<~TS
//
// DO NOT MODIFY: This file was automatically generated by TypesFromSerializers.
Expand All @@ -426,6 +436,19 @@ def serializer_interface_content(interface)
TS
end

def declaration_interface_definition(interface)
<<~TS
//
// DO NOT MODIFY: This file was automatically generated by TypesFromSerializers.
#{interface.used_imports.empty? ? "export {}\n" : interface.used_imports.join}
declare global {
namespace #{config.namespace} {
#{interface.as_typescript}
}
}
TS
end

# Internal: Returns true if the cache key has changed since the last codegen.
def stale?(file, cache_key_comment)
@force_generation || file.gets != cache_key_comment
Expand Down
Loading