@tolgee/cli 1.1.2 → 1.3.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.
@@ -0,0 +1,491 @@
1
+ import { createMachine, assign, send, forwardTo } from 'xstate';
2
+ import translateCallMachine from '../shared/translateCall.js';
3
+ import propertiesMachine from '../shared/properties.js';
4
+ import commentsService from '../shared/comments.js';
5
+ const VOID_KEY = { keyName: '', line: -1 };
6
+ export default createMachine({
7
+ predictableActionArguments: true,
8
+ id: 'vueExtractor',
9
+ type: 'parallel',
10
+ context: {
11
+ line: 0,
12
+ key: VOID_KEY,
13
+ useTranslate: null,
14
+ ignore: null,
15
+ currentState: 0 /* State.EXTERNAL */,
16
+ keys: [],
17
+ warnings: [],
18
+ },
19
+ states: {
20
+ comments: {
21
+ invoke: {
22
+ id: 'comments',
23
+ src: () => commentsService,
24
+ },
25
+ on: {
26
+ // Service messages
27
+ MAGIC_COMMENT: [
28
+ {
29
+ actions: 'ignoreNextLine',
30
+ cond: (_ctx, evt) => evt.kind === 'ignore',
31
+ },
32
+ {
33
+ actions: 'pushImmediateKey',
34
+ cond: (_ctx, evt) => evt.kind === 'key',
35
+ },
36
+ ],
37
+ WARNING: {
38
+ actions: 'pushWarning',
39
+ },
40
+ // Code messages
41
+ 'comment.line.double-slash.ts': {
42
+ actions: send((_ctx, evt) => ({
43
+ type: 'COMMENT',
44
+ data: evt.token,
45
+ line: evt.line,
46
+ }), { to: 'comments' }),
47
+ },
48
+ 'comment.block.ts': {
49
+ actions: send((_ctx, evt) => ({
50
+ type: 'COMMENT',
51
+ data: evt.token,
52
+ line: evt.line,
53
+ }), { to: 'comments' }),
54
+ },
55
+ 'comment.block.html': {
56
+ actions: send((_ctx, evt) => ({
57
+ type: 'COMMENT',
58
+ data: evt.token,
59
+ line: evt.line,
60
+ }), { to: 'comments' }),
61
+ },
62
+ newline: {
63
+ actions: 'warnUnusedIgnore',
64
+ cond: (ctx, evt) => ctx.ignore?.type === 'ignore' && ctx.ignore.line === evt.line,
65
+ },
66
+ },
67
+ },
68
+ useTranslate: {
69
+ initial: 'idle',
70
+ states: {
71
+ idle: {
72
+ on: {
73
+ 'entity.name.function.ts': {
74
+ target: 'func',
75
+ actions: 'storeLine',
76
+ cond: (ctx, evt) => (ctx.currentState === 1 /* State.SETUP */ ||
77
+ ctx.currentState === 0 /* State.EXTERNAL */) &&
78
+ evt.token === 'useTranslate',
79
+ },
80
+ },
81
+ },
82
+ func: {
83
+ on: {
84
+ '*': 'idle',
85
+ newline: undefined,
86
+ 'meta.block.ts': undefined,
87
+ 'meta.var.expr.ts': undefined,
88
+ 'meta.brace.round.ts': [
89
+ {
90
+ target: 'idle',
91
+ actions: 'consumeIgnoredLine',
92
+ cond: (ctx, evt) => ctx.ignore?.line === ctx.line &&
93
+ ctx.ignore.type === 'ignore' &&
94
+ evt.token === '(',
95
+ },
96
+ {
97
+ target: 'call',
98
+ cond: (_ctx, evt) => evt.token === '(',
99
+ },
100
+ ],
101
+ },
102
+ },
103
+ call: {
104
+ on: {
105
+ 'punctuation.definition.string.begin.ts': 'namespace',
106
+ 'punctuation.definition.string.template.begin.ts': 'namespace',
107
+ 'variable.other.readwrite.ts': {
108
+ target: 'idle',
109
+ actions: ['storeUseTranslate', 'markUseTranslateAsDynamic'],
110
+ },
111
+ 'meta.brace.round.ts': {
112
+ target: 'idle',
113
+ cond: (_ctx, evt) => evt.token === ')',
114
+ actions: 'storeUseTranslate',
115
+ },
116
+ },
117
+ },
118
+ namespace: {
119
+ on: {
120
+ '*': {
121
+ target: 'namespace_end',
122
+ actions: 'storeNamespacedUseTranslate',
123
+ },
124
+ },
125
+ },
126
+ namespace_end: {
127
+ on: {
128
+ 'punctuation.separator.comma.ts': 'idle',
129
+ 'meta.brace.round.ts': 'idle',
130
+ 'punctuation.definition.template-expression.begin.ts': {
131
+ target: 'idle',
132
+ actions: 'markUseTranslateAsDynamic',
133
+ },
134
+ 'keyword.operator.arithmetic.ts': {
135
+ target: 'idle',
136
+ actions: 'markUseTranslateAsDynamic',
137
+ },
138
+ },
139
+ },
140
+ done: { type: 'final' },
141
+ },
142
+ },
143
+ t: {
144
+ initial: 'idle',
145
+ states: {
146
+ idle: {
147
+ on: {
148
+ 'variable.other.object.ts': {
149
+ target: 'tRef',
150
+ actions: 'storeLine',
151
+ cond: (ctx, evt) => ctx.currentState !== 2 /* State.SCRIPT */ &&
152
+ ctx.currentState !== 3 /* State.TEMPLATE */ &&
153
+ ctx.useTranslate !== null &&
154
+ evt.token === 't',
155
+ },
156
+ 'entity.name.function.ts': [
157
+ {
158
+ target: 'func',
159
+ actions: 'storeLine',
160
+ cond: (ctx, evt) => ctx.currentState === 3 /* State.TEMPLATE */ &&
161
+ ctx.useTranslate !== null &&
162
+ evt.token === 't',
163
+ },
164
+ {
165
+ target: 'func',
166
+ actions: 'storeLine',
167
+ cond: (ctx, evt) => ctx.currentState === 3 /* State.TEMPLATE */ && evt.token === '$t',
168
+ },
169
+ ],
170
+ 'variable.language.this.ts': {
171
+ cond: (ctx) => ctx.currentState !== 3 /* State.TEMPLATE */ &&
172
+ ctx.currentState !== 0 /* State.EXTERNAL */,
173
+ actions: 'storeLine',
174
+ target: 'this',
175
+ },
176
+ },
177
+ },
178
+ this: {
179
+ on: {
180
+ 'meta.function-call.ts': undefined,
181
+ 'punctuation.accessor.ts': 'thisDot',
182
+ '*': 'idle',
183
+ },
184
+ },
185
+ thisDot: {
186
+ on: {
187
+ 'meta.function-call.ts': undefined,
188
+ 'entity.name.function.ts': {
189
+ cond: (_, evt) => evt.token === '$t',
190
+ target: 'func',
191
+ },
192
+ },
193
+ },
194
+ tRef: {
195
+ on: {
196
+ 'meta.function-call.ts': undefined,
197
+ 'punctuation.accessor.ts': 'tRefDot',
198
+ '*': 'idle',
199
+ },
200
+ },
201
+ tRefDot: {
202
+ on: {
203
+ 'meta.function-call.ts': undefined,
204
+ 'entity.name.function.ts': {
205
+ cond: (_, evt) => evt.token === 'value',
206
+ target: 'func',
207
+ },
208
+ },
209
+ },
210
+ func: {
211
+ on: {
212
+ '*': 'idle',
213
+ newline: undefined,
214
+ 'source.ts': undefined,
215
+ 'source.ts.embedded.html.vue': undefined,
216
+ 'meta.brace.round.ts': [
217
+ {
218
+ target: 'idle',
219
+ actions: 'consumeIgnoredLine',
220
+ cond: (ctx, evt) => ctx.ignore?.line === ctx.line && evt.token === '(',
221
+ },
222
+ {
223
+ target: 'call',
224
+ cond: (_ctx, evt) => evt.token === '(',
225
+ },
226
+ ],
227
+ },
228
+ },
229
+ call: {
230
+ invoke: {
231
+ id: 'tCall',
232
+ src: translateCallMachine,
233
+ onDone: [
234
+ {
235
+ target: 'idle',
236
+ actions: 'dynamicKeyName',
237
+ cond: (_, evt) => evt.data.keyName === false,
238
+ },
239
+ {
240
+ target: 'idle',
241
+ actions: 'dynamicNamespace',
242
+ cond: (_, evt) => evt.data.namespace === false,
243
+ },
244
+ {
245
+ target: 'idle',
246
+ actions: 'dynamicOptions',
247
+ cond: (_, evt) => evt.data.dynamicOptions,
248
+ },
249
+ {
250
+ target: 'idle',
251
+ cond: (_, evt) => !evt.data.keyName,
252
+ },
253
+ {
254
+ target: 'idle',
255
+ actions: 'consumeTranslateCall',
256
+ },
257
+ ],
258
+ },
259
+ on: {
260
+ '*': {
261
+ actions: forwardTo('tCall'),
262
+ },
263
+ },
264
+ },
265
+ },
266
+ },
267
+ component: {
268
+ initial: 'idle',
269
+ states: {
270
+ idle: {
271
+ on: {
272
+ 'punctuation.definition.tag.begin.html': {
273
+ target: 'tag',
274
+ actions: 'storeLine',
275
+ },
276
+ },
277
+ },
278
+ tag: {
279
+ on: {
280
+ '*': 'idle',
281
+ newline: undefined,
282
+ 'text.html.derivative': undefined,
283
+ 'entity.name.tag.T.html.vue': [
284
+ {
285
+ target: 'idle',
286
+ actions: 'consumeIgnoredLine',
287
+ cond: (ctx) => ctx.ignore?.line === ctx.line,
288
+ },
289
+ {
290
+ target: 'props',
291
+ },
292
+ ],
293
+ },
294
+ },
295
+ props: {
296
+ invoke: {
297
+ id: 'propertiesMachine',
298
+ src: propertiesMachine,
299
+ onDone: [
300
+ {
301
+ target: 'idle',
302
+ actions: 'emitWarningFromParameters',
303
+ cond: 'isPropertiesDataDynamic',
304
+ },
305
+ {
306
+ target: 'idle',
307
+ actions: ['consumeParameters', 'pushKey'],
308
+ cond: (ctx, evt) => evt.data.lastEvent.token !== '/>' &&
309
+ ((!ctx.key.keyName && !evt.data.keyName) ||
310
+ (!ctx.key.defaultValue && !evt.data.defaultValue)),
311
+ },
312
+ {
313
+ target: 'idle',
314
+ actions: ['consumeParameters', 'pushKey'],
315
+ },
316
+ ],
317
+ },
318
+ on: {
319
+ '*': {
320
+ actions: forwardTo('propertiesMachine'),
321
+ },
322
+ },
323
+ },
324
+ },
325
+ },
326
+ },
327
+ on: {
328
+ SETUP: { actions: 'markAsInSetup' },
329
+ SCRIPT: { actions: 'markAsInScript' },
330
+ TEMPLATE: { actions: 'markAsInTemplate' },
331
+ },
332
+ }, {
333
+ guards: {
334
+ isPropertiesDataDynamic: (_ctx, evt) => evt.data.keyName === false || evt.data.namespace === false,
335
+ },
336
+ actions: {
337
+ storeLine: assign({
338
+ line: (_ctx, evt) => evt.line,
339
+ }),
340
+ ignoreNextLine: assign({
341
+ ignore: (_ctx, evt) => ({ type: 'ignore', line: evt.line + 1 }),
342
+ }),
343
+ consumeIgnoredLine: assign({
344
+ ignore: (_ctx, _evt) => null,
345
+ }),
346
+ warnUnusedIgnore: assign({
347
+ warnings: (ctx, evt) => [
348
+ ...ctx.warnings,
349
+ { warning: 'W_UNUSED_IGNORE', line: evt.line - 1 },
350
+ ],
351
+ }),
352
+ storeUseTranslate: assign({
353
+ useTranslate: (_ctx, _evt) => '',
354
+ }),
355
+ storeNamespacedUseTranslate: assign({
356
+ useTranslate: (_ctx, evt) => evt.token,
357
+ }),
358
+ markUseTranslateAsDynamic: assign({
359
+ useTranslate: (_ctx, _evt) => false,
360
+ warnings: (ctx, _evt) => [
361
+ ...ctx.warnings,
362
+ { warning: 'W_DYNAMIC_NAMESPACE', line: ctx.line },
363
+ ],
364
+ }),
365
+ consumeTranslateCall: assign({
366
+ warnings: (ctx, evt) => {
367
+ if (!evt.data.namespace && ctx.useTranslate === false) {
368
+ return [
369
+ ...ctx.warnings,
370
+ { warning: 'W_UNRESOLVABLE_NAMESPACE', line: ctx.line },
371
+ ];
372
+ }
373
+ if (evt.data.defaultValue === false) {
374
+ return [
375
+ ...ctx.warnings,
376
+ { warning: 'W_DYNAMIC_DEFAULT_VALUE', line: ctx.line },
377
+ ];
378
+ }
379
+ return ctx.warnings;
380
+ },
381
+ keys: (ctx, evt) => {
382
+ const ns = evt.data.namespace === null ? ctx.useTranslate : evt.data.namespace;
383
+ if (!evt.data.keyName || ns === false)
384
+ return ctx.keys;
385
+ return [
386
+ ...ctx.keys,
387
+ {
388
+ keyName: evt.data.keyName,
389
+ namespace: ns || undefined,
390
+ defaultValue: evt.data.defaultValue || undefined,
391
+ line: ctx.line,
392
+ },
393
+ ];
394
+ },
395
+ }),
396
+ consumeParameters: assign({
397
+ key: (ctx, evt) => ({
398
+ keyName: ctx.key.keyName || evt.data.keyName,
399
+ defaultValue: ctx.key.defaultValue || evt.data.defaultValue || undefined,
400
+ namespace: evt.data.namespace ?? ctx.key.namespace,
401
+ line: ctx.line,
402
+ }),
403
+ warnings: (ctx, evt) => {
404
+ if (evt.data.defaultValue !== false)
405
+ return ctx.warnings;
406
+ return [
407
+ ...ctx.warnings,
408
+ { warning: 'W_DYNAMIC_DEFAULT_VALUE', line: ctx.line },
409
+ ];
410
+ },
411
+ }),
412
+ emitWarningFromParameters: assign({
413
+ warnings: (ctx, evt) => [
414
+ ...ctx.warnings,
415
+ {
416
+ warning: evt.data.keyName === false
417
+ ? 'W_DYNAMIC_KEY'
418
+ : 'W_DYNAMIC_NAMESPACE',
419
+ line: ctx.line,
420
+ },
421
+ ],
422
+ key: VOID_KEY,
423
+ }),
424
+ dynamicKeyName: assign({
425
+ warnings: (ctx, _evt) => [
426
+ ...ctx.warnings,
427
+ { warning: 'W_DYNAMIC_KEY', line: ctx.line },
428
+ ],
429
+ key: VOID_KEY,
430
+ }),
431
+ dynamicNamespace: assign({
432
+ warnings: (ctx, _evt) => [
433
+ ...ctx.warnings,
434
+ { warning: 'W_DYNAMIC_NAMESPACE', line: ctx.line },
435
+ ],
436
+ key: VOID_KEY,
437
+ }),
438
+ dynamicOptions: assign({
439
+ key: VOID_KEY,
440
+ warnings: (ctx, _evt) => [
441
+ ...ctx.warnings,
442
+ { warning: 'W_DYNAMIC_OPTIONS', line: ctx.line },
443
+ ],
444
+ }),
445
+ pushKey: assign({
446
+ keys: (ctx, _evt) => {
447
+ if (!ctx.key.keyName || ctx.key.namespace === false)
448
+ return ctx.keys;
449
+ return [
450
+ ...ctx.keys,
451
+ {
452
+ keyName: ctx.key.keyName.trim(),
453
+ namespace: ctx.key.namespace === ''
454
+ ? undefined
455
+ : ctx.key.namespace?.trim(),
456
+ defaultValue: ctx.key.defaultValue?.trim().replace(/\s+/g, ' '),
457
+ line: ctx.line,
458
+ },
459
+ ];
460
+ },
461
+ key: (_ctx, _evt) => ({ keyName: '', line: 0 }),
462
+ }),
463
+ pushImmediateKey: assign({
464
+ ignore: (_ctx, evt) => ({ type: 'key', line: evt.line + 1 }),
465
+ keys: (ctx, evt) => [
466
+ ...ctx.keys,
467
+ {
468
+ keyName: evt.keyName,
469
+ namespace: evt.namespace,
470
+ defaultValue: evt.defaultValue,
471
+ line: evt.line,
472
+ },
473
+ ],
474
+ }),
475
+ pushWarning: assign({
476
+ warnings: (ctx, evt) => [
477
+ ...ctx.warnings,
478
+ { warning: evt.kind, line: evt.line },
479
+ ],
480
+ }),
481
+ markAsInSetup: assign({
482
+ currentState: 1 /* State.SETUP */,
483
+ }),
484
+ markAsInScript: assign({
485
+ currentState: 2 /* State.SCRIPT */,
486
+ }),
487
+ markAsInTemplate: assign({
488
+ currentState: 3 /* State.TEMPLATE */,
489
+ }),
490
+ },
491
+ });
@@ -0,0 +1,55 @@
1
+ // Vue SFC are a bit of a nightmare. :D
2
+ //
3
+ // 1: We need to extract the script setup, script and template blocks.
4
+ // 2: We need to resolve them if necessary (script src="" is allowed).
5
+ // 3: We need to define uses of `useTranslate` in setup script.
6
+ // 4: We finally can go through the template file.
7
+ import { interpret } from 'xstate';
8
+ import tokenizer from '../tokenizer.js';
9
+ import vueSfcDecoderMachine from '../machines/vue/decoder.js';
10
+ import vueSfcExtractorMachine from '../machines/vue/extract.js';
11
+ function extractSfcTokens(tokens) {
12
+ const machine = interpret(vueSfcDecoderMachine);
13
+ machine.start();
14
+ for (let i = 0; i < tokens.length; i++) {
15
+ machine.send(tokens[i]);
16
+ }
17
+ const snapshot = machine.getSnapshot();
18
+ return snapshot.context;
19
+ }
20
+ function sort(a, b) {
21
+ if (a.line === b.line)
22
+ return 0;
23
+ if (a.line > b.line)
24
+ return 1;
25
+ return -1;
26
+ }
27
+ export default async function handleVueSfc(code, fileName) {
28
+ const tokens = await tokenizer(code, fileName);
29
+ const decoded = extractSfcTokens(tokens);
30
+ const machine = interpret(vueSfcExtractorMachine);
31
+ machine.start();
32
+ machine.send({ type: 'SETUP' });
33
+ for (let i = 0; i < decoded.setup.length; i++) {
34
+ machine.send(decoded.setup[i]);
35
+ }
36
+ machine.send({ type: 'SCRIPT' });
37
+ for (let i = 0; i < decoded.script.length; i++) {
38
+ machine.send(decoded.script[i]);
39
+ }
40
+ machine.send({ type: 'TEMPLATE' });
41
+ for (let i = 0; i < decoded.template.length; i++) {
42
+ machine.send(decoded.template[i]);
43
+ }
44
+ const snapshot = machine.getSnapshot();
45
+ const warnings = decoded.invalidSetup
46
+ ? [
47
+ ...snapshot.context.warnings,
48
+ { warning: 'W_VUE_SETUP_IS_A_REFERENCE', line: decoded.invalidSetup },
49
+ ]
50
+ : snapshot.context.warnings;
51
+ return {
52
+ warnings: warnings.sort(sort),
53
+ keys: snapshot.context.keys.sort(sort),
54
+ };
55
+ }
@@ -8,6 +8,9 @@ const GrammarFiles = {
8
8
  ["source.ts" /* Grammar.TYPESCRIPT */]: new URL('TypeScript.tmLanguage', GRAMMAR_PATH),
9
9
  ["source.tsx" /* Grammar.TYPESCRIPT_TSX */]: new URL('TypeScriptReact.tmLanguage', GRAMMAR_PATH),
10
10
  ["source.svelte" /* Grammar.SVELTE */]: new URL('Svelte.tmLanguage', GRAMMAR_PATH),
11
+ ["source.vue" /* Grammar.VUE */]: new URL('Vue.tmLanguage', GRAMMAR_PATH),
12
+ ["text.html.basic" /* Grammar.HTML */]: new URL('HTML.tmLanguage', GRAMMAR_PATH),
13
+ ["text.html.derivative" /* Grammar.HTML_D */]: new URL('HTML.tmLanguage', GRAMMAR_PATH),
11
14
  };
