@openpkg-ts/extract 0.20.0 → 0.21.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-rej3ws8m.js";
4
+ } from "../shared/chunk-yh8v9dbt.js";
5
5
 
6
6
  // src/cli/spec.ts
7
7
  import * as fs from "node:fs";
@@ -451,13 +451,21 @@ function createProgram() {
451
451
  }
452
452
  fs.writeFileSync(options.output, JSON.stringify(normalized, null, 2));
453
453
  spin.success(`Extracted to ${options.output}`);
454
+ if (result.runtimeSchemas) {
455
+ const { extracted, merged, vendors } = result.runtimeSchemas;
456
+ console.log(`ℹ Runtime schemas: ${merged}/${extracted} merged (${vendors.join(", ")})`);
457
+ }
454
458
  for (const diag of result.diagnostics) {
455
459
  if (diag.severity === "info" && !options.verbose)
456
460
  continue;
457
461
  const prefix2 = diag.severity === "error" ? "✗" : diag.severity === "warning" ? "⚠" : "ℹ";
458
462
  console.log(`${prefix2} ${diag.message}`);
459
463
  }
460
- summary().addKeyValue("Exports", normalized.exports.length).addKeyValue("Types", normalized.types?.length || 0).print();
464
+ const sum = summary().addKeyValue("Exports", normalized.exports.length).addKeyValue("Types", normalized.types?.length || 0);
465
+ if (result.runtimeSchemas) {
466
+ sum.addKeyValue("Runtime Schemas", result.runtimeSchemas.merged);
467
+ }
468
+ sum.print();
461
469
  });
462
470
  return program;
463
471
  }
@@ -164,8 +164,8 @@ function buildSchema(type, checker, ctx, _depth = 0) {
164
164
  return { type: "number", enum: [literal] };
165
165
  }
166
166
  if (type.flags & ts.TypeFlags.BooleanLiteral) {
167
- const intrinsicName = type.intrinsicName;
168
- return { type: "boolean", enum: [intrinsicName === "true"] };
167
+ const typeString = checker.typeToString(type);
168
+ return { type: "boolean", enum: [typeString === "true"] };
169
169
  }
