typespun-codegen 0.0.4

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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +95 -0
  3. package/dist/analyzer/analyze.d.ts +4 -0
  4. package/dist/analyzer/analyze.d.ts.map +1 -0
  5. package/dist/analyzer/analyze.js +414 -0
  6. package/dist/analyzer/annotations.d.ts +14 -0
  7. package/dist/analyzer/annotations.d.ts.map +1 -0
  8. package/dist/analyzer/annotations.js +102 -0
  9. package/dist/analyzer/default-expression.d.ts +10 -0
  10. package/dist/analyzer/default-expression.d.ts.map +1 -0
  11. package/dist/analyzer/default-expression.js +67 -0
  12. package/dist/analyzer/diagnostic.d.ts +5 -0
  13. package/dist/analyzer/diagnostic.d.ts.map +1 -0
  14. package/dist/analyzer/diagnostic.js +16 -0
  15. package/dist/analyzer/ir.d.ts +2 -0
  16. package/dist/analyzer/ir.d.ts.map +1 -0
  17. package/dist/analyzer/ir.js +1 -0
  18. package/dist/bin.d.ts +3 -0
  19. package/dist/bin.d.ts.map +1 -0
  20. package/dist/bin.js +3 -0
  21. package/dist/cli/diagnostics.d.ts +7 -0
  22. package/dist/cli/diagnostics.d.ts.map +1 -0
  23. package/dist/cli/diagnostics.js +26 -0
  24. package/dist/cli/init.d.ts +18 -0
  25. package/dist/cli/init.d.ts.map +1 -0
  26. package/dist/cli/init.js +417 -0
  27. package/dist/cli/main.d.ts +13 -0
  28. package/dist/cli/main.d.ts.map +1 -0
  29. package/dist/cli/main.js +157 -0
  30. package/dist/contracts.d.ts +33 -0
  31. package/dist/contracts.d.ts.map +1 -0
  32. package/dist/contracts.js +1 -0
  33. package/dist/emitter/emit.d.ts +13 -0
  34. package/dist/emitter/emit.d.ts.map +1 -0
  35. package/dist/emitter/emit.js +65 -0
  36. package/dist/emitter/fingerprint.d.ts +10 -0
  37. package/dist/emitter/fingerprint.d.ts.map +1 -0
  38. package/dist/emitter/fingerprint.js +28 -0
  39. package/dist/generate.d.ts +29 -0
  40. package/dist/generate.d.ts.map +1 -0
  41. package/dist/generate.js +267 -0
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +2 -0
  45. package/dist/project/config.d.ts +20 -0
  46. package/dist/project/config.d.ts.map +1 -0
  47. package/dist/project/config.js +140 -0
  48. package/dist/project/defaults.d.ts +22 -0
  49. package/dist/project/defaults.d.ts.map +1 -0
  50. package/dist/project/defaults.js +216 -0
  51. package/dist/project/discovery.d.ts +9 -0
  52. package/dist/project/discovery.d.ts.map +1 -0
  53. package/dist/project/discovery.js +66 -0
  54. package/package.json +54 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Typespun contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/omkar273/typespun/main/docs/assets/typespun-mark.svg" width="64" height="64" alt="Typespun threads converging into typed brackets">