12
15
  let oniguruma;
13
16
  let registry;
@@ -28,9 +31,7 @@ async function loadGrammar(scope) {
28
31
  if (!file)
29
32
  return null;
30
33
  const grammar = await readFile(file, 'utf8');
31
- return grammar.startsWith('{')
32
- ? JSON.parse(grammar)
33
- : TextMate.parseRawGrammar(grammar);
34
+ return JSON.parse(grammar);
34
35
  }
35
36
  function extnameToGrammar(extname) {
36
37
  switch (extname) {
@@ -44,8 +45,12 @@ function extnameToGrammar(extname) {
44
45
  case '.jsx':
45
46
  case '.tsx':
46
47
  return "source.tsx" /* Grammar.TYPESCRIPT_TSX */;
48
+ case '.vue':
49
+ return "source.vue" /* Grammar.VUE */;
47
50
  case '.svelte':
48
51
  return "source.svelte" /* Grammar.SVELTE */;
52
+ case '.html':
53
+ return "text.html.basic" /* Grammar.HTML */;
49
54
  }
50
55
  }
51
56
  function tokenize(code, grammar) {
@@ -57,10 +62,10 @@ function tokenize(code, grammar) {
57
62
  const line = lines[i];
58
63
  const res = grammar.tokenizeLine(line, stack);
59
64
  for (const token of res.tokens) {
60
- // Opinionated take: if a token is scope-less, chances are we don't care about it.
65
+ const codeToken = code.slice(linePtr + token.startIndex, linePtr + token.endIndex);
66
+ // Opinionated take: if a token is scope-less and void, chances are we don't care about it.
61
67
  // Ditching it allows us to reduce complexity from the state machine's POV.
62
- if (token.scopes.length !== 1) {
63
- const codeToken = code.slice(linePtr + token.startIndex, linePtr + token.endIndex);
68
+ if (token.scopes.length !== 1 || codeToken.trim()) {
64
69
  tokens.push({
65
70
  type: token.scopes[token.scopes.length - 1],
66
71
  token: codeToken,
@@ -32,6 +32,10 @@ export const WarningMessages = {
32
32
  name: 'Invalid key override',
33
33
  description: 'The `key` must be present, and the `key`, `ns`, and `defaultValue` must be strings.',
34
34
  },
35
+ W_VUE_SETUP_IS_A_REFERENCE: {
36
+ name: 'Vue setup function is a reference',
37
+ description: 'The setup function must be directly defined on-site, and not be a reference to a previously defined function.',
38
+ },
35
39
  };
36
40
  /**
37
41
  * Dumps warnings emitted during an extraction to stdout, with GitHub integration, and counts them.
@@ -1,3 +1,4 @@
1
+ import { fileURLToPath } from 'url';
1
2
  import { resolve, extname } from 'path';
2
3
  import { Worker, isMainThread, parentPort } from 'worker_threads';
3
4
  import { readFile } from 'fs/promises';
@@ -33,11 +34,11 @@ let worker;
33
34
  const jobQueue = [];
34
35
  function createWorker() {
35
36
  const worker = IS_TS_NODE
36
- ? new Worker(new URL(import.meta.url).pathname.replace('.ts', '.js'), {
37
+ ? new Worker(fileURLToPath(new URL(import.meta.url)).replace('.ts', '.js'), {
37
38
  // ts-node workaround
38
39
  execArgv: ['--require', 'ts-node/register'],
39
40
  })
40
- : new Worker(new URL(import.meta.url).pathname);
41
+ : new Worker(fileURLToPath(new URL(import.meta.url)));
41
42
  let timeout;
42
43
  let currentDeferred;
43
44
  function workOrDie() {
package/extractor.d.ts CHANGED
@@ -5,12 +5,14 @@ export type Key = {
5
5
  };
6
6
  export type ExtractedKey = Key & {
7
7
  line: number;
8
+ /** Specified when the file differs from the file being processed (sub-file) */
9
+ file?: string;
8
10
  };
9
11
  export type Warning = {
10
12
  warning: string;
11
13
  line: number;
12
14
  };
13
- export type Extractor = (fileContents: string, fileName: string) => string[];
15
+ export type Extractor = (fileContents: string, fileName: string) => ExtractionResult[];
14
16
  export type ExtractionResult = {
15
17
  keys: ExtractedKey[];
16
18
  warnings: Warning[];