@formspec/build 0.1.0-alpha.26 → 0.1.0-alpha.28

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/dist/index.cjs CHANGED
@@ -36,7 +36,10 @@ __export(index_exports, {
36
36
  generateJsonSchema: () => generateJsonSchema,
37
37
  generateSchemas: () => generateSchemas,
38
38
  generateSchemasFromClass: () => generateSchemasFromClass,
39
+ generateSchemasFromProgram: () => generateSchemasFromProgram,
39
40
  generateUiSchema: () => generateUiSchema,
41
+ jsonSchema7Schema: () => jsonSchema7Schema,
42
+ uiSchemaSchema: () => uiSchema,
40
43
  writeSchemas: () => writeSchemas
41
44
  });
42
45
  module.exports = __toCommonJS(index_exports);
@@ -1184,12 +1187,78 @@ function createExtensionRegistry(extensions) {
1184
1187
  };
1185
1188
  }
1186
1189
 
1190
+ // src/json-schema/schema.ts
1191
+ var import_zod3 = require("zod");
1192
+ var jsonSchemaTypeSchema = import_zod3.z.enum([
1193
+ "string",
1194
+ "number",
1195
+ "integer",
1196
+ "boolean",
1197
+ "object",
1198
+ "array",
1199
+ "null"
1200
+ ]);
1201
+ var jsonSchema7Schema = import_zod3.z.lazy(
1202
+ () => import_zod3.z.object({
1203
+ $schema: import_zod3.z.string().optional(),
1204
+ $id: import_zod3.z.string().optional(),
1205
+ $ref: import_zod3.z.string().optional(),
1206
+ // Metadata
1207
+ title: import_zod3.z.string().optional(),
1208
+ description: import_zod3.z.string().optional(),
1209
+ deprecated: import_zod3.z.boolean().optional(),
1210
+ // Type
1211
+ type: import_zod3.z.union([jsonSchemaTypeSchema, import_zod3.z.array(jsonSchemaTypeSchema)]).optional(),
1212
+ // String validation
1213
+ minLength: import_zod3.z.number().optional(),
1214
+ maxLength: import_zod3.z.number().optional(),
1215
+ pattern: import_zod3.z.string().optional(),
1216
+ // Number validation
1217
+ minimum: import_zod3.z.number().optional(),
1218
+ maximum: import_zod3.z.number().optional(),
1219
+ exclusiveMinimum: import_zod3.z.number().optional(),
1220
+ exclusiveMaximum: import_zod3.z.number().optional(),
1221
+ // Enum
1222
+ enum: import_zod3.z.array(import_zod3.z.union([import_zod3.z.string(), import_zod3.z.number(), import_zod3.z.boolean(), import_zod3.z.null()])).readonly().optional(),
1223
+ const: import_zod3.z.union([import_zod3.z.string(), import_zod3.z.number(), import_zod3.z.boolean(), import_zod3.z.null()]).optional(),
1224
+ // Object
1225
+ properties: import_zod3.z.record(import_zod3.z.string(), jsonSchema7Schema).optional(),
1226
+ required: import_zod3.z.array(import_zod3.z.string()).optional(),
1227
+ additionalProperties: import_zod3.z.union([import_zod3.z.boolean(), jsonSchema7Schema]).optional(),
1228
+ // Array
1229
+ items: import_zod3.z.union([jsonSchema7Schema, import_zod3.z.array(jsonSchema7Schema)]).optional(),
1230
+ minItems: import_zod3.z.number().optional(),
1231
+ maxItems: import_zod3.z.number().optional(),
1232
+ // Composition
1233
+ allOf: import_zod3.z.array(jsonSchema7Schema).optional(),
1234
+ anyOf: import_zod3.z.array(jsonSchema7Schema).optional(),
1235
+ oneOf: import_zod3.z.array(jsonSchema7Schema).optional(),
1236
+ not: jsonSchema7Schema.optional(),
1237
+ // Conditional
1238
+ if: jsonSchema7Schema.optional(),
1239
+ then: jsonSchema7Schema.optional(),
1240
+ else: jsonSchema7Schema.optional(),
1241
+ // Format
1242
+ format: import_zod3.z.string().optional(),
1243
+ // Default
1244
+ default: import_zod3.z.unknown().optional(),
1245
+ // FormSpec extensions
1246
+ "x-formspec-source": import_zod3.z.string().optional(),
1247
+ "x-formspec-params": import_zod3.z.array(import_zod3.z.string()).readonly().optional(),
1248
+ "x-formspec-schemaSource": import_zod3.z.string().optional()
1249
+ }).passthrough()
1250
+ );
1251
+
1252
+ // src/generators/class-schema.ts
1253
+ var ts5 = require("typescript");
1254
+
1187
1255
  // src/analyzer/program.ts
