haveapi-typescript-client 0.31.2 → 0.32.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c7d66d8b71e72b4cb440001fba34829c9c390dbe1e9a9330a4737724061348e
4
- data.tar.gz: 0c07960f84ef143335056b9f577c7579c76104a2c877e851650e0a25f3c84008
3
+ metadata.gz: f82e8d0c5f9a906a98f55b2969f7a00a9f69efaa7aea7586eccb19ca03022102
4
+ data.tar.gz: ad342219999ef8533db09fd6aba8ccd546102af69c907916226e91bd6dba4d61
5
5
  SHA512:
6
- metadata.gz: cf15eb58ef3a367bbb3626b6ef49e2a210b2e57cd09d62aac61701f7d6deafab98d3ea08b21398cee68b0129317605797c4e837096146829ec8c524eb4f26b2d
7
- data.tar.gz: cc4ac2c6e44ac306b56482144521f24c726e2533e1cc1963778c236c513fd0eb45263cfedd6bd9ab336181f9c7c482c487a2ccf7794ab613ef733e376c3de042
6
+ metadata.gz: 6656aa62ba57f2cae74bd8f64d9e9b528860384b43f8d03624b97c0024360d8f6e24facd256892ab6bd000788ecc17a2c15154c5d1b8f9ebcf0ad7153dff406f
7
+ data.tar.gz: 8fb7f096cc83859f4a4415ebf33d387247367f8f2c8b05e93d560f5c41f10c9470b6334cb7fb2dc72b7cd999691decd8bb2b134584cd9bd59896393cc2f6a557
data/README.md CHANGED
@@ -38,6 +38,22 @@ using the prepared dependencies and runs without contacting the API.
38
38
  Applications need a TypeScript compiler and the `haveapi-client` JavaScript
39
39
  runtime. The supported runtime version is exactly `0.30.2`.
40
40
 
41
+ Both generation and `generate --check` require **Prettier 3.9.10** and Node.
42
+ The repository's default Nix shell supplies them. For an installed Ruby gem,
43
+ prepare the formatter in your application's tooling before generating:
44
+
45
+ ```sh
46
+ npm install --save-dev --save-exact prettier@3.9.10
47
+ export PRETTIER="$PWD/node_modules/.bin/prettier"
48
+ ```
49
+
50
+ `PRETTIER` is an executable path, not a shell command. Alternatively, put the
51
+ exact version's `prettier` executable on PATH. The Ruby gem does not install npm
52
+ dependencies. Generation checks the formatter version and never downloads or
53
+ installs tools. Fetch, help, library loading and documentation rendering do not
54
+ require Prettier. Running an already generated client needs neither Prettier nor
55
+ the Ruby generator.
56
+
41
57
  ## Fetch, save and generate
42
58
 
43
59
  The example API at `https://example.test/api/v1.0/` has `Widget#Update` at
@@ -69,6 +85,11 @@ The explicit generation version must match the saved snapshot. `--check` reports
69
85
  missing or stale output without changing it. Keep handwritten files outside
70
86
  `src/generated`.
71
87
 
88
+ Generation formats only its temporary emitted source tree, using the packaged
89
+ `lib/haveapi/typescript_client/prettier.json` defaults. Application Prettier
90
+ configuration, EditorConfig and ignore files do not change generated output.
91
+ The pure emitter and documentation adapters remain formatter-free.
92
+
72
93
  For Basic capture, set `WIDGET_PASSWORD` in your environment, then run:
73
94
 
