@openpkg-ts/extract 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/tspec.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  extract
4
- } from "../shared/chunk-ksf9k654.js";
4
+ } from "../shared/chunk-v4cnenxs.js";
5
5
 
6
6
  // src/cli/spec.ts
7
7
  import * as fs from "node:fs";
@@ -248,7 +248,7 @@ class TypeRegistry {
248
248
  return { type: checker.typeToString(t) };
249
249
  }
250
250
  if (sym && !sym.getName().startsWith("__")) {
251
- return { $ref: sym.getName() };
251
+ return { $ref: `#/types/${sym.getName()}` };
252
252
  }
253
253
  if (t.flags & ts.TypeFlags.StringLiteral) {
254
254
  return { type: "string", enum: [t.value] };
@@ -265,7 +265,7 @@ class TypeRegistry {
265
265
  allOf: type.types.map((t) => {
266
266
  const sym = t.getSymbol() || t.aliasSymbol;
267
267
  if (sym && !sym.getName().startsWith("__")) {
268
- return { $ref: sym.getName() };
268
+ return { $ref: `#/types/${sym.getName()}` };
269
269
  }
270
270
  return { type: checker.typeToString(t) };
271
271
  })
@@ -287,7 +287,7 @@ class TypeRegistry {
287
287
  const propSym = propType.getSymbol() || propType.aliasSymbol;
288
288
  const symName = propSym?.getName();
289
289
  if (propSym && symName && !symName.startsWith("__") && symName !== propName) {
290
- properties[propName] = { $ref: symName };
290
+ properties[propName] = { $ref: `#/types/${symName}` };
291
291
  } else {
292
292
  properties[propName] = { type: checker.typeToString(propType) };
293
293
  }
@@ -316,7 +316,7 @@ class TypeRegistry {
316
316
  members.push({
317
317
  name,
318
318
  kind: "property",
319
- schema: sym && !sym.getName().startsWith("__") ? { $ref: sym.getName() } : { type: checker.typeToString(type) }
319
+ schema: sym && !sym.getName().startsWith("__") ? { $ref: `#/types/${sym.getName()}` } : { type: checker.typeToString(type) }
320
320
  });
321
321
  } else if (ts.isMethodDeclaration(member) || ts.isMethodSignature(member)) {
322
322
  const name = member.name?.getText();
@@ -428,6 +428,21 @@ function extractTypeParameters(node, checker) {
428
428
  };
429
429
  });
430
430
  }
431
+ function isSymbolDeprecated(symbol) {
432
+ if (!symbol) {
433
+ return false;
434
+ }
435
+ const jsDocTags = symbol.getJsDocTags();
436
+ if (jsDocTags.some((tag) => tag.name.toLowerCase() === "deprecated")) {
437
+ return true;
438
+ }
439
+ for (const declaration of symbol.getDeclarations() ?? []) {
440
+ if (ts2.getJSDocDeprecatedTag(declaration)) {
441
+ return true;
442
+ }
443
+ }
444
+ return false;
445
+ }
431
446
 
432
447
  // src/compiler/program.ts
433
448
  import * as path2 from "node:path";
@@ -480,6 +495,30 @@ function createProgram({
480
495
 
481
496
  // src/types/schema-builder.ts
482
497
  import ts4 from "typescript";
498
+ var BUILTIN_TYPE_SCHEMAS = {
499
+ Date: { type: "string", format: "date-time" },
500
+ RegExp: { type: "object", description: "RegExp" },
501
+ Error: { type: "object" },
502
+ Promise: { type: "object" },
503
+ Map: { type: "object" },
504
+ Set: { type: "object" },
505
+ WeakMap: { type: "object" },
506
+ WeakSet: { type: "object" },
507
+ Function: { type: "object" },
508
+ ArrayBuffer: { type: "string", format: "binary" },
509
+ ArrayBufferLike: { type: "string", format: "binary" },
510
+ DataView: { type: "string", format: "binary" },
511
+ Uint8Array: { type: "string", format: "byte" },
512
+ Uint16Array: { type: "string", format: "byte" },
513
+ Uint32Array: { type: "string", format: "byte" },
514
+ Int8Array: { type: "string", format: "byte" },
515
+ Int16Array: { type: "string", format: "byte" },
516
+ Int32Array: { type: "string", format: "byte" },
517
+ Float32Array: { type: "string", format: "byte" },
518
+ Float64Array: { type: "string", format: "byte" },
519
+ BigInt64Array: { type: "string", format: "byte" },
520
+ BigUint64Array: { type: "string", format: "byte" }
521
+ };
483
522
  var PRIMITIVES2 = new Set([
484
523
  "string",
485
524
  "number",
@@ -575,10 +614,10 @@ function buildSchema(type, checker, ctx, _depth = 0) {
575
614
  if (isAtMaxDepth(ctx)) {
576
615
  return { type: checker.typeToString(type) };
577
616
  }
578
- if (ctx && ctx.visitedTypes.has(type)) {
617
+ if (ctx?.visitedTypes.has(type)) {
579
618
  const symbol2 = type.getSymbol() || type.aliasSymbol;
580
619
  if (symbol2) {
581
- return { $ref: symbol2.getName() };
620
+ return { $ref: `#/types/${symbol2.getName()}` };
582
621
  }
583
622
  return { type: checker.typeToString(type) };
584
623
  }
@@ -680,17 +719,17 @@ function buildSchema(type, checker, ctx, _depth = 0) {
680
719
  const symbol2 = typeRef.target.getSymbol();
681
720
  const name = symbol2?.getName();
682
721
  if (name && BUILTIN_TYPES.has(name)) {
683
- return { $ref: name };
722
+ return { $ref: `#/types/${name}` };
684
723
  }
685
724
  if (name && (isBuiltinGeneric(name) || !isAnonymous(typeRef.target))) {
686
725
  if (ctx) {
687
726
  return withDepth(ctx, () => ({
688
- $ref: name,
727
+ $ref: `#/types/${name}`,
689
728
  typeArguments: typeRef.typeArguments.map((t) => buildSchema(t, checker, ctx))
690
729
  }));
691
730
  }
692
731
  return {
693
- $ref: name,
732
+ $ref: `#/types/${name}`,
694
733
  typeArguments: typeRef.typeArguments.map((t) => buildSchema(t, checker, ctx))
695
734
  };
696
735
  }
@@ -702,10 +741,10 @@ function buildSchema(type, checker, ctx, _depth = 0) {
702
741
  return { type: name };
703
742
  }
704
743
  if (BUILTIN_TYPES.has(name)) {
705
- return { $ref: name };
744
+ return { $ref: `#/types/${name}` };
706
745
  }
707
746
  if (!name.startsWith("__")) {
708
- return { $ref: name };
747
+ return { $ref: `#/types/${name}` };
709
748
  }
710
749
  }
711
750
  if (type.flags & ts4.TypeFlags.Object) {
@@ -772,6 +811,110 @@ function buildObjectSchema(properties, checker, ctx) {
772
811
  }
773
812
  return buildProps();
774
813
  }
814
+ function isPureRefSchema(schema) {
815
+ return typeof schema === "object" && Object.keys(schema).length === 1 && "$ref" in schema;
816
+ }
817
+ function withDescription(schema, description) {
818
+ if (isPureRefSchema(schema)) {
819
+ return {
820
+ allOf: [schema],
821
+ description
822
+ };
823
+ }
824
+ return { ...schema, description };
825
+ }
826
+ function schemaIsAny(schema) {
827
+ if (typeof schema === "string") {
828
+ return schema === "any";
829
+ }
830
+ if ("type" in schema && schema.type === "any" && Object.keys(schema).length === 1) {
831
+ return true;
832
+ }
833
+ return false;
834
+ }
835
+ function schemasAreEqual(left, right) {
836
+ if (typeof left !== typeof right) {
837
+ return false;
838
+ }
839
+ if (typeof left === "string" && typeof right === "string") {
840
+ return left === right;
841
+ }
842
+ if (left == null || right == null) {
843
+ return left === right;
844
+ }
845
+ const normalize = (value) => {
846
+ if (Array.isArray(value)) {
847
+ return value.map((item) => normalize(item));
848
+ }
849
+ if (value && typeof value === "object") {
850
+ const sortedEntries = Object.entries(value).map(([key, val]) => [key, normalize(val)]).sort(([keyA], [keyB]) => keyA.localeCompare(keyB));
851
+ return Object.fromEntries(sortedEntries);
852
+ }
853
+ return value;
854
+ };
855
+ return JSON.stringify(normalize(left)) === JSON.stringify(normalize(right));
856
+ }
857
+ function deduplicateSchemas(schemas) {
858
+ const result = [];
859
+ for (const schema of schemas) {
860
+ const isDuplicate = result.some((existing) => schemasAreEqual(existing, schema));
861
+ if (!isDuplicate) {
862
+ result.push(schema);
863
+ }
864
+ }
865
+ return result;
866
+ }
867
+ function findDiscriminatorProperty(unionTypes, checker) {
868
+ const memberProps = [];
869
+ for (const t of unionTypes) {
870
+ if (t.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined)) {
871
+ continue;
872
+ }
873
+ const props = t.getProperties();
874
+ if (!props || props.length === 0) {
875
+ return;
876
+ }
877
+ const propValues = new Map;
878
+ for (const prop of props) {
879
+ const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
880
+ if (!declaration)
881
+ continue;
882
+ try {
883
+ const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
884
+ if (propType.isStringLiteral()) {
885
+ propValues.set(prop.getName(), propType.value);
886
+ } else if (propType.isNumberLiteral()) {
887
+ propValues.set(prop.getName(), propType.value);
888
+ }
889
+ } catch {}
890
+ }
891
+ memberProps.push(propValues);
892
+ }
893
+ if (memberProps.length < 2) {
894
+ return;
895
+ }
896
+ const firstMember = memberProps[0];
897
+ for (const [propName, firstValue] of firstMember) {
898
+ const values = new Set([firstValue]);
899
+ let isDiscriminator = true;
900
+ for (let i = 1;i < memberProps.length; i++) {
901
+ const value = memberProps[i].get(propName);
902
+ if (value === undefined) {
903
+ isDiscriminator = false;
904
+ break;
905
+ }
906
+ if (values.has(value)) {
907
+ isDiscriminator = false;
908
+ break;
909
+ }
910
+ values.add(value);
911
+ }
912
+ if (isDiscriminator) {
913
+ return propName;
914
+ }
915
+ }
916
+ return;
917
+ }
775
918
 
776
919
  // src/types/parameters.ts
777
920
  import ts5 from "typescript";
@@ -941,7 +1084,7 @@ function serializeClass(node, ctx) {
941
1084
  members.push(propMember);
942
1085
  } else if (ts6.isMethodDeclaration(member)) {
943
1086
  const methodMember = serializeMethod(member, ctx);
944
- if (methodMember && methodMember.name) {
1087
+ if (methodMember?.name) {
945
1088
  if (!methodsByName.has(methodMember.name)) {
946
1089
  methodsByName.set(methodMember.name, methodMember);
947
1090
  } else {
@@ -1247,7 +1390,7 @@ function serializeInterface(node, ctx) {
1247
1390
  members.push(propMember);
1248
1391
  } else if (ts7.isMethodSignature(member)) {
1249
1392
  const methodMember = serializeMethodSignature(member, ctx);
1250
- if (methodMember && methodMember.name) {
1393
+ if (methodMember?.name) {
1251
1394
  if (!methodsByName.has(methodMember.name)) {
1252
1395
  methodsByName.set(methodMember.name, methodMember);
1253
1396
  }
@@ -1440,7 +1583,7 @@ function serializeVariable(node, statement, ctx) {
1440
1583
  // src/builder/spec-builder.ts
1441
1584
  import * as fs2 from "node:fs";
1442
1585
  import * as path3 from "node:path";
1443
- import { SCHEMA_VERSION } from "@openpkg-ts/spec";
1586
+ import { SCHEMA_URL, SCHEMA_VERSION } from "@openpkg-ts/spec";
1444
1587
  import ts8 from "typescript";
1445
1588
 
1446
1589
  // src/serializers/context.ts
@@ -1460,15 +1603,52 @@ function createContext(program, sourceFile, options = {}) {
1460
1603
  }
1461
1604
 
1462
1605
  // src/builder/spec-builder.ts
1606
+ var BUILTIN_TYPES2 = new Set([
1607
+ "Array",
1608
+ "Promise",
1609
+ "Map",
1610
+ "Set",
1611
+ "Record",
1612
+ "Partial",
1613
+ "Required",
1614
+ "Pick",
1615
+ "Omit",
1616
+ "Exclude",
1617
+ "Extract",
1618
+ "NonNullable",
1619
+ "Parameters",
1620
+ "ReturnType",
1621
+ "Readonly",
1622
+ "ReadonlyArray",
1623
+ "Awaited",
1624
+ "Date",
1625
+ "RegExp",
1626
+ "Error",
1627
+ "Function",
1628
+ "Object",
1629
+ "String",
1630
+ "Number",
1631
+ "Boolean",
1632
+ "Symbol",
1633
+ "BigInt"
1634
+ ]);
1463
1635
  async function extract(options) {
1464
- const { entryFile, baseDir, content, maxTypeDepth, maxExternalTypeDepth, resolveExternalTypes } = options;
1636
+ const {
1637
+ entryFile,
1638
+ baseDir,
1639
+ content,
1640
+ maxTypeDepth,
1641
+ maxExternalTypeDepth,
1642
+ resolveExternalTypes,
1643
+ includeSchema
1644
+ } = options;
1465
1645
  const diagnostics = [];
1466
1646
  const exports = [];
1467
1647
  const result = createProgram({ entryFile, baseDir, content });
1468
1648
  const { program, sourceFile } = result;
1469
1649
  if (!sourceFile) {
1470
1650
  return {
1471
- spec: createEmptySpec(entryFile),
1651
+ spec: createEmptySpec(entryFile, includeSchema),
1472
1652
  diagnostics: [{ message: `Could not load source file: ${entryFile}`, severity: "error" }]
1473
1653
  };
1474
1654
  }
@@ -1476,7 +1656,7 @@ async function extract(options) {
1476
1656
  const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile);
1477
1657
  if (!moduleSymbol) {
1478
1658
  return {
1479
- spec: createEmptySpec(entryFile),
1659
+ spec: createEmptySpec(entryFile, includeSchema),
1480
1660
  diagnostics: [{ message: "Could not get module symbol", severity: "warning" }]
1481
1661
  };
1482
1662
  }
@@ -1485,7 +1665,11 @@ async function extract(options) {
1485
1665
  for (const symbol of exportedSymbols) {
1486
1666
  exportedIds.add(symbol.getName());
1487
1667
  }
1488
- const ctx = createContext(program, sourceFile, { maxTypeDepth, maxExternalTypeDepth, resolveExternalTypes });
1668
+ const ctx = createContext(program, sourceFile, {
1669
+ maxTypeDepth,
1670
+ maxExternalTypeDepth,
1671
+ resolveExternalTypes
1672
+ });
1489
1673
  ctx.exportedIds = exportedIds;
1490
1674
  for (const symbol of exportedSymbols) {
1491
1675
  try {
@@ -1504,11 +1688,31 @@ async function extract(options) {
1504
1688
  }
1505
1689
  }
1506
1690
  const meta = await getPackageMeta(entryFile, baseDir);
1691
+ const types = ctx.typeRegistry.getAll();
1692
+ const danglingRefs = collectDanglingRefs(exports, types);
1693
+ for (const ref of danglingRefs) {
1694
+ diagnostics.push({
1695
+ message: `Type '${ref}' is referenced but not defined in types[].`,
1696
+ severity: "warning",
1697
+ code: "DANGLING_REF",
1698
+ suggestion: "The type may be from an external package. Check import paths."
1699
+ });
1700
+ }
1701
+ const externalTypes = types.filter((t) => t.kind === "external");
1702
+ if (externalTypes.length > 0) {
1703
+ diagnostics.push({
1704
+ message: `${externalTypes.length} external type(s) could not be fully resolved: ${externalTypes.slice(0, 5).map((t) => t.id).join(", ")}${externalTypes.length > 5 ? "..." : ""}`,
1705
+ severity: "warning",
1706
+ code: "EXTERNAL_TYPE_STUBS",
1707
+ suggestion: "Types are from external packages. Full resolution requires type declarations."
1708
+ });
1709
+ }
1507
1710
  const spec = {
1711
+ ...includeSchema ? { $schema: SCHEMA_URL } : {},
1508
1712
  openpkg: SCHEMA_VERSION,
1509
1713
  meta,
1510
1714
  exports,
1511
- types: ctx.typeRegistry.getAll(),
1715
+ types,
1512
1716
  generation: {
1513
1717
  generator: "@openpkg-ts/extract",
1514
1718
  timestamp: new Date().toISOString()
@@ -1516,6 +1720,32 @@ async function extract(options) {
1516
1720
  };
1517
1721
  return { spec, diagnostics };
1518
1722
  }
1723
+ function collectAllRefs(obj, refs) {
1724
+ if (obj === null || obj === undefined)
1725
+ return;
1726
+ if (Array.isArray(obj)) {
1727
+ for (const item of obj) {
1728
+ collectAllRefs(item, refs);
1729
+ }
1730
+ return;
1731
+ }
1732
+ if (typeof obj === "object") {
1733
+ const record = obj;
1734
+ if (typeof record.$ref === "string" && record.$ref.startsWith("#/types/")) {
1735
+ refs.add(record.$ref.slice("#/types/".length));
1736
+ }
1737
+ for (const value of Object.values(record)) {
1738
+ collectAllRefs(value, refs);
1739
+ }
1740
+ }
1741
+ }
1742
+ function collectDanglingRefs(exports, types) {
1743
+ const definedTypes = new Set(types.map((t) => t.id));
1744
+ const referencedTypes = new Set;
1745
+ collectAllRefs(exports, referencedTypes);
1746
+ collectAllRefs(types, referencedTypes);
1747
+ return Array.from(referencedTypes).filter((ref) => !definedTypes.has(ref) && !BUILTIN_TYPES2.has(ref));
1748
+ }
1519
1749
  function resolveExportTarget(symbol, checker) {
1520
1750
  let targetSymbol = symbol;
1521
1751
  if (symbol.flags & ts8.SymbolFlags.Alias) {
@@ -1528,7 +1758,7 @@ function resolveExportTarget(symbol, checker) {
1528
1758
  const declaration = targetSymbol.valueDeclaration || declarations.find((decl) => decl.kind !== ts8.SyntaxKind.ExportSpecifier) || declarations[0];
1529
1759
  return { declaration, targetSymbol };
1530
1760
  }
1531
- function serializeDeclaration(declaration, exportSymbol, targetSymbol, exportName, ctx) {
1761
+ function serializeDeclaration(declaration, exportSymbol, _targetSymbol, exportName, ctx) {
1532
1762
  let result = null;
1533
1763
  if (ts8.isFunctionDeclaration(declaration)) {
1534
1764
  result = serializeFunctionExport(declaration, ctx);
@@ -1546,16 +1776,16 @@ function serializeDeclaration(declaration, exportSymbol, targetSymbol, exportNam
1546
1776
  result = serializeVariable(declaration, varStatement, ctx);
1547
1777
  }
1548
1778
  } else if (ts8.isNamespaceExport(declaration) || ts8.isModuleDeclaration(declaration)) {
1549
- result = serializeNamespaceExport(exportSymbol, exportName, ctx);
1779
+ result = serializeNamespaceExport(exportSymbol, exportName);
1550
1780
  } else if (ts8.isSourceFile(declaration)) {
1551
- result = serializeNamespaceExport(exportSymbol, exportName, ctx);
1781
+ result = serializeNamespaceExport(exportSymbol, exportName);
1552
1782
  }
1553
1783
  if (result) {
1554
1784
  result = withExportName(result, exportName);
1555
1785
  }
1556
1786
  return result;
1557
1787
  }
1558
- function serializeNamespaceExport(symbol, exportName, ctx) {
1788
+ function serializeNamespaceExport(symbol, exportName) {
1559
1789
  const { description, tags, examples } = getJSDocFromExportSymbol(symbol);
1560
1790
  return {
1561
1791
  id: exportName,
@@ -1633,8 +1863,9 @@ function withExportName(entry, exportName) {
1633
1863
  name: entry.name
1634
1864
  };
1635
1865
  }
1636
- function createEmptySpec(entryFile) {
1866
+ function createEmptySpec(entryFile, includeSchema) {
1637
1867
  return {
1868
+ ...includeSchema ? { $schema: SCHEMA_URL } : {},
1638
1869
  openpkg: SCHEMA_VERSION,
1639
1870
  meta: { name: path3.basename(entryFile, path3.extname(entryFile)) },
1640
1871
  exports: [],
@@ -1659,4 +1890,4 @@ async function getPackageMeta(entryFile, baseDir) {
1659
1890
  } catch {}
1660
1891
  return { name: path3.basename(searchDir) };
1661
1892
  }
1662
- export { TypeRegistry, getJSDocComment, getSourceLocation, createProgram, isPrimitiveName, isBuiltinGeneric, isAnonymous, buildSchema, extractParameters, registerReferencedTypes, serializeClass, serializeEnum, serializeFunctionExport, serializeInterface, serializeTypeAlias, serializeVariable, extract };
1893
+ export { TypeRegistry, getJSDocComment, getSourceLocation, getParamDescription, extractTypeParameters, isSymbolDeprecated, createProgram, BUILTIN_TYPE_SCHEMAS, isPrimitiveName, isBuiltinGeneric, isAnonymous, buildSchema, isPureRefSchema, withDescription, schemaIsAny, schemasAreEqual, deduplicateSchemas, findDiscriminatorProperty, extractParameters, registerReferencedTypes, serializeClass, serializeEnum, serializeFunctionExport, serializeInterface, serializeTypeAlias, serializeVariable, extract };
@@ -25,7 +25,7 @@ declare class TypeRegistry {
25
25
  private extractShallowMembers;
26
26
  registerFromSymbol(symbol: ts.Symbol, checker: ts.TypeChecker): SpecType | undefined;
27
27
  }
28
- import { SpecExample, SpecSource, SpecTag } from "@openpkg-ts/spec";
28
+ import { SpecExample, SpecSource, SpecTag, SpecTypeParameter } from "@openpkg-ts/spec";
29
29
  import ts2 from "typescript";
30
30
  declare function getJSDocComment(node: ts2.Node): {
31
31
  description?: string;
@@ -33,6 +33,23 @@ declare function getJSDocComment(node: ts2.Node): {
33
33
  examples: SpecExample[];
34
34
  };
35
35
  declare function getSourceLocation(node: ts2.Node, sourceFile: ts2.SourceFile): SpecSource;
36
+ /**
37
+ * Get description for a destructured parameter property from JSDoc @param tags.
38
+ * Matches patterns like:
39
+ * - @param paramName - exact match
40
+ * - @param opts.paramName - dotted notation with alias
41
+ * - @param {type} paramName - type annotation format
42
+ */
43
+ declare function getParamDescription(propertyName: string, jsdocTags: readonly ts2.JSDocTag[], inferredAlias?: string): string | undefined;
44
+ type DeclarationWithTypeParams = ts2.FunctionDeclaration | ts2.ClassDeclaration | ts2.InterfaceDeclaration | ts2.TypeAliasDeclaration | ts2.MethodDeclaration | ts2.ArrowFunction;
45
+ /**
46
+ * Extract type parameters from declarations like `<T extends Base, K = Default>`
47
+ */
48
+ declare function extractTypeParameters(node: DeclarationWithTypeParams, checker: ts2.TypeChecker): SpecTypeParameter[] | undefined;
49
+ /**
50
+ * Check if a symbol is marked as deprecated via @deprecated JSDoc tag.
51
+ */
52
+ declare function isSymbolDeprecated(symbol: ts2.Symbol | undefined): boolean;
36
53
  import { OpenPkg } from "@openpkg-ts/spec";
37
54
  interface ExtractOptions {
38
55
  entryFile: string;
@@ -42,6 +59,8 @@ interface ExtractOptions {
42
59
  maxExternalTypeDepth?: number;
43
60
  resolveExternalTypes?: boolean;
44
61
  schemaExtraction?: "static" | "hybrid";
62
+ /** Include $schema URL in output */
63
+ includeSchema?: boolean;
45
64
  }
46
65
  interface ExtractResult {
47
66
  spec: OpenPkg;
@@ -50,6 +69,8 @@ interface ExtractResult {
50
69
  interface Diagnostic {
51
70
  message: string;
52
71
  severity: "error" | "warning" | "info";
72
+ code?: string;
73
+ suggestion?: string;
53
74
  location?: {
54
75
  file?: string;
55
76
  line?: number;
@@ -71,35 +92,135 @@ interface ProgramResult {
71
92
  configPath?: string;
72
93
  }
73
94
  declare function createProgram({ entryFile, baseDir, content }: ProgramOptions): ProgramResult;
74
- import { SpecSchema } from "@openpkg-ts/spec";
75
- import ts4 from "typescript";
76
- type SchemaAdapter = {
77
- name: string;
78
- detect: (node: ts4.Node, checker: ts4.TypeChecker) => boolean;
79
- extract: (node: ts4.Node, checker: ts4.TypeChecker) => SpecSchema | null;
80
- };
95
+ import * as TS from "typescript";
96
+ /**
97
+ * A schema adapter can detect and extract output types from a specific
98
+ * schema validation library.
99
+ */
100
+ interface SchemaAdapter {
101
+ /** Unique identifier for this adapter */
102
+ readonly id: string;
103
+ /** npm package name(s) this adapter handles */
104
+ readonly packages: readonly string[];
105
+ /**
106
+ * Check if a type matches this adapter's schema library.
107
+ * Should be fast - called for every export.
108
+ */
109
+ matches(type: TS.Type, checker: TS.TypeChecker): boolean;
110
+ /**
111
+ * Extract the output type from a schema type.
112
+ * Returns null if extraction fails.
113
+ */
114
+ extractOutputType(type: TS.Type, checker: TS.TypeChecker): TS.Type | null;
115
+ /**
116
+ * Extract the input type from a schema type (optional).
117
+ * Useful for transforms where input differs from output.
118
+ */
119
+ extractInputType?(type: TS.Type, checker: TS.TypeChecker): TS.Type | null;
120
+ }
121
+ /**
122
+ * Result of schema type extraction
123
+ */
124
+ interface SchemaExtractionResult {
125
+ /** The adapter that matched */
126
+ adapter: SchemaAdapter;
127
+ /** The extracted output type */
128
+ outputType: TS.Type;
129
+ /** The extracted input type (if different from output) */
130
+ inputType?: TS.Type;
131
+ }
132
+ /**
133
+ * Utility: Check if type is an object type reference (has type arguments)
134
+ */
135
+ declare function isTypeReference(type: TS.Type): type is TS.TypeReference;
136
+ /**
137
+ * Utility: Remove undefined/null from a union type
138
+ */
139
+ declare function getNonNullableType(type: TS.Type): TS.Type;
81
140
  declare function registerAdapter(adapter: SchemaAdapter): void;
82
- declare function findAdapter(node: ts4.Node, checker: ts4.TypeChecker): SchemaAdapter | undefined;
83
- declare function isSchemaType(node: ts4.Node, checker: ts4.TypeChecker): boolean;
84
- declare function extractSchemaType(node: ts4.Node, checker: ts4.TypeChecker): SpecSchema | null;
141
+ declare function findAdapter(type: TS.Type, checker: TS.TypeChecker): SchemaAdapter | undefined;
142
+ declare function isSchemaType(type: TS.Type, checker: TS.TypeChecker): boolean;
143
+ declare function extractSchemaType(type: TS.Type, checker: TS.TypeChecker): SchemaExtractionResult | null;
85
144
  declare const arktypeAdapter: SchemaAdapter;
86
145
  declare const typeboxAdapter: SchemaAdapter;
87
146
  declare const valibotAdapter: SchemaAdapter;
88
147
  declare const zodAdapter: SchemaAdapter;
89
- import { SpecSchema as SpecSchema2 } from "@openpkg-ts/spec";
90
- import ts5 from "typescript";
91
- interface StandardSchemaResult {
92
- schema: SpecSchema2;
148
+ /**
149
+ * Standard JSON Schema v1 interface (minimal for detection).
150
+ */
151
+ interface StandardJSONSchemaV1 {
152
+ "~standard": {
153
+ version: number;
154
+ vendor: string;
155
+ jsonSchema?: {
156
+ output: (target?: string) => Record<string, unknown>;
157
+ input?: (target?: string) => Record<string, unknown>;
158
+ };
159
+ };
160
+ }
161
+ /**
162
+ * Result of extracting Standard Schema from an export.
163
+ */
164
+ interface StandardSchemaExtractionResult {
165
+ exportName: string;
93
166
  vendor: string;
167
+ outputSchema: Record<string, unknown>;
168
+ inputSchema?: Record<string, unknown>;
169
+ }
170
+ /**
171
+ * Options for runtime Standard Schema extraction.
172
+ */
173
+ interface ExtractStandardSchemasOptions {
174
+ /** Timeout in milliseconds (default: 10000) */
175
+ timeout?: number;
176
+ /** JSON Schema target version (default: 'draft-2020-12') */
177
+ target?: "draft-2020-12" | "draft-07" | "openapi-3.0";
94
178
  }
95
- declare function extractStandardSchemas(_program: ts5.Program, _entryFile: string): StandardSchemaResult[];
179
+ /**
180
+ * Result of Standard Schema extraction.
181
+ */
182
+ interface StandardSchemaExtractionOutput {
183
+ schemas: Map<string, StandardSchemaExtractionResult>;
184
+ errors: string[];
185
+ }
186
+ /**
187
+ * Check if an object implements StandardJSONSchemaV1.
188
+ * This is a static type guard - doesn't require runtime.
189
+ */
190
+ declare function isStandardJSONSchema(obj: unknown): obj is StandardJSONSchemaV1;
191
+ /**
192
+ * Resolve compiled JS path from TypeScript source.
193
+ * Tries common output locations: dist/, build/, lib/, same dir.
194
+ */
195
+ declare function resolveCompiledPath(tsPath: string, baseDir: string): string | null;
196
+ /**
197
+ * Extract Standard Schema JSON Schemas from a compiled JS module.
198
+ *
199
+ * **Security Note**: This executes the module in a subprocess.
200
+ * Only use with trusted code (user's own packages).
201
+ *
202
+ * @param compiledJsPath - Path to compiled .js file
203
+ * @param options - Extraction options
204
+ * @returns Extraction results with schemas and any errors
205
+ */
206
+ declare function extractStandardSchemas(compiledJsPath: string, options?: ExtractStandardSchemasOptions): Promise<StandardSchemaExtractionOutput>;
207
+ /**
208
+ * Extract Standard Schema from a TypeScript project.
209
+ *
210
+ * Convenience function that resolves compiled JS and extracts schemas.
211
+ *
212
+ * @param entryFile - TypeScript entry file path
213
+ * @param baseDir - Project base directory
214
+ * @param options - Extraction options
215
+ */
216
+ declare function extractStandardSchemasFromProject(entryFile: string, baseDir: string, options?: ExtractStandardSchemasOptions): Promise<StandardSchemaExtractionOutput>;
96
217
  import { SpecExport } from "@openpkg-ts/spec";
97
- import ts7 from "typescript";
98
- import ts6 from "typescript";
218
+ import ts5 from "typescript";
219
+ import ts4 from "typescript";
99
220
  interface SerializerContext {
100
- typeChecker: ts6.TypeChecker;
101
- program: ts6.Program;
102
- sourceFile: ts6.SourceFile;
221
+ typeChecker: ts4.TypeChecker;
222
+ program: ts4.Program;
223
+ sourceFile: ts4.SourceFile;
103
224
  maxTypeDepth: number;
104
225
  maxExternalTypeDepth: number;
105
226
  currentDepth: number;
@@ -107,37 +228,39 @@ interface SerializerContext {
107
228
  typeRegistry: TypeRegistry;
108
229
  exportedIds: Set<string>;
109
230
  /** Track visited types to prevent infinite recursion */
110
- visitedTypes: Set<ts6.Type>;
231
+ visitedTypes: Set<ts4.Type>;
111
232
  }
112
- declare function serializeClass(node: ts7.ClassDeclaration, ctx: SerializerContext): SpecExport | null;
233
+ declare function serializeClass(node: ts5.ClassDeclaration, ctx: SerializerContext): SpecExport | null;
113
234
  import { SpecExport as SpecExport2 } from "@openpkg-ts/spec";
114
- import ts8 from "typescript";
115
- declare function serializeEnum(node: ts8.EnumDeclaration, ctx: SerializerContext): SpecExport2 | null;
235
+ import ts6 from "typescript";
236
+ declare function serializeEnum(node: ts6.EnumDeclaration, ctx: SerializerContext): SpecExport2 | null;
116
237
  import { SpecExport as SpecExport3 } from "@openpkg-ts/spec";
117
- import ts9 from "typescript";
118
- declare function serializeFunctionExport(node: ts9.FunctionDeclaration | ts9.ArrowFunction, ctx: SerializerContext): SpecExport3 | null;
238
+ import ts7 from "typescript";
239
+ declare function serializeFunctionExport(node: ts7.FunctionDeclaration | ts7.ArrowFunction, ctx: SerializerContext): SpecExport3 | null;
119
240
  import { SpecExport as SpecExport4 } from "@openpkg-ts/spec";
120
- import ts10 from "typescript";
121
- declare function serializeInterface(node: ts10.InterfaceDeclaration, ctx: SerializerContext): SpecExport4 | null;
241
+ import ts8 from "typescript";
242
+ declare function serializeInterface(node: ts8.InterfaceDeclaration, ctx: SerializerContext): SpecExport4 | null;
122
243
  import { SpecExport as SpecExport5 } from "@openpkg-ts/spec";
123
- import ts11 from "typescript";
124
- declare function serializeTypeAlias(node: ts11.TypeAliasDeclaration, ctx: SerializerContext): SpecExport5 | null;
244
+ import ts9 from "typescript";
245
+ declare function serializeTypeAlias(node: ts9.TypeAliasDeclaration, ctx: SerializerContext): SpecExport5 | null;
125
246
  import { SpecExport as SpecExport6 } from "@openpkg-ts/spec";
126
- import ts12 from "typescript";
127
- declare function serializeVariable(node: ts12.VariableDeclaration, statement: ts12.VariableStatement, ctx: SerializerContext): SpecExport6 | null;
128
- import ts13 from "typescript";
129
- declare function formatTypeReference(type: ts13.Type, checker: ts13.TypeChecker): string;
130
- declare function collectReferencedTypes(type: ts13.Type, checker: ts13.TypeChecker, visited?: Set<string>): string[];
247
+ import ts10 from "typescript";
248
+ declare function serializeVariable(node: ts10.VariableDeclaration, statement: ts10.VariableStatement, ctx: SerializerContext): SpecExport6 | null;
131
249
  import { SpecSignatureParameter } from "@openpkg-ts/spec";
132
- import ts14 from "typescript";
133
- declare function extractParameters(signature: ts14.Signature, ctx: SerializerContext): SpecSignatureParameter[];
250
+ import ts11 from "typescript";
251
+ declare function extractParameters(signature: ts11.Signature, ctx: SerializerContext): SpecSignatureParameter[];
134
252
  /**
135
253
  * Recursively register types referenced by a ts.Type.
136
254
  * Uses ctx.visitedTypes to prevent infinite recursion on circular types.
137
255
  */
138
- declare function registerReferencedTypes(type: ts14.Type, ctx: SerializerContext): void;
139
- import { SpecSchema as SpecSchema3 } from "@openpkg-ts/spec";
140
- import ts15 from "typescript";
256
+ declare function registerReferencedTypes(type: ts11.Type, ctx: SerializerContext): void;
257
+ import { SpecSchema } from "@openpkg-ts/spec";
258
+ import ts12 from "typescript";
259
+ /**
260
+ * Built-in type schemas with JSON Schema format hints.
261
+ * Used for types that have specific serialization formats.
262
+ */
263
+ declare const BUILTIN_TYPE_SCHEMAS: Record<string, SpecSchema>;
141
264
  /**
142
265
  * Check if a name is a primitive type
143
266
  */
@@ -149,13 +272,41 @@ declare function isBuiltinGeneric(name: string): boolean;
149
272
  /**
150
273
  * Check if a type is anonymous (no meaningful symbol name)
151
274
  */
152
- declare function isAnonymous(type: ts15.Type): boolean;
275
+ declare function isAnonymous(type: ts12.Type): boolean;
153
276
  /**
154
277
  * Build a structured SpecSchema from a TypeScript type.
155
278
  * Uses $ref for named types and typeArguments for generics.
156
279
  */
157
- declare function buildSchema(type: ts15.Type, checker: ts15.TypeChecker, ctx?: SerializerContext, _depth?: number): SpecSchema3;
158
- import ts16 from "typescript";
159
- declare function isExported(node: ts16.Node): boolean;
160
- declare function getNodeName(node: ts16.Node): string | undefined;
161
- export { zodAdapter, valibotAdapter, typeboxAdapter, serializeVariable, serializeTypeAlias, serializeInterface, serializeFunctionExport, serializeEnum, serializeClass, registerReferencedTypes, registerAdapter, isSchemaType, isPrimitiveName, isExported, isBuiltinGeneric, isAnonymous, getSourceLocation, getNodeName, getJSDocComment, formatTypeReference, findAdapter, extractStandardSchemas, extractSchemaType, extractParameters, extract, createProgram, collectReferencedTypes, buildSchema, arktypeAdapter, TypeRegistry, StandardSchemaResult, SerializerContext, SchemaAdapter, ProgramResult, ProgramOptions, ExtractResult, ExtractOptions, Diagnostic };
280
+ declare function buildSchema(type: ts12.Type, checker: ts12.TypeChecker, ctx?: SerializerContext, _depth?: number): SpecSchema;
281
+ /**
282
+ * Check if a schema is a pure $ref (only has $ref property)
283
+ */
284
+ declare function isPureRefSchema(schema: SpecSchema): schema is {
285
+ $ref: string;
286
+ };
287
+ /**
288
+ * Add description to a schema, handling $ref properly.
289
+ * For pure $ref schemas, wraps in allOf to preserve the reference.
290
+ */
291
+ declare function withDescription(schema: SpecSchema, description: string): SpecSchema;
292
+ /**
293
+ * Check if a schema represents the 'any' type
294
+ */
295
+ declare function schemaIsAny(schema: SpecSchema): boolean;
296
+ /**
297
+ * Deep equality comparison for schemas
298
+ */
299
+ declare function schemasAreEqual(left: SpecSchema, right: SpecSchema): boolean;
300
+ /**
301
+ * Remove duplicate schemas from an array while preserving order.
302
+ */
303
+ declare function deduplicateSchemas(schemas: SpecSchema[]): SpecSchema[];
304
+ /**
305
+ * Find a discriminator property in a union of object types (tagged union pattern).
306
+ * A valid discriminator has a unique literal value in each union member.
307
+ */
308
+ declare function findDiscriminatorProperty(unionTypes: ts12.Type[], checker: ts12.TypeChecker): string | undefined;
309
+ import ts13 from "typescript";
310
+ declare function isExported(node: ts13.Node): boolean;
311
+ declare function getNodeName(node: ts13.Node): string | undefined;
312
+ export { zodAdapter, withDescription, valibotAdapter, typeboxAdapter, serializeVariable, serializeTypeAlias, serializeInterface, serializeFunctionExport, serializeEnum, serializeClass, schemasAreEqual, schemaIsAny, resolveCompiledPath, registerReferencedTypes, registerAdapter, isTypeReference, isSymbolDeprecated, isStandardJSONSchema, isSchemaType, isPureRefSchema, isPrimitiveName, isExported, isBuiltinGeneric, isAnonymous, getSourceLocation, getParamDescription, getNonNullableType, getNodeName, getJSDocComment, findDiscriminatorProperty, findAdapter, extractTypeParameters, extractStandardSchemasFromProject, extractStandardSchemas, extractSchemaType, extractParameters, extract, deduplicateSchemas, createProgram, buildSchema, arktypeAdapter, TypeRegistry, StandardSchemaExtractionResult, StandardSchemaExtractionOutput, StandardJSONSchemaV1, SerializerContext, SchemaExtractionResult, SchemaAdapter, ProgramResult, ProgramOptions, ExtractStandardSchemasOptions, ExtractResult, ExtractOptions, Diagnostic, BUILTIN_TYPE_SCHEMAS };
package/dist/src/index.js CHANGED
@@ -1,78 +1,383 @@
1
1
  import {
2
+ BUILTIN_TYPE_SCHEMAS,
2
3
  TypeRegistry,
3
4
  buildSchema,
4
5
  createProgram,
6
+ deduplicateSchemas,
5
7
  extract,
6
8
  extractParameters,
9
+ extractTypeParameters,
10
+ findDiscriminatorProperty,
7
11
  getJSDocComment,
12
+ getParamDescription,
8
13
  getSourceLocation,
9
14
  isAnonymous,
10
15
  isBuiltinGeneric,
11
16
  isPrimitiveName,
17
+ isPureRefSchema,
18
+ isSymbolDeprecated,
12
19
  registerReferencedTypes,
20
+ schemaIsAny,
21
+ schemasAreEqual,
13
22
  serializeClass,
14
23
  serializeEnum,
15
24
  serializeFunctionExport,
16
25
  serializeInterface,
17
26
  serializeTypeAlias,
18
- serializeVariable
19
- } from "../shared/chunk-ksf9k654.js";
27
+ serializeVariable,
28
+ withDescription
29
+ } from "../shared/chunk-v4cnenxs.js";
30
+ // src/schema/registry.ts
31
+ function isTypeReference(type) {
32
+ return !!(type.flags & 524288 && type.objectFlags && type.objectFlags & 4);
33
+ }
34
+ function getNonNullableType(type) {
35
+ if (type.isUnion()) {
36
+ const nonNullable = type.types.filter((t) => !(t.flags & 32768) && !(t.flags & 65536));
37
+ if (nonNullable.length === 1) {
38
+ return nonNullable[0];
39
+ }
40
+ }
41
+ return type;
42
+ }
43
+ var adapters = [];
44
+ function registerAdapter(adapter) {
45
+ adapters.push(adapter);
46
+ }
47
+ function findAdapter(type, checker) {
48
+ return adapters.find((a) => a.matches(type, checker));
49
+ }
50
+ function isSchemaType(type, checker) {
51
+ return adapters.some((a) => a.matches(type, checker));
52
+ }
53
+ function extractSchemaType(type, checker) {
54
+ const adapter = findAdapter(type, checker);
55
+ if (!adapter)
56
+ return null;
57
+ const outputType = adapter.extractOutputType(type, checker);
58
+ if (!outputType)
59
+ return null;
60
+ const inputType = adapter.extractInputType?.(type, checker);
61
+ return {
62
+ adapter,
63
+ outputType,
64
+ inputType
65
+ };
66
+ }
67
+
20
68
  // src/schema/adapters/arktype.ts
69
+ var ARKTYPE_TYPE_PATTERN = /^Type</;
21
70
  var arktypeAdapter = {
22
- name: "arktype",
23
- detect: () => false,
24
- extract: () => null
71
+ id: "arktype",
72
+ packages: ["arktype"],
73
+ matches(type, checker) {
74
+ const typeName = checker.typeToString(type);
75
+ return ARKTYPE_TYPE_PATTERN.test(typeName);
76
+ },
77
+ extractOutputType(type, checker) {
78
+ if (!isTypeReference(type)) {
79
+ return null;
80
+ }
81
+ const args = checker.getTypeArguments(type);
82
+ if (args.length < 1) {
83
+ return null;
84
+ }
85
+ return args[0];
86
+ },
87
+ extractInputType(type, checker) {
88
+ if (!isTypeReference(type)) {
89
+ return null;
90
+ }
91
+ const args = checker.getTypeArguments(type);
92
+ if (args.length < 2) {
93
+ return null;
94
+ }
95
+ return args[1];
96
+ }
25
97
  };
98
+
26
99
  // src/schema/adapters/typebox.ts
100
+ var TYPEBOX_TYPE_PATTERN = /^T[A-Z]/;
27
101
  var typeboxAdapter = {
28
- name: "typebox",
29
- detect: () => false,
30
- extract: () => null
102
+ id: "typebox",
103
+ packages: ["@sinclair/typebox"],
104
+ matches(type, checker) {
105
+ const typeName = checker.typeToString(type);
106
+ if (!TYPEBOX_TYPE_PATTERN.test(typeName)) {
107
+ return false;
108
+ }
109
+ const typeProperty = type.getProperty("type");
110
+ return typeProperty !== undefined;
111
+ },
112
+ extractOutputType(type, checker) {
113
+ const staticSymbol = type.getProperty("static");
114
+ if (staticSymbol) {
115
+ return checker.getTypeOfSymbol(staticSymbol);
116
+ }
117
+ return null;
118
+ }
31
119
  };
120
+
32
121
  // src/schema/adapters/valibot.ts
122
+ var VALIBOT_TYPE_PATTERN = /Schema(<|$)/;
33
123
  var valibotAdapter = {
34
- name: "valibot",
35
- detect: () => false,
36
- extract: () => null
124
+ id: "valibot",
125
+ packages: ["valibot"],
126
+ matches(type, checker) {
127
+ const typeName = checker.typeToString(type);
128
+ return VALIBOT_TYPE_PATTERN.test(typeName) && !typeName.includes("Zod");
129
+ },
130
+ extractOutputType(type, checker) {
131
+ const typesSymbol = type.getProperty("~types");
132
+ if (!typesSymbol) {
133
+ return null;
134
+ }
135
+ let typesType = checker.getTypeOfSymbol(typesSymbol);
136
+ typesType = getNonNullableType(typesType);
137
+ const outputSymbol = typesType.getProperty("output");
138
+ if (!outputSymbol) {
139
+ return null;
140
+ }
141
+ return checker.getTypeOfSymbol(outputSymbol);
142
+ },
143
+ extractInputType(type, checker) {
144
+ const typesSymbol = type.getProperty("~types");
145
+ if (!typesSymbol) {
146
+ return null;
147
+ }
148
+ let typesType = checker.getTypeOfSymbol(typesSymbol);
149
+ typesType = getNonNullableType(typesType);
150
+ const inputSymbol = typesType.getProperty("input");
151
+ if (!inputSymbol) {
152
+ return null;
153
+ }
154
+ return checker.getTypeOfSymbol(inputSymbol);
155
+ }
37
156
  };
157
+
38
158
  // src/schema/adapters/zod.ts
159
+ var ZOD_TYPE_PATTERN = /^Zod[A-Z]/;
39
160
  var zodAdapter = {
40
- name: "zod",
41
- detect: () => false,
42
- extract: () => null
161
+ id: "zod",
162
+ packages: ["zod"],
163
+ matches(type, checker) {
164
+ const typeName = checker.typeToString(type);
165
+ return ZOD_TYPE_PATTERN.test(typeName);
166
+ },
167
+ extractOutputType(type, checker) {
168
+ const outputSymbol = type.getProperty("_output");
169
+ if (outputSymbol) {
170
+ return checker.getTypeOfSymbol(outputSymbol);
171
+ }
172
+ const typeSymbol = type.getProperty("_type");
173
+ if (typeSymbol) {
174
+ return checker.getTypeOfSymbol(typeSymbol);
175
+ }
176
+ return null;
177
+ },
178
+ extractInputType(type, checker) {
179
+ const inputSymbol = type.getProperty("_input");
180
+ if (inputSymbol) {
181
+ return checker.getTypeOfSymbol(inputSymbol);
182
+ }
183
+ return null;
184
+ }
43
185
  };
44
- // src/schema/registry.ts
45
- var adapters = [];
46
- function registerAdapter(adapter) {
47
- adapters.push(adapter);
186
+
187
+ // src/schema/adapters/index.ts
188
+ registerAdapter(zodAdapter);
189
+ registerAdapter(valibotAdapter);
190
+ registerAdapter(arktypeAdapter);
191
+ registerAdapter(typeboxAdapter);
192
+ // src/schema/standard-schema.ts
193
+ import { spawn } from "node:child_process";
194
+ import * as fs from "node:fs";
195
+ import * as path from "node:path";
196
+ function isStandardJSONSchema(obj) {
197
+ if (typeof obj !== "object" || obj === null)
198
+ return false;
199
+ const std = obj["~standard"];
200
+ if (typeof std !== "object" || std === null)
201
+ return false;
202
+ const stdObj = std;
203
+ if (typeof stdObj.version !== "number")
204
+ return false;
205
+ if (typeof stdObj.vendor !== "string")
206
+ return false;
207
+ const jsonSchema = stdObj.jsonSchema;
208
+ if (typeof jsonSchema !== "object" || jsonSchema === null)
209
+ return false;
210
+ const jsObj = jsonSchema;
211
+ return typeof jsObj.output === "function";
48
212
  }
49
- function findAdapter(node, checker) {
50
- return adapters.find((a) => a.detect(node, checker));
213
+ var WORKER_SCRIPT = `
214
+ const path = require('path');
215
+ const { pathToFileURL } = require('url');
216
+
217
+ // TypeBox detection: schemas have Symbol.for('TypeBox.Kind') and are JSON Schema
218
+ const TYPEBOX_KIND = Symbol.for('TypeBox.Kind');
219
+
220
+ function isTypeBoxSchema(obj) {
221
+ if (!obj || typeof obj !== 'object') return false;
222
+ // TypeBox schemas always have Kind symbol (Union, Object, String, etc.)
223
+ // Also check for common JSON Schema props to avoid false positives
224
+ if (!obj[TYPEBOX_KIND]) return false;
225
+ return typeof obj.type === 'string' || 'anyOf' in obj || 'oneOf' in obj || 'allOf' in obj;
51
226
  }
52
- function isSchemaType(node, checker) {
53
- return adapters.some((a) => a.detect(node, checker));
227
+
228
+ function sanitizeTypeBoxSchema(schema) {
229
+ // JSON.stringify removes symbol keys, keeping only JSON Schema props
230
+ return JSON.parse(JSON.stringify(schema));
54
231
  }
55
- function extractSchemaType(node, checker) {
56
- const adapter = findAdapter(node, checker);
57
- return adapter?.extract(node, checker) ?? null;
232
+
233
+ async function extract() {
234
+ // With node -e, argv is: [node, arg1, arg2, ...]
235
+ // (the -e script is NOT in argv)
236
+ const [modulePath, target] = process.argv.slice(1);
237
+
238
+ try {
239
+ // Import the module using dynamic import (works with ESM and CJS)
240
+ const absPath = path.resolve(modulePath);
241
+ const mod = await import(pathToFileURL(absPath).href);
242
+ const results = [];
243
+
244
+ // Build exports map - handle both ESM and CJS (where exports are in mod.default)
245
+ const exports = {};
246
+ for (const [name, value] of Object.entries(mod)) {
247
+ if (name === 'default' && typeof value === 'object' && value !== null) {
248
+ // CJS module: spread default exports
249
+ Object.assign(exports, value);
250
+ } else if (name !== 'default') {
251
+ exports[name] = value;
252
+ }
253
+ }
254
+
255
+ // Check each export
256
+ for (const [name, value] of Object.entries(exports)) {
257
+ if (name.startsWith('_')) continue;
258
+ if (typeof value !== 'object' || value === null) continue;
259
+
260
+ // Priority 1: Standard Schema (Zod 4.2+, ArkType, etc.)
261
+ const std = value['~standard'];
262
+ if (std && typeof std === 'object' && typeof std.version === 'number' && typeof std.vendor === 'string' && std.jsonSchema && typeof std.jsonSchema.output === 'function') {
263
+ try {
264
+ const outputSchema = std.jsonSchema.output(target);
265
+ const inputSchema = std.jsonSchema.input ? std.jsonSchema.input(target) : undefined;
266
+ results.push({
267
+ exportName: name,
268
+ vendor: std.vendor,
269
+ outputSchema,
270
+ inputSchema
271
+ });
272
+ } catch (e) {
273
+ // Skip schemas that fail to extract
274
+ }
275
+ continue;
276
+ }
277
+
278
+ // Priority 2: TypeBox (schema IS JSON Schema)
279
+ if (isTypeBoxSchema(value)) {
280
+ try {
281
+ results.push({
282
+ exportName: name,
283
+ vendor: 'typebox',
284
+ outputSchema: sanitizeTypeBoxSchema(value)
285
+ });
286
+ } catch (e) {
287
+ // Skip schemas that fail to extract
288
+ }
289
+ continue;
290
+ }
291
+ }
292
+
293
+ console.log(JSON.stringify({ success: true, results }));
294
+ } catch (e) {
295
+ console.log(JSON.stringify({ success: false, error: e.message }));
296
+ }
58
297
  }
59
- // src/schema/standard-schema.ts
60
- function extractStandardSchemas(_program, _entryFile) {
61
- return [];
298
+
299
+ extract();
300
+ `;
301
+ function resolveCompiledPath(tsPath, baseDir) {
302
+ const relativePath = path.relative(baseDir, tsPath);
303
+ const withoutExt = relativePath.replace(/\.tsx?$/, "");
304
+ const candidates = [
305
+ path.join(baseDir, `${withoutExt}.js`),
306
+ path.join(baseDir, "dist", `${withoutExt.replace(/^src\//, "")}.js`),
307
+ path.join(baseDir, "build", `${withoutExt.replace(/^src\//, "")}.js`),
308
+ path.join(baseDir, "lib", `${withoutExt.replace(/^src\//, "")}.js`)
309
+ ];
310
+ for (const candidate of candidates) {
311
+ if (fs.existsSync(candidate)) {
312
+ return candidate;
313
+ }
314
+ }
315
+ return null;
62
316
  }
63
- // src/types/formatter.ts
64
- function formatTypeReference(type, checker) {
65
- return checker.typeToString(type);
317
+ async function extractStandardSchemas(compiledJsPath, options = {}) {
318
+ const { timeout = 1e4, target = "draft-2020-12" } = options;
319
+ const result = {
320
+ schemas: new Map,
321
+ errors: []
322
+ };
323
+ if (!fs.existsSync(compiledJsPath)) {
324
+ result.errors.push(`Compiled JS not found: ${compiledJsPath}`);
325
+ return result;
326
+ }
327
+ return new Promise((resolve) => {
328
+ const child = spawn("node", ["-e", WORKER_SCRIPT, compiledJsPath, target], {
329
+ timeout,
330
+ stdio: ["ignore", "pipe", "pipe"]
331
+ });
332
+ let stdout = "";
333
+ let stderr = "";
334
+ child.stdout.on("data", (data) => {
335
+ stdout += data.toString();
336
+ });
337
+ child.stderr.on("data", (data) => {
338
+ stderr += data.toString();
339
+ });
340
+ child.on("close", (code) => {
341
+ if (code !== 0) {
342
+ result.errors.push(`Extraction process failed: ${stderr || `exit code ${code}`}`);
343
+ resolve(result);
344
+ return;
345
+ }
346
+ try {
347
+ const parsed = JSON.parse(stdout);
348
+ if (!parsed.success) {
349
+ result.errors.push(`Extraction failed: ${parsed.error}`);
350
+ resolve(result);
351
+ return;
352
+ }
353
+ for (const item of parsed.results) {
354
+ result.schemas.set(item.exportName, {
355
+ exportName: item.exportName,
356
+ vendor: item.vendor,
357
+ outputSchema: item.outputSchema,
358
+ inputSchema: item.inputSchema
359
+ });
360
+ }
361
+ } catch (e) {
362
+ result.errors.push(`Failed to parse extraction output: ${e}`);
363
+ }
364
+ resolve(result);
365
+ });
366
+ child.on("error", (err) => {
367
+ result.errors.push(`Subprocess error: ${err.message}`);
368
+ resolve(result);
369
+ });
370
+ });
66
371
  }
67
- function collectReferencedTypes(type, checker, visited = new Set) {
68
- const symbol = type.getSymbol();
69
- if (!symbol)
70
- return [];
71
- const name = symbol.getName();
72
- if (visited.has(name))
73
- return [];
74
- visited.add(name);
75
- return [name];
372
+ async function extractStandardSchemasFromProject(entryFile, baseDir, options = {}) {
373
+ const compiledPath = resolveCompiledPath(entryFile, baseDir);
374
+ if (!compiledPath) {
375
+ return {
376
+ schemas: new Map,
377
+ errors: [`Could not find compiled JS for ${entryFile}. Build the project first.`]
378
+ };
379
+ }
380
+ return extractStandardSchemas(compiledPath, options);
76
381
  }
77
382
  // src/types/utils.ts
78
383
  function isExported(node) {
@@ -90,6 +395,7 @@ function getNodeName(node) {
90
395
  }
91
396
  export {
92
397
  zodAdapter,
398
+ withDescription,
93
399
  valibotAdapter,
94
400
  typeboxAdapter,
95
401
  serializeVariable,
@@ -98,25 +404,37 @@ export {
98
404
  serializeFunctionExport,
99
405
  serializeEnum,
100
406
  serializeClass,
407
+ schemasAreEqual,
408
+ schemaIsAny,
409
+ resolveCompiledPath,
101
410
  registerReferencedTypes,
102
411
  registerAdapter,
412
+ isTypeReference,
413
+ isSymbolDeprecated,
414
+ isStandardJSONSchema,
103
415
  isSchemaType,
416
+ isPureRefSchema,
104
417
  isPrimitiveName,
105
418
  isExported,
106
419
  isBuiltinGeneric,
107
420
  isAnonymous,
108
421
  getSourceLocation,
422
+ getParamDescription,
423
+ getNonNullableType,
109
424
  getNodeName,
110
425
  getJSDocComment,
111
- formatTypeReference,
426
+ findDiscriminatorProperty,
112
427
  findAdapter,
428
+ extractTypeParameters,
429
+ extractStandardSchemasFromProject,
113
430
  extractStandardSchemas,
114
431
  extractSchemaType,
115
432
  extractParameters,
116
433
  extract,
434
+ deduplicateSchemas,
117
435
  createProgram,
118
- collectReferencedTypes,
119
436
  buildSchema,
120
437
  arktypeAdapter,
121
- TypeRegistry
438
+ TypeRegistry,
439
+ BUILTIN_TYPE_SCHEMAS
122
440
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpkg-ts/extract",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "TypeScript export extraction to OpenPkg spec",
5
5
  "keywords": [
6
6
  "openpkg",