1188
1256
  var ts4 = __toESM(require("typescript"), 1);
1189
1257
  var path = __toESM(require("path"), 1);
1190
1258
 
1191
1259
  // src/analyzer/class-analyzer.ts
1192
1260
  var ts3 = __toESM(require("typescript"), 1);
1261
+ var import_internal2 = require("@formspec/analysis/internal");
1193
1262
 
1194
1263
  // src/analyzer/jsdoc-constraints.ts
1195
1264
  var ts2 = __toESM(require("typescript"), 1);
@@ -2112,9 +2181,17 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
2112
2181
  }
2113
2182
  }
2114
2183
  }
2184
+ const specializedFields = applyDeclarationDiscriminatorToFields(
2185
+ fields,
2186
+ classDecl,
2187
+ classType,
2188
+ checker,
2189
+ file,
2190
+ diagnostics
2191
+ );
2115
2192
  return {
2116
2193
  name,
2117
- fields,
2194
+ fields: specializedFields,
2118
2195
  fieldLayouts,
2119
2196
  typeRegistry,
2120
2197
  ...annotations.length > 0 && { annotations },
@@ -2154,10 +2231,18 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
2154
2231
  }
2155
2232
  }
2156
2233
  }
2157
- const fieldLayouts = fields.map(() => ({}));
2234
+ const specializedFields = applyDeclarationDiscriminatorToFields(
2235
+ fields,
2236
+ interfaceDecl,
2237
+ interfaceType,
2238
+ checker,
2239
+ file,
2240
+ diagnostics
2241
+ );
2242
+ const fieldLayouts = specializedFields.map(() => ({}));
2158
2243
  return {
2159
2244
  name,
2160
- fields,
2245
+ fields: specializedFields,
2161
2246
  fieldLayouts,
2162
2247
  typeRegistry,
2163
2248
  ...annotations.length > 0 && { annotations },
@@ -2206,12 +2291,20 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
2206
2291
  }
2207
2292
  }
2208
2293
  }
2294
+ const specializedFields = applyDeclarationDiscriminatorToFields(
2295
+ fields,
2296
+ typeAlias,
2297
+ aliasType,
2298
+ checker,
2299
+ file,
2300
+ diagnostics
2301
+ );
2209
2302
  return {
2210
2303
  ok: true,
2211
2304
  analysis: {
2212
2305
  name,
2213
- fields,
2214
- fieldLayouts: fields.map(() => ({})),
2306
+ fields: specializedFields,
2307
+ fieldLayouts: specializedFields.map(() => ({})),
2215
2308
  typeRegistry,
2216
2309
  ...annotations.length > 0 && { annotations },
2217
2310
  ...diagnostics.length > 0 && { diagnostics },
@@ -2220,6 +2313,396 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
2220
2313
  }
2221
2314
  };
2222
2315
  }
