@openpkg-ts/extract 0.22.1 → 0.23.1

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @openpkg-ts/extract
2
2
 
3
- TypeScript API extraction library. Generates OpenPkg specs from TypeScript source code.
3
+ TypeScript API extraction library. Generates OpenPkg specs from TypeScript source code with **JSON Schema 2020-12** output.
4
4
 
5
5
  ## Install
6
6
 
@@ -14,6 +14,9 @@ npm install @openpkg-ts/extract
14
14
  # Extract API spec from entry point
15
15
  tspec src/index.ts -o openpkg.json
16
16
 
17
+ # With runtime schema extraction (Zod, Valibot, etc.)
18
+ tspec src/index.ts --runtime
19
+
17
20
  # With options
18
21
  tspec src/index.ts --max-depth 4 --verbose
19
22
  ```
@@ -46,7 +49,107 @@ for (const diag of result.diagnostics) {
46
49
  | `baseDir` | `string` | cwd | Base directory for resolution |
47
50
  | `maxTypeDepth` | `number` | 4 | Max depth for type traversal |
48
51
  | `resolveExternalTypes` | `boolean` | true | Resolve types from node_modules |
49
- | `schemaExtraction` | `'static' \| 'hybrid'` | 'static' | Schema extraction mode |
52
+ | `schemaExtraction` | `'static' \| 'hybrid'` | `'static'` | Schema extraction mode |
53
+ | `schemaTarget` | `'draft-2020-12' \| 'draft-07' \| 'openapi-3.0'` | `'draft-2020-12'` | Target JSON Schema dialect |
54
+ | `only` | `string[]` | - | Only extract these exports (supports `*` wildcards) |
55
+ | `ignore` | `string[]` | - | Ignore these exports (supports `*` wildcards) |
56
+
57
+ ## JSON Schema 2020-12 Output
58
+
59
+ All schema output is normalized to valid **JSON Schema 2020-12**. This ensures consistency between static TypeScript analysis and runtime schema extraction from libraries like Zod and Valibot.
60
+
61
+ ### Interface/Class Output Format
62
+
63
+ Interfaces and classes include a `schema` property containing a JSON Schema object representation:
64
+
65
+ ```json
66
+ {
67
+ "kind": "interface",
68
+ "name": "User",
69
+ "schema": {
70
+ "type": "object",
71
+ "properties": {
72
+ "id": { "type": "string" },
73
+ "age": { "type": "number" },
74
+ "email": { "type": "string" }
75
+ },
76
+ "required": ["id", "email"]
77
+ },
78
+ "members": [...]
79
+ }
80
+ ```
81
+
82
+ ### TypeScript Extension Fields (`x-ts-*`)
83
+
84
+ TypeScript constructs that don't map directly to JSON Schema are preserved using extension fields:
85
+
86
+ | Extension | Purpose | Example |
87
+ |-----------|---------|---------|
88
+ | `x-ts-type` | Preserves original TypeScript type | `{ "type": "integer", "x-ts-type": "bigint" }` |
89
+ | `x-ts-function` | Marks function types | `{ "x-ts-function": true, "x-ts-signatures": [...] }` |
90
+ | `x-ts-signatures` | Function/method signatures | Array of signature objects with parameters and returns |
91
+ | `x-ts-type-arguments` | Generic type arguments | `{ "$ref": "#/types/Promise", "x-ts-type-arguments": [{ "type": "string" }] }` |
92
+ | `x-ts-accessor` | Getter/setter markers | `{ "type": "string", "x-ts-accessor": "getter" }` |
93
+
94
+ ### Type Mappings
95
+
96
+ | TypeScript Type | JSON Schema Output |
97
+ |-----------------|-------------------|
98
+ | `void` | `{ "type": "null" }` |
99
+ | `never` | `{ "not": {} }` |
100
+ | `any` | `{}` |
101
+ | `unknown` | `{}` |
102
+ | `undefined` | `{ "type": "null" }` |
103
+ | `bigint` | `{ "type": "integer", "x-ts-type": "bigint" }` |
104
+ | `symbol` | `{ "type": "string", "x-ts-type": "symbol" }` |
105
+ | `[T, U]` (tuple) | `{ "type": "array", "prefixedItems": [...], "minItems": 2, "maxItems": 2 }` |
106
+ | `() => T` (function) | `{ "x-ts-function": true, "x-ts-signatures": [...] }` |
107
+ | `Promise<T>` | `{ "$ref": "#/types/Promise", "x-ts-type-arguments": [<T schema>] }` |
108
+
109
+ ### Example: Function Schema
110
+
111
+ ```json
112
+ {
113
+ "kind": "function",
114
+ "name": "fetchUser",
115
+ "signatures": [{
116
+ "parameters": [{
117
+ "name": "id",
118
+ "schema": { "type": "string" },
119
+ "required": true
120
+ }],
121
+ "returns": {
122
+ "schema": {
123
+ "$ref": "#/types/Promise",
124
+ "x-ts-type-arguments": [{ "$ref": "#/types/User" }]
125
+ }
126
+ }
127
+ }]
128
+ }
129
+ ```
130
+
131
+ ### Example: Interface with Methods
132
+
133
+ ```json
134
+ {
135
+ "kind": "interface",
136
+ "name": "Repository",
137
+ "schema": {
138
+ "type": "object",
139
+ "properties": {
140
+ "id": { "type": "string" },
141
+ "find": {
142
+ "x-ts-function": true,
143
+ "x-ts-signatures": [{
144
+ "parameters": [{ "name": "query", "schema": { "type": "string" } }],
145
+ "returns": { "schema": { "type": "array", "items": { "$ref": "#/types/Item" } } }
146
+ }]
147
+ }
148
+ },
149
+ "required": ["id", "find"]
150
+ }
151
+ }
152
+ ```
50
153
 
51
154
  ## Exports
52
155
 
@@ -61,6 +164,12 @@ for (const diag of result.diagnostics) {
61
164
  - `TypeRegistry` - Track and dedupe extracted types
62
165
  - `serializeType` - Convert TS types to schema
63
166
 
167
+ ### Schema Normalizer
168
+ - `normalizeSchema(schema, options)` - Convert SpecSchema to JSON Schema 2020-12
169
+ - `normalizeExport(exp, options)` - Normalize a SpecExport including nested schemas
170
+ - `normalizeType(type, options)` - Normalize a SpecType including nested schemas
171
+ - `normalizeMembers(members, options)` - Convert members array to JSON Schema properties
172
+
64
173
  ### Schema Adapters
65
174
  - `ZodAdapter`, `ValibotAdapter` - Runtime schema extraction
66
175
 
@@ -70,7 +179,8 @@ for (const diag of result.diagnostics) {
70
179
  2. Extracts all exported symbols
71
180
  3. Serializes each export (functions, classes, types, variables)
72
181
  4. Resolves type references and builds a type registry
73
- 5. Outputs an OpenPkg-compliant JSON spec
182
+ 5. **Normalizes all schemas to JSON Schema 2020-12**
183
+ 6. Outputs an OpenPkg-compliant JSON spec
74
184
 
75
185
  ## License
76
186
 
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-7fp2zqf8.js";
4
+ } from "../shared/chunk-qwkv7jxy.js";
5
5
 
6
6
  // src/cli/spec.ts
7
7
  import * as fs from "node:fs";
@@ -2156,6 +2156,423 @@ async function extractStandardSchemasFromProject(entryFile, baseDir, options = {
2156
2156
  };
2157
2157
  }
2158
2158
 
2159
+ // src/types/schema-normalizer.ts
2160
+ var SCHEMA_DIALECT_URLS = {
2161
+ "draft-2020-12": "https://json-schema.org/draft/2020-12/schema",
2162
+ "draft-07": "http://json-schema.org/draft-07/schema#"
2163
+ };
2164
+ var TS_PRIMITIVE_NORMALIZATIONS = {
2165
+ void: () => ({ type: "null" }),
2166
+ never: () => ({ not: {} }),
2167
+ any: () => ({}),
2168
+ unknown: () => ({}),
2169
+ undefined: () => ({ type: "null" }),
2170
+ bigint: () => ({ type: "integer", "x-ts-type": "bigint" }),
2171
+ symbol: () => ({ type: "string", "x-ts-type": "symbol" })
2172
+ };
2173
+ function normalizeSchema(schema, options = {}) {
2174
+ const { includeSchemaField = false, dialect = "draft-2020-12" } = options;
2175
+ const normalized = normalizeSchemaInternal(schema, options);
2176
+ if (includeSchemaField && typeof normalized === "object") {
2177
+ return {
2178
+ $schema: SCHEMA_DIALECT_URLS[dialect],
2179
+ ...normalized
2180
+ };
2181
+ }
2182
+ return normalized;
2183
+ }
2184
+ function normalizeSchemaInternal(schema, options) {
2185
+ if (typeof schema === "string") {
2186
+ return normalizeStringType(schema);
2187
+ }
2188
+ if (schema == null) {
2189
+ return {};
2190
+ }
2191
+ if (typeof schema !== "object") {
2192
+ return {};
2193
+ }
2194
+ if ("anyOf" in schema && Array.isArray(schema.anyOf)) {
2195
+ return normalizeCombinator("anyOf", schema.anyOf, schema, options);
2196
+ }
2197
+ if ("allOf" in schema && Array.isArray(schema.allOf)) {
2198
+ return normalizeCombinator("allOf", schema.allOf, schema, options);
2199
+ }
2200
+ if ("oneOf" in schema && Array.isArray(schema.oneOf)) {
2201
+ return normalizeCombinator("oneOf", schema.oneOf, schema, options);
2202
+ }
2203
+ if ("$ref" in schema && typeof schema.$ref === "string") {
2204
+ return normalizeRef(schema, options);
2205
+ }
2206
+ if ("type" in schema && typeof schema.type === "string") {
2207
+ return normalizeTypedSchema(schema, options);
2208
+ }
2209
+ return normalizeGenericObject(schema, options);
2210
+ }
2211
+ function normalizeStringType(type) {
2212
+ const specialNormalization = TS_PRIMITIVE_NORMALIZATIONS[type];
2213
+ if (specialNormalization) {
2214
+ return specialNormalization();
2215
+ }
2216
+ if (["string", "number", "boolean", "integer", "null", "object", "array"].includes(type)) {
2217
+ return { type };
2218
+ }
2219
+ return { "x-ts-type": type };
2220
+ }
2221
+ function normalizeTypedSchema(schema, options) {
2222
+ const { type } = schema;
2223
+ const specialNormalization = TS_PRIMITIVE_NORMALIZATIONS[type];
2224
+ if (specialNormalization) {
2225
+ const normalized = specialNormalization();
2226
+ return mergeSchemaFields(normalized, schema, ["type"]);
2227
+ }
2228
+ if (type === "function") {
2229
+ return normalizeFunctionType(schema, options);
2230
+ }
2231
+ if (type === "tuple") {
2232
+ return normalizeTupleType(schema, options);
2233
+ }
2234
+ if (type === "array") {
2235
+ return normalizeArrayType(schema, options);
2236
+ }
2237
+ if (type === "object") {
2238
+ return normalizeObjectType(schema, options);
2239
+ }
2240
+ if (["string", "number", "boolean", "integer", "null"].includes(type)) {
2241
+ return normalizeStandardType(schema, options);
2242
+ }
2243
+ const result = { "x-ts-type": type };
2244
+ return mergeSchemaFields(result, schema, ["type"]);
2245
+ }
2246
+ function normalizeFunctionType(schema, options) {
2247
+ const result = {
2248
+ "x-ts-function": true
2249
+ };
2250
+ if ("signatures" in schema && Array.isArray(schema.signatures)) {
2251
+ result["x-ts-signatures"] = schema.signatures.map((sig) => normalizeSignature(sig, options));
2252
+ }
2253
+ if ("description" in schema && schema.description) {
2254
+ result.description = schema.description;
2255
+ }
2256
+ return result;
2257
+ }
2258
+ function normalizeSignature(signature, options) {
2259
+ const result = {};
2260
+ if (signature.parameters) {
2261
+ result.parameters = signature.parameters.map((param) => ({
2262
+ name: param.name,
2263
+ schema: normalizeSchemaInternal(param.schema, options),
2264
+ ...param.required !== undefined ? { required: param.required } : {},
2265
+ ...param.description ? { description: param.description } : {},
2266
+ ...param.default !== undefined ? { default: param.default } : {},
2267
+ ...param.rest ? { rest: param.rest } : {}
2268
+ }));
2269
+ }
2270
+ if (signature.returns) {
2271
+ result.returns = {
2272
+ schema: normalizeSchemaInternal(signature.returns.schema, options),
2273
+ ...signature.returns.description ? { description: signature.returns.description } : {}
2274
+ };
2275
+ }
2276
+ if (signature.description) {
2277
+ result.description = signature.description;
2278
+ }
2279
+ if (signature.typeParameters) {
2280
+ result.typeParameters = signature.typeParameters;
2281
+ }
2282
+ return result;
2283
+ }
2284
+ function normalizeTupleType(schema, options) {
2285
+ const result = { type: "array" };
2286
+ if ("items" in schema && Array.isArray(schema.items)) {
2287
+ result.prefixedItems = schema.items.map((item) => normalizeSchemaInternal(item, options));
2288
+ result.minItems = schema.items.length;
2289
+ result.maxItems = schema.items.length;
2290
+ }
2291
+ if ("prefixedItems" in schema && Array.isArray(schema.prefixedItems)) {
2292
+ result.prefixedItems = schema.prefixedItems.map((item) => normalizeSchemaInternal(item, options));
2293
+ }
2294
+ if ("minItems" in schema && typeof schema.minItems === "number") {
2295
+ result.minItems = schema.minItems;
2296
+ }
2297
+ if ("maxItems" in schema && typeof schema.maxItems === "number") {
2298
+ result.maxItems = schema.maxItems;
2299
+ }
2300
+ if ("description" in schema && schema.description) {
2301
+ result.description = schema.description;
2302
+ }
2303
+ return result;
2304
+ }
2305
+ function normalizeArrayType(schema, options) {
2306
+ const result = { type: "array" };
2307
+ if ("items" in schema && schema.items && !Array.isArray(schema.items)) {
2308
+ result.items = normalizeSchemaInternal(schema.items, options);
2309
+ }
2310
+ if ("prefixedItems" in schema && Array.isArray(schema.prefixedItems)) {
2311
+ result.prefixedItems = schema.prefixedItems.map((item) => normalizeSchemaInternal(item, options));
2312
+ }
2313
+ if ("minItems" in schema && typeof schema.minItems === "number") {
2314
+ result.minItems = schema.minItems;
2315
+ }
2316
+ if ("maxItems" in schema && typeof schema.maxItems === "number") {
2317
+ result.maxItems = schema.maxItems;
2318
+ }
2319
+ if ("description" in schema && schema.description) {
2320
+ result.description = schema.description;
2321
+ }
2322
+ return result;
2323
+ }
2324
+ function normalizeObjectType(schema, options) {
2325
+ const result = { type: "object" };
2326
+ if ("properties" in schema && schema.properties) {
2327
+ const properties = schema.properties;
2328
+ result.properties = Object.fromEntries(Object.entries(properties).map(([key, value]) => [
2329
+ key,
2330
+ normalizeSchemaInternal(value, options)
2331
+ ]));
2332
+ }
2333
+ if ("required" in schema && Array.isArray(schema.required)) {
2334
+ result.required = schema.required;
2335
+ }
2336
+ if ("additionalProperties" in schema) {
2337
+ if (typeof schema.additionalProperties === "boolean") {
2338
+ result.additionalProperties = schema.additionalProperties;
2339
+ } else if (schema.additionalProperties) {
2340
+ result.additionalProperties = normalizeSchemaInternal(schema.additionalProperties, options);
2341
+ }
2342
+ }
2343
+ if ("description" in schema && schema.description) {
2344
+ result.description = schema.description;
2345
+ }
2346
+ return result;
2347
+ }
2348
+ function normalizeStandardType(schema, options) {
2349
+ const result = { type: schema.type };
2350
+ const validationKeywords = [
2351
+ "enum",
2352
+ "const",
2353
+ "format",
2354
+ "pattern",
2355
+ "minimum",
2356
+ "maximum",
2357
+ "exclusiveMinimum",
2358
+ "exclusiveMaximum",
2359
+ "multipleOf",
2360
+ "minLength",
2361
+ "maxLength",
2362
+ "description",
2363
+ "default",
2364
+ "examples",
2365
+ "title"
2366
+ ];
2367
+ for (const keyword of validationKeywords) {
2368
+ if (keyword in schema && schema[keyword] !== undefined) {
2369
+ result[keyword] = schema[keyword];
2370
+ }
2371
+ }
2372
+ return result;
2373
+ }
2374
+ function normalizeRef(schema, options) {
2375
+ const result = { $ref: schema.$ref };
2376
+ if (schema.typeArguments && schema.typeArguments.length > 0) {
2377
+ result["x-ts-type-arguments"] = schema.typeArguments.map((arg) => normalizeSchemaInternal(arg, options));
2378
+ }
2379
+ return result;
2380
+ }
2381
+ function normalizeCombinator(keyword, schemas, originalSchema, options) {
2382
+ const result = {
2383
+ [keyword]: schemas.map((s) => normalizeSchemaInternal(s, options))
2384
+ };
2385
+ if ((keyword === "anyOf" || keyword === "oneOf") && "discriminator" in originalSchema && originalSchema.discriminator) {
2386
+ result.discriminator = originalSchema.discriminator;
2387
+ }
2388
+ if ("description" in originalSchema && originalSchema.description) {
2389
+ result.description = originalSchema.description;
2390
+ }
2391
+ return result;
2392
+ }
2393
+ function normalizeGenericObject(schema, options) {
2394
+ const result = {};
2395
+ for (const [key, value] of Object.entries(schema)) {
2396
+ if (value == null)
2397
+ continue;
2398
+ if (isSchemaLike(value)) {
2399
+ result[key] = normalizeSchemaInternal(value, options);
2400
+ } else if (Array.isArray(value)) {
2401
+ result[key] = value.map((item) => isSchemaLike(item) ? normalizeSchemaInternal(item, options) : item);
2402
+ } else if (typeof value === "object") {
2403
+ result[key] = normalizeGenericObject(value, options);
2404
+ } else {
2405
+ result[key] = value;
2406
+ }
2407
+ }
2408
+ return result;
2409
+ }
2410
+ function isSchemaLike(value) {
2411
+ if (typeof value !== "object" || value == null)
2412
+ return false;
2413
+ if (typeof value === "string")
2414
+ return true;
2415
+ const obj = value;
2416
+ return "type" in obj || "$ref" in obj || "anyOf" in obj || "allOf" in obj || "oneOf" in obj || "properties" in obj || "items" in obj || "prefixedItems" in obj;
2417
+ }
2418
+ function mergeSchemaFields(target, source, excludeKeys) {
2419
+ if (typeof source !== "object" || source == null) {
2420
+ return target;
2421
+ }
2422
+ const excludeSet = new Set(excludeKeys);
2423
+ const result = { ...target };
2424
+ for (const [key, value] of Object.entries(source)) {
2425
+ if (!excludeSet.has(key) && value !== undefined) {
2426
+ if (!(key in result)) {
2427
+ result[key] = value;
2428
+ }
2429
+ }
2430
+ }
2431
+ return result;
2432
+ }
2433
+ function normalizeExport(exp, options = {}) {
2434
+ const result = { ...exp };
2435
+ if (exp.schema) {
2436
+ result.schema = normalizeSchema(exp.schema, options);
2437
+ }
2438
+ if (exp.signatures) {
2439
+ result.signatures = exp.signatures.map((sig) => normalizeSignatureSpec(sig, options));
2440
+ }
2441
+ if (exp.members) {
2442
+ result.members = exp.members.map((member) => normalizeMember(member, options));
2443
+ }
2444
+ if (shouldGenerateMembersSchema(exp.kind) && exp.members && exp.members.length > 0) {
2445
+ result.schema = normalizeMembers(exp.members, options);
2446
+ }
2447
+ return result;
2448
+ }
2449
+ function normalizeType(type, options = {}) {
2450
+ const result = { ...type };
2451
+ if (type.schema) {
2452
+ result.schema = normalizeSchema(type.schema, options);
2453
+ }
2454
+ if (type.members) {
2455
+ result.members = type.members.map((member) => normalizeMember(member, options));
2456
+ }
2457
+ if (shouldGenerateMembersSchema(type.kind) && type.members && type.members.length > 0) {
2458
+ result.schema = normalizeMembers(type.members, options);
2459
+ }
2460
+ return result;
2461
+ }
2462
+ function shouldGenerateMembersSchema(kind) {
2463
+ return kind === "interface" || kind === "class";
2464
+ }
2465
+ function normalizeSignatureSpec(signature, options) {
2466
+ const result = { ...signature };
2467
+ if (signature.parameters) {
2468
+ result.parameters = signature.parameters.map((param) => ({
2469
+ ...param,
2470
+ schema: normalizeSchema(param.schema, options)
2471
+ }));
2472
+ }
2473
+ if (signature.returns) {
2474
+ result.returns = {
2475
+ ...signature.returns,
2476
+ schema: normalizeSchema(signature.returns.schema, options)
2477
+ };
2478
+ }
2479
+ return result;
2480
+ }
2481
+ function normalizeMember(member, options) {
2482
+ const result = { ...member };
2483
+ if (member.schema) {
2484
+ result.schema = normalizeSchema(member.schema, options);
2485
+ }
2486
+ if (member.signatures) {
2487
+ result.signatures = member.signatures.map((sig) => normalizeSignatureSpec(sig, options));
2488
+ }
2489
+ return result;
2490
+ }
2491
+ function normalizeMembers(members, options = {}) {
2492
+ const properties = {};
2493
+ const required = [];
2494
+ let additionalProperties;
2495
+ for (const member of members) {
2496
+ const { name, kind } = member;
2497
+ if (kind === "index" || kind === "index-signature") {
2498
+ additionalProperties = normalizeMemberToSchema(member, options);
2499
+ continue;
2500
+ }
2501
+ if (!name)
2502
+ continue;
2503
+ const memberSchema = normalizeMemberToSchema(member, options);
2504
+ properties[name] = memberSchema;
2505
+ if (!isOptionalMember(member)) {
2506
+ required.push(name);
2507
+ }
2508
+ }
2509
+ const result = {
2510
+ type: "object",
2511
+ properties
2512
+ };
2513
+ if (required.length > 0) {
2514
+ result.required = required;
2515
+ }
2516
+ if (additionalProperties !== undefined) {
2517
+ result.additionalProperties = additionalProperties;
2518
+ }
2519
+ return result;
2520
+ }
2521
+ function normalizeMemberToSchema(member, options) {
2522
+ const { kind, schema, signatures, description } = member;
2523
+ if (kind === "method" || kind === "call-signature") {
2524
+ return normalizeMethodMember(member, options);
2525
+ }
2526
+ if (kind === "getter") {
2527
+ const baseSchema2 = schema ? normalizeSchemaInternal(schema, options) : {};
2528
+ return {
2529
+ ...baseSchema2,
2530
+ "x-ts-accessor": "getter",
2531
+ ...description ? { description } : {}
2532
+ };
2533
+ }
2534
+ if (kind === "setter") {
2535
+ const baseSchema2 = schema ? normalizeSchemaInternal(schema, options) : {};
2536
+ return {
2537
+ ...baseSchema2,
2538
+ "x-ts-accessor": "setter",
2539
+ ...description ? { description } : {}
2540
+ };
2541
+ }
2542
+ if (kind === "index" || kind === "index-signature") {
2543
+ if (schema && typeof schema === "object" && "additionalProperties" in schema) {
2544
+ return normalizeSchemaInternal(schema.additionalProperties, options);
2545
+ }
2546
+ return schema ? normalizeSchemaInternal(schema, options) : {};
2547
+ }
2548
+ if (signatures && signatures.length > 0) {
2549
+ return normalizeMethodMember(member, options);
2550
+ }
2551
+ const baseSchema = schema ? normalizeSchemaInternal(schema, options) : {};
2552
+ return description ? { ...baseSchema, description } : baseSchema;
2553
+ }
2554
+ function normalizeMethodMember(member, options) {
2555
+ const result = {
2556
+ "x-ts-function": true
2557
+ };
2558
+ if (member.signatures && member.signatures.length > 0) {
2559
+ result["x-ts-signatures"] = member.signatures.map((sig) => normalizeSignature(sig, options));
2560
+ }
2561
+ if (member.description) {
2562
+ result.description = member.description;
2563
+ }
2564
+ return result;
2565
+ }
2566
+ function isOptionalMember(member) {
2567
+ if (member.flags?.optional === true) {
2568
+ return true;
2569
+ }
2570
+ if (member.name?.endsWith("?")) {
2571
+ return true;
2572
+ }
2573
+ return false;
2574
+ }
2575
+
2159
2576
  // src/builder/spec-builder.ts
2160
2577
  import * as fs2 from "node:fs";
2161
2578
  import * as path3 from "node:path";
@@ -2351,7 +2768,8 @@ async function extract(options) {
2351
2768
  }
2352
2769
  const meta = await getPackageMeta(entryFile, baseDir);
2353
2770
  const types = ctx.typeRegistry.getAll();
2354
- const forgottenExports = collectForgottenExports(exports, types, program, sourceFile, exportedIds);
2771
+ const projectBaseDir = baseDir ?? path3.dirname(entryFile);
2772
+ const forgottenExports = collectForgottenExports(exports, types, program, sourceFile, exportedIds, projectBaseDir);
2355
2773
  for (const forgotten of forgottenExports) {
2356
2774
  const refSummary = forgotten.referencedBy.slice(0, 3).map((r) => `${r.exportName} (${r.location})`).join(", ");
2357
2775
  const moreRefs = forgotten.referencedBy.length > 3 ? ` +${forgotten.referencedBy.length - 3} more` : "";
@@ -2382,8 +2800,8 @@ async function extract(options) {
2382
2800
  }
2383
2801
  let runtimeMetadata;
2384
2802
  if (options.schemaExtraction === "hybrid") {
2385
- const projectBaseDir = baseDir || path3.dirname(entryFile);
2386
- const runtimeResult = await extractStandardSchemasFromProject(entryFile, projectBaseDir, {
2803
+ const projectBaseDir2 = baseDir || path3.dirname(entryFile);
2804
+ const runtimeResult = await extractStandardSchemasFromProject(entryFile, projectBaseDir2, {
2387
2805
  target: options.schemaTarget || "draft-2020-12",
2388
2806
  timeout: 15000
2389
2807
  });
@@ -2407,12 +2825,14 @@ async function extract(options) {
2407
2825
  });
2408
2826
  }
2409
2827
  }
2828
+ const normalizedExports = exports.map((exp) => normalizeExport(exp, { dialect: "draft-2020-12" }));
2829
+ const normalizedTypes = types.map((t) => normalizeType(t, { dialect: "draft-2020-12" }));
2410
2830
  const spec = {
2411
2831
  ...includeSchema ? { $schema: SCHEMA_URL } : {},
2412
2832
  openpkg: SCHEMA_VERSION,
2413
2833
  meta,
2414
- exports,
2415
- types,
2834
+ exports: normalizedExports,
2835
+ types: normalizedTypes,
2416
2836
  generation: {
2417
2837
  generator: "@openpkg-ts/extract",
2418
2838
  timestamp: new Date().toISOString(),
@@ -2497,10 +2917,14 @@ function findTypeDefinition(typeName, program, sourceFile) {
2497
2917
  }
2498
2918
  return;
2499
2919
  }
2500
- function isExternalType2(definedIn) {
2920
+ function isExternalType2(definedIn, baseDir) {
2501
2921
  if (!definedIn)
2502
2922
  return true;
2503
- return definedIn.includes("node_modules");
2923
+ if (definedIn.includes("node_modules"))
2924
+ return true;
2925
+ const normalizedDefined = path3.resolve(definedIn);
2926
+ const normalizedBase = path3.resolve(baseDir);
2927
+ return !normalizedDefined.startsWith(normalizedBase);
2504
2928
  }
2505
2929
  function hasInternalTag(typeName, program, sourceFile) {
2506
2930
  const checker = program.getTypeChecker();
@@ -2510,7 +2934,7 @@ function hasInternalTag(typeName, program, sourceFile) {
2510
2934
  const jsTags = symbol.getJsDocTags();
2511
2935
  return jsTags.some((tag) => tag.name === "internal");
2512
2936
  }
2513
- function collectForgottenExports(exports, types, program, sourceFile, exportedIds) {
2937
+ function collectForgottenExports(exports, types, program, sourceFile, exportedIds, baseDir) {
2514
2938
  const definedTypes = new Set(types.map((t) => t.id));
2515
2939
  const referencedTypes = new Map;
2516
2940
  for (const exp of exports) {
@@ -2540,7 +2964,7 @@ function collectForgottenExports(exports, types, program, sourceFile, exportedId
2540
2964
  if (exportedIds.has(typeName))
2541
2965
  continue;
2542
2966
  const definedIn = findTypeDefinition(typeName, program, sourceFile);
2543
- const isExternal = isExternalType2(definedIn);
2967
+ const isExternal = isExternalType2(definedIn, baseDir);
2544
2968
  forgottenExports.push({
2545
2969
  name: typeName,
2546
2970
  definedIn,
@@ -2753,4 +3177,4 @@ async function getPackageMeta(entryFile, baseDir) {
2753
3177
  } catch {}
2754
3178
  return { name: path3.basename(searchDir) };
2755
3179
  }
2756
- 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, isTypeReference, getNonNullableType, registerAdapter, findAdapter, isSchemaType, extractSchemaType, arktypeAdapter, typeboxAdapter, valibotAdapter, zodAdapter, serializeVariable, isStandardJSONSchema, detectTsRuntime, extractStandardSchemasFromTs, resolveCompiledPath, extractStandardSchemas, extractStandardSchemasFromProject, extract };
3180
+ 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, isTypeReference, getNonNullableType, registerAdapter, findAdapter, isSchemaType, extractSchemaType, arktypeAdapter, typeboxAdapter, valibotAdapter, zodAdapter, serializeVariable, isStandardJSONSchema, detectTsRuntime, extractStandardSchemasFromTs, resolveCompiledPath, extractStandardSchemas, extractStandardSchemasFromProject, normalizeSchema, normalizeExport, normalizeType, normalizeMembers, extract };
@@ -425,7 +425,69 @@ declare function deduplicateSchemas(schemas: SpecSchema[]): SpecSchema[];
425
425
  * A valid discriminator has a unique literal value in each union member.
426
426
  */
427
427
  declare function findDiscriminatorProperty(unionTypes: ts12.Type[], checker: ts12.TypeChecker): string | undefined;
428
+ import { SpecSchema as SpecSchema2, SpecExport as SpecExport8, SpecType as SpecType2, SpecMember } from "@openpkg-ts/spec";
429
+ /**
430
+ * Options for schema normalization
431
+ */
432
+ interface NormalizeOptions {
433
+ /** Include $schema field in output */
434
+ includeSchemaField?: boolean;
435
+ /** Target JSON Schema dialect (default: 'draft-2020-12') */
436
+ dialect?: "draft-2020-12" | "draft-07";
437
+ }
438
+ /**
439
+ * JSON Schema 2020-12 compatible output type.
440
+ * Uses Record<string, unknown> for flexibility since JSON Schema is highly polymorphic.
441
+ */
442
+ type JSONSchema = Record<string, unknown>;
443
+ /**
444
+ * Normalize a SpecSchema to JSON Schema 2020-12.
445
+ *
446
+ * @param schema - The SpecSchema to normalize
447
+ * @param options - Normalization options
448
+ * @returns JSON Schema 2020-12 compatible schema
449
+ */
450
+ declare function normalizeSchema(schema: SpecSchema2, options?: NormalizeOptions): JSONSchema;
451
+ /**
452
+ * Normalize a SpecExport, normalizing its schema and nested schemas.
453
+ *
454
+ * For interfaces and classes, this function will:
455
+ * 1. Normalize any existing schema
456
+ * 2. Normalize member schemas
457
+ * 3. Generate a JSON Schema from members if members exist (populates `schema` field)
458
+ */
459
+ declare function normalizeExport(exp: SpecExport8, options?: NormalizeOptions): SpecExport8;
460
+ /**
461
+ * Normalize a SpecType, normalizing its schema and nested schemas.
462
+ *
463
+ * For interfaces and classes, this function will:
464
+ * 1. Normalize any existing schema
465
+ * 2. Normalize member schemas
466
+ * 3. Generate a JSON Schema from members if members exist (populates `schema` field)
467
+ */
468
+ declare function normalizeType(type: SpecType2, options?: NormalizeOptions): SpecType2;
469
+ /**
470
+ * Convert a members array to JSON Schema properties format.
471
+ *
472
+ * This function transforms the SpecMember[] array representation used by
473
+ * interfaces/classes into a JSON Schema 2020-12 object schema with properties,
474
+ * required array, and additionalProperties.
475
+ *
476
+ * Member Kind Mappings:
477
+ * | Member Kind | JSON Schema Output |
478
+ * |--------------------|-------------------------------------------------------|
479
+ * | property | Direct schema in properties |
480
+ * | method | { "x-ts-function": true, "x-ts-signatures": [...] } |
481
+ * | getter | Schema in properties (read-only via extension) |
482
+ * | setter | Schema in properties (write-only via extension) |
483
+ * | index | additionalProperties schema |
484
+ *
485
+ * @param members - The members array from an interface/class
486
+ * @param options - Normalization options
487
+ * @returns JSON Schema object with properties, required, and additionalProperties
488
+ */
489
+ declare function normalizeMembers(members: SpecMember[], options?: NormalizeOptions): JSONSchema;
428
490
  import ts13 from "typescript";
429
491
  declare function isExported(node: ts13.Node): boolean;
430
492
  declare function getNodeName(node: ts13.Node): string | undefined;
431
- 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, extractStandardSchemasFromTs, extractStandardSchemasFromProject, extractStandardSchemas, extractSchemaType, extractParameters, extract, detectTsRuntime, deduplicateSchemas, createProgram, buildSchema, arktypeAdapter, TypeRegistry, TypeReference2 as TypeReference, TsRuntime, StandardSchemaExtractionResult, StandardSchemaExtractionOutput, StandardJSONSchemaV1, StandardJSONSchemaTarget, StandardJSONSchemaOptions, SerializerContext, SchemaExtractionResult, SchemaAdapter, ProjectExtractionOutput, ProjectExtractionInfo, ProgramResult, ProgramOptions, ForgottenExport, ExtractStandardSchemasOptions, ExtractResult, ExtractOptions, ExtractFromProjectOptions, Diagnostic, BUILTIN_TYPE_SCHEMAS };
493
+ export { zodAdapter, withDescription, valibotAdapter, typeboxAdapter, serializeVariable, serializeTypeAlias, serializeInterface, serializeFunctionExport, serializeEnum, serializeClass, schemasAreEqual, schemaIsAny, resolveCompiledPath, registerReferencedTypes, registerAdapter, normalizeType, normalizeSchema, normalizeMembers, normalizeExport, isTypeReference, isSymbolDeprecated, isStandardJSONSchema, isSchemaType, isPureRefSchema, isPrimitiveName, isExported, isBuiltinGeneric, isAnonymous, getSourceLocation, getParamDescription, getNonNullableType, getNodeName, getJSDocComment, findDiscriminatorProperty, findAdapter, extractTypeParameters, extractStandardSchemasFromTs, extractStandardSchemasFromProject, extractStandardSchemas, extractSchemaType, extractParameters, extract, detectTsRuntime, deduplicateSchemas, createProgram, buildSchema, arktypeAdapter, TypeRegistry, TypeReference2 as TypeReference, TsRuntime, StandardSchemaExtractionResult, StandardSchemaExtractionOutput, StandardJSONSchemaV1, StandardJSONSchemaTarget, StandardJSONSchemaOptions, SerializerContext, SchemaExtractionResult, SchemaAdapter, ProjectExtractionOutput, ProjectExtractionInfo, ProgramResult, ProgramOptions, NormalizeOptions, JSONSchema, ForgottenExport, ExtractStandardSchemasOptions, ExtractResult, ExtractOptions, ExtractFromProjectOptions, Diagnostic, BUILTIN_TYPE_SCHEMAS };
package/dist/src/index.js CHANGED
@@ -27,6 +27,10 @@ import {
27
27
  isStandardJSONSchema,
28
28
  isSymbolDeprecated,
29
29
  isTypeReference,
30
+ normalizeExport,
31
+ normalizeMembers,
32
+ normalizeSchema,
33
+ normalizeType,
30
34
  registerAdapter,
31
35
  registerReferencedTypes,
32
36
  resolveCompiledPath,
@@ -42,7 +46,7 @@ import {
42
46
  valibotAdapter,
43
47
  withDescription,
44
48
  zodAdapter
45
- } from "../shared/chunk-7fp2zqf8.js";
49
+ } from "../shared/chunk-qwkv7jxy.js";
46
50
  // src/types/utils.ts
47
51
  function isExported(node) {
48
52
  const modifiers = node.modifiers;
@@ -73,6 +77,10 @@ export {
73
77
  resolveCompiledPath,
74
78
  registerReferencedTypes,
75
79
  registerAdapter,
80
+ normalizeType,
81
+ normalizeSchema,
82
+ normalizeMembers,
83
+ normalizeExport,
76
84
  isTypeReference,
77
85
  isSymbolDeprecated,
78
86
  isStandardJSONSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpkg-ts/extract",
3
- "version": "0.22.1",
3
+ "version": "0.23.1",
4
4
  "description": "TypeScript export extraction to OpenPkg spec",
5
5
  "keywords": [
6
6
  "openpkg",
@@ -40,7 +40,7 @@
40
40
  "format": "biome format --write src/"
41
41
  },
42
42
  "dependencies": {
43
- "@openpkg-ts/spec": "^0.19.0",
43
+ "@openpkg-ts/spec": "^0.23.0",
44
44
  "chalk": "^5.4.1",
45
45
  "commander": "^12.0.0",
46
46
  "tree-sitter-wasms": "^0.1.13",