74
95
  ```sh
@@ -190,10 +211,20 @@ Commit the whole generated tree and manifest together. Regenerate with the
190
211
  recorded schema and HaveAPI revision, and keep that generator/client pair
191
212
  together when reverting versions.
192
213
 
214
+ The format-1 manifest records the formatter name, exact version and configuration
215
+ SHA-256 alongside hashes of the final formatted files. Existing format-1 trees
216
+ without formatter provenance can be regenerated normally; `--check` reports
217
+ them as stale without writing. Missing tools, version mismatches and formatting
218
+ errors leave the destination unchanged. Identical regeneration preserves file
219
+ timestamps.
220
+
193
221
  An interrupted write can leave mixed output. Restore the committed generated
194
222
  tree before regenerating. If the first generation was interrupted before an
195
223
  ownership manifest was written, preserve that partial directory and generate
196
224
  into a fresh empty `src/generated`. Do not edit the manifest to bypass a refusal.
225
+ When rolling back, restore the matching generator revision, generated tree and
226
+ manifest together. An older generator's `--check` need not accept source
227
+ formatted by a newer generator.
197
228
 
198
229
  The Ruby server can render examples using this generated interface; see
199
230
  [generated documentation profiles](docs/documentation.md).
@@ -69,10 +69,16 @@ input wire names map to generated input names. The DSL has no separate input
69
69
  metadata carrier, so actions requiring input metadata produce a limitation.
70
70
  Inputs preserve omission, null, false, zero and empty strings. Unsafe integer IDs,
71
71
  invalid JSON and structured Custom query values cannot be rendered. Output keys
72
- remain wire names, with guards for optional, null and unknown values.
73
-
74
- Successful examples retain `data`, `meta`, `envelope` and `httpStatus`. Rejection
75
- examples inspect the runtime's error receipt without printing server diagnostics.
72
+ remain wire names. Simple examples print the supplied output fields directly;
73
+ thorough examples guard optional, null and unknown values before using them.
74
+
75
+ Simple action examples assume that initialization and authentication have
76
+ established `api`. They use `await` in an async context and show the exact call
77
+ and sample output. Rejection examples import the runtime's `AsyncError` class.
78
+ **With error handling** includes setup, error handling and the response receipt's
79
+ `data`, `meta`, `envelope` and `httpStatus`. Both levels use the same validation
80
+ and limitations. Rejection examples inspect the runtime's error receipt without
81
+ printing server diagnostics.
76
82
  Requests are not retried. A successful request does not establish completion of
77
83
  a server-side operation; raw receipts cannot be passed to `waitForCompletion`.
78
84
 
@@ -81,8 +87,9 @@ a server-side operation; raw receipts cannot be passed to `waitForCompletion`.
81
87
  The generator provides `HaveAPI::TypeScriptClient::Documentation` through
82
88
  `require 'haveapi/typescript_client/documentation'`. Its factories accept a full
83
89
  description or saved snapshot. `resolve` returns a frozen canonical action;
84
- `render` returns the server's code or limitation result. Rendering performs no
85
- file, network, generator or compiler work.
90
+ `render` returns the server's code or limitation result. Its context must expose
91
+ `detail` as `:simple` or `:thorough`. Rendering performs no file, network,
92
+ generator or compiler work.
86
93
 
87
94
  Rendering selects the current action, authentication provider and metadata
88
95
  namespace before recursively normalizing their contents. It leaves unrelated
@@ -90,8 +97,9 @@ description descendants untouched and keeps authorization decisions local to
90
97
  each render. Deterministic work checks grow unrelated subtrees while keeping
91
98
  the selected inputs and rendered source fixed.
92
99
 
93
- Focused unit checks compile the actual rendered programs against generated
94
- fixture modules with TypeScript 6, NodeNext/ES2022, `exactOptionalPropertyTypes`,
100
+ The compilation checks compose simple action fragments with separately rendered
101
+ setup and compile both detail levels against generated fixture modules with
102
+ TypeScript 6, NodeNext/ES2022, `exactOptionalPropertyTypes`,
95
103
  `noUncheckedIndexedAccess` and `verbatimModuleSyntax`. They cover aliases, nested
96
104
  paths, filtered fields, stale contracts, supported authentication and errors.
97
105
 
@@ -14,5 +14,5 @@ Gem::Specification.new do |spec|
14
14
  spec.bindir = 'bin'
15
15
  spec.executables = ['haveapi-typescript-client']
16
16
  spec.require_paths = ['lib']
17
- spec.add_dependency 'haveapi-client', '~> 0.31.2'
17
+ spec.add_dependency 'haveapi-client', '~> 0.32.0'
18
18
  end
@@ -2,7 +2,7 @@ require_relative '../typescript_client'
2
2
  require 'uri'
3
3
 
4
4
  module HaveAPI::TypeScriptClient
5
- # Names belong to the complete SDK; request-local help only limits their use.
5
+ # Names belong to the complete API client; request-local help only limits their use.
6
6
  class Documentation
7
7
  class Unsupported < StandardError; end
8
8
 
@@ -49,14 +49,20 @@ module HaveAPI::TypeScriptClient
49
49
  unsupported! unless mode == :async && context.version.to_s == @snapshot.fetch('api_version')
50
50
  source = case kind
51
51
  when :init then program(context, ['void api;'])
52
- when :auth then program(context, ['void api;'], authentication(context, auth_method, auth_description))
52
+ when :auth then program(context, ['void api;'], authentication(context, auth_method, auth_description), errors: context.detail == :thorough)
53
53
  when :example then example(context)
54
54
  else unsupported!
55
55
  end
56
56
  { status: :code, code: source.freeze }.freeze
57
57
  rescue Unsupported, JSON::GeneratorError, EncodingError
58
- # Never name fields or actions from the private SDK in public diagnostics.
59
- { status: :limitation, reason: 'This sample is unavailable or incompatible with the configured TypeScript SDK.'.freeze }.freeze
58
+ # Never name fields or actions from the private API client in public diagnostics.
59
+ { status: :limitation, reason: 'This sample is unavailable or incompatible with the configured TypeScript API client.'.freeze }.freeze
60
+ end
61
+
62
+ def detail_levels_for(kind:, context:, result:, mode:, auth_method: nil, auth_description: nil)
63
+ return [:simple].freeze unless result[:status] == :code && %i[auth example].include?(kind) && mode == :async
64
+
65
+ %i[simple thorough].freeze
60
66
  end
61
67
 
62
68
  private
@@ -103,28 +109,42 @@ module HaveAPI::TypeScriptClient
103
109
  end
104
110
 
105
111
  def literal(value)
106
- return "{#{value.map { |key, item| "#{key == '__proto__' ? '["__proto__"]' : literal(key)}:#{literal(item)}" }.join(',')}}" if value.is_a?(Hash)
107
- return "[#{value.map { |item| literal(item) }.join(',')}]" if value.is_a?(Array)
112
+ if value.is_a?(Hash)
113
+ return '{}' if value.empty?
114
+
115
+ return "{ #{value.map { |key, item| "#{key == '__proto__' ? '["__proto__"]' : literal(key)}: #{literal(item)}" }.join(', ')} }"
116
+ end
117
+ return "[#{value.map { |item| literal(item) }.join(', ')}]" if value.is_a?(Array)
108
118
 
109
119
  JSON.generate(value).gsub("\u2028", '\\u2028').gsub("\u2029", '\\u2029')
110
120
  end
111
121
 
112
- def program(context, body, auth = [])
113
- version_url = context.version_url.to_s.sub(%r{/+\z}, '')
114
- uri = URI.parse(version_url)
115
- unsupported! unless uri.is_a?(URI::HTTP) && uri.host && !uri.userinfo && !uri.query && !uri.fragment &&
116
- uri.path.end_with?("/v#{@snapshot.fetch('api_version')}") && Snapshot.safe_path?(uri.path)
117
- dependencies = "// Configured SDK: #{@package_name}#{"@#{@package_version}" if @package_version}; runtime: haveapi-client@0.30.2."
118
- lines = [dependencies, "import {AsyncClient, AsyncError} from 'haveapi-client';", "import {createClient} from #{literal(@package_name)};"]
119
- unless auth.empty?
120
- lines += ["import {env} from 'node:process';", '', 'function credential(name: string): string {', ' const value = env[name];',
121
- ' if (!value) throw new Error(`Missing environment variable ${name}`);', ' return value;', '}']
122
+ def member_access(name)
123
+ name.match?(/\A[$_\p{ID_Start}][$\p{ID_Continue}\u200c\u200d]*\z/u) ? ".#{name}" : "[#{literal(name)}]"
124
+ end
125
+
126
+ def program(context, body, auth = [], errors: false)
127
+ version_url = version_url(context)
128
+ dependencies = "// Configured API client: #{@package_name}#{"@#{@package_version}" if @package_version}."
129
+ runtime_imports = errors ? 'AsyncClient, AsyncError' : 'AsyncClient'
130
+ lines = [dependencies, "import { #{runtime_imports} } from #{literal('haveapi-client')};", "import { createClient } from #{literal(@package_name)};"]
131
+ lines += ['', 'async function main(): Promise<void> {']
132
+ operation = ["const runtime = new AsyncClient(#{literal(version_url)});"] + auth + ['const api = createClient({ execute: spec => runtime.request(spec) });'] + body
133
+ if errors
134
+ operation = ['try {'] + operation.map { |line| " #{line}" } +
135
+ ['} catch (error) {', ' if (!(error instanceof AsyncError)) throw error;',
136
+ ' console.error(error.kind, error.httpStatus);', ' throw error;', '}']
122
137
  end
123
- lines += ['', 'async function main(): Promise<void> {', " const runtime = new AsyncClient(#{literal(version_url)});"]
124
- lines.concat(auth.map { |line| " #{line}" })
125
- lines << ' const api = createClient({execute: spec => runtime.request(spec)});'
126
- lines.concat(body.map { |line| " #{line}" })
138
+ lines.concat(operation.map { |line| " #{line}" })
127
139
  lines.push('}', '', 'main().catch(error => { throw error; });', '').join("\n")
140
+ end
141
+
142
+ def version_url(context)
143
+ value = context.version_url.to_s.sub(%r{/+\z}, '')
144
+ uri = URI.parse(value)
145
+ unsupported! unless uri.is_a?(URI::HTTP) && uri.host && !uri.userinfo && !uri.query && !uri.fragment &&
146
+ uri.path.end_with?("/v#{@snapshot.fetch('api_version')}") && Snapshot.safe_path?(uri.path)
147
+ value
128
148
  rescue URI::InvalidURIError
129
149
  unsupported!
130
150
  end
@@ -139,11 +159,11 @@ module HaveAPI::TypeScriptClient
139
159
  unsupported! if description && normalize(description) != live
140
160
  case method.to_s
141
161
  when 'basic'
142
- ['await runtime.authenticate(\'basic\', {user: credential("HAVEAPI_USER"), password: credential("HAVEAPI_PASSWORD")});']
162
+ ['await runtime.authenticate("basic", { user: "user", password: "secret" });']
143
163
  when 'token'
144
164
  header = live['http_header']
145
165
  unsupported! unless header.is_a?(String) && header.match?(/\A[!#$%&'*+.^_`|~0-9A-Za-z-]+\z/) && stored['http_header'] == header