2316
+ function makeAnalysisDiagnostic(code, message, primaryLocation, relatedLocations = []) {
2317
+ return {
2318
+ code,
2319
+ message,
2320
+ severity: "error",
2321
+ primaryLocation,
2322
+ relatedLocations
2323
+ };
2324
+ }
2325
+ function getLeadingParsedTags(node) {
2326
+ const sourceFile = node.getSourceFile();
2327
+ const sourceText = sourceFile.getFullText();
2328
+ const commentRanges = ts3.getLeadingCommentRanges(sourceText, node.getFullStart());
2329
+ if (commentRanges === void 0) {
2330
+ return [];
2331
+ }
2332
+ const parsedTags = [];
2333
+ for (const range of commentRanges) {
2334
+ if (range.kind !== ts3.SyntaxKind.MultiLineCommentTrivia) {
2335
+ continue;
2336
+ }
2337
+ const commentText = sourceText.slice(range.pos, range.end);
2338
+ if (!commentText.startsWith("/**")) {
2339
+ continue;
2340
+ }
2341
+ parsedTags.push(...(0, import_internal2.parseCommentBlock)(commentText, { offset: range.pos }).tags);
2342
+ }
2343
+ return parsedTags;
2344
+ }
2345
+ function findDiscriminatorProperty(node, fieldName) {
2346
+ if (ts3.isClassDeclaration(node)) {
2347
+ for (const member of node.members) {
2348
+ if (ts3.isPropertyDeclaration(member) && ts3.isIdentifier(member.name) && member.name.text === fieldName) {
2349
+ return member;
2350
+ }
2351
+ }
2352
+ return null;
2353
+ }
2354
+ if (ts3.isInterfaceDeclaration(node)) {
2355
+ for (const member of node.members) {
2356
+ if (ts3.isPropertySignature(member) && ts3.isIdentifier(member.name) && member.name.text === fieldName) {
2357
+ return member;
2358
+ }
2359
+ }
2360
+ return null;
2361
+ }
2362
+ if (ts3.isTypeLiteralNode(node.type)) {
2363
+ for (const member of node.type.members) {
2364
+ if (ts3.isPropertySignature(member) && ts3.isIdentifier(member.name) && member.name.text === fieldName) {
2365
+ return member;
2366
+ }
2367
+ }
2368
+ }
2369
+ return null;
2370
+ }
2371
+ function isLocalTypeParameterName(node, typeParameterName) {
2372
+ return node.typeParameters?.some((typeParameter) => typeParameter.name.text === typeParameterName) ?? false;
2373
+ }
2374
+ function isNullishSemanticType(type) {
2375
+ if (type.flags & (ts3.TypeFlags.Null | ts3.TypeFlags.Undefined | ts3.TypeFlags.Void | ts3.TypeFlags.Unknown | ts3.TypeFlags.Any)) {
2376
+ return true;
2377
+ }
2378
+ return type.isUnion() && type.types.some((member) => isNullishSemanticType(member));
2379
+ }
2380
+ function isStringLikeSemanticType(type) {
2381
+ if (type.flags & ts3.TypeFlags.StringLike) {
2382
+ return true;
2383
+ }
2384
+ if (type.isUnion()) {
2385
+ return type.types.length > 0 && type.types.every((member) => isStringLikeSemanticType(member));
2386
+ }
2387
+ return false;
2388
+ }
2389
+ function extractDiscriminatorDirective(node, file, diagnostics) {
2390
+ const discriminatorTags = getLeadingParsedTags(node).filter(
2391
+ (tag) => tag.normalizedTagName === "discriminator"
2392
+ );
2393
+ if (discriminatorTags.length === 0) {
2394
+ return null;
2395
+ }
2396
+ const [firstTag, ...duplicateTags] = discriminatorTags;
2397
+ for (const _duplicateTag of duplicateTags) {
2398
+ diagnostics.push(
2399
+ makeAnalysisDiagnostic(
2400
+ "DUPLICATE_TAG",
2401
+ 'Duplicate "@discriminator" tag. Only one discriminator declaration is allowed per declaration.',
2402
+ provenanceForNode(node, file)
2403
+ )
2404
+ );
2405
+ }
2406
+ if (firstTag === void 0) {
2407
+ return null;
2408
+ }
2409
+ const firstTarget = firstTag.target;
2410
+ if (firstTarget?.path === null || firstTarget?.valid !== true) {
2411
+ diagnostics.push(
2412
+ makeAnalysisDiagnostic(
2413
+ "INVALID_TAG_ARGUMENT",
2414
+ 'Tag "@discriminator" requires a direct path target like ":kind".',
2415
+ provenanceForNode(node, file)
2416
+ )
2417
+ );
2418
+ return null;
2419
+ }
2420
+ if (firstTarget.path.segments.length !== 1) {
2421
+ diagnostics.push(
2422
+ makeAnalysisDiagnostic(
2423
+ "INVALID_TAG_ARGUMENT",
2424
+ 'Tag "@discriminator" only supports direct property targets in v1; nested paths are out of scope.',
2425
+ provenanceForNode(node, file)
2426
+ )
2427
+ );
2428
+ return null;
2429
+ }
2430
+ const typeParameterName = firstTag.argumentText.trim();
2431
+ if (!/^[A-Za-z_$][\w$]*$/u.test(typeParameterName)) {
2432
+ diagnostics.push(
2433
+ makeAnalysisDiagnostic(
2434
+ "INVALID_TAG_ARGUMENT",
2435
+ 'Tag "@discriminator" requires a local type parameter name as its source operand.',
2436
+ provenanceForNode(node, file)
2437
+ )
2438
+ );
2439
+ return null;
2440
+ }
2441
+ return {
2442
+ fieldName: firstTarget.path.segments[0] ?? firstTarget.rawText,
2443
+ typeParameterName,
2444
+ provenance: provenanceForNode(node, file)
2445
+ };
2446
+ }
2447
+ function validateDiscriminatorDirective(node, checker, file, diagnostics) {
2448
+ const directive = extractDiscriminatorDirective(node, file, diagnostics);
2449
+ if (directive === null) {
2450
+ return null;
2451
+ }
2452
+ if (!isLocalTypeParameterName(node, directive.typeParameterName)) {
2453
+ diagnostics.push(
2454
+ makeAnalysisDiagnostic(
2455
+ "INVALID_TAG_ARGUMENT",
2456
+ `Tag "@discriminator" references "${directive.typeParameterName}", but the source operand must be a type parameter declared on the same declaration.`,
2457
+ directive.provenance
2458
+ )
2459
+ );
2460
+ return null;
2461
+ }
2462
+ const propertyDecl = findDiscriminatorProperty(node, directive.fieldName);
2463
+ if (propertyDecl === null) {
2464
+ diagnostics.push(
2465
+ makeAnalysisDiagnostic(
2466
+ "UNKNOWN_PATH_TARGET",
2467
+ `Tag "@discriminator" targets "${directive.fieldName}", but no direct property with that name exists on this declaration.`,
2468
+ directive.provenance
2469
+ )
2470
+ );
2471
+ return null;
2472
+ }
2473
+ if (propertyDecl.questionToken !== void 0) {
2474
+ diagnostics.push(
2475
+ makeAnalysisDiagnostic(
2476
+ "TYPE_MISMATCH",
2477
+ `Discriminator field "${directive.fieldName}" must be required; optional discriminator fields are not supported.`,
2478
+ directive.provenance,
2479
+ [provenanceForNode(propertyDecl, file)]
2480
+ )
2481
+ );
2482
+ return null;
2483
+ }
2484
+ const propertyType = checker.getTypeAtLocation(propertyDecl);
2485
+ if (isNullishSemanticType(propertyType)) {
2486
+ diagnostics.push(
2487
+ makeAnalysisDiagnostic(
2488
+ "TYPE_MISMATCH",
2489
+ `Discriminator field "${directive.fieldName}" must not be nullable.`,
2490
+ directive.provenance,
2491
+ [provenanceForNode(propertyDecl, file)]
2492
+ )
2493
+ );
2494
+ return null;
2495
+ }
2496
+ if (!isStringLikeSemanticType(propertyType)) {
2497
+ diagnostics.push(
2498
+ makeAnalysisDiagnostic(
2499
+ "TYPE_MISMATCH",
2500
+ `Discriminator field "${directive.fieldName}" must be string-like.`,
2501
+ directive.provenance,
2502
+ [provenanceForNode(propertyDecl, file)]
2503
+ )
2504
+ );
2505
+ return null;
2506
+ }
2507
+ return directive;
2508
+ }
2509
+ function getConcreteTypeArgumentForDiscriminator(node, subjectType, checker, typeParameterName) {
2510
+ const typeParameterIndex = node.typeParameters?.findIndex(
2511
+ (typeParameter) => typeParameter.name.text === typeParameterName
2512
+ ) ?? -1;
2513
+ if (typeParameterIndex < 0) {
2514
+ return null;
2515
+ }
2516
+ const referenceTypeArguments = (isTypeReference(subjectType) ? subjectType.typeArguments : void 0) ?? subjectType.aliasTypeArguments;
2517
+ if (referenceTypeArguments?.[typeParameterIndex] !== void 0) {
2518
+ return referenceTypeArguments[typeParameterIndex] ?? null;
2519
+ }
2520
+ const localTypeParameter = node.typeParameters?.[typeParameterIndex];
2521
+ return localTypeParameter === void 0 ? null : checker.getTypeAtLocation(localTypeParameter);
2522
+ }
2523
+ function extractDeclarationApiName(node) {
2524
+ for (const tag of getLeadingParsedTags(node)) {
2525
+ if (tag.normalizedTagName !== "apiName") {
2526
+ continue;
2527
+ }
2528
+ if (tag.target === null && tag.argumentText.trim() !== "") {
2529
+ return tag.argumentText.trim();
2530
+ }
2531
+ if (tag.target?.kind === "variant" && tag.target.rawText === "singular") {
2532
+ const value = tag.argumentText.trim();
2533
+ if (value !== "") {
2534
+ return value;
2535
+ }
2536
+ }
2537
+ }
2538
+ return null;
2539
+ }
2540
+ function inferJsonFacingName(name) {
2541
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
2542
+ }
2543
+ function resolveNamedDiscriminatorDeclaration(type, checker, seen = /* @__PURE__ */ new Set()) {
2544
+ if (seen.has(type)) {
2545
+ return null;
2546
+ }
2547
+ seen.add(type);
2548
+ const symbol = type.aliasSymbol ?? type.getSymbol();
2549
+ if (symbol !== void 0) {
2550
+ const aliased = symbol.flags & ts3.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : void 0;
2551
+ const targetSymbol = aliased ?? symbol;
2552
+ const declaration = targetSymbol.declarations?.find(
2553
+ (candidate) => ts3.isClassDeclaration(candidate) || ts3.isInterfaceDeclaration(candidate) || ts3.isTypeAliasDeclaration(candidate) || ts3.isEnumDeclaration(candidate)
2554
+ );
2555
+ if (declaration !== void 0) {
2556
+ if (ts3.isTypeAliasDeclaration(declaration) && ts3.isTypeReferenceNode(declaration.type) && checker.getTypeFromTypeNode(declaration.type) !== type) {
2557
+ return resolveNamedDiscriminatorDeclaration(
2558
+ checker.getTypeFromTypeNode(declaration.type),
2559
+ checker,
2560
+ seen
2561
+ );
2562
+ }
2563
+ return declaration;
2564
+ }
2565
+ }
2566
+ return null;
2567
+ }
2568
+ function resolveDiscriminatorValue(boundType, checker, provenance, diagnostics) {
2569
+ if (boundType === null) {
2570
+ diagnostics.push(
2571
+ makeAnalysisDiagnostic(
2572
+ "INVALID_TAG_ARGUMENT",
2573
+ "Discriminator resolution failed because no concrete type argument is available for the referenced type parameter.",
2574
+ provenance
2575
+ )
2576
+ );
2577
+ return null;
2578
+ }
2579
+ if (boundType.isStringLiteral()) {
2580
+ return boundType.value;
2581
+ }
2582
+ if (boundType.isUnion()) {
2583
+ const nonNullMembers = boundType.types.filter(
2584
+ (member) => !(member.flags & (ts3.TypeFlags.Null | ts3.TypeFlags.Undefined))
2585
+ );
2586
+ if (nonNullMembers.every((member) => member.isStringLiteral())) {
2587
+ diagnostics.push(
2588
+ makeAnalysisDiagnostic(
2589
+ "INVALID_TAG_ARGUMENT",
2590
+ "Discriminator resolution for unions of string literals is out of scope for v1.",
2591
+ provenance
2592
+ )
2593
+ );
2594
+ return null;
2595
+ }
2596
+ }
2597
+ const declaration = resolveNamedDiscriminatorDeclaration(boundType, checker);
2598
+ if (declaration !== null) {
2599
+ return extractDeclarationApiName(declaration) ?? inferJsonFacingName(getDeclarationName(declaration));
2600
+ }
2601
+ diagnostics.push(
2602
+ makeAnalysisDiagnostic(
2603
+ "INVALID_TAG_ARGUMENT",
2604
+ "Discriminator resolution could not derive a JSON-facing discriminator value from the referenced type argument.",
2605
+ provenance
2606
+ )
2607
+ );
2608
+ return null;
2609
+ }
2610
+ function getDeclarationName(node) {
2611
+ if (ts3.isClassDeclaration(node) || ts3.isInterfaceDeclaration(node) || ts3.isTypeAliasDeclaration(node) || ts3.isEnumDeclaration(node)) {
2612
+ return node.name?.text ?? "anonymous";
2613
+ }
2614
+ return "anonymous";
2615
+ }
2616
+ function applyDeclarationDiscriminatorToFields(fields, node, subjectType, checker, file, diagnostics) {
2617
+ const directive = validateDiscriminatorDirective(node, checker, file, diagnostics);
2618
+ if (directive === null) {
2619
+ return [...fields];
2620
+ }
2621
+ const discriminatorValue = resolveDiscriminatorValue(
2622
+ getConcreteTypeArgumentForDiscriminator(
2623
+ node,
2624
+ subjectType,
2625
+ checker,
2626
+ directive.typeParameterName
2627
+ ),
2628
+ checker,
2629
+ directive.provenance,
2630
+ diagnostics
2631
+ );
2632
+ if (discriminatorValue === null) {
2633
+ return [...fields];
2634
+ }
2635
+ return fields.map(
2636
+ (field) => field.name === directive.fieldName ? {
2637
+ ...field,
2638
+ type: {
2639
+ kind: "enum",
2640
+ members: [{ value: discriminatorValue }]
2641
+ }
2642
+ } : field
2643
+ );
2644
+ }
2645
+ function buildInstantiatedReferenceName(baseName, typeArguments, checker) {
2646
+ const renderedArguments = typeArguments.map(
2647
+ (typeArgument) => checker.typeToString(typeArgument).replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "")
2648
+ ).filter((value) => value !== "");
2649
+ return renderedArguments.length === 0 ? baseName : `${baseName}__${renderedArguments.join("__")}`;
2650
+ }
2651
+ function extractReferenceTypeArguments(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
2652
+ const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
2653
+ if (typeNode === void 0) {
2654
+ return [];
2655
+ }
2656
+ const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
2657
+ if (!ts3.isTypeReferenceNode(resolvedTypeNode) || resolvedTypeNode.typeArguments === void 0) {
2658
+ return [];
2659
+ }
2660
+ return resolvedTypeNode.typeArguments.map((argumentNode) => {
2661
+ const argumentType = checker.getTypeFromTypeNode(argumentNode);
2662
+ return {
2663
+ tsType: argumentType,
2664
+ typeNode: resolveTypeNode(
2665
+ argumentType,
2666
+ checker,
2667
+ file,
2668
+ typeRegistry,
2669
+ visiting,
2670
+ argumentNode,
2671
+ extensionRegistry,
2672
+ diagnostics
2673
+ )
2674
+ };
2675
+ });
2676
+ }
2677
+ function applyDiscriminatorToObjectProperties(properties, node, subjectType, checker, file, diagnostics) {
2678
+ const directive = validateDiscriminatorDirective(node, checker, file, diagnostics);
2679
+ if (directive === null) {
2680
+ return properties;
2681
+ }
2682
+ const discriminatorValue = resolveDiscriminatorValue(
2683
+ getConcreteTypeArgumentForDiscriminator(
2684
+ node,
2685
+ subjectType,
2686
+ checker,
2687
+ directive.typeParameterName
2688
+ ),
2689
+ checker,
2690
+ directive.provenance,
2691
+ diagnostics
2692
+ );
2693
+ if (discriminatorValue === null) {
2694
+ return properties;
2695
+ }
2696
+ return properties.map(
2697
+ (property) => property.name === directive.fieldName ? {
2698
+ ...property,
2699
+ type: {
2700
+ kind: "enum",
2701
+ members: [{ value: discriminatorValue }]
2702
+ }
2703
+ } : property
2704
+ );
2705
+ }
2223
2706
  function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, extensionRegistry) {
2224
2707
  if (!ts3.isIdentifier(prop.name)) {
2225
2708
  return null;
@@ -2508,6 +2991,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
2508
2991
  file,
2509
2992
  typeRegistry,
2510
2993
  visiting,
2994
+ sourceNode,
2511
2995
  extensionRegistry,
2512
2996
  diagnostics
2513
2997
  );
@@ -2771,35 +3255,60 @@ function typeNodeContainsReference(type, targetName) {
2771
3255
  }
2772
3256
  }
