Skip to content

Commit

Permalink
feat: add namespace option to generate .d.ts files (#9)
Browse files Browse the repository at this point in the history
Co-authored-by: Maximo Mussini <[email protected]>
  • Loading branch information
aviemet and ElMassimo committed Jul 19, 2023
1 parent 1c97048 commit 6f67b1a
Show file tree
Hide file tree
Showing 21 changed files with 224 additions and 38 deletions.
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

0 comments on commit 6f67b1a

Please sign in to comment.