146
- ['await runtime.authenticate(\'token\', {token: credential("HAVEAPI_TOKEN")});']
166
+ ['await runtime.authenticate("token", { token: "example-token" });']
147
167
  else unsupported!
148
168
  end
149
169
  end
@@ -186,7 +206,7 @@ module HaveAPI::TypeScriptClient
186
206
 
187
207
  def compatible_action!(action, saved, live, context)
188
208
  unsupported! unless live.is_a?(Hash)
189
- expected_path = URI.parse(context.version_url.to_s).path.sub(%r{/+\z}, '') + action.fetch(:path_template)
209
+ expected_path = URI.parse(version_url(context)).path + action.fetch(:path_template)
190
210
  unsupported! unless live['path'] == expected_path && live['method'] == action.fetch(:method) &&
191
211
  (live['auth'] != false) == action.fetch(:auth) && (live['blocking'] == true) == action.fetch(:blocking)
192
212
  namespace = normalized_members(normalized_members(context.description)['meta'])['namespace']
@@ -292,32 +312,46 @@ module HaveAPI::TypeScriptClient
292
312
  validate_object!(saved['output'], current['output'], value, input: false)
293
313
  end
294
314
  end
295
- members = (@accessors.fetch(action.fetch(:schema_path)) + [action.fetch(:accessor_name)]).map { |name| "[#{literal(name)}]" }.join
315
+ members = (@accessors.fetch(action.fetch(:schema_path)) + [action.fetch(:accessor_name)]).map { |name| member_access(name) }.join
296
316
  call = "api#{members}"
