@vibe-lang/runtime 0.2.5
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.
- package/package.json +46 -0
- package/src/ast/index.ts +375 -0
- package/src/ast.ts +2 -0
- package/src/debug/advanced-features.ts +482 -0
- package/src/debug/bun-inspector.ts +424 -0
- package/src/debug/handoff-manager.ts +283 -0
- package/src/debug/index.ts +150 -0
- package/src/debug/runner.ts +365 -0
- package/src/debug/server.ts +565 -0
- package/src/debug/stack-merger.ts +267 -0
- package/src/debug/state.ts +581 -0
- package/src/debug/test/advanced-features.test.ts +300 -0
- package/src/debug/test/e2e.test.ts +218 -0
- package/src/debug/test/handoff-manager.test.ts +256 -0
- package/src/debug/test/runner.test.ts +256 -0
- package/src/debug/test/stack-merger.test.ts +163 -0
- package/src/debug/test/state.test.ts +400 -0
- package/src/debug/test/ts-debug-integration.test.ts +374 -0
- package/src/debug/test/ts-import-tracker.test.ts +125 -0
- package/src/debug/test/ts-source-map.test.ts +169 -0
- package/src/debug/ts-import-tracker.ts +151 -0
- package/src/debug/ts-source-map.ts +171 -0
- package/src/errors/index.ts +124 -0
- package/src/index.ts +358 -0
- package/src/lexer/index.ts +348 -0
- package/src/lexer.ts +2 -0
- package/src/parser/index.ts +792 -0
- package/src/parser/parse.ts +45 -0
- package/src/parser/test/async.test.ts +248 -0
- package/src/parser/test/destructuring.test.ts +167 -0
- package/src/parser/test/do-expression.test.ts +486 -0
- package/src/parser/test/errors/do-expression.test.ts +95 -0
- package/src/parser/test/errors/error-locations.test.ts +230 -0
- package/src/parser/test/errors/invalid-expressions.test.ts +144 -0
- package/src/parser/test/errors/missing-tokens.test.ts +126 -0
- package/src/parser/test/errors/model-declaration.test.ts +185 -0
- package/src/parser/test/errors/nested-blocks.test.ts +226 -0
- package/src/parser/test/errors/unclosed-delimiters.test.ts +122 -0
- package/src/parser/test/errors/unexpected-tokens.test.ts +120 -0
- package/src/parser/test/import-export.test.ts +143 -0
- package/src/parser/test/literals.test.ts +404 -0
- package/src/parser/test/model-declaration.test.ts +161 -0
- package/src/parser/test/nested-blocks.test.ts +402 -0
- package/src/parser/test/parser.test.ts +743 -0
- package/src/parser/test/private.test.ts +136 -0
- package/src/parser/test/template-literal.test.ts +127 -0
- package/src/parser/test/tool-declaration.test.ts +302 -0
- package/src/parser/test/ts-block.test.ts +252 -0
- package/src/parser/test/type-annotations.test.ts +254 -0
- package/src/parser/visitor/helpers.ts +330 -0
- package/src/parser/visitor.ts +794 -0
- package/src/parser.ts +2 -0
- package/src/runtime/ai/cache-chunking.test.ts +69 -0
- package/src/runtime/ai/cache-chunking.ts +73 -0
- package/src/runtime/ai/client.ts +109 -0
- package/src/runtime/ai/context.ts +168 -0
- package/src/runtime/ai/formatters.ts +316 -0
- package/src/runtime/ai/index.ts +38 -0
- package/src/runtime/ai/language-ref.ts +38 -0
- package/src/runtime/ai/providers/anthropic.ts +253 -0
- package/src/runtime/ai/providers/google.ts +201 -0
- package/src/runtime/ai/providers/openai.ts +156 -0
- package/src/runtime/ai/retry.ts +100 -0
- package/src/runtime/ai/return-tools.ts +301 -0
- package/src/runtime/ai/test/client.test.ts +83 -0
- package/src/runtime/ai/test/formatters.test.ts +485 -0
- package/src/runtime/ai/test/retry.test.ts +137 -0
- package/src/runtime/ai/test/return-tools.test.ts +450 -0
- package/src/runtime/ai/test/tool-loop.test.ts +319 -0
- package/src/runtime/ai/test/tool-schema.test.ts +241 -0
- package/src/runtime/ai/tool-loop.ts +203 -0
- package/src/runtime/ai/tool-schema.ts +151 -0
- package/src/runtime/ai/types.ts +113 -0
- package/src/runtime/ai-logger.ts +255 -0
- package/src/runtime/ai-provider.ts +347 -0
- package/src/runtime/async/dependencies.ts +276 -0
- package/src/runtime/async/executor.ts +293 -0
- package/src/runtime/async/index.ts +43 -0
- package/src/runtime/async/scheduling.ts +163 -0
- package/src/runtime/async/test/dependencies.test.ts +284 -0
- package/src/runtime/async/test/executor.test.ts +388 -0
- package/src/runtime/context.ts +357 -0
- package/src/runtime/exec/ai.ts +139 -0
- package/src/runtime/exec/expressions.ts +475 -0
- package/src/runtime/exec/frames.ts +26 -0
- package/src/runtime/exec/functions.ts +305 -0
- package/src/runtime/exec/interpolation.ts +312 -0
- package/src/runtime/exec/statements.ts +604 -0
- package/src/runtime/exec/tools.ts +129 -0
- package/src/runtime/exec/typescript.ts +215 -0
- package/src/runtime/exec/variables.ts +279 -0
- package/src/runtime/index.ts +975 -0
- package/src/runtime/modules.ts +452 -0
- package/src/runtime/serialize.ts +103 -0
- package/src/runtime/state.ts +489 -0
- package/src/runtime/stdlib/core.ts +45 -0
- package/src/runtime/stdlib/directory.test.ts +156 -0
- package/src/runtime/stdlib/edit.test.ts +154 -0
- package/src/runtime/stdlib/fastEdit.test.ts +201 -0
- package/src/runtime/stdlib/glob.test.ts +106 -0
- package/src/runtime/stdlib/grep.test.ts +144 -0
- package/src/runtime/stdlib/index.ts +16 -0
- package/src/runtime/stdlib/readFile.test.ts +123 -0
- package/src/runtime/stdlib/tools/index.ts +707 -0
- package/src/runtime/stdlib/writeFile.test.ts +157 -0
- package/src/runtime/step.ts +969 -0
- package/src/runtime/test/ai-context.test.ts +1086 -0
- package/src/runtime/test/ai-result-object.test.ts +419 -0
- package/src/runtime/test/ai-tool-flow.test.ts +859 -0
- package/src/runtime/test/async-execution-order.test.ts +618 -0
- package/src/runtime/test/async-execution.test.ts +344 -0
- package/src/runtime/test/async-nested.test.ts +660 -0
- package/src/runtime/test/async-parallel-timing.test.ts +546 -0
- package/src/runtime/test/basic1.test.ts +154 -0
- package/src/runtime/test/binary-operators.test.ts +431 -0
- package/src/runtime/test/break-statement.test.ts +257 -0
- package/src/runtime/test/context-modes.test.ts +650 -0
- package/src/runtime/test/context.test.ts +466 -0
- package/src/runtime/test/core-functions.test.ts +228 -0
- package/src/runtime/test/e2e.test.ts +88 -0
- package/src/runtime/test/error-locations/error-locations.test.ts +80 -0
- package/src/runtime/test/error-locations/main-error.vibe +4 -0
- package/src/runtime/test/error-locations/main-import-error.vibe +3 -0
- package/src/runtime/test/error-locations/utils/helper.vibe +5 -0
- package/src/runtime/test/for-in.test.ts +312 -0
- package/src/runtime/test/helpers.ts +69 -0
- package/src/runtime/test/imports.test.ts +334 -0
- package/src/runtime/test/json-expressions.test.ts +232 -0
- package/src/runtime/test/literals.test.ts +372 -0
- package/src/runtime/test/logical-indexing.test.ts +478 -0
- package/src/runtime/test/member-methods.test.ts +324 -0
- package/src/runtime/test/model-config.test.ts +338 -0
- package/src/runtime/test/null-handling.test.ts +342 -0
- package/src/runtime/test/private-visibility.test.ts +332 -0
- package/src/runtime/test/runtime-state.test.ts +514 -0
- package/src/runtime/test/scoping.test.ts +370 -0
- package/src/runtime/test/string-interpolation.test.ts +354 -0
- package/src/runtime/test/template-literal.test.ts +181 -0
- package/src/runtime/test/tool-execution.test.ts +467 -0
- package/src/runtime/test/tool-schema-generation.test.ts +477 -0
- package/src/runtime/test/tostring.test.ts +210 -0
- package/src/runtime/test/ts-block.test.ts +594 -0
- package/src/runtime/test/ts-error-location.test.ts +231 -0
- package/src/runtime/test/types.test.ts +732 -0
- package/src/runtime/test/verbose-logger.test.ts +710 -0
- package/src/runtime/test/vibe-expression.test.ts +54 -0
- package/src/runtime/test/vibe-value-errors.test.ts +541 -0
- package/src/runtime/test/while.test.ts +232 -0
- package/src/runtime/tools/builtin.ts +30 -0
- package/src/runtime/tools/directory-tools.ts +70 -0
- package/src/runtime/tools/file-tools.ts +228 -0
- package/src/runtime/tools/index.ts +5 -0
- package/src/runtime/tools/registry.ts +48 -0
- package/src/runtime/tools/search-tools.ts +134 -0
- package/src/runtime/tools/security.ts +36 -0
- package/src/runtime/tools/system-tools.ts +312 -0
- package/src/runtime/tools/test/fixtures/base-types.ts +40 -0
- package/src/runtime/tools/test/fixtures/test-types.ts +132 -0
- package/src/runtime/tools/test/registry.test.ts +713 -0
- package/src/runtime/tools/test/security.test.ts +86 -0
- package/src/runtime/tools/test/system-tools.test.ts +679 -0
- package/src/runtime/tools/test/ts-schema.test.ts +357 -0
- package/src/runtime/tools/ts-schema.ts +341 -0
- package/src/runtime/tools/types.ts +89 -0
- package/src/runtime/tools/utility-tools.ts +198 -0
- package/src/runtime/ts-eval.ts +126 -0
- package/src/runtime/types.ts +797 -0
- package/src/runtime/validation.ts +160 -0
- package/src/runtime/verbose-logger.ts +459 -0
- package/src/runtime.ts +2 -0
- package/src/semantic/analyzer-context.ts +62 -0
- package/src/semantic/analyzer-validators.ts +575 -0
- package/src/semantic/analyzer-visitors.ts +534 -0
- package/src/semantic/analyzer.ts +83 -0
- package/src/semantic/index.ts +11 -0
- package/src/semantic/symbol-table.ts +58 -0
- package/src/semantic/test/async-validation.test.ts +301 -0
- package/src/semantic/test/compress-validation.test.ts +179 -0
- package/src/semantic/test/const-reassignment.test.ts +111 -0
- package/src/semantic/test/control-flow.test.ts +346 -0
- package/src/semantic/test/destructuring.test.ts +185 -0
- package/src/semantic/test/duplicate-declarations.test.ts +168 -0
- package/src/semantic/test/export-validation.test.ts +111 -0
- package/src/semantic/test/fixtures/math.ts +31 -0
- package/src/semantic/test/imports.test.ts +148 -0
- package/src/semantic/test/json-type.test.ts +68 -0
- package/src/semantic/test/literals.test.ts +127 -0
- package/src/semantic/test/model-validation.test.ts +179 -0
- package/src/semantic/test/prompt-validation.test.ts +343 -0
- package/src/semantic/test/scoping.test.ts +312 -0
- package/src/semantic/test/tool-validation.test.ts +306 -0
- package/src/semantic/test/ts-type-checking.test.ts +563 -0
- package/src/semantic/test/type-constraints.test.ts +111 -0
- package/src/semantic/test/type-inference.test.ts +87 -0
- package/src/semantic/test/type-validation.test.ts +552 -0
- package/src/semantic/test/undefined-variables.test.ts +163 -0
- package/src/semantic/ts-block-checker.ts +204 -0
- package/src/semantic/ts-signatures.ts +194 -0
- package/src/semantic/ts-types.ts +170 -0
- package/src/semantic/types.ts +58 -0
- package/tests/fixtures/conditional-logic.vibe +14 -0
- package/tests/fixtures/function-call.vibe +16 -0
- package/tests/fixtures/imports/cycle-detection/a.vibe +6 -0
- package/tests/fixtures/imports/cycle-detection/b.vibe +5 -0
- package/tests/fixtures/imports/cycle-detection/main.vibe +3 -0
- package/tests/fixtures/imports/module-isolation/main-b.vibe +8 -0
- package/tests/fixtures/imports/module-isolation/main.vibe +9 -0
- package/tests/fixtures/imports/module-isolation/moduleA.vibe +6 -0
- package/tests/fixtures/imports/module-isolation/moduleB.vibe +6 -0
- package/tests/fixtures/imports/nested-import/helper.vibe +6 -0
- package/tests/fixtures/imports/nested-import/main.vibe +3 -0
- package/tests/fixtures/imports/nested-import/utils.ts +3 -0
- package/tests/fixtures/imports/nested-isolation/file2.vibe +15 -0
- package/tests/fixtures/imports/nested-isolation/file3.vibe +10 -0
- package/tests/fixtures/imports/nested-isolation/main.vibe +21 -0
- package/tests/fixtures/imports/pure-cycle/a.vibe +5 -0
- package/tests/fixtures/imports/pure-cycle/b.vibe +5 -0
- package/tests/fixtures/imports/pure-cycle/main.vibe +3 -0
- package/tests/fixtures/imports/ts-boolean/checks.ts +14 -0
- package/tests/fixtures/imports/ts-boolean/main.vibe +10 -0
- package/tests/fixtures/imports/ts-boolean/type-mismatch.vibe +5 -0
- package/tests/fixtures/imports/ts-boolean/use-constant.vibe +18 -0
- package/tests/fixtures/imports/ts-error-handling/helpers.ts +42 -0
- package/tests/fixtures/imports/ts-error-handling/main.vibe +5 -0
- package/tests/fixtures/imports/ts-import/main.vibe +4 -0
- package/tests/fixtures/imports/ts-import/math.ts +9 -0
- package/tests/fixtures/imports/ts-variables/call-non-function.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/data.ts +10 -0
- package/tests/fixtures/imports/ts-variables/import-json.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/import-type-mismatch.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/import-variable.vibe +5 -0
- package/tests/fixtures/imports/vibe-import/greet.vibe +5 -0
- package/tests/fixtures/imports/vibe-import/main.vibe +3 -0
- package/tests/fixtures/multiple-ai-calls.vibe +10 -0
- package/tests/fixtures/simple-greeting.vibe +6 -0
- package/tests/fixtures/template-literals.vibe +11 -0
- package/tests/integration/basic-ai/basic-ai.integration.test.ts +166 -0
- package/tests/integration/basic-ai/basic-ai.vibe +12 -0
- package/tests/integration/bug-fix/bug-fix.integration.test.ts +201 -0
- package/tests/integration/bug-fix/buggy-code.ts +22 -0
- package/tests/integration/bug-fix/fix-bug.vibe +21 -0
- package/tests/integration/compress/compress.integration.test.ts +206 -0
- package/tests/integration/destructuring/destructuring.integration.test.ts +92 -0
- package/tests/integration/hello-world-translator/hello-world-translator.integration.test.ts +61 -0
- package/tests/integration/line-annotator/context-modes.integration.test.ts +261 -0
- package/tests/integration/line-annotator/line-annotator.integration.test.ts +148 -0
- package/tests/integration/multi-feature/cumulative-sum.integration.test.ts +75 -0
- package/tests/integration/multi-feature/number-analyzer.integration.test.ts +191 -0
- package/tests/integration/multi-feature/number-analyzer.vibe +59 -0
- package/tests/integration/tool-calls/tool-calls.integration.test.ts +93 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { describe, expect, test, beforeAll } from 'bun:test';
|
|
2
|
+
import { vibeTypeToJsonSchema, extractTypeSchema, createTypeExtractor, clearSchemaCache } from '../ts-schema';
|
|
3
|
+
import type { TypeExtractor } from '../ts-schema';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
describe('vibeTypeToJsonSchema', () => {
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Primitive types
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
test('converts text type to string schema', () => {
|
|
12
|
+
const schema = vibeTypeToJsonSchema('text');
|
|
13
|
+
expect(schema).toEqual({ type: 'string' });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('converts prompt type to string schema', () => {
|
|
17
|
+
const schema = vibeTypeToJsonSchema('prompt');
|
|
18
|
+
expect(schema).toEqual({ type: 'string' });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('converts number type to number schema', () => {
|
|
22
|
+
const schema = vibeTypeToJsonSchema('number');
|
|
23
|
+
expect(schema).toEqual({ type: 'number' });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('converts boolean type to boolean schema', () => {
|
|
27
|
+
const schema = vibeTypeToJsonSchema('boolean');
|
|
28
|
+
expect(schema).toEqual({ type: 'boolean' });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('converts json type to object schema', () => {
|
|
32
|
+
const schema = vibeTypeToJsonSchema('json');
|
|
33
|
+
expect(schema).toEqual({ type: 'object', additionalProperties: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Array types
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
test('converts text[] to array of strings', () => {
|
|
41
|
+
const schema = vibeTypeToJsonSchema('text[]');
|
|
42
|
+
expect(schema).toEqual({
|
|
43
|
+
type: 'array',
|
|
44
|
+
items: { type: 'string' },
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('converts number[] to array of numbers', () => {
|
|
49
|
+
const schema = vibeTypeToJsonSchema('number[]');
|
|
50
|
+
expect(schema).toEqual({
|
|
51
|
+
type: 'array',
|
|
52
|
+
items: { type: 'number' },
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('converts boolean[] to array of booleans', () => {
|
|
57
|
+
const schema = vibeTypeToJsonSchema('boolean[]');
|
|
58
|
+
expect(schema).toEqual({
|
|
59
|
+
type: 'array',
|
|
60
|
+
items: { type: 'boolean' },
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('converts json[] to array of objects', () => {
|
|
65
|
+
const schema = vibeTypeToJsonSchema('json[]');
|
|
66
|
+
expect(schema).toEqual({
|
|
67
|
+
type: 'array',
|
|
68
|
+
items: { type: 'object', additionalProperties: true },
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Unknown types
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
test('returns generic object for unknown type', () => {
|
|
77
|
+
const schema = vibeTypeToJsonSchema('UnknownType');
|
|
78
|
+
expect(schema).toEqual({ type: 'object', additionalProperties: true });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('falls back to generic object for unmapped type', () => {
|
|
82
|
+
const importedTypes = new Map<string, string>();
|
|
83
|
+
const schema = vibeTypeToJsonSchema('UnknownType', importedTypes);
|
|
84
|
+
expect(schema).toEqual({ type: 'object', additionalProperties: true });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// TypeScript type extraction tests using fixtures
|
|
90
|
+
// Compiles once in beforeAll for efficiency (~1s instead of ~10s)
|
|
91
|
+
// ============================================================================
|
|
92
|
+
|
|
93
|
+
describe('extractTypeSchema with fixtures', () => {
|
|
94
|
+
const fixturesDir = join(__dirname, 'fixtures');
|
|
95
|
+
const testTypesFile = join(fixturesDir, 'test-types.ts');
|
|
96
|
+
const baseTypesFile = join(fixturesDir, 'base-types.ts');
|
|
97
|
+
|
|
98
|
+
let extractor: TypeExtractor;
|
|
99
|
+
|
|
100
|
+
beforeAll(() => {
|
|
101
|
+
clearSchemaCache();
|
|
102
|
+
// Compile both files once - this is where the time is spent
|
|
103
|
+
extractor = createTypeExtractor([testTypesFile, baseTypesFile]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Simple primitives from test-types.ts
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
test('extracts Person interface with primitives', () => {
|
|
111
|
+
const schema = extractor.extract('Person');
|
|
112
|
+
expect(schema).toEqual({
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {
|
|
115
|
+
name: { type: 'string' },
|
|
116
|
+
age: { type: 'number' },
|
|
117
|
+
active: { type: 'boolean' },
|
|
118
|
+
},
|
|
119
|
+
required: ['name', 'age', 'active'],
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('extracts Config interface with optional properties', () => {
|
|
124
|
+
const schema = extractor.extract('Config');
|
|
125
|
+
expect(schema.properties).toHaveProperty('required');
|
|
126
|
+
expect(schema.properties).toHaveProperty('optional');
|
|
127
|
+
expect(schema.required).toContain('required');
|
|
128
|
+
expect(schema.required).not.toContain('optional');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('extracts Container interface with array property', () => {
|
|
132
|
+
const schema = extractor.extract('Container');
|
|
133
|
+
expect(schema.properties?.items).toEqual({
|
|
134
|
+
type: 'array',
|
|
135
|
+
items: { type: 'string' },
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('extracts Status type alias', () => {
|
|
140
|
+
const schema = extractor.extract('Status');
|
|
141
|
+
expect(schema).toEqual({ type: 'string' });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ============================================================================
|
|
145
|
+
// JSDoc extraction
|
|
146
|
+
// ============================================================================
|
|
147
|
+
|
|
148
|
+
test('extracts JSDoc descriptions from Documented interface', () => {
|
|
149
|
+
const schema = extractor.extract('Documented');
|
|
150
|
+
expect(schema.properties?.id.description).toBe("The user's unique identifier");
|
|
151
|
+
expect(schema.properties?.name.description).toBe("The user's display name");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// Types from base-types.ts (imported by test-types.ts)
|
|
156
|
+
// ============================================================================
|
|
157
|
+
|
|
158
|
+
test('extracts Address interface from base-types', () => {
|
|
159
|
+
const schema = extractor.extract('Address');
|
|
160
|
+
expect(schema).toMatchObject({
|
|
161
|
+
type: 'object',
|
|
162
|
+
properties: {
|
|
163
|
+
street: { type: 'string' },
|
|
164
|
+
city: { type: 'string' },
|
|
165
|
+
zipCode: { type: 'string' },
|
|
166
|
+
country: { type: 'string' },
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
expect(schema.required).toContain('street');
|
|
170
|
+
expect(schema.required).toContain('city');
|
|
171
|
+
expect(schema.required).not.toContain('country'); // optional
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('extracts ContactInfo with nested Address', () => {
|
|
175
|
+
const schema = extractor.extract('ContactInfo');
|
|
176
|
+
expect(schema.properties?.email).toEqual({ type: 'string' });
|
|
177
|
+
expect(schema.properties?.address).toMatchObject({
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: {
|
|
180
|
+
street: { type: 'string' },
|
|
181
|
+
city: { type: 'string' },
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('extracts Metadata interface with JSDoc and optional array', () => {
|
|
187
|
+
const schema = extractor.extract('Metadata');
|
|
188
|
+
expect(schema.properties?.createdAt.description).toBe('When the entity was created');
|
|
189
|
+
expect(schema.properties?.updatedAt.description).toBe('When the entity was last updated');
|
|
190
|
+
expect(schema.properties?.tags).toMatchObject({
|
|
191
|
+
type: 'array',
|
|
192
|
+
items: { type: 'string' },
|
|
193
|
+
});
|
|
194
|
+
expect(schema.required).not.toContain('tags'); // optional
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('extracts OrderItem interface', () => {
|
|
198
|
+
const schema = extractor.extract('OrderItem');
|
|
199
|
+
expect(schema).toMatchObject({
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
productId: { type: 'string' },
|
|
203
|
+
productName: { type: 'string' },
|
|
204
|
+
quantity: { type: 'number' },
|
|
205
|
+
unitPrice: { type: 'number' },
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// Complex types using imports (test-types.ts using base-types.ts)
|
|
212
|
+
// ============================================================================
|
|
213
|
+
|
|
214
|
+
test('extracts User with imported ContactInfo and Metadata', () => {
|
|
215
|
+
const schema = extractor.extract('User');
|
|
216
|
+
expect(schema.properties?.id).toEqual({ type: 'string' });
|
|
217
|
+
expect(schema.properties?.username).toEqual({ type: 'string' });
|
|
218
|
+
expect(schema.properties?.contact).toMatchObject({
|
|
219
|
+
type: 'object',
|
|
220
|
+
properties: {
|
|
221
|
+
email: { type: 'string' },
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
expect(schema.properties?.metadata).toMatchObject({
|
|
225
|
+
type: 'object',
|
|
226
|
+
properties: {
|
|
227
|
+
createdAt: { type: 'string' },
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('extracts Customer with optional shippingAddress', () => {
|
|
233
|
+
const schema = extractor.extract('Customer');
|
|
234
|
+
expect(schema.required).toContain('billingAddress');
|
|
235
|
+
expect(schema.required).not.toContain('shippingAddress'); // optional
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('extracts Order with array of OrderItems', () => {
|
|
239
|
+
const schema = extractor.extract('Order');
|
|
240
|
+
expect(schema.properties?.items).toMatchObject({
|
|
241
|
+
type: 'array',
|
|
242
|
+
items: {
|
|
243
|
+
type: 'object',
|
|
244
|
+
properties: {
|
|
245
|
+
productId: { type: 'string' },
|
|
246
|
+
quantity: { type: 'number' },
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ============================================================================
|
|
253
|
+
// Array types interface
|
|
254
|
+
// ============================================================================
|
|
255
|
+
|
|
256
|
+
test('extracts ArrayTypes with various array properties', () => {
|
|
257
|
+
const schema = extractor.extract('ArrayTypes');
|
|
258
|
+
expect(schema.properties?.strings).toEqual({ type: 'array', items: { type: 'string' } });
|
|
259
|
+
expect(schema.properties?.numbers).toEqual({ type: 'array', items: { type: 'number' } });
|
|
260
|
+
expect(schema.properties?.booleans).toEqual({ type: 'array', items: { type: 'boolean' } });
|
|
261
|
+
expect(schema.properties?.nested).toMatchObject({
|
|
262
|
+
type: 'array',
|
|
263
|
+
items: { type: 'object' },
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ============================================================================
|
|
268
|
+
// Optional-only types
|
|
269
|
+
// ============================================================================
|
|
270
|
+
|
|
271
|
+
test('extracts AllOptional with no required fields', () => {
|
|
272
|
+
const schema = extractor.extract('AllOptional');
|
|
273
|
+
expect(schema.required).toBeUndefined();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ============================================================================
|
|
277
|
+
// Error handling
|
|
278
|
+
// ============================================================================
|
|
279
|
+
|
|
280
|
+
test('throws error for non-existent type', () => {
|
|
281
|
+
expect(() => extractor.extract('NonExistentType')).toThrow(
|
|
282
|
+
"Type 'NonExistentType' not found"
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ============================================================================
|
|
288
|
+
// Tests for extractTypeSchema (the single-file API with caching)
|
|
289
|
+
// ============================================================================
|
|
290
|
+
|
|
291
|
+
describe('extractTypeSchema caching', () => {
|
|
292
|
+
const fixturesDir = join(__dirname, 'fixtures');
|
|
293
|
+
const testTypesFile = join(fixturesDir, 'test-types.ts');
|
|
294
|
+
|
|
295
|
+
test('caches schema for same file and type', () => {
|
|
296
|
+
clearSchemaCache();
|
|
297
|
+
|
|
298
|
+
// First call - compiles the file
|
|
299
|
+
const schema1 = extractTypeSchema(testTypesFile, 'Person');
|
|
300
|
+
|
|
301
|
+
// Second call - should use cache
|
|
302
|
+
const schema2 = extractTypeSchema(testTypesFile, 'Person');
|
|
303
|
+
|
|
304
|
+
expect(schema1).toEqual(schema2);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('throws error for non-existent file', () => {
|
|
308
|
+
expect(() => extractTypeSchema('/nonexistent/path/file.ts', 'SomeType')).toThrow();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test('throws error for non-existent type in file', () => {
|
|
312
|
+
expect(() => extractTypeSchema(testTypesFile, 'NonExistentType')).toThrow(
|
|
313
|
+
"Type 'NonExistentType' not found"
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ============================================================================
|
|
319
|
+
// vibeTypeToJsonSchema with imported types (uses fixture file)
|
|
320
|
+
// ============================================================================
|
|
321
|
+
|
|
322
|
+
describe('vibeTypeToJsonSchema with imported types', () => {
|
|
323
|
+
const fixturesDir = join(__dirname, 'fixtures');
|
|
324
|
+
const testTypesFile = join(fixturesDir, 'test-types.ts');
|
|
325
|
+
|
|
326
|
+
test('resolves imported type from map', () => {
|
|
327
|
+
clearSchemaCache();
|
|
328
|
+
const importedTypes = new Map<string, string>();
|
|
329
|
+
importedTypes.set('Person', testTypesFile);
|
|
330
|
+
|
|
331
|
+
const schema = vibeTypeToJsonSchema('Person', importedTypes);
|
|
332
|
+
expect(schema).toMatchObject({
|
|
333
|
+
type: 'object',
|
|
334
|
+
properties: {
|
|
335
|
+
name: { type: 'string' },
|
|
336
|
+
age: { type: 'number' },
|
|
337
|
+
active: { type: 'boolean' },
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('handles array of imported type', () => {
|
|
343
|
+
const importedTypes = new Map<string, string>();
|
|
344
|
+
importedTypes.set('Person', testTypesFile);
|
|
345
|
+
|
|
346
|
+
const schema = vibeTypeToJsonSchema('Person[]', importedTypes);
|
|
347
|
+
expect(schema).toMatchObject({
|
|
348
|
+
type: 'array',
|
|
349
|
+
items: {
|
|
350
|
+
type: 'object',
|
|
351
|
+
properties: {
|
|
352
|
+
name: { type: 'string' },
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type { JsonSchema } from './types';
|
|
3
|
+
|
|
4
|
+
// Cache for extracted schemas to avoid re-parsing the same file
|
|
5
|
+
const schemaCache = new Map<string, Map<string, JsonSchema>>();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extract JSON Schema from a TypeScript type definition.
|
|
9
|
+
*
|
|
10
|
+
* @param sourceFile - Path to the TypeScript source file
|
|
11
|
+
* @param typeName - Name of the type/interface to extract
|
|
12
|
+
* @returns JSON Schema representation of the type
|
|
13
|
+
*/
|
|
14
|
+
export function extractTypeSchema(sourceFile: string, typeName: string): JsonSchema {
|
|
15
|
+
// Check cache first
|
|
16
|
+
const fileCache = schemaCache.get(sourceFile);
|
|
17
|
+
if (fileCache?.has(typeName)) {
|
|
18
|
+
return fileCache.get(typeName)!;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Create TypeScript program to analyze the file
|
|
22
|
+
const program = ts.createProgram([sourceFile], {
|
|
23
|
+
target: ts.ScriptTarget.ESNext,
|
|
24
|
+
module: ts.ModuleKind.ESNext,
|
|
25
|
+
strict: true,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const checker = program.getTypeChecker();
|
|
29
|
+
const source = program.getSourceFile(sourceFile);
|
|
30
|
+
|
|
31
|
+
if (!source) {
|
|
32
|
+
throw new Error(`Could not load source file: ${sourceFile}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Find the type declaration
|
|
36
|
+
const typeSymbol = findTypeSymbol(source, typeName, checker);
|
|
37
|
+
if (!typeSymbol) {
|
|
38
|
+
throw new Error(`Type '${typeName}' not found in ${sourceFile}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const type = checker.getDeclaredTypeOfSymbol(typeSymbol);
|
|
42
|
+
const schema = typeToJsonSchema(type, checker, new Set());
|
|
43
|
+
|
|
44
|
+
// Cache the result
|
|
45
|
+
if (!schemaCache.has(sourceFile)) {
|
|
46
|
+
schemaCache.set(sourceFile, new Map());
|
|
47
|
+
}
|
|
48
|
+
schemaCache.get(sourceFile)!.set(typeName, schema);
|
|
49
|
+
|
|
50
|
+
return schema;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Find a type symbol by name in a source file.
|
|
55
|
+
*/
|
|
56
|
+
function findTypeSymbol(
|
|
57
|
+
source: ts.SourceFile,
|
|
58
|
+
typeName: string,
|
|
59
|
+
checker: ts.TypeChecker
|
|
60
|
+
): ts.Symbol | undefined {
|
|
61
|
+
let result: ts.Symbol | undefined;
|
|
62
|
+
|
|
63
|
+
function visit(node: ts.Node) {
|
|
64
|
+
if (result) return;
|
|
65
|
+
|
|
66
|
+
if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
|
|
67
|
+
if (node.name.text === typeName) {
|
|
68
|
+
result = checker.getSymbolAtLocation(node.name);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ts.forEachChild(node, visit);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
visit(source);
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Convert a TypeScript type to JSON Schema.
|
|
81
|
+
*
|
|
82
|
+
* @param type - The TypeScript type to convert
|
|
83
|
+
* @param checker - TypeScript type checker
|
|
84
|
+
* @param visited - Set of visited type IDs to prevent infinite recursion
|
|
85
|
+
*/
|
|
86
|
+
function typeToJsonSchema(
|
|
87
|
+
type: ts.Type,
|
|
88
|
+
checker: ts.TypeChecker,
|
|
89
|
+
visited: Set<number>
|
|
90
|
+
): JsonSchema {
|
|
91
|
+
// Prevent infinite recursion for recursive types
|
|
92
|
+
const typeId = (type as { id?: number }).id;
|
|
93
|
+
if (typeId !== undefined && visited.has(typeId)) {
|
|
94
|
+
return { type: 'object', additionalProperties: true };
|
|
95
|
+
}
|
|
96
|
+
if (typeId !== undefined) {
|
|
97
|
+
visited.add(typeId);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Handle primitive types
|
|
101
|
+
if (type.flags & ts.TypeFlags.String) {
|
|
102
|
+
return { type: 'string' };
|
|
103
|
+
}
|
|
104
|
+
if (type.flags & ts.TypeFlags.Number) {
|
|
105
|
+
return { type: 'number' };
|
|
106
|
+
}
|
|
107
|
+
if (type.flags & ts.TypeFlags.Boolean) {
|
|
108
|
+
return { type: 'boolean' };
|
|
109
|
+
}
|
|
110
|
+
if (type.flags & ts.TypeFlags.Null || type.flags & ts.TypeFlags.Undefined) {
|
|
111
|
+
return { type: 'object' }; // JSON doesn't have null type, use object
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Handle string literal types
|
|
115
|
+
if (type.flags & ts.TypeFlags.StringLiteral) {
|
|
116
|
+
return { type: 'string' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Handle number literal types
|
|
120
|
+
if (type.flags & ts.TypeFlags.NumberLiteral) {
|
|
121
|
+
return { type: 'number' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Handle boolean literal types
|
|
125
|
+
if (type.flags & ts.TypeFlags.BooleanLiteral) {
|
|
126
|
+
return { type: 'boolean' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Handle union types (e.g., string | number, or T | undefined for optional)
|
|
130
|
+
if (type.flags & ts.TypeFlags.Union) {
|
|
131
|
+
const unionType = type as ts.UnionType;
|
|
132
|
+
|
|
133
|
+
// Filter out undefined and null from the union (common for optional types)
|
|
134
|
+
const nonNullTypes = unionType.types.filter(
|
|
135
|
+
(t) => !(t.flags & ts.TypeFlags.Undefined) && !(t.flags & ts.TypeFlags.Null)
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// If only one type remains after filtering, use that
|
|
139
|
+
if (nonNullTypes.length === 1) {
|
|
140
|
+
return typeToJsonSchema(nonNullTypes[0], checker, new Set(visited));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// For remaining types, try to find a common type
|
|
144
|
+
const types = nonNullTypes.map((t) => typeToJsonSchema(t, checker, new Set(visited)));
|
|
145
|
+
|
|
146
|
+
// If all types are the same, return that type
|
|
147
|
+
const typeSet = new Set(types.map((t) => t.type));
|
|
148
|
+
if (typeSet.size === 1) {
|
|
149
|
+
return types[0];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Otherwise, return a generic object
|
|
153
|
+
return { type: 'object', additionalProperties: true };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Handle arrays
|
|
157
|
+
if (checker.isArrayType(type)) {
|
|
158
|
+
const typeRef = type as ts.TypeReference;
|
|
159
|
+
const elementType = typeRef.typeArguments?.[0];
|
|
160
|
+
if (elementType) {
|
|
161
|
+
return {
|
|
162
|
+
type: 'array',
|
|
163
|
+
items: typeToJsonSchema(elementType, checker, new Set(visited)),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return { type: 'array' };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Handle tuples as arrays
|
|
170
|
+
if (checker.isTupleType(type)) {
|
|
171
|
+
const tupleType = type as ts.TupleType;
|
|
172
|
+
// Use the first element type or object for mixed tuples
|
|
173
|
+
if (tupleType.typeArguments && tupleType.typeArguments.length > 0) {
|
|
174
|
+
return {
|
|
175
|
+
type: 'array',
|
|
176
|
+
items: typeToJsonSchema(tupleType.typeArguments[0], checker, new Set(visited)),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return { type: 'array' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Handle objects/interfaces
|
|
183
|
+
if (type.flags & ts.TypeFlags.Object) {
|
|
184
|
+
const objectType = type as ts.ObjectType;
|
|
185
|
+
|
|
186
|
+
// Check if it's a function type
|
|
187
|
+
const callSignatures = type.getCallSignatures();
|
|
188
|
+
if (callSignatures.length > 0) {
|
|
189
|
+
return { type: 'object', additionalProperties: true };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const properties: Record<string, JsonSchema> = {};
|
|
193
|
+
const required: string[] = [];
|
|
194
|
+
|
|
195
|
+
const typeProperties = objectType.getProperties();
|
|
196
|
+
for (const prop of typeProperties) {
|
|
197
|
+
const propType = checker.getTypeOfSymbol(prop);
|
|
198
|
+
const propSchema = typeToJsonSchema(propType, checker, new Set(visited));
|
|
199
|
+
|
|
200
|
+
// Get JSDoc comment if available
|
|
201
|
+
const jsDocComment = prop.getDocumentationComment(checker);
|
|
202
|
+
if (jsDocComment.length > 0) {
|
|
203
|
+
propSchema.description = jsDocComment.map((c) => c.text).join('\n');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
properties[prop.name] = propSchema;
|
|
207
|
+
|
|
208
|
+
// Check if property is required (not optional)
|
|
209
|
+
if (!(prop.flags & ts.SymbolFlags.Optional)) {
|
|
210
|
+
required.push(prop.name);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
type: 'object',
|
|
216
|
+
properties,
|
|
217
|
+
required: required.length > 0 ? required : undefined,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Default fallback
|
|
222
|
+
return { type: 'object', additionalProperties: true };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Convert a Vibe type annotation to JSON Schema.
|
|
227
|
+
* Handles both built-in types and imported TS type names.
|
|
228
|
+
*
|
|
229
|
+
* @param vibeType - The Vibe type annotation (e.g., "text", "CustomerInfo", "Order[]")
|
|
230
|
+
* @param importedTypes - Map of type name to source file for resolving imported types
|
|
231
|
+
*/
|
|
232
|
+
export function vibeTypeToJsonSchema(
|
|
233
|
+
vibeType: string,
|
|
234
|
+
importedTypes?: Map<string, string>
|
|
235
|
+
): JsonSchema {
|
|
236
|
+
// Handle array types
|
|
237
|
+
if (vibeType.endsWith('[]')) {
|
|
238
|
+
const elementType = vibeType.slice(0, -2);
|
|
239
|
+
return {
|
|
240
|
+
type: 'array',
|
|
241
|
+
items: vibeTypeToJsonSchema(elementType, importedTypes),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Handle built-in Vibe types
|
|
246
|
+
switch (vibeType) {
|
|
247
|
+
case 'text':
|
|
248
|
+
case 'prompt':
|
|
249
|
+
return { type: 'string' };
|
|
250
|
+
case 'number':
|
|
251
|
+
return { type: 'number' };
|
|
252
|
+
case 'boolean':
|
|
253
|
+
return { type: 'boolean' };
|
|
254
|
+
case 'json':
|
|
255
|
+
return { type: 'object', additionalProperties: true };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Handle imported TS type
|
|
259
|
+
if (importedTypes?.has(vibeType)) {
|
|
260
|
+
const sourceFile = importedTypes.get(vibeType)!;
|
|
261
|
+
return extractTypeSchema(sourceFile, vibeType);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Unknown type - return generic object
|
|
265
|
+
return { type: 'object', additionalProperties: true };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Clear the schema cache (useful for testing or when files change).
|
|
270
|
+
*/
|
|
271
|
+
export function clearSchemaCache(): void {
|
|
272
|
+
schemaCache.clear();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* A type extractor that has already compiled a TypeScript program.
|
|
277
|
+
* Use this when you need to extract multiple types from the same file(s)
|
|
278
|
+
* to avoid recompiling for each extraction.
|
|
279
|
+
*/
|
|
280
|
+
export interface TypeExtractor {
|
|
281
|
+
/**
|
|
282
|
+
* Extract a type by name from any of the compiled source files.
|
|
283
|
+
*/
|
|
284
|
+
extract(typeName: string): JsonSchema;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get the type checker for advanced use cases.
|
|
288
|
+
*/
|
|
289
|
+
getChecker(): ts.TypeChecker;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Create a type extractor by compiling TypeScript files once.
|
|
294
|
+
* Much more efficient than calling extractTypeSchema multiple times.
|
|
295
|
+
*
|
|
296
|
+
* @param sourceFiles - Array of TypeScript file paths to compile
|
|
297
|
+
* @returns A TypeExtractor that can extract types without recompiling
|
|
298
|
+
*/
|
|
299
|
+
export function createTypeExtractor(sourceFiles: string[]): TypeExtractor {
|
|
300
|
+
const program = ts.createProgram(sourceFiles, {
|
|
301
|
+
target: ts.ScriptTarget.ESNext,
|
|
302
|
+
module: ts.ModuleKind.ESNext,
|
|
303
|
+
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
|
304
|
+
strict: true,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const checker = program.getTypeChecker();
|
|
308
|
+
const sources = sourceFiles.map((f) => program.getSourceFile(f)).filter(Boolean) as ts.SourceFile[];
|
|
309
|
+
|
|
310
|
+
// Build a map of all exported types across all source files
|
|
311
|
+
const typeSymbols = new Map<string, ts.Symbol>();
|
|
312
|
+
|
|
313
|
+
for (const source of sources) {
|
|
314
|
+
function visit(node: ts.Node) {
|
|
315
|
+
if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
|
|
316
|
+
const symbol = checker.getSymbolAtLocation(node.name);
|
|
317
|
+
if (symbol) {
|
|
318
|
+
typeSymbols.set(node.name.text, symbol);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
ts.forEachChild(node, visit);
|
|
322
|
+
}
|
|
323
|
+
visit(source);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
extract(typeName: string): JsonSchema {
|
|
328
|
+
const symbol = typeSymbols.get(typeName);
|
|
329
|
+
if (!symbol) {
|
|
330
|
+
throw new Error(`Type '${typeName}' not found in compiled sources`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const type = checker.getDeclaredTypeOfSymbol(symbol);
|
|
334
|
+
return typeToJsonSchema(type, checker, new Set());
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
getChecker(): ts.TypeChecker {
|
|
338
|
+
return checker;
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|