170
170
  if (type.isUnion()) {
171
171
  const types = type.types;
@@ -329,6 +329,9 @@ function isPureRefSchema(schema) {
329
329
  return typeof schema === "object" && Object.keys(schema).length === 1 && "$ref" in schema;
330
330
  }
331
331
  function withDescription(schema, description) {
332
+ if (typeof schema === "string") {
333
+ return { type: schema, description };
334
+ }
332
335
  if (isPureRefSchema(schema)) {
333
336
  return {
334
337
  allOf: [schema],
@@ -702,9 +705,9 @@ function getJSDocComment(node) {
702
705
  }
703
706
  return { name: tag.tagName.text, text: rawText };
704
707
  });
705
- const jsDocComments = node.jsDoc;
708
+ const jsDocComments = ts3.getJSDocCommentsAndTags(node).filter(ts3.isJSDoc);
706
709
  let description;
707
- if (jsDocComments && jsDocComments.length > 0) {
710
+ if (jsDocComments.length > 0) {
708
711
  const firstDoc = jsDocComments[0];
709
712
  if (firstDoc.comment) {
710
713
  description = typeof firstDoc.comment === "string" ? firstDoc.comment : ts3.getTextOfJSDocComment(firstDoc.comment);
@@ -1076,7 +1079,7 @@ function getMemberName(member) {
1076
1079
  return member.name.getText();
1077
1080
  }
1078
1081
  function getVisibility(member) {
1079
- const modifiers = ts6.getModifiers(member);
1082
+ const modifiers = ts6.canHaveModifiers(member) ? ts6.getModifiers(member) : undefined;
1080
1083
  if (!modifiers)
1081
1084
  return;
1082
1085
  for (const mod of modifiers) {
@@ -1090,11 +1093,11 @@ function getVisibility(member) {
1090
1093
  return;
1091
1094
  }
1092
1095
  function isStatic(member) {
1093
- const modifiers = ts6.getModifiers(member);
1096
+ const modifiers = ts6.canHaveModifiers(member) ? ts6.getModifiers(member) : undefined;
1094
1097
  return modifiers?.some((m) => m.kind === ts6.SyntaxKind.StaticKeyword) ?? false;
1095
1098
  }
1096
1099
  function isReadonly(member) {
1097
- const modifiers = ts6.getModifiers(member);
1100
+ const modifiers = ts6.canHaveModifiers(member) ? ts6.getModifiers(member) : undefined;
1098
1101
  return modifiers?.some((m) => m.kind === ts6.SyntaxKind.ReadonlyKeyword) ?? false;
1099
1102
  }
1100
1103
  function serializeProperty(node, ctx) {
@@ -1463,7 +1466,7 @@ function getInterfaceExtends(node, checker) {
1463
1466
  const type = checker.getTypeAtLocation(expr);
1464
1467
  return type.getSymbol()?.getName() ?? expr.expression.getText();
1465
1468
  });
1466
- return names.length === 1 ? names[0] : names;
1469
+ return names.join(" & ");
1467
1470
  }
1468
1471
  }
1469
1472
  return;
@@ -1519,9 +1522,240 @@ function serializeVariable(node, statement, ctx) {
1519
1522
  };
1520
1523
  }
1521
1524
 
1522
- // src/builder/spec-builder.ts
1525
+ // src/schema/standard-schema.ts
1526
+ import { spawn } from "node:child_process";
1523
1527
  import * as fs from "node:fs";
1524
1528
  import * as path2 from "node:path";
1529
+ function isStandardJSONSchema(obj) {
1530
+ if (typeof obj !== "object" || obj === null)
1531
+ return false;
1532
+ const std = obj["~standard"];
1533
+ if (typeof std !== "object" || std === null)
1534
+ return false;
1535
+ const stdObj = std;
1536
+ if (stdObj.version !== 1)
1537
+ return false;
1538
+ if (typeof stdObj.vendor !== "string")
1539
+ return false;
1540
+ const jsonSchema = stdObj.jsonSchema;
1541
+ if (typeof jsonSchema !== "object" || jsonSchema === null)
1542
+ return false;
1543
+ const jsObj = jsonSchema;
1544
+ return typeof jsObj.output === "function" && typeof jsObj.input === "function";
1545
+ }
1546
+ var WORKER_SCRIPT = `
1547
+ const path = require('path');
1548
+ const { pathToFileURL } = require('url');
1549
+
1550
+ // TypeBox detection: schemas have Symbol.for('TypeBox.Kind') and are JSON Schema
1551
+ const TYPEBOX_KIND = Symbol.for('TypeBox.Kind');
1552
+
1553
+ function isTypeBoxSchema(obj) {
1554
+ if (!obj || typeof obj !== 'object') return false;
1555
+ // TypeBox schemas always have Kind symbol (Union, Object, String, etc.)
1556
+ // Also check for common JSON Schema props to avoid false positives
1557
+ if (!obj[TYPEBOX_KIND]) return false;
1558
+ return typeof obj.type === 'string' || 'anyOf' in obj || 'oneOf' in obj || 'allOf' in obj;
1559
+ }
1560
+
1561
+ function sanitizeTypeBoxSchema(schema) {
1562
+ // JSON.stringify removes symbol keys, keeping only JSON Schema props
1563
+ return JSON.parse(JSON.stringify(schema));
1564
+ }
1565
+
1566
+ async function extract() {
1567
+ // With node -e, argv is: [node, arg1, arg2, ...]
1568
+ // (the -e script is NOT in argv)
1569
+ const [modulePath, optionsJson] = process.argv.slice(1);
1570
+ const { target, libraryOptions } = JSON.parse(optionsJson || '{}');
1571
+
1572
+ try {
1573
+ // Import the module using dynamic import (works with ESM and CJS)
1574
+ const absPath = path.resolve(modulePath);
1575
+ const mod = await import(pathToFileURL(absPath).href);
1576
+ const results = [];
1577
+
1578
+ // Build exports map - handle both ESM and CJS (where exports are in mod.default)
1579
+ const exports = {};
1580
+ for (const [name, value] of Object.entries(mod)) {
1581
+ if (name === 'default' && typeof value === 'object' && value !== null) {
1582
+ // CJS module: spread default exports
1583
+ Object.assign(exports, value);
1584
+ } else if (name !== 'default') {
1585
+ exports[name] = value;
1586
+ }
1587
+ }
1588
+
1589
+ // Check each export
1590
+ for (const [name, value] of Object.entries(exports)) {
1591
+ if (name.startsWith('_')) continue;
1592
+ if (typeof value !== 'object' || value === null) continue;
1593
+
1594
+ // Priority 1: Standard JSON Schema (Zod 4.2+, ArkType 2.1.28+, Valibot 1.2+)
1595
+ const std = value['~standard'];
1596
+ if (std && typeof std === 'object' && std.version === 1 && typeof std.vendor === 'string' && std.jsonSchema && typeof std.jsonSchema.output === 'function') {
1597
+ try {
1598
+ // Per spec: pass options object with target and optional libraryOptions
1599
+ const options = { target: target || 'draft-2020-12', ...(libraryOptions && { libraryOptions }) };
1600
+ const outputSchema = std.jsonSchema.output(options);
1601
+ const inputSchema = typeof std.jsonSchema.input === 'function' ? std.jsonSchema.input(options) : undefined;
1602
+ results.push({
1603
+ exportName: name,
1604
+ vendor: std.vendor,
1605
+ outputSchema,
1606
+ inputSchema
1607
+ });
1608
+ } catch (e) {
1609
+ // Skip schemas that fail to extract
1610
+ }
1611
+ continue;
1612
+ }
1613
+
1614
+ // Priority 2: TypeBox (schema IS JSON Schema)
1615
+ if (isTypeBoxSchema(value)) {
1616
+ try {
1617
+ results.push({
1618
+ exportName: name,
1619
+ vendor: 'typebox',
1620
+ outputSchema: sanitizeTypeBoxSchema(value)
1621
+ });
1622
+ } catch (e) {
1623
+ // Skip schemas that fail to extract
1624
+ }
1625
+ continue;
1626
+ }
1627
+ }
1628
+
1629
+ console.log(JSON.stringify({ success: true, results }));
1630
+ } catch (e) {
1631
+ console.log(JSON.stringify({ success: false, error: e.message }));
1632
+ }
1633
+ }
1634
+
1635
+ extract();
1636
+ `;
1637
+ function readTsconfigOutDir(baseDir) {
1638
+ const tsconfigPath = path2.join(baseDir, "tsconfig.json");
1639
+ try {
1640
+ if (!fs.existsSync(tsconfigPath)) {
1641
+ return null;
1642
+ }
1643
+ const content = fs.readFileSync(tsconfigPath, "utf-8");
1644
+ const stripped = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
1645
+ const tsconfig = JSON.parse(stripped);
1646
+ if (tsconfig.compilerOptions?.outDir) {
1647
+ return tsconfig.compilerOptions.outDir.replace(/^\.\//, "");
1648
+ }
1649
+ } catch {}
1650
+ return null;
1651
+ }
1652
+ function resolveCompiledPath(tsPath, baseDir) {
1653
+ const relativePath = path2.relative(baseDir, tsPath);
1654
+ const withoutExt = relativePath.replace(/\.tsx?$/, "");
1655
+ const srcPrefix = withoutExt.replace(/^src\//, "");
1656
+ const tsconfigOutDir = readTsconfigOutDir(baseDir);
1657
+ const extensions = [".js", ".mjs", ".cjs"];
1658
+ const candidates = [];
1659
+ if (tsconfigOutDir) {
1660
+ for (const ext of extensions) {
1661
+ candidates.push(path2.join(baseDir, tsconfigOutDir, `${srcPrefix}${ext}`));
1662
+ }
1663
+ }
1664
+ const commonOutDirs = ["dist", "build", "lib", "out"];
1665
+ for (const outDir of commonOutDirs) {
1666
+ if (outDir === tsconfigOutDir)
1667
+ continue;
1668
+ for (const ext of extensions) {
1669
+ candidates.push(path2.join(baseDir, outDir, `${srcPrefix}${ext}`));
1670
+ }
1671
+ }
1672
+ for (const ext of extensions) {
1673
+ candidates.push(path2.join(baseDir, `${withoutExt}${ext}`));
1674
+ }
1675
+ const workspaceMatch = baseDir.match(/^(.+\/packages\/[^/]+)$/);
1676
+ if (workspaceMatch) {
1677
+ const pkgRoot = workspaceMatch[1];
1678
+ for (const ext of extensions) {
1679
+ candidates.push(path2.join(pkgRoot, "dist", `${srcPrefix}${ext}`));
1680
+ }
1681
+ }
1682
+ for (const candidate of candidates) {
1683
+ if (fs.existsSync(candidate)) {
1684
+ return candidate;
1685
+ }
1686
+ }
1687
+ return null;
1688
+ }
1689
+ async function extractStandardSchemas(compiledJsPath, options = {}) {
1690
+ const { timeout = 1e4, target = "draft-2020-12", libraryOptions } = options;
1691
+ const result = {
1692
+ schemas: new Map,
1693
+ errors: []
1694
+ };
1695
+ if (!fs.existsSync(compiledJsPath)) {
1696
+ result.errors.push(`Compiled JS not found: ${compiledJsPath}`);
1697
+ return result;
1698
+ }
1699
+ const optionsJson = JSON.stringify({ target, libraryOptions });
1700
+ return new Promise((resolve) => {
1701
+ const child = spawn("node", ["-e", WORKER_SCRIPT, compiledJsPath, optionsJson], {
1702
+ timeout,
1703
+ stdio: ["ignore", "pipe", "pipe"]
1704
+ });
1705
+ let stdout = "";
1706
+ let stderr = "";
1707
+ child.stdout.on("data", (data) => {
1708
+ stdout += data.toString();
1709
+ });
1710
+ child.stderr.on("data", (data) => {
1711
+ stderr += data.toString();
1712
+ });
1713
+ child.on("close", (code) => {
1714
+ if (code !== 0) {
1715
+ result.errors.push(`Extraction process failed: ${stderr || `exit code ${code}`}`);
1716
+ resolve(result);
1717
+ return;
1718
+ }
1719
+ try {
1720
+ const parsed = JSON.parse(stdout);
1721
+ if (!parsed.success) {
1722
+ result.errors.push(`Extraction failed: ${parsed.error}`);
1723
+ resolve(result);
1724
+ return;
1725
+ }
1726
+ for (const item of parsed.results) {
1727
+ result.schemas.set(item.exportName, {
1728
+ exportName: item.exportName,
1729
+ vendor: item.vendor,
1730
+ outputSchema: item.outputSchema,
1731
+ inputSchema: item.inputSchema
1732
+ });
1733
+ }
1734
+ } catch (e) {
1735
+ result.errors.push(`Failed to parse extraction output: ${e}`);
1736
+ }
1737
+ resolve(result);
1738
+ });
1739
+ child.on("error", (err) => {
1740
+ result.errors.push(`Subprocess error: ${err.message}`);
1741
+ resolve(result);
1742
+ });
1743
+ });
1744
+ }
1745
+ async function extractStandardSchemasFromProject(entryFile, baseDir, options = {}) {
1746
+ const compiledPath = resolveCompiledPath(entryFile, baseDir);
1747
+ if (!compiledPath) {
1748
+ return {
1749
+ schemas: new Map,
1750
+ errors: [`Could not find compiled JS for ${entryFile}. Build the project first.`]
1751
+ };
1752
+ }
1753
+ return extractStandardSchemas(compiledPath, options);
1754
+ }
1755
+
1756
+ // src/builder/spec-builder.ts
1757
+ import * as fs2 from "node:fs";
1758
+ import * as path3 from "node:path";
1525
1759
  import { SCHEMA_URL, SCHEMA_VERSION } from "@openpkg-ts/spec";
1526
1760
  import ts8 from "typescript";
1527
1761
 
@@ -1541,6 +1775,34 @@ function createContext(program, sourceFile, options = {}) {
1541
1775
  };
1542
1776
  }
1543
1777
 
1778
+ // src/builder/schema-merger.ts
1779
+ function mergeRuntimeSchemas(staticExports, runtimeSchemas) {
1780
+ let merged = 0;
1781
+ const exports = staticExports.map((exp) => {
1782
+ const runtime = runtimeSchemas.get(exp.name);
1783
+ if (!runtime)
1784
+ return exp;
1785
+ merged++;
1786
+ const mergedExport = {
1787
+ ...exp,
1788
+ schema: runtime.outputSchema,
1789
+ tags: [
1790
+ ...exp.tags || [],
1791
+ { name: "vendor", text: runtime.vendor },
1792
+ { name: "schema-source", text: "standard-json-schema" }
1793
+ ]
1794
+ };
1795
+ if (runtime.inputSchema && JSON.stringify(runtime.inputSchema) !== JSON.stringify(runtime.outputSchema)) {
1796
+ mergedExport.flags = {
1797
+ ...mergedExport.flags,
1798
+ inputSchema: runtime.inputSchema
1799
+ };
1800
+ }
1801
+ return mergedExport;
1802
+ });
1803
+ return { merged, exports };
1804
+ }
1805
+
1544
1806
  // src/builder/spec-builder.ts
1545
1807
  var BUILTIN_TYPES2 = new Set([
1546
1808
  "Array",
@@ -1638,7 +1900,7 @@ async function extract(options) {
1638
1900
  ignore
1639
1901
  } = options;
1640
1902
  const diagnostics = [];
1641
- const exports = [];
1903
+ let exports = [];
1642
1904
  const result = createProgram({ entryFile, baseDir, content });
1643
1905
  const { program, sourceFile } = result;
1644
1906
  if (!sourceFile) {
@@ -1715,6 +1977,41 @@ async function extract(options) {
1715
1977
  code: "EXTERNAL_TYPES"
1716
1978
  });
1717
1979
  }
1980
+ let runtimeMetadata;
1981
+ if (options.schemaExtraction === "hybrid") {
1982
+ const projectBaseDir = baseDir || path3.dirname(entryFile);
1983
+ const compiledPath = resolveCompiledPath(entryFile, projectBaseDir);
1984
+ if (compiledPath) {
1985
+ const runtimeResult = await extractStandardSchemas(compiledPath, {
1986
+ target: options.schemaTarget || "draft-2020-12",
1987
+ timeout: 15000
1988
+ });
1989
+ if (runtimeResult.schemas.size > 0) {
1990
+ const mergeResult = mergeRuntimeSchemas(exports, runtimeResult.schemas);
1991
+ exports = mergeResult.exports;
1992
+ runtimeMetadata = {
1993
+ extracted: runtimeResult.schemas.size,
1994
+ merged: mergeResult.merged,
1995
+ vendors: [...new Set([...runtimeResult.schemas.values()].map((s) => s.vendor))],
1996
+ errors: runtimeResult.errors
1997
+ };
1998
+ }
1999
+ for (const error of runtimeResult.errors) {
2000
+ diagnostics.push({
2001
+ message: `Runtime schema extraction: ${error}`,
2002
+ severity: "warning",
2003
+ code: "RUNTIME_SCHEMA_ERROR"
2004
+ });
2005
+ }
2006
+ } else {
2007
+ diagnostics.push({
2008
+ message: "Hybrid mode: Could not find compiled JS. Build the project first.",
2009
+ severity: "warning",
2010
+ code: "NO_COMPILED_JS",
2011
+ suggestion: "Run `npm run build` or `tsc` before extraction"
2012
+ });
2013
+ }
2014
+ }
1718
2015
  const spec = {
1719
2016
  ...includeSchema ? { $schema: SCHEMA_URL } : {},
1720
2017
  openpkg: SCHEMA_VERSION,
@@ -1723,14 +2020,16 @@ async function extract(options) {
1723
2020
  types,
1724
2021
  generation: {
1725
2022
  generator: "@openpkg-ts/extract",
1726
- timestamp: new Date().toISOString()
2023
+ timestamp: new Date().toISOString(),
2024
+ ...options.schemaExtraction === "hybrid" ? { schemaExtraction: "hybrid" } : {}
1727
2025
  }
1728
2026
  };
1729
2027
  const internalForgotten = forgottenExports.filter((f) => !f.isExternal);
1730
2028
  return {
1731
2029
  spec,
1732
2030
  diagnostics,
1733
- ...internalForgotten.length > 0 ? { forgottenExports: internalForgotten } : {}
2031
+ ...internalForgotten.length > 0 ? { forgottenExports: internalForgotten } : {},
2032
+ ...runtimeMetadata ? { runtimeSchemas: runtimeMetadata } : {}
1734
2033
  };
1735
2034
  }
1736
2035
  function collectAllRefsWithContext(obj, refs, state) {
@@ -2036,7 +2335,7 @@ function createEmptySpec(entryFile, includeSchema) {
2036
2335
  return {
2037
2336
  ...includeSchema ? { $schema: SCHEMA_URL } : {},
2038
2337
  openpkg: SCHEMA_VERSION,
2039
- meta: { name: path2.basename(entryFile, path2.extname(entryFile)) },
2338
+ meta: { name: path3.basename(entryFile, path3.extname(entryFile)) },
2040
2339
  exports: [],
2041
2340
  generation: {
2042
2341
  generator: "@openpkg-ts/extract",
@@ -2045,18 +2344,18 @@ function createEmptySpec(entryFile, includeSchema) {
2045
2344
  };
2046
2345
  }
2047
2346
  async function getPackageMeta(entryFile, baseDir) {
2048
- const searchDir = baseDir ?? path2.dirname(entryFile);
2049
- const pkgPath = path2.join(searchDir, "package.json");
2347
+ const searchDir = baseDir ?? path3.dirname(entryFile);
2348
+ const pkgPath = path3.join(searchDir, "package.json");
2050
2349
  try {
2051
- if (fs.existsSync(pkgPath)) {
2052
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
2350
+ if (fs2.existsSync(pkgPath)) {
2351
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
2053
2352
  return {
2054
- name: pkg.name ?? path2.basename(searchDir),
2353
+ name: pkg.name ?? path3.basename(searchDir),
2055
2354
  version: pkg.version,
2056
2355
  description: pkg.description
2057
2356
  };
2058
2357
  }
2059
2358
  } catch {}
2060
- return { name: path2.basename(searchDir) };
2359
+ return { name: path3.basename(searchDir) };
2061
2360
  }
2062
- export { BUILTIN_TYPE_SCHEMAS, isPrimitiveName, isBuiltinGeneric, isAnonymous, buildSchema, isPureRefSchema, withDescription, schemaIsAny, schemasAreEqual, deduplicateSchemas, findDiscriminatorProperty, TypeRegistry, getJSDocComment, getSourceLocation, getParamDescription, extractTypeParameters, isSymbolDeprecated, createProgram, extractParameters, registerReferencedTypes, serializeClass, serializeEnum, serializeFunctionExport, serializeInterface, serializeTypeAlias, serializeVariable, extract };
2361
+ export { BUILTIN_TYPE_SCHEMAS, isPrimitiveName, isBuiltinGeneric, isAnonymous, buildSchema, isPureRefSchema, withDescription, schemaIsAny, schemasAreEqual, deduplicateSchemas, findDiscriminatorProperty, TypeRegistry, getJSDocComment, getSourceLocation, getParamDescription, extractTypeParameters, isSymbolDeprecated, createProgram, extractParameters, registerReferencedTypes, serializeClass, serializeEnum, serializeFunctionExport, serializeInterface, serializeTypeAlias, serializeVariable, isStandardJSONSchema, resolveCompiledPath, extractStandardSchemas, extractStandardSchemasFromProject, extract };
@@ -73,6 +73,8 @@ interface ExtractOptions {
73
73
  maxExternalTypeDepth?: number;
74
74
  resolveExternalTypes?: boolean;
75
75
  schemaExtraction?: "static" | "hybrid";
76
+ /** Target JSON Schema dialect for runtime schema extraction */
77
+ schemaTarget?: "draft-2020-12" | "draft-07" | "openapi-3.0";
76
78
  /** Include $schema URL in output */
77
79
  includeSchema?: boolean;
78
80
  /** Only extract these exports (supports * wildcards) */
@@ -84,6 +86,17 @@ interface ExtractResult {
84
86
  spec: OpenPkg;
85
87
  diagnostics: Diagnostic[];
86
88
  forgottenExports?: ForgottenExport[];
89
+ /** Metadata about runtime schema extraction (when schemaExtraction: 'hybrid') */
90
+ runtimeSchemas?: {
91
+ /** Number of schema exports found */
92
+ extracted: number;
93
+ /** Number of schemas successfully merged with static types */
94
+ merged: number;
95
+ /** Schema vendors detected (e.g., 'zod', 'arktype', 'valibot', 'typebox') */
96
+ vendors: string[];
97
+ /** Any errors encountered during runtime extraction */
98
+ errors: string[];
99
+ };
87
100
  }
88
101
  interface Diagnostic {
89
102
  message: string;
@@ -112,73 +125,6 @@ interface ForgottenExport {
112
125
  fix?: string;
113
126
  }
114
127
  declare function extract(options: ExtractOptions): Promise<ExtractResult>;
115
- import ts4 from "typescript";
116
- interface ProgramOptions {
117
- entryFile: string;
118
- baseDir?: string;
119
- content?: string;
120
- }
121
- interface ProgramResult {
122
- program: ts4.Program;
123
- compilerHost: ts4.CompilerHost;
124
- compilerOptions: ts4.CompilerOptions;
125
- sourceFile?: ts4.SourceFile;
126
- configPath?: string;
127
- }
128
- declare function createProgram({ entryFile, baseDir, content }: ProgramOptions): ProgramResult;
129
- import * as TS from "typescript";
130
- /**
131
- * A schema adapter can detect and extract output types from a specific
132
- * schema validation library.
133
- */
134
- interface SchemaAdapter {
135
- /** Unique identifier for this adapter */
136
- readonly id: string;
137
- /** npm package name(s) this adapter handles */
138
- readonly packages: readonly string[];
139
- /**
140
- * Check if a type matches this adapter's schema library.
141
- * Should be fast - called for every export.
142
- */
143
- matches(type: TS.Type, checker: TS.TypeChecker): boolean;
144
- /**
145
- * Extract the output type from a schema type.
146
- * Returns null if extraction fails.
147
- */
148
- extractOutputType(type: TS.Type, checker: TS.TypeChecker): TS.Type | null;
149
- /**
150
- * Extract the input type from a schema type (optional).
151
- * Useful for transforms where input differs from output.
152
- */
153
- extractInputType?(type: TS.Type, checker: TS.TypeChecker): TS.Type | null;
154
- }
155
- /**
156
- * Result of schema type extraction
157
- */
158
- interface SchemaExtractionResult {
159
- /** The adapter that matched */
160
- adapter: SchemaAdapter;
161
- /** The extracted output type */
162
- outputType: TS.Type;
163
- /** The extracted input type (if different from output) */
164
- inputType?: TS.Type;
165
- }
166
- /**
167
- * Utility: Check if type is an object type reference (has type arguments)
168
- */
169
- declare function isTypeReference(type: TS.Type): type is TS.TypeReference;
170
- /**
171
- * Utility: Remove undefined/null from a union type
172
- */
173
- declare function getNonNullableType(type: TS.Type): TS.Type;
174
- declare function registerAdapter(adapter: SchemaAdapter): void;
175
- declare function findAdapter(type: TS.Type, checker: TS.TypeChecker): SchemaAdapter | undefined;
176
- declare function isSchemaType(type: TS.Type, checker: TS.TypeChecker): boolean;
177
- declare function extractSchemaType(type: TS.Type, checker: TS.TypeChecker): SchemaExtractionResult | null;
178
- declare const arktypeAdapter: SchemaAdapter;
179
- declare const typeboxAdapter: SchemaAdapter;
180
- declare const valibotAdapter: SchemaAdapter;
181
- declare const zodAdapter: SchemaAdapter;
182
128
  /**
183
129
  * Target version for JSON Schema generation.
184
130
  * @see https://standardschema.dev/json-schema
@@ -254,7 +200,8 @@ interface StandardSchemaExtractionOutput {
254
200
  declare function isStandardJSONSchema(obj: unknown): obj is StandardJSONSchemaV1;
255
201
  /**
256
202
  * Resolve compiled JS path from TypeScript source.
257
- * Tries common output locations: dist/, build/, lib/, same dir.
203
+ * Reads tsconfig.json for outDir and tries multiple output patterns.
204
+ * Supports .js, .mjs, and .cjs extensions.
258
205
  */
259
206
  declare function resolveCompiledPath(tsPath: string, baseDir: string): string | null;
260
207
  /**
@@ -278,24 +225,91 @@ declare function extractStandardSchemas(compiledJsPath: string, options?: Extrac
278
225
  * @param options - Extraction options
279
226
  */
280
227
  declare function extractStandardSchemasFromProject(entryFile: string, baseDir: string, options?: ExtractStandardSchemasOptions): Promise<StandardSchemaExtractionOutput>;
281
- import { SpecExport } from "@openpkg-ts/spec";
282
- import ts5 from "typescript";
283
- declare function serializeClass(node: ts5.ClassDeclaration, ctx: SerializerContext): SpecExport | null;
228
+ import ts4 from "typescript";
229
+ interface ProgramOptions {
230
+ entryFile: string;
231
+ baseDir?: string;
232
+ content?: string;
233
+ }
234
+ interface ProgramResult {
235
+ program: ts4.Program;
236
+ compilerHost: ts4.CompilerHost;
237
+ compilerOptions: ts4.CompilerOptions;
238
+ sourceFile?: ts4.SourceFile;
239
+ configPath?: string;
240
+ }
241
+ declare function createProgram({ entryFile, baseDir, content }: ProgramOptions): ProgramResult;
242
+ import * as TS from "typescript";
243
+ /**
244
+ * A schema adapter can detect and extract output types from a specific
245
+ * schema validation library.
246
+ */
247
+ interface SchemaAdapter {
248
+ /** Unique identifier for this adapter */
249
+ readonly id: string;
250
+ /** npm package name(s) this adapter handles */
251
+ readonly packages: readonly string[];
252
+ /**
253
+ * Check if a type matches this adapter's schema library.
254
+ * Should be fast - called for every export.
255
+ */
256
+ matches(type: TS.Type, checker: TS.TypeChecker): boolean;
257
+ /**
258
+ * Extract the output type from a schema type.
259
+ * Returns null if extraction fails.
260
+ */
261
+ extractOutputType(type: TS.Type, checker: TS.TypeChecker): TS.Type | null;
262
+ /**
263
+ * Extract the input type from a schema type (optional).
264
+ * Useful for transforms where input differs from output.
265
+ */
266
+ extractInputType?(type: TS.Type, checker: TS.TypeChecker): TS.Type | null;
267
+ }
268
+ /**
269
+ * Result of schema type extraction
270
+ */
271
+ interface SchemaExtractionResult {
272
+ /** The adapter that matched */
273
+ adapter: SchemaAdapter;
274
+ /** The extracted output type */
275
+ outputType: TS.Type;
276
+ /** The extracted input type (if different from output) */
277
+ inputType?: TS.Type;
278
+ }
279
+ /**
280
+ * Utility: Check if type is an object type reference (has type arguments)
281
+ */
282
+ declare function isTypeReference(type: TS.Type): type is TS.TypeReference;
283
+ /**
284
+ * Utility: Remove undefined/null from a union type
285
+ */
286
+ declare function getNonNullableType(type: TS.Type): TS.Type;
287
+ declare function registerAdapter(adapter: SchemaAdapter): void;
288
+ declare function findAdapter(type: TS.Type, checker: TS.TypeChecker): SchemaAdapter | undefined;
289
+ declare function isSchemaType(type: TS.Type, checker: TS.TypeChecker): boolean;
290
+ declare function extractSchemaType(type: TS.Type, checker: TS.TypeChecker): SchemaExtractionResult | null;
291
+ declare const arktypeAdapter: SchemaAdapter;
292
+ declare const typeboxAdapter: SchemaAdapter;
293
+ declare const valibotAdapter: SchemaAdapter;
294
+ declare const zodAdapter: SchemaAdapter;
284
295
  import { SpecExport as SpecExport2 } from "@openpkg-ts/spec";
285
- import ts6 from "typescript";
286
- declare function serializeEnum(node: ts6.EnumDeclaration, ctx: SerializerContext): SpecExport2 | null;
296
+ import ts5 from "typescript";
297
+ declare function serializeClass(node: ts5.ClassDeclaration, ctx: SerializerContext): SpecExport2 | null;
287
298
  import { SpecExport as SpecExport3 } from "@openpkg-ts/spec";
288
- import ts7 from "typescript";
289
- declare function serializeFunctionExport(node: ts7.FunctionDeclaration | ts7.ArrowFunction, ctx: SerializerContext): SpecExport3 | null;
299
+ import ts6 from "typescript";
300
+ declare function serializeEnum(node: ts6.EnumDeclaration, ctx: SerializerContext): SpecExport3 | null;
290
301
  import { SpecExport as SpecExport4 } from "@openpkg-ts/spec";
291
- import ts8 from "typescript";
292
- declare function serializeInterface(node: ts8.InterfaceDeclaration, ctx: SerializerContext): SpecExport4 | null;
302
+ import ts7 from "typescript";
303
+ declare function serializeFunctionExport(node: ts7.FunctionDeclaration | ts7.ArrowFunction, ctx: SerializerContext): SpecExport4 | null;
293
304
  import { SpecExport as SpecExport5 } from "@openpkg-ts/spec";
294
- import ts9 from "typescript";
295
- declare function serializeTypeAlias(node: ts9.TypeAliasDeclaration, ctx: SerializerContext): SpecExport5 | null;
305
+ import ts8 from "typescript";
306
+ declare function serializeInterface(node: ts8.InterfaceDeclaration, ctx: SerializerContext): SpecExport5 | null;
296
307
  import { SpecExport as SpecExport6 } from "@openpkg-ts/spec";
308
+ import ts9 from "typescript";
309
+ declare function serializeTypeAlias(node: ts9.TypeAliasDeclaration, ctx: SerializerContext): SpecExport6 | null;
310
+ import { SpecExport as SpecExport7 } from "@openpkg-ts/spec";
297
311
  import ts10 from "typescript";
298
- declare function serializeVariable(node: ts10.VariableDeclaration, statement: ts10.VariableStatement, ctx: SerializerContext): SpecExport6 | null;
312
+ declare function serializeVariable(node: ts10.VariableDeclaration, statement: ts10.VariableStatement, ctx: SerializerContext): SpecExport7 | null;
299
313
  import { SpecSignatureParameter } from "@openpkg-ts/spec";
300
314
  import ts11 from "typescript";
301
315
  declare function extractParameters(signature: ts11.Signature, ctx: SerializerContext): SpecSignatureParameter[];
package/dist/src/index.js CHANGED
@@ -6,6 +6,8 @@ import {
6
6
  deduplicateSchemas,
7
7
  extract,
8
8
  extractParameters,
9
+ extractStandardSchemas,
10
+ extractStandardSchemasFromProject,
9
11
  extractTypeParameters,
10
12
  findDiscriminatorProperty,
11
13
  getJSDocComment,
@@ -15,8 +17,10 @@ import {
15
17
  isBuiltinGeneric,
16
18
  isPrimitiveName,
17
19
  isPureRefSchema,
20
+ isStandardJSONSchema,
18
21
  isSymbolDeprecated,
19
22
  registerReferencedTypes,
23
+ resolveCompiledPath,
20
24
  schemaIsAny,
21
25
  schemasAreEqual,
22
26
  serializeClass,
@@ -26,7 +30,7 @@ import {
26
30
  serializeTypeAlias,
27
31
  serializeVariable,
28
32
  withDescription
29
- } from "../shared/chunk-rej3ws8m.js";
33
+ } from "../shared/chunk-yh8v9dbt.js";
30
34
  // src/schema/registry.ts
31
35
  function isTypeReference(type) {
32
36
  return !!(type.flags & 524288 && type.objectFlags && type.objectFlags & 4);
@@ -57,7 +61,7 @@ function extractSchemaType(type, checker) {
57
61
  const outputType = adapter.extractOutputType(type, checker);
58
62
  if (!outputType)
59
63
  return null;
60
- const inputType = adapter.extractInputType?.(type, checker);
64
+ const inputType = adapter.extractInputType?.(type, checker) ?? undefined;
61
65
  return {
62
66
  adapter,
63
67
  outputType,
@@ -189,200 +193,6 @@ registerAdapter(zodAdapter);
189
193
  registerAdapter(valibotAdapter);
190
194
  registerAdapter(arktypeAdapter);
191
195
  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 (stdObj.version !== 1)
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" && typeof jsObj.input === "function";
212
- }
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;
226
- }
227
-
228
- function sanitizeTypeBoxSchema(schema) {
229
- // JSON.stringify removes symbol keys, keeping only JSON Schema props
230
- return JSON.parse(JSON.stringify(schema));
231
- }
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, optionsJson] = process.argv.slice(1);
237
- const { target, libraryOptions } = JSON.parse(optionsJson || '{}');
238
-
239
- try {
240
- // Import the module using dynamic import (works with ESM and CJS)
241
- const absPath = path.resolve(modulePath);
242
- const mod = await import(pathToFileURL(absPath).href);
243
- const results = [];
244
-
245
- // Build exports map - handle both ESM and CJS (where exports are in mod.default)
246
- const exports = {};
247
- for (const [name, value] of Object.entries(mod)) {
248
- if (name === 'default' && typeof value === 'object' && value !== null) {
249
- // CJS module: spread default exports
250
- Object.assign(exports, value);
251
- } else if (name !== 'default') {
252
- exports[name] = value;
253
- }
254
- }
255
-
256
- // Check each export
257
- for (const [name, value] of Object.entries(exports)) {
258
- if (name.startsWith('_')) continue;
259
- if (typeof value !== 'object' || value === null) continue;
260
-
261
- // Priority 1: Standard JSON Schema (Zod 4.2+, ArkType 2.1.28+, Valibot 1.2+)
262
- const std = value['~standard'];
263
- if (std && typeof std === 'object' && std.version === 1 && typeof std.vendor === 'string' && std.jsonSchema && typeof std.jsonSchema.output === 'function') {
264
- try {
265
- // Per spec: pass options object with target and optional libraryOptions
266
- const options = { target: target || 'draft-2020-12', ...(libraryOptions && { libraryOptions }) };
267
- const outputSchema = std.jsonSchema.output(options);
268
- const inputSchema = typeof std.jsonSchema.input === 'function' ? std.jsonSchema.input(options) : undefined;
269
- results.push({
270
- exportName: name,
271
- vendor: std.vendor,
272
- outputSchema,
273
- inputSchema
274
- });
275
- } catch (e) {
276
- // Skip schemas that fail to extract
277
- }
278
- continue;
279
- }
280
-
281
- // Priority 2: TypeBox (schema IS JSON Schema)
282
- if (isTypeBoxSchema(value)) {
283
- try {
284
- results.push({
285
- exportName: name,
286
- vendor: 'typebox',
287
- outputSchema: sanitizeTypeBoxSchema(value)
288
- });
289
- } catch (e) {
290
- // Skip schemas that fail to extract
291
- }
292
- continue;
293
- }
294
- }
295
-
296
- console.log(JSON.stringify({ success: true, results }));
297
- } catch (e) {
298
- console.log(JSON.stringify({ success: false, error: e.message }));
299
- }
300
- }
301
-
302
- extract();
303
- `;
304
- function resolveCompiledPath(tsPath, baseDir) {
305
- const relativePath = path.relative(baseDir, tsPath);
306
- const withoutExt = relativePath.replace(/\.tsx?$/, "");
307
- const candidates = [
308
- path.join(baseDir, `${withoutExt}.js`),
309
- path.join(baseDir, "dist", `${withoutExt.replace(/^src\//, "")}.js`),
310
- path.join(baseDir, "build", `${withoutExt.replace(/^src\//, "")}.js`),
311
- path.join(baseDir, "lib", `${withoutExt.replace(/^src\//, "")}.js`)
312
- ];
313
- for (const candidate of candidates) {
314
- if (fs.existsSync(candidate)) {
315
- return candidate;
316
- }
317
- }
318
- return null;
319
- }
320
- async function extractStandardSchemas(compiledJsPath, options = {}) {
321
- const { timeout = 1e4, target = "draft-2020-12", libraryOptions } = options;
322
- const result = {
323
- schemas: new Map,
324
- errors: []
325
- };
326
- if (!fs.existsSync(compiledJsPath)) {
327
- result.errors.push(`Compiled JS not found: ${compiledJsPath}`);
328
- return result;
329
- }
330
- const optionsJson = JSON.stringify({ target, libraryOptions });
331
- return new Promise((resolve) => {
332
- const child = spawn("node", ["-e", WORKER_SCRIPT, compiledJsPath, optionsJson], {
333
- timeout,
334
- stdio: ["ignore", "pipe", "pipe"]
335
- });
336
- let stdout = "";
337
- let stderr = "";
338
- child.stdout.on("data", (data) => {
339
- stdout += data.toString();
340
- });
341
- child.stderr.on("data", (data) => {
342
- stderr += data.toString();
343
- });
344
- child.on("close", (code) => {
345
- if (code !== 0) {
346
- result.errors.push(`Extraction process failed: ${stderr || `exit code ${code}`}`);
347
- resolve(result);
348
- return;
349
- }
350
- try {
351
- const parsed = JSON.parse(stdout);
352
- if (!parsed.success) {
353
- result.errors.push(`Extraction failed: ${parsed.error}`);
354
- resolve(result);
355
- return;
356
- }
357
- for (const item of parsed.results) {
358
- result.schemas.set(item.exportName, {
359
- exportName: item.exportName,
360
- vendor: item.vendor,
361
- outputSchema: item.outputSchema,
362
- inputSchema: item.inputSchema
363
- });
364
- }
365
- } catch (e) {
366
- result.errors.push(`Failed to parse extraction output: ${e}`);
367
- }
368
- resolve(result);
369
- });
370
- child.on("error", (err) => {
371
- result.errors.push(`Subprocess error: ${err.message}`);
372
- resolve(result);
373
- });
374
- });
375
- }
376
- async function extractStandardSchemasFromProject(entryFile, baseDir, options = {}) {
377
- const compiledPath = resolveCompiledPath(entryFile, baseDir);
378
- if (!compiledPath) {
379
- return {
380
- schemas: new Map,
381
- errors: [`Could not find compiled JS for ${entryFile}. Build the project first.`]
382
- };
383
- }
384
- return extractStandardSchemas(compiledPath, options);
385
- }
386
196
  // src/types/utils.ts
387
197
  function isExported(node) {
388
198
  const modifiers = node.modifiers;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpkg-ts/extract",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "TypeScript export extraction to OpenPkg spec",
5
5
  "keywords": [
6
6
  "openpkg",