297
317
  call += "(#{literal(options) unless options.empty?})"
298
318
  if sample['status'] == false || (sample['http_status'].is_a?(Integer) && sample['http_status'] >= 400)
299
319
  body = ['try {', " await #{call};", ' throw new Error("Expected the documented request to fail");',
300
320
  '} catch (error) {', ' if (!(error instanceof AsyncError)) throw error;',
301
- ' void [error.kind, error.httpStatus, error.errors, error.envelope, error.response];', '}']
321
+ ' console.error(error.kind, error.httpStatus);']
322
+ body << ' void [error.errors, error.envelope, error.response];' if context.detail == :thorough
323
+ body << '}'
324
+ failure = true
302
325
  else
303
- body = ["const result = await #{call};", 'void [result.data, result.meta, result.envelope, result.httpStatus];']
304
- body.concat(output_lines(action, response))
326
+ body = ["const result = await #{call};"]
327
+ body << 'void [result.data, result.meta, result.envelope, result.httpStatus];' if context.detail == :thorough
328
+ body.concat(output_lines(action, response, thorough: context.detail == :thorough))
305
329
  end
306
330
  body << '// Request success does not establish completion of a server-side operation.' if action.fetch(:blocking)
307
- program(context, body, action_authentication(action, context))
331
+ auth = action_authentication(action, context)
332
+ if context.detail == :thorough
333
+ return program(context, body, auth, errors: true)
334
+ end
335
+
336
+ imports = failure ? ["import { AsyncError } from #{literal('haveapi-client')};", ''] : []
337
+ (imports + ['// Requires an initialized api and an async context.'] + body + ['']).join("\n")
308
338
  end