3
+ </p>
4
+
5
+ # typespun-codegen
6
+
7
+ The TypeScript analyzer and `typespun` CLI that generate deterministic
8
+ configuration loaders.
9
+
10
+ [![npm](https://img.shields.io/npm/v/typespun-codegen.svg)](https://www.npmjs.com/package/typespun-codegen)
11
+ [![license](https://img.shields.io/npm/l/typespun-codegen.svg)](LICENSE)
12
+
13
+ Install this package as a development dependency beside the runtime:
14
+
15
+ ```sh
16
+ bun add typespun
17
+ bun add --dev typespun-codegen
18
+ ```
19
+
20
+ The npm package is named `typespun-codegen`; the executable it installs is named
21
+ `typespun`. The package root intentionally has no programmatic compiler API in
22
+ version 0.1.
23
+
24
+ ## Generate your first loader
25
+
26
+ Add local scripts:
27
+
28
+ ```json
29
+ {
30
+ "scripts": {
31
+ "config:generate": "typespun generate",
32
+ "config:check": "typespun check"
33
+ }
34
+ }
35
+ ```
36
+
37
+ Declare one exported root in `src/config.ts`:
38
+
39
+ ```ts
40
+ /** @typespun */
41
+ export interface AppConfig {
42
+ server: { host: string; port: number };
43
+ /** @secret */
44
+ token: string;
45
+ }
46
+ ```
47
+
48
+ Then generate:
49
+
50
+ ```sh
51
+ bun run config:generate
52
+ # Generated src/generated/typespun.ts.
53
+ ```
54
+
55
+ The generated module exports the declaration-backed `Config` type and a
56
+ `loadConfig()` function. Commit the file, then keep it current in CI:
57
+
58
+ ```sh
59
+ bun run config:check
60
+ ```
61
+
62
+ ## CLI
63
+
64
+ ```text
65
+ typespun init [--style interface|class] [--input <path>]
66
+ [--output <path>] [--env-prefix <prefix>]
67
+ typespun generate [--config <path>]
68
+ typespun check [--config <path>]
69
+ ```
70
+
71
+ - `init` creates missing schema/config files and package scripts without
72
+ overwriting application-owned output.
73
+ - `generate` statically analyzes TypeScript, validates optional JSON/YAML
74
+ defaults, and atomically writes canonical output when it changes.
75
+ - `check` performs the same analysis without writing and exits nonzero for
76
+ missing or stale output.
77
+
78
+ With no explicit configuration, the CLI expects exactly one conventional
79
+ `src/config.ts`, `.mts`, or `.cts` file and discovers at most one conventional
80
+ JSON/YAML defaults file. Use `typespun.json` for explicit paths, an environment
81
+ prefix, defaults policies, and secret-default policy.
82
+
83
+ ## Why a separate package?
84
+
85
+ Generation uses the TypeScript compiler and YAML parser during development.
86
+ Keeping them in `typespun-codegen` lets applications depend on the smaller
87
+ `typespun` runtime while CI and schema authors retain the compiler. Generated
88
+ output is deterministic for the same declaration, settings, defaults, and
89
+ generator version.
90
+
91
+ Read the [getting-started guide](https://github.com/omkar273/typespun/blob/main/docs/getting-started.md),
92
+ [CLI reference](https://github.com/omkar273/typespun/blob/main/docs/api/cli.md),
93
+ and [`typespun.json` reference](https://github.com/omkar273/typespun/blob/main/docs/reference/configuration.md).
94
+
95
+ MIT © Typespun contributors
@@ -0,0 +1,4 @@
1
+ import ts from 'typescript';
2
+ import type { AnalyzeResult } from '../contracts.js';
3
+ export declare function analyzeProgram(program: ts.Program, inputPath: string, envPrefix?: string): AnalyzeResult;
4
+ //# sourceMappingURL=analyze.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/analyzer/analyze.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,YAAY,CAAC;AAG5B,OAAO,KAAK,EACV,aAAa,EAId,MAAM,iBAAiB,CAAC;AASzB,wBAAgB,cAAc,CAC5B,OAAO,EAAE,EAAE,CAAC,OAAO,EACnB,SAAS,EAAE,MAAM,EACjB,SAAS,SAAK,GACb,aAAa,CAqmBf"}
@@ -0,0 +1,414 @@
1
+ import path from 'node:path';
2
+ import ts from 'typescript';
3
+ import { validateTypedValue } from 'typespun/generated';
4
+ import { readAnnotations, typespunSymbols } from './annotations.js';
5
+ import { locationOf, sortDiagnostics } from './diagnostic.js';
6
+ import { evaluateDefault } from './default-expression.js';
7
+ function isJavaScriptSource(node) {
8
+ return /\.(?:[cm]?js|jsx)$/i.test(node.getSourceFile().fileName);
9
+ }
10
+ export function analyzeProgram(program, inputPath, envPrefix = '') {
11
+ const checker = program.getTypeChecker();
12
+ const diagnostics = [];
13
+ const fields = [];
14
+ const report = (code, message, node) => diagnostics.push({ code, message, location: locationOf(node) });
15
+ const symbols = typespunSymbols(program);
16
+ const annotationsFor = (node) => readAnnotations(node, report, checker, symbols);
17
+ const source = program.getSourceFile(path.resolve(inputPath));
18
+ if (!source)
19
+ return {
20
+ inputPath,
21
+ fields,
22
+ diagnostics: [
23
+ {
24
+ code: 'input_not_found',
25
+ message: 'Schema input is not in the TypeScript program.',
26
+ location: { file: inputPath, line: 1, column: 1 },
27
+ },
28
+ ],
29
+ };
30
+ if (isJavaScriptSource(source)) {
31
+ report('javascript_schema', 'JavaScript schema declarations are not supported.', source);
32
+ return { inputPath, fields, diagnostics: sortDiagnostics(diagnostics) };
33
+ }
34
+ const moduleSymbol = checker.getSymbolAtLocation(source);
35
+ const moduleExports = moduleSymbol
36
+ ? checker.getExportsOfModule(moduleSymbol)
37
+ : [];
38
+ const exportedSymbols = new Set(moduleExports.map((symbol) => symbol.flags & ts.SymbolFlags.Alias
39
+ ? checker.getAliasedSymbol(symbol)
40
+ : symbol));
41
+ const roots = source.statements.filter((node) => (ts.isInterfaceDeclaration(node) || ts.isClassDeclaration(node)) &&
42
+ !!node.name &&
43
+ exportedSymbols.has(checker.getSymbolAtLocation(node.name)) &&
44
+ annotationsFor(node).root);
45
+ if (roots.length !== 1) {
46
+ report('root_count', 'The input must export exactly one marked configuration root.', source);
47
+ return { inputPath, fields, diagnostics };
48
+ }
49
+ const root = roots[0];
50
+ const rootSymbol = checker.getSymbolAtLocation(root.name);
51
+ const exportNames = moduleExports
52
+ .filter((symbol) => (symbol.flags & ts.SymbolFlags.Alias
53
+ ? checker.getAliasedSymbol(symbol)
54
+ : symbol) === rootSymbol)
55
+ .map((symbol) => symbol.name)
56
+ .sort();
57
+ const exportName = exportNames.find((name) => name === root.name.text) ??
58
+ exportNames.find((name) => name !== 'default') ??
59
+ 'default';
60
+ const rootExport = exportName === 'default'
61
+ ? { kind: 'default' }
62
+ : { kind: 'named', name: exportName };
63
+ if (root.typeParameters?.length) {
64
+ report('generic_schema', 'Configuration roots cannot have type parameters.', root.name ?? root);
65
+ return { inputPath, fields, diagnostics: sortDiagnostics(diagnostics) };
66
+ }
67
+ const rootType = checker.getTypeAtLocation(root);
68
+ if ((rootType.symbol?.declarations?.length ?? 0) > 1) {
69
+ report('declaration_merging', 'Configuration roots cannot use declaration merging.', root.name ?? root);
70
+ return { inputPath, fields, diagnostics: sortDiagnostics(diagnostics) };
71
+ }
72
+ for (const index of checker.getIndexInfosOfType(rootType)) {
73
+ report('unsupported_type', 'Index signatures are not supported.', index.declaration ?? root);
74
+ }
75
+ for (const signature of [
76
+ ...rootType.getCallSignatures(),
77
+ ...rootType.getConstructSignatures(),
78
+ ]) {
79
+ report('unsupported_type', 'Call and construct signatures are not supported.', signature.declaration ?? root);
80
+ }
81
+ const invalidMembers = new Set();
82
+ if (ts.isClassDeclaration(root)) {
83
+ for (const heritage of root.heritageClauses ?? [])
84
+ report('class_inheritance', 'Configuration classes cannot inherit other classes.', heritage);
85
+ for (const member of root.members) {
86
+ if (!ts.isPropertyDeclaration(member) ||
87
+ ts.isPrivateIdentifier(member.name) ||
88
+ member.modifiers?.some((modifier) => [
89
+ ts.SyntaxKind.StaticKeyword,
90
+ ts.SyntaxKind.PrivateKeyword,
91
+ ts.SyntaxKind.ProtectedKeyword,
92
+ ].includes(modifier.kind))) {
93
+ report('invalid_class_member', 'Configuration classes allow only public instance data properties.', member);
94
+ invalidMembers.add(member);
95
+ }
96
+ }
97
+ }
98
+ function containsIntersection(node, seen = new Set()) {
99
+ if (!node)
100
+ return false;
101
+ if (ts.isIntersectionTypeNode(node))
102
+ return true;
103
+ if (ts.isParenthesizedTypeNode(node))
104
+ return containsIntersection(node.type, seen);
105
+ if (ts.isTypeReferenceNode(node) ||
106
+ ts.isExpressionWithTypeArguments(node)) {
107
+ let symbol = checker.getSymbolAtLocation(ts.isTypeReferenceNode(node) ? node.typeName : node.expression);
108
+ if (symbol?.flags && symbol.flags & ts.SymbolFlags.Alias)
109
+ symbol = checker.getAliasedSymbol(symbol);
110
+ if (!symbol || seen.has(symbol))
111
+ return false;
112
+ seen.add(symbol);
113
+ return (symbol.declarations?.some((declaration) => ts.isTypeAliasDeclaration(declaration) &&
114
+ containsIntersection(declaration.type, seen)) ?? false);
115
+ }
116
+ return false;
117
+ }
118
+ const validatedHeritage = new Set();
119
+ function validateHeritage(type) {
120
+ const javaScriptDeclaration = type.symbol?.declarations?.find(isJavaScriptSource);
121
+ if (javaScriptDeclaration) {
122
+ report('javascript_schema', 'JavaScript schema declarations are not supported.', javaScriptDeclaration);
123
+ return;
124
+ }
125
+ for (const declaration of type.symbol?.declarations ?? []) {
126
+ if (!ts.isInterfaceDeclaration(declaration) ||
127
+ validatedHeritage.has(declaration))
128
+ continue;
129
+ validatedHeritage.add(declaration);
130
+ for (const clause of declaration.heritageClauses ?? []) {
131
+ for (const base of clause.types) {
132
+ if (containsIntersection(base)) {
133
+ report('unsupported_type', 'Intersections are not supported in heritage.', base);
134
+ }
135
+ else {
136
+ validateHeritage(checker.getTypeAtLocation(base));
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+ validateHeritage(rootType);
143
+ function kindOf(type) {
144
+ if (type.flags & ts.TypeFlags.String)
145
+ return { type: 'string' };
146
+ if (type.flags & ts.TypeFlags.Number)
147
+ return { type: 'number' };
148
+ if (type.flags & ts.TypeFlags.Boolean)
149
+ return { type: 'boolean' };
150
+ if (type.flags & ts.TypeFlags.StringLiteral)
151
+ return { type: 'enum', values: [type.value] };
152
+ if (type.isUnion() &&
153
+ type.types.every((item) => item.flags & ts.TypeFlags.StringLiteral)) {
154
+ return {
155
+ type: 'enum',
156
+ values: type.types.map((item) => item.value),
157
+ };
158
+ }
159
+ if (checker.isArrayType(type)) {
160
+ const element = checker.getTypeArguments(type)[0];
161
+ const kind = element && kindOf(element);
162
+ if (kind && ['string', 'number', 'boolean'].includes(kind.type))
163
+ return {
164
+ type: 'array',
165
+ element: kind.type,
166
+ };
167
+ }
168
+ return undefined;
169
+ }
170
+ const activeTypes = new Set();
171
+ const activeProperties = new Set();
172
+ function hasConditionalType(node, seen = new Set()) {
173
+ if (ts.isConditionalTypeNode(node))
174
+ return true;
175
+ if (ts.isTypeReferenceNode(node)) {
176
+ let symbol = checker.getSymbolAtLocation(node.typeName);
177
+ if (symbol?.flags && symbol.flags & ts.SymbolFlags.Alias)
178
+ symbol = checker.getAliasedSymbol(symbol);
179
+ if (symbol && !seen.has(symbol)) {
180
+ seen.add(symbol);
181
+ if (symbol.declarations?.some((declaration) => ts.isTypeAliasDeclaration(declaration) &&
182
+ hasConditionalType(declaration.type, seen)))
183
+ return true;
184
+ }
185
+ }
186
+ return (ts.forEachChild(node, (child) => hasConditionalType(child, seen) || undefined) ?? false);
187
+ }
188
+ function canChangeShape(node) {
189
+ if (node.type && hasConditionalType(node.type))
190
+ return true;
191
+ for (let parent = node.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) {
192
+ if (ts.isConditionalTypeNode(parent))
193
+ return true;
194
+ }
195
+ return false;
196
+ }
197
+ const envNames = new Set();
198
+ const defaultsPaths = new Set();
199
+ function mergeInlineDefaults(lower, upper) {
200
+ if (!lower ||
201
+ !upper ||
202
+ typeof lower !== 'object' ||
203
+ typeof upper !== 'object' ||
204
+ Array.isArray(lower) ||
205
+ Array.isArray(upper))
206
+ return upper;
207
+ const lowerObject = lower;
208
+ const upperObject = upper;
209
+ return Object.fromEntries([
210
+ ...new Set([...Object.keys(lowerObject), ...Object.keys(upperObject)]),
211
+ ].map((key) => [
212
+ key,
213
+ Object.hasOwn(upperObject, key)
214
+ ? mergeInlineDefaults(lowerObject[key], upperObject[key])
215
+ : lowerObject[key],
216
+ ]));
217
+ }
218
+ function isObjectShape(type) {
219
+ return (!!(type.flags & ts.TypeFlags.Object) &&
220
+ !checker.isArrayType(type) &&
221
+ !checker.isTupleType(type) &&
222
+ checker.getIndexInfosOfType(type).length === 0 &&
223
+ type.getCallSignatures().length === 0 &&
224
+ type.getConstructSignatures().length === 0 &&
225
+ !(type.symbol?.declarations?.some((declaration) => program.isSourceFileDefaultLibrary(declaration.getSourceFile())) ?? false));
226
+ }
227
+ function visit(type, propertyPath, defaultsPath, optionalParents, secret, parentDefault) {
228
+ activeTypes.add(type);
229
+ for (const property of checker.getPropertiesOfType(type)) {
230
+ const node = property.valueDeclaration ?? property.declarations?.[0];
231
+ if (!node || invalidMembers.has(node))
232
+ continue;
233
+ if (isJavaScriptSource(node)) {
234
+ report('javascript_schema', 'JavaScript schema declarations are not supported.', node);
235
+ continue;
236
+ }
237
+ const annotations = annotationsFor(node);
238
+ if (annotations.ignore)
239
+ continue;
240
+ const name = property.getName();
241
+ if (!(ts.isPropertySignature(node) || ts.isPropertyDeclaration(node))) {
242
+ report('unsupported_type', 'Only data properties are supported.', node);
243
+ continue;
244
+ }
245
+ if (ts.isPropertyDeclaration(node) &&
246
+ (ts.isPrivateIdentifier(node.name) ||
247
+ !!(ts.getCombinedModifierFlags(node) &
248
+ (ts.ModifierFlags.Private |
249
+ ts.ModifierFlags.Protected |
250
+ ts.ModifierFlags.Static)))) {
251
+ report('invalid_class_member', 'Configuration classes allow only public instance data properties.', node);
252
+ continue;
253
+ }
254
+ // Generic types can keep producing fresh identities forever, including
255
+ // inheritance that substitutes a fresh type into a type-parameter field.
256
+ // Bound that undecidable expansion separately from proven type cycles.
257
+ if (activeProperties.has(node) && propertyPath.length >= 128) {
258
+ report('schema_too_deep', 'Generic type expansion exceeds the supported analysis depth of 128.', node);
259
+ continue;
260
+ }
261
+ if (!ts.isIdentifier(node.name) ||
262
+ ['__proto__', 'constructor', 'prototype'].includes(name)) {
263
+ report('invalid_property', 'Properties must use safe identifier names.', node);
264
+ continue;
265
+ }
266
+ if (containsIntersection(node.type)) {
267
+ report('unsupported_type', 'Intersections are not supported.', node);
268
+ continue;
269
+ }
270
+ if (annotations.key !== undefined &&
271
+ (!annotations.key ||
272
+ annotations.key.includes('.') ||
273
+ ['__proto__', 'constructor', 'prototype'].includes(annotations.key))) {
274
+ report('invalid_annotation', 'Key must be one nonempty, safe path segment.', node);
275
+ continue;
276
+ }
277
+ if (annotations.env !== undefined &&
278
+ !/^[A-Za-z_][A-Za-z0-9_]*$/.test(annotations.env)) {
279
+ report('invalid_annotation', 'Env must be a valid complete environment name.', node);
280
+ continue;
281
+ }
282
+ if (ts.isPropertyDeclaration(node) && node.initializer) {
283
+ const result = evaluateDefault(node.initializer, checker);
284
+ if (!result.ok) {
285
+ report('invalid_default', 'Initializers must be supported static values.', node);
286
+ continue;
287
+ }
288
+ if (!annotations.hasDefault) {
289
+ annotations.hasDefault = true;
290
+ annotations.defaultValue = result.value;
291
+ }
292
+ }
293
+ if (parentDefault &&
294
+ typeof parentDefault === 'object' &&
295
+ Object.hasOwn(parentDefault, name)) {
296
+ annotations.hasDefault = true;
297
+ annotations.defaultValue = mergeInlineDefaults(annotations.defaultValue, parentDefault[name]);
298
+ }
299
+ const currentPath = [...propertyPath, name];
300
+ const currentDefaultsPath = [...defaultsPath, annotations.key ?? name];
301
+ const optional = !!(property.flags & ts.SymbolFlags.Optional);
302
+ let propertyType = checker.getTypeOfSymbolAtLocation(property, node);
303
+ if (propertyType.isUnion() &&
304
+ propertyType.types.some((item) => item.flags & ts.TypeFlags.Null)) {
305
+ report('unsupported_type', 'Nullable unions are not supported.', node);
306
+ continue;
307
+ }
308
+ if (optional)
309
+ propertyType = checker.getNonNullableType(propertyType);
310
+ const javaScriptDeclaration = propertyType.symbol?.declarations?.find(isJavaScriptSource);
311
+ if (javaScriptDeclaration) {
312
+ report('javascript_schema', 'JavaScript schema declarations are not supported.', javaScriptDeclaration);
313
+ continue;
314
+ }
315
+ const kind = kindOf(propertyType);
316
+ const defaultsKey = JSON.stringify(currentDefaultsPath);
317
+ if (defaultsPaths.has(defaultsKey)) {
318
+ report('duplicate_defaults_path', 'Multiple fields resolve to conflicting defaults paths.', node);
319
+ }
320
+ defaultsPaths.add(defaultsKey);
321
+ if (!kind && isObjectShape(propertyType)) {
322
+ validateHeritage(propertyType);
323
+ if (annotations.env !== undefined)
324
+ report('object_env', 'Env annotations apply only to leaves.', node);
325
+ const resolvesTypeParameter = node.type &&
326
+ !!(checker.getTypeFromTypeNode(node.type).flags &
327
+ ts.TypeFlags.TypeParameter);
328
+ if (activeTypes.has(propertyType) ||
329
+ (activeProperties.has(node) &&
330
+ !resolvesTypeParameter &&
331
+ !canChangeShape(node))) {
332
+ report('recursive_type', 'Circular configuration shapes are not supported.', node);
333
+ continue;
334
+ }
335
+ if (checker.getPropertiesOfType(propertyType).length === 0) {
336
+ report('unsupported_type', 'Empty object configuration shapes are not supported.', node);
337
+ continue;
338
+ }
339
+ if (annotations.hasDefault &&
340
+ (!annotations.defaultValue ||
341
+ typeof annotations.defaultValue !== 'object' ||
342
+ Array.isArray(annotations.defaultValue))) {
343
+ report('invalid_default', 'Object fields require object defaults.', node);
344
+ continue;
345
+ }
346
+ if (annotations.hasDefault &&
347
+ Object.keys(annotations.defaultValue).some((key) => ['__proto__', 'constructor', 'prototype'].includes(key) ||
348
+ !checker.getPropertyOfType(propertyType, key))) {
349
+ report('invalid_default', 'Object defaults contain unknown or unsafe properties.', node);
350
+ continue;
351
+ }
352
+ const alreadyActive = activeProperties.has(node);
353
+ activeProperties.add(node);
354
+ const fieldsBeforeVisit = fields.length;
355
+ const diagnosticsBeforeVisit = diagnostics.length;
356
+ visit(propertyType, currentPath, currentDefaultsPath, optional ? [...optionalParents, currentPath] : optionalParents, secret || annotations.secret, annotations.defaultValue);
357
+ if (!alreadyActive)
358
+ activeProperties.delete(node);
359
+ if (fields.length === fieldsBeforeVisit &&
360
+ diagnostics.length === diagnosticsBeforeVisit) {
361
+ report('unsupported_type', 'Configuration object shapes must contain at least one included leaf.', node);
362
+ }
363
+ continue;
364
+ }
365
+ if (!kind) {
366
+ report('unsupported_type', 'This field type is not supported.', node);
367
+ continue;
368
+ }
369
+ if (annotations.hasDefault &&
370
+ validateTypedValue(kind, annotations.defaultValue) !== undefined) {
371
+ report('invalid_default', 'The inline default does not match its field type.', node);
372
+ continue;
373
+ }
374
+ const snake = currentPath
375
+ .map((part) => part
376
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
377
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
378
+ .toUpperCase())
379
+ .join('_');
380
+ const prefix = envPrefix.replace(/_+$/, '');
381
+ const envName = annotations.env ?? (prefix ? `${prefix}_${snake}` : snake);
382
+ if (envNames.has(envName))
383
+ report('duplicate_env', 'Multiple fields resolve to the same environment name.', node);
384
+ envNames.add(envName);
385
+ fields.push({
386
+ propertyPath: currentPath,
387
+ defaultsPath: currentDefaultsPath,
388
+ envName,
389
+ kind,
390
+ required: !optional,
391
+ secret: secret || annotations.secret,
392
+ hasDefault: annotations.hasDefault,
393
+ ...(annotations.hasDefault
394
+ ? { defaultValue: annotations.defaultValue }
395
+ : {}),
396
+ optionalParents,
397
+ location: locationOf(node),
398
+ });
399
+ }
400
+ activeTypes.delete(type);
401
+ }
402
+ visit(rootType, [], [], [], false);
403
+ if (fields.length === 0 && diagnostics.length === 0) {
404
+ report('unsupported_type', 'Configuration roots must contain at least one included leaf.', root);
405
+ }
406
+ return {
407
+ ...(diagnostics.length === 0 && root.name
408
+ ? { rootName: root.name.text, rootExport }
409
+ : {}),
410
+ inputPath,
411
+ fields,
412
+ diagnostics: sortDiagnostics(diagnostics),
413
+ };
414
+ }
@@ -0,0 +1,14 @@
1
+ import ts from 'typescript';
2
+ export interface Annotations {
3
+ root: boolean;
4
+ ignore: boolean;
5
+ secret: boolean;
6
+ key?: string;
7
+ env?: string;
8
+ hasDefault: boolean;
9
+ defaultValue?: unknown;
10
+ }
11
+ export type Report = (code: string, message: string, node: ts.Node) => void;
12
+ export declare function typespunSymbols(program: ts.Program): Map<ts.Symbol, string>;
13
+ export declare function readAnnotations(node: ts.Node, report: Report, checker?: ts.TypeChecker, symbols?: Map<ts.Symbol, string>): Annotations;
14
+ //# sourceMappingURL=annotations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"annotations.d.ts","sourceRoot":"","sources":["../../src/analyzer/annotations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAG5B,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,IAAI,CAAC;AAE5E,wBAAgB,eAAe,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,GAAG,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CA+B3E;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,EAAE,CAAC,WAAW,EACxB,OAAO,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,WAAW,CA+Eb"}
@@ -0,0 +1,102 @@
1
+ import ts from 'typescript';
2
+ import { evaluateDefault } from './default-expression.js';
3
+ export function typespunSymbols(program) {
4
+ const checker = program.getTypeChecker();
5
+ const symbols = new Map();
6
+ for (const source of program.getSourceFiles()) {
7
+ for (const statement of source.statements) {
8
+ if (!(ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)))
9
+ continue;
10
+ const specifier = statement.moduleSpecifier;
11
+ if (!specifier ||
12
+ !ts.isStringLiteral(specifier) ||
13
+ specifier.text !== 'typespun')
14
+ continue;
15
+ const moduleSymbol = checker.getSymbolAtLocation(specifier);
16
+ if (!moduleSymbol)
17
+ continue;
18
+ for (const symbol of checker.getExportsOfModule(moduleSymbol)) {
19
+ symbols.set(symbol.flags & ts.SymbolFlags.Alias
20
+ ? checker.getAliasedSymbol(symbol)
21
+ : symbol, symbol.name);
22
+ }
23
+ }
24
+ }
25
+ return symbols;
26
+ }
27
+ export function readAnnotations(node, report, checker, symbols) {
28
+ const result = {
29
+ root: false,
30
+ ignore: false,
31
+ secret: false,
32
+ hasDefault: false,
33
+ };
34
+ for (const tag of ts.getJSDocTags(node)) {
35
+ const text = ts.getTextOfJSDocComment(tag.comment)?.trim() ?? '';
36
+ switch (tag.tagName.text) {
37
+ case 'typespun':
38
+ result.root = true;
39
+ break;
40
+ case 'ignore':
41
+ result.ignore = true;
42
+ break;
43
+ case 'secret':
44
+ result.secret = true;
45
+ break;
46
+ case 'key':
47
+ result.key = text;
48
+ break;
49
+ case 'env':
50
+ result.env = text;
51
+ break;
52
+ case 'default':
53
+ try {
54
+ result.defaultValue = JSON.parse(text);
55
+ result.hasDefault = true;
56
+ }
57
+ catch {
58
+ report('invalid_default', 'Inline defaults must use valid JSON.', tag);
59
+ }
60
+ break;
61
+ }
62
+ }
63
+ if (checker && symbols && ts.canHaveDecorators(node)) {
64
+ for (const decorator of ts.getDecorators(node) ?? []) {
65
+ if (!ts.isCallExpression(decorator.expression))
66
+ continue;
67
+ const call = decorator.expression;
68
+ let symbol = checker.getSymbolAtLocation(call.expression);
69
+ if (symbol?.flags && symbol.flags & ts.SymbolFlags.Alias)
70
+ symbol = checker.getAliasedSymbol(symbol);
71
+ const name = symbol && symbols.get(symbol);
72
+ if (!name)
73
+ continue;
74
+ if (name === 'Config')
75
+ result.root = true;
76
+ if (name === 'Ignore')
77
+ result.ignore = true;
78
+ if (name === 'Secret')
79
+ result.secret = true;
80
+ if (name === 'Default' || name === 'Key' || name === 'Env') {
81
+ const value = call.arguments.length === 1 && call.arguments[0]
82
+ ? evaluateDefault(call.arguments[0], checker)
83
+ : { ok: false };
84
+ if (!value.ok) {
85
+ report(name === 'Default' ? 'invalid_default' : 'invalid_annotation', 'Annotation arguments must be supported static values.', decorator);
86
+ continue;
87
+ }
88
+ if (name === 'Default') {
89
+ result.hasDefault = true;
90
+ result.defaultValue = value.value;
91
+ }
92
+ else if (typeof value.value !== 'string')
93
+ report('invalid_annotation', 'Key and Env require a string.', decorator);
94
+ else if (name === 'Key')
95
+ result.key = value.value;
96
+ else
97
+ result.env = value.value;
98
+ }
99
+ }
100
+ }
101
+ return result;
102
+ }
@@ -0,0 +1,10 @@
1
+ import ts from 'typescript';
2
+ export type StaticValue = {
3
+ readonly ok: true;
4
+ readonly value: unknown;
5
+ } | {
6
+ readonly ok: false;
7
+ };
8
+ /** Reads syntax only. No schema expressions or user modules are executed. */
9
+ export declare function evaluateDefault(expression: ts.Expression, checker: ts.TypeChecker): StaticValue;
10
+ //# sourceMappingURL=default-expression.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-expression.d.ts","sourceRoot":"","sources":["../../src/analyzer/default-expression.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC9C;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAA;CAAE,CAAC;AAG3B,6EAA6E;AAC7E,wBAAgB,eAAe,CAC7B,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,OAAO,EAAE,EAAE,CAAC,WAAW,GACtB,WAAW,CAyEb"}