From 6f67b1ad9283868e8e3325042645bceccc85b047 Mon Sep 17 00:00:00 2001 From: Avram Walden Date: Wed, 19 Jul 2023 15:01:24 -0700 Subject: [PATCH] feat: add `namespace` option to generate `.d.ts` files (#9) Co-authored-by: Maximo Mussini --- README.md | 7 ++ .../interfaces_ComposerSerializer.snap | 1 - ...nterfaces_ComposerWithSongsSerializer.snap | 1 - .../interfaces_ModelSerializer.snap | 1 - ...> interfaces_Nested__AlbumSerializer.snap} | 1 - .../interfaces_SnakeComposerSerializer.snap | 1 - .../interfaces_SongSerializer.snap | 1 - .../interfaces_SongWithVideosSerializer.snap | 1 - .../interfaces_VideoSerializer.snap | 1 - .../interfaces_VideoWithSongSerializer.snap | 1 - ...mespace_interfaces_ComposerSerializer.snap | 15 ++++ ...nterfaces_ComposerWithSongsSerializer.snap | 16 +++++ .../namespace_interfaces_ModelSerializer.snap | 13 ++++ ...ce_interfaces_Nested__AlbumSerializer.snap | 15 ++++ ...ce_interfaces_SnakeComposerSerializer.snap | 15 ++++ .../namespace_interfaces_SongSerializer.snap | 14 ++++ ...e_interfaces_SongWithVideosSerializer.snap | 16 +++++ .../namespace_interfaces_VideoSerializer.snap | 17 +++++ ...ce_interfaces_VideoWithSongSerializer.snap | 18 +++++ spec/types_from_serializers/generator_spec.rb | 70 +++++++++++++------ .../lib/types_from_serializers/generator.rb | 37 ++++++++-- 21 files changed, 224 insertions(+), 38 deletions(-) rename spec/types_from_serializers/__snapshots__/{interfaces_Nested::AlbumSerializer.snap => interfaces_Nested__AlbumSerializer.snap} (99%) create mode 100644 spec/types_from_serializers/__snapshots__/namespace_interfaces_ComposerSerializer.snap create mode 100644 spec/types_from_serializers/__snapshots__/namespace_interfaces_ComposerWithSongsSerializer.snap create mode 100644 spec/types_from_serializers/__snapshots__/namespace_interfaces_ModelSerializer.snap create mode 100644 spec/types_from_serializers/__snapshots__/namespace_interfaces_Nested__AlbumSerializer.snap create mode 100644 spec/types_from_serializers/__snapshots__/namespace_interfaces_SnakeComposerSerializer.snap create mode 100644 spec/types_from_serializers/__snapshots__/namespace_interfaces_SongSerializer.snap create mode 100644 spec/types_from_serializers/__snapshots__/namespace_interfaces_SongWithVideosSerializer.snap create mode 100644 spec/types_from_serializers/__snapshots__/namespace_interfaces_VideoSerializer.snap create mode 100644 spec/types_from_serializers/__snapshots__/namespace_interfaces_VideoWithSongSerializer.snap diff --git a/README.md b/README.md index 811bfef..1637e20 100644 --- a/README.md +++ b/README.md @@ -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"]` diff --git a/spec/types_from_serializers/__snapshots__/interfaces_ComposerSerializer.snap b/spec/types_from_serializers/__snapshots__/interfaces_ComposerSerializer.snap index 0945d96..ae929be 100644 --- a/spec/types_from_serializers/__snapshots__/interfaces_ComposerSerializer.snap +++ b/spec/types_from_serializers/__snapshots__/interfaces_ComposerSerializer.snap @@ -8,4 +8,3 @@ export default interface Composer { lastName?: string name: string } - diff --git a/spec/types_from_serializers/__snapshots__/interfaces_ComposerWithSongsSerializer.snap b/spec/types_from_serializers/__snapshots__/interfaces_ComposerWithSongsSerializer.snap index 8d98b23..1ccb876 100644 --- a/spec/types_from_serializers/__snapshots__/interfaces_ComposerWithSongsSerializer.snap +++ b/spec/types_from_serializers/__snapshots__/interfaces_ComposerWithSongsSerializer.snap @@ -10,4 +10,3 @@ export default interface ComposerWithSongs { name: string songs: Model[] } - diff --git a/spec/types_from_serializers/__snapshots__/interfaces_ModelSerializer.snap b/spec/types_from_serializers/__snapshots__/interfaces_ModelSerializer.snap index 63f406e..962f86f 100644 --- a/spec/types_from_serializers/__snapshots__/interfaces_ModelSerializer.snap +++ b/spec/types_from_serializers/__snapshots__/interfaces_ModelSerializer.snap @@ -7,4 +7,3 @@ export default interface Model { id: AnyModel['id'] title: AnyModel['title'] } - diff --git a/spec/types_from_serializers/__snapshots__/interfaces_Nested::AlbumSerializer.snap b/spec/types_from_serializers/__snapshots__/interfaces_Nested__AlbumSerializer.snap similarity index 99% rename from spec/types_from_serializers/__snapshots__/interfaces_Nested::AlbumSerializer.snap rename to spec/types_from_serializers/__snapshots__/interfaces_Nested__AlbumSerializer.snap index e8c0f5f..f26f5d7 100644 --- a/spec/types_from_serializers/__snapshots__/interfaces_Nested::AlbumSerializer.snap +++ b/spec/types_from_serializers/__snapshots__/interfaces_Nested__AlbumSerializer.snap @@ -9,4 +9,3 @@ export default interface NestedAlbum { title: AnyModel['title'] tracks: Song[] } - diff --git a/spec/types_from_serializers/__snapshots__/interfaces_SnakeComposerSerializer.snap b/spec/types_from_serializers/__snapshots__/interfaces_SnakeComposerSerializer.snap index 733b1d0..2de33e3 100644 --- a/spec/types_from_serializers/__snapshots__/interfaces_SnakeComposerSerializer.snap +++ b/spec/types_from_serializers/__snapshots__/interfaces_SnakeComposerSerializer.snap @@ -8,4 +8,3 @@ export default interface SnakeComposer { last_name?: string name: string } - diff --git a/spec/types_from_serializers/__snapshots__/interfaces_SongSerializer.snap b/spec/types_from_serializers/__snapshots__/interfaces_SongSerializer.snap index d90a7b0..283a9b4 100644 --- a/spec/types_from_serializers/__snapshots__/interfaces_SongSerializer.snap +++ b/spec/types_from_serializers/__snapshots__/interfaces_SongSerializer.snap @@ -8,4 +8,3 @@ export default interface Song { composer: Composer title?: string } - diff --git a/spec/types_from_serializers/__snapshots__/interfaces_SongWithVideosSerializer.snap b/spec/types_from_serializers/__snapshots__/interfaces_SongWithVideosSerializer.snap index 8b4a6d4..9ee0a0f 100644 --- a/spec/types_from_serializers/__snapshots__/interfaces_SongWithVideosSerializer.snap +++ b/spec/types_from_serializers/__snapshots__/interfaces_SongWithVideosSerializer.snap @@ -10,4 +10,3 @@ export default interface SongWithVideos { title?: string videos: Video[] } - diff --git a/spec/types_from_serializers/__snapshots__/interfaces_VideoSerializer.snap b/spec/types_from_serializers/__snapshots__/interfaces_VideoSerializer.snap index c416943..0633ec4 100644 --- a/spec/types_from_serializers/__snapshots__/interfaces_VideoSerializer.snap +++ b/spec/types_from_serializers/__snapshots__/interfaces_VideoSerializer.snap @@ -10,4 +10,3 @@ export default interface Video { youtubeId?: string youtubeUrl?: string } - diff --git a/spec/types_from_serializers/__snapshots__/interfaces_VideoWithSongSerializer.snap b/spec/types_from_serializers/__snapshots__/interfaces_VideoWithSongSerializer.snap index 7bf550d..e06f480 100644 --- a/spec/types_from_serializers/__snapshots__/interfaces_VideoWithSongSerializer.snap +++ b/spec/types_from_serializers/__snapshots__/interfaces_VideoWithSongSerializer.snap @@ -12,4 +12,3 @@ export default interface VideoWithSong { youtubeId?: string youtubeUrl?: string } - diff --git a/spec/types_from_serializers/__snapshots__/namespace_interfaces_ComposerSerializer.snap b/spec/types_from_serializers/__snapshots__/namespace_interfaces_ComposerSerializer.snap new file mode 100644 index 0000000..9657bd9 --- /dev/null +++ b/spec/types_from_serializers/__snapshots__/namespace_interfaces_ComposerSerializer.snap @@ -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 + } + } +} diff --git a/spec/types_from_serializers/__snapshots__/namespace_interfaces_ComposerWithSongsSerializer.snap b/spec/types_from_serializers/__snapshots__/namespace_interfaces_ComposerWithSongsSerializer.snap new file mode 100644 index 0000000..4ce7ac6 --- /dev/null +++ b/spec/types_from_serializers/__snapshots__/namespace_interfaces_ComposerWithSongsSerializer.snap @@ -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[] + } + } +} diff --git a/spec/types_from_serializers/__snapshots__/namespace_interfaces_ModelSerializer.snap b/spec/types_from_serializers/__snapshots__/namespace_interfaces_ModelSerializer.snap new file mode 100644 index 0000000..2cb373a --- /dev/null +++ b/spec/types_from_serializers/__snapshots__/namespace_interfaces_ModelSerializer.snap @@ -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'] + } + } +} diff --git a/spec/types_from_serializers/__snapshots__/namespace_interfaces_Nested__AlbumSerializer.snap b/spec/types_from_serializers/__snapshots__/namespace_interfaces_Nested__AlbumSerializer.snap new file mode 100644 index 0000000..c2d69dd --- /dev/null +++ b/spec/types_from_serializers/__snapshots__/namespace_interfaces_Nested__AlbumSerializer.snap @@ -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[] + } + } +} diff --git a/spec/types_from_serializers/__snapshots__/namespace_interfaces_SnakeComposerSerializer.snap b/spec/types_from_serializers/__snapshots__/namespace_interfaces_SnakeComposerSerializer.snap new file mode 100644 index 0000000..b7dd325 --- /dev/null +++ b/spec/types_from_serializers/__snapshots__/namespace_interfaces_SnakeComposerSerializer.snap @@ -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 + } + } +} diff --git a/spec/types_from_serializers/__snapshots__/namespace_interfaces_SongSerializer.snap b/spec/types_from_serializers/__snapshots__/namespace_interfaces_SongSerializer.snap new file mode 100644 index 0000000..2636995 --- /dev/null +++ b/spec/types_from_serializers/__snapshots__/namespace_interfaces_SongSerializer.snap @@ -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 + } + } +} diff --git a/spec/types_from_serializers/__snapshots__/namespace_interfaces_SongWithVideosSerializer.snap b/spec/types_from_serializers/__snapshots__/namespace_interfaces_SongWithVideosSerializer.snap new file mode 100644 index 0000000..eabd0a9 --- /dev/null +++ b/spec/types_from_serializers/__snapshots__/namespace_interfaces_SongWithVideosSerializer.snap @@ -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[] + } + } +} diff --git a/spec/types_from_serializers/__snapshots__/namespace_interfaces_VideoSerializer.snap b/spec/types_from_serializers/__snapshots__/namespace_interfaces_VideoSerializer.snap new file mode 100644 index 0000000..8ebaf24 --- /dev/null +++ b/spec/types_from_serializers/__snapshots__/namespace_interfaces_VideoSerializer.snap @@ -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 + } + } +} diff --git a/spec/types_from_serializers/__snapshots__/namespace_interfaces_VideoWithSongSerializer.snap b/spec/types_from_serializers/__snapshots__/namespace_interfaces_VideoWithSongSerializer.snap new file mode 100644 index 0000000..21ff425 --- /dev/null +++ b/spec/types_from_serializers/__snapshots__/namespace_interfaces_VideoWithSongSerializer.snap @@ -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 + } + } +} diff --git a/spec/types_from_serializers/generator_spec.rb b/spec/types_from_serializers/generator_spec.rb index dcc6226..1dd76ec 100644 --- a/spec/types_from_serializers/generator_spec.rb +++ b/spec/types_from_serializers/generator_spec.rb @@ -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 @@ -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 diff --git a/types_from_serializers/lib/types_from_serializers/generator.rb b/types_from_serializers/lib/types_from_serializers/generator.rb index 8617dd4..22c60fb 100644 --- a/types_from_serializers/lib/types_from_serializers/generator.rb +++ b/types_from_serializers/lib/types_from_serializers/generator.rb @@ -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 @@ -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 @@ -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) @@ -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 @@ -387,6 +390,9 @@ 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 @@ -394,8 +400,8 @@ def default_config(root) # 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| @@ -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. @@ -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