309
339
 
310
- def output_lines(action, response)
340
+ def output_lines(action, response, thorough:)
311
341
  return [] unless response && action[:output]
312
342
 
313
343
  list = action[:output][:layout].end_with?('_list')
314
344
  values = list ? response : [response]
315
345
  names = values.flat_map(&:keys).uniq
316
346
  body = names.flat_map do |name|
317
- expression = "item[#{literal(name)}]"
318
- ["const value#{names.index(name)}: unknown = #{expression};", "if (value#{names.index(name)} !== null && value#{names.index(name)} !== undefined) void value#{names.index(name)};"]
347
+ expression = "#{list || thorough ? 'item' : 'result.data'}#{member_access(name)}"
348
+ next ["console.log(#{expression});"] unless thorough
349
+
350
+ ["const value#{names.index(name)}: unknown = #{expression};", "if (value#{names.index(name)} !== null && value#{names.index(name)} !== undefined) console.log(value#{names.index(name)});"]
319
351
  end
320
- list ? ['for (const item of result.data) {'] + body.map { |line| " #{line}" } + ['}'] : ['const item = result.data;'] + body
352
+ return ['for (const item of result.data) {'] + body.map { |line| " #{line}" } + ['}'] if list
353
+
354
+ (thorough ? ['const item = result.data;'] : []) + body
321
355
  end
322
356
  end
323
357
  end
@@ -0,0 +1,57 @@
1
+ require 'digest'
2
+ require 'fileutils'
3
+ require 'open3'
4
+ require 'tmpdir'
5
+
6
+ module HaveAPI::TypeScriptClient
7
+ # Format only the validated emission tree, before the generator hashes it.
8
+ class Formatter
9
+ VERSION = '3.9.10'.freeze
10
+ CONFIG = File.expand_path('prettier.json', __dir__).freeze
11
+
12
+ def initialize
13
+ @executable = ENV.fetch('PRETTIER', 'prettier')
14
+ @executable = File.expand_path(@executable) if @executable.include?(File::SEPARATOR)
15
+ end
16
+
17
+ def version
18
+ actual = capture('--version').strip
19
+ raise Error, "Prettier #{VERSION} is required; found #{actual.inspect}" unless actual == VERSION
20
+
21
+ actual
22
+ end
23
+
24
+ def provenance
25
+ { 'name' => 'prettier', 'version' => VERSION, 'config_sha256' => Digest::SHA256.file(CONFIG).hexdigest }
26
+ end
27
+
28
+ def format(files)
29
+ version
30
+ Dir.mktmpdir('haveapi-typescript-format-') do |directory|
31
+ files.each do |path, content|
32
+ target = File.join(directory, path)
33
+ FileUtils.mkdir_p(File.dirname(target))
34
+ File.write(target, content)
35
+ end
36
+ capture('--config', CONFIG, '--no-editorconfig', '--ignore-path', File::NULL,
37
+ '--parser', 'typescript', '--write', '**/*.ts', chdir: directory)
38
+ files.to_h do |path, _content|
39
+ [path, File.read(File.join(directory, path), encoding: 'UTF-8')]
40
+ end
41
+ end
42
+ rescue SystemCallError => e
43
+ raise Error, "Prettier formatting failed: #{e.message}"
44
+ end
45
+
46
+ private
47
+
48
+ def capture(*, **)
49
+ stdout, stderr, status = Open3.capture3(@executable, *, **)
50
+ raise Error, "Prettier failed: #{stderr.strip}" unless status.success?
51
+
52
+ stdout
53
+ rescue SystemCallError => e
54
+ raise Error, "Unable to run Prettier #{@executable.inspect}: #{e.message}"
55
+ end
56
+ end
57
+ end
@@ -11,7 +11,7 @@ module HaveAPI::TypeScriptClient
11
11
 