2773
3257
  }
2774
- function resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
3258
+ function resolveObjectType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
3259
+ const collectedDiagnostics = diagnostics ?? [];
2775
3260
  const typeName = getNamedTypeName(type);
2776
3261
  const namedTypeName = typeName ?? void 0;
2777
3262
  const namedDecl = getNamedTypeDeclaration(type);
2778
- const shouldRegisterNamedType = namedTypeName !== void 0 && !(namedTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
3263
+ const referenceTypeArguments = extractReferenceTypeArguments(
3264
+ type,
3265
+ checker,
3266
+ file,
3267
+ typeRegistry,
3268
+ visiting,
3269
+ sourceNode,
3270
+ extensionRegistry,
3271
+ collectedDiagnostics
3272
+ );
3273
+ const instantiatedTypeName = namedTypeName !== void 0 && referenceTypeArguments.length > 0 ? buildInstantiatedReferenceName(
3274
+ namedTypeName,
3275
+ referenceTypeArguments.map((argument) => argument.tsType),
3276
+ checker
3277
+ ) : void 0;
3278
+ const registryTypeName = instantiatedTypeName ?? namedTypeName;
3279
+ const shouldRegisterNamedType = registryTypeName !== void 0 && !(registryTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
2779
3280
  const clearNamedTypeRegistration = () => {
2780
- if (namedTypeName === void 0 || !shouldRegisterNamedType) {
3281
+ if (registryTypeName === void 0 || !shouldRegisterNamedType) {
2781
3282
  return;
2782
3283
  }
2783
- Reflect.deleteProperty(typeRegistry, namedTypeName);
3284
+ Reflect.deleteProperty(typeRegistry, registryTypeName);
2784
3285
  };
2785
3286
  if (visiting.has(type)) {
2786
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2787
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
3287
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
3288
+ return {
3289
+ kind: "reference",
3290
+ name: registryTypeName,
3291
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
3292
+ };
2788
3293
  }
2789
3294
  return { kind: "object", properties: [], additionalProperties: false };
2790
3295
  }
2791
- if (namedTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[namedTypeName]) {
2792
- typeRegistry[namedTypeName] = {
2793
- name: namedTypeName,
3296
+ if (registryTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[registryTypeName]) {
3297
+ typeRegistry[registryTypeName] = {
3298
+ name: registryTypeName,
2794
3299
  type: RESOLVING_TYPE_PLACEHOLDER,
2795
3300
  provenance: provenanceForDeclaration(namedDecl, file)
2796
3301
  };
2797
3302
  }
2798
3303
  visiting.add(type);
2799
- if (namedTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[namedTypeName]?.type !== void 0) {
2800
- if (typeRegistry[namedTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
3304
+ if (registryTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[registryTypeName]?.type !== void 0) {
3305
+ if (typeRegistry[registryTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
2801
3306
  visiting.delete(type);
2802
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
3307
+ return {
3308
+ kind: "reference",
3309
+ name: registryTypeName,
3310
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
3311
+ };
2803
3312
  }
2804
3313
  }
2805
3314
  const recordNode = tryResolveRecordType(
@@ -2809,24 +3318,28 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2809
3318
  typeRegistry,
2810
3319
  visiting,
2811
3320
  extensionRegistry,
2812
- diagnostics
3321
+ collectedDiagnostics
2813
3322
  );
2814
3323
  if (recordNode) {
2815
3324
  visiting.delete(type);
2816
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2817
- const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, namedTypeName);
3325
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
3326
+ const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, registryTypeName);
2818
3327
  if (!isRecursiveRecord) {
2819
3328
  clearNamedTypeRegistration();
2820
3329
  return recordNode;
2821
3330
  }
2822
3331
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2823
- typeRegistry[namedTypeName] = {
2824
- name: namedTypeName,
3332
+ typeRegistry[registryTypeName] = {
3333
+ name: registryTypeName,
2825
3334
  type: recordNode,
2826
3335
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
2827
3336
  provenance: provenanceForDeclaration(namedDecl, file)
2828
3337
  };
2829
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
3338
+ return {
3339
+ kind: "reference",
3340
+ name: registryTypeName,
3341
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
3342
+ };
2830
3343
  }
2831
3344
  return recordNode;
2832
3345
  }
@@ -2837,7 +3350,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2837
3350
  file,
2838
3351
  typeRegistry,
2839
3352
  visiting,
2840
- diagnostics ?? [],
3353
+ collectedDiagnostics,
2841
3354
  extensionRegistry
2842
3355
  );
2843
3356
  for (const prop of type.getProperties()) {
@@ -2853,7 +3366,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2853
3366
  visiting,
2854
3367
  declaration,
2855
3368
  extensionRegistry,
2856
- diagnostics
3369
+ collectedDiagnostics
2857
3370
  );
2858
3371
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
2859
3372
  properties.push({
@@ -2868,18 +3381,29 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2868
3381
  visiting.delete(type);
2869
3382
  const objectNode = {
2870
3383
  kind: "object",
2871
- properties,
3384
+ properties: namedDecl !== void 0 && (ts3.isClassDeclaration(namedDecl) || ts3.isInterfaceDeclaration(namedDecl) || ts3.isTypeAliasDeclaration(namedDecl)) ? applyDiscriminatorToObjectProperties(
3385
+ properties,
3386
+ namedDecl,
3387
+ type,
3388
+ checker,
3389
+ file,
3390
+ collectedDiagnostics
3391
+ ) : properties,
2872
3392
  additionalProperties: true
2873
3393
  };
2874
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
3394
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
2875
3395
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2876
- typeRegistry[namedTypeName] = {
2877
- name: namedTypeName,
3396
+ typeRegistry[registryTypeName] = {
3397
+ name: registryTypeName,
2878
3398
  type: objectNode,
2879
3399
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
2880
3400
  provenance: provenanceForDeclaration(namedDecl, file)
2881
3401
  };
2882
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
3402
+ return {
3403
+ kind: "reference",
3404
+ name: registryTypeName,
3405
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
3406
+ };
2883
3407
  }
2884
3408
  return objectNode;
2885
3409
  }
@@ -3140,6 +3664,18 @@ function detectFormSpecReference(typeNode) {
3140
3664
  }
3141
3665
 
3142
3666
  // src/analyzer/program.ts
3667
+ function createProgramContextFromProgram(program, filePath) {
3668
+ const absolutePath = path.resolve(filePath);
3669
+ const sourceFile = program.getSourceFile(absolutePath) ?? program.getSourceFile(filePath);
3670
+ if (!sourceFile) {
3671
+ throw new Error(`Could not find source file in provided program: ${absolutePath}`);
3672
+ }
3673
+ return {
3674
+ program,
3675
+ checker: program.getTypeChecker(),
3676
+ sourceFile
3677
+ };
3678
+ }
3143
3679
  function createProgramContext(filePath) {
3144
3680
  const absolutePath = path.resolve(filePath);
3145
3681
  const fileDir = path.dirname(absolutePath);
@@ -3210,31 +3746,40 @@ function findTypeAliasByName(sourceFile, aliasName) {
3210
3746
  }
3211
3747
  function analyzeNamedTypeToIR(filePath, typeName, extensionRegistry) {
3212
3748
  const ctx = createProgramContext(filePath);
3749
+ return analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensionRegistry);
3750
+ }
3751
+ function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensionRegistry) {
3752
+ const analysisFilePath = path.resolve(filePath);
3213
3753
  const classDecl = findClassByName(ctx.sourceFile, typeName);
3214
3754
  if (classDecl !== null) {
3215
- return analyzeClassToIR(classDecl, ctx.checker, filePath, extensionRegistry);
3755
+ return analyzeClassToIR(classDecl, ctx.checker, analysisFilePath, extensionRegistry);
3216
3756
  }
3217
3757
  const interfaceDecl = findInterfaceByName(ctx.sourceFile, typeName);
3218
3758
  if (interfaceDecl !== null) {
3219
- return analyzeInterfaceToIR(interfaceDecl, ctx.checker, filePath, extensionRegistry);
3759
+ return analyzeInterfaceToIR(interfaceDecl, ctx.checker, analysisFilePath, extensionRegistry);
3220
3760
  }
3221
3761
  const typeAlias = findTypeAliasByName(ctx.sourceFile, typeName);
3222
3762
  if (typeAlias !== null) {
3223
- const result = analyzeTypeAliasToIR(typeAlias, ctx.checker, filePath, extensionRegistry);
3763
+ const result = analyzeTypeAliasToIR(
3764
+ typeAlias,
3765
+ ctx.checker,
3766
+ analysisFilePath,
3767
+ extensionRegistry
3768
+ );
3224
3769
  if (result.ok) {
3225
3770
  return result.analysis;
3226
3771
  }
3227
3772
  throw new Error(result.error);
3228
3773
  }
3229
3774
  throw new Error(
3230
- `Type "${typeName}" not found as a class, interface, or type alias in ${filePath}`
3775
+ `Type "${typeName}" not found as a class, interface, or type alias in ${analysisFilePath}`
3231
3776
  );
3232
3777
  }
3233
3778
 
3234
3779
  // src/validate/constraint-validator.ts
3235
- var import_internal2 = require("@formspec/analysis/internal");
3780
+ var import_internal3 = require("@formspec/analysis/internal");
3236
3781
  function validateFieldNode(ctx, field) {
3237
- const analysis = (0, import_internal2.analyzeConstraintTargets)(
3782
+ const analysis = (0, import_internal3.analyzeConstraintTargets)(
3238
3783
  field.name,
3239
3784
  field.type,
3240
3785
  field.constraints,
@@ -3252,7 +3797,7 @@ function validateFieldNode(ctx, field) {
3252
3797
  }
3253
3798
  function validateObjectProperty(ctx, parentName, property) {
3254
3799
  const qualifiedName = `${parentName}.${property.name}`;
3255
- const analysis = (0, import_internal2.analyzeConstraintTargets)(
3800
+ const analysis = (0, import_internal3.analyzeConstraintTargets)(
3256
3801
  qualifiedName,
3257
3802
  property.type,
3258
3803
  property.constraints,
@@ -3361,12 +3906,28 @@ function generateSchemasFromClass(options) {
3361
3906
  );
3362
3907
  }
3363
3908
  function generateSchemas(options) {
3364
- const analysis = analyzeNamedTypeToIR(
3909
+ const ctx = createProgramContext(options.filePath);
3910
+ return generateSchemasFromProgram({
3911
+ ...options,
3912
+ program: ctx.program
3913
+ });
3914
+ }
3915
+ function generateSchemasFromProgram(options) {
3916
+ const ctx = createProgramContextFromProgram(options.program, options.filePath);
3917
+ const analysis = analyzeNamedTypeToIRFromProgramContext(
3918
+ ctx,
3365
3919
  options.filePath,
3366
3920
  options.typeName,
3367
3921
  options.extensionRegistry
3368
3922
  );
3369
- return generateClassSchemas(analysis, { file: options.filePath }, options);
3923
+ return generateClassSchemas(
3924
+ analysis,
3925
+ { file: options.filePath },
3926
+ {
3927
+ extensionRegistry: options.extensionRegistry,
3928
+ vendorPrefix: options.vendorPrefix
3929
+ }
3930
+ );
3370
3931
  }
3371
3932
 
3372
3933
  // src/generators/mixed-authoring.ts
@@ -3573,7 +4134,10 @@ function writeSchemas(form, options) {
3573
4134
  generateJsonSchema,
3574
4135
  generateSchemas,
3575
4136
  generateSchemasFromClass,
4137
+ generateSchemasFromProgram,
3576
4138
  generateUiSchema,
4139
+ jsonSchema7Schema,
4140
+ uiSchemaSchema,
3577
4141
  writeSchemas
3578
4142
  });
3579
4143
  //# sourceMappingURL=index.cjs.map