12
12
  def self.source_files
13
13
  Dir.chdir(ROOT) do
14
- Dir['bin/**/*', 'lib/**/*.rb', 'template/**/*', 'Gemfile', '*.gemspec', 'Rakefile']
14
+ Dir['bin/**/*', 'lib/**/*.{rb,json}', 'template/**/*', 'Gemfile', '*.gemspec', 'Rakefile']
15
15
  .select { |path| File.file?(path) }.sort
16
16
  end
17
17
  end
@@ -41,12 +41,16 @@ module HaveAPI::TypeScriptClient
41
41
  require_relative 'emitter'
42
42
  files = Emitter.new(@ir, @config).files
43
43
  validate_files(files)
44
+ require_relative 'formatter'
45
+ formatter = Formatter.new
46
+ files = formatter.format(files)
47
+ validate_files(files)
44
48
  manifest = {
45
49
  'owner' => OWNER, 'format' => 1, 'generator_version' => VERSION,
46
50
  'generator_source_sha256' => self.class.source_digest,
47
51
  'schema_sha256' => Snapshot.sha256(@schema), 'api_version' => @schema.fetch('api_version'),
48
52
  'protocol_version' => @schema.fetch('protocol_version'), 'profile' => @schema.fetch('profile'),
49
- 'config' => @config.transform_keys(&:to_s), 'coverage' => coverage,
53
+ 'config' => @config.transform_keys(&:to_s), 'formatter' => formatter.provenance, 'coverage' => coverage,
50
54
  'files' => files.sort.to_h.transform_values { |content| Digest::SHA256.hexdigest(content) }
51
55
  }
52
56
  candidate = files.merge(OWNER_FILE => Snapshot.canonical(manifest))
@@ -1,5 +1,5 @@
1
1
  module HaveAPI
2
2
  module TypeScriptClient
3
- VERSION = '0.31.2'.freeze
3
+ VERSION = '0.32.0'.freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: haveapi-typescript-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.31.2
4
+ version: 0.32.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakub Skokan
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.31.2
18
+ version: 0.32.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 0.31.2
25
+ version: 0.32.0
26
26
  executables:
27
27
  - haveapi-typescript-client
28
28
  extensions: []
@@ -39,9 +39,11 @@ files:
39
39
  - lib/haveapi/typescript_client/cli.rb
40
40
  - lib/haveapi/typescript_client/documentation.rb
41
41
  - lib/haveapi/typescript_client/emitter.rb
42
+ - lib/haveapi/typescript_client/formatter.rb
42
43
  - lib/haveapi/typescript_client/generator.rb
43
44
  - lib/haveapi/typescript_client/ir.rb
44
45
  - lib/haveapi/typescript_client/naming.rb
46
+ - lib/haveapi/typescript_client/prettier.json
45
47
  - lib/haveapi/typescript_client/snapshot.rb
46
48
  - lib/haveapi/typescript_client/version.rb
47
49
  - template/action.ts.erb