@kubb/ast 5.13.1 → 5.14.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/dist/index.cjs +569 -385
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +111 -4
- package/dist/index.d.ts +111 -4
- package/dist/index.js +567 -386
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -30,11 +30,35 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
enumerable: true
|
|
31
31
|
}) : target, mod));
|
|
32
32
|
//#endregion
|
|
33
|
+
let node_crypto = require("node:crypto");
|
|
33
34
|
let node_path = require("node:path");
|
|
34
35
|
node_path = __toESM(node_path, 1);
|
|
35
|
-
let node_crypto = require("node:crypto");
|
|
36
36
|
//#region src/constants.ts
|
|
37
37
|
/**
|
|
38
|
+
* Node kind discriminators used by every AST node (for example `node.kind === nodeKinds.text`).
|
|
39
|
+
*/
|
|
40
|
+
const nodeKinds = {
|
|
41
|
+
input: "Input",
|
|
42
|
+
operation: "Operation",
|
|
43
|
+
schema: "Schema",
|
|
44
|
+
property: "Property",
|
|
45
|
+
parameter: "Parameter",
|
|
46
|
+
response: "Response",
|
|
47
|
+
requestBody: "RequestBody",
|
|
48
|
+
content: "Content",
|
|
49
|
+
type: "Type",
|
|
50
|
+
file: "File",
|
|
51
|
+
import: "Import",
|
|
52
|
+
export: "Export",
|
|
53
|
+
source: "Source",
|
|
54
|
+
const: "Const",
|
|
55
|
+
function: "Function",
|
|
56
|
+
arrowFunction: "ArrowFunction",
|
|
57
|
+
text: "Text",
|
|
58
|
+
break: "Break",
|
|
59
|
+
jsx: "Jsx"
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
38
62
|
* Schema type discriminators used by all AST schema nodes.
|
|
39
63
|
*
|
|
40
64
|
* Each value is a stable discriminator across the AST (for example `schema.type === schemaTypes.object`).
|
|
@@ -243,7 +267,7 @@ function isNodeRecord(value) {
|
|
|
243
267
|
* ```
|
|
244
268
|
*/
|
|
245
269
|
function getChildren(node, recurse) {
|
|
246
|
-
if (node.kind ===
|
|
270
|
+
if (node.kind === nodeKinds.schema && !recurse) return [];
|
|
247
271
|
const children = [];
|
|
248
272
|
visitChildNodes(node, (child) => {
|
|
249
273
|
children.push(child);
|
|
@@ -301,7 +325,7 @@ function visitChildNodes(node, visit) {
|
|
|
301
325
|
*/
|
|
302
326
|
function forEachSchema(node, visit) {
|
|
303
327
|
const walk = (current) => {
|
|
304
|
-
if (current.kind ===
|
|
328
|
+
if (current.kind === nodeKinds.schema) visit(current);
|
|
305
329
|
forEachChild(current, walk);
|
|
306
330
|
};
|
|
307
331
|
walk(node);
|
|
@@ -319,7 +343,7 @@ function forEachSchema(node, visit) {
|
|
|
319
343
|
function findSchema(node, { predicate, followRefs = false }) {
|
|
320
344
|
const seen = followRefs ? /* @__PURE__ */ new WeakSet() : void 0;
|
|
321
345
|
const walk = (current) => {
|
|
322
|
-
if (current.kind ===
|
|
346
|
+
if (current.kind === nodeKinds.schema) {
|
|
323
347
|
if (predicate(current)) return current;
|
|
324
348
|
if (seen) {
|
|
325
349
|
if (seen.has(current)) return void 0;
|
|
@@ -395,7 +419,7 @@ function transformNode(node, visitor, recurse, parent) {
|
|
|
395
419
|
* (e.g. `additionalProperties: true`) intact.
|
|
396
420
|
*/
|
|
397
421
|
function transformChildren(node, visitor, recurse) {
|
|
398
|
-
const keys = node.kind ===
|
|
422
|
+
const keys = node.kind === nodeKinds.schema && !recurse ? void 0 : VISITOR_KEYS[node.kind];
|
|
399
423
|
if (!keys) return node;
|
|
400
424
|
const record = node;
|
|
401
425
|
let updates;
|
|
@@ -680,11 +704,11 @@ function extractStringsFromNodes(nodes) {
|
|
|
680
704
|
if (node) collected.push(node);
|
|
681
705
|
continue;
|
|
682
706
|
}
|
|
683
|
-
if (node.kind ===
|
|
707
|
+
if (node.kind === nodeKinds.text || node.kind === nodeKinds.jsx) {
|
|
684
708
|
if (node.value) collected.push(node.value);
|
|
685
709
|
continue;
|
|
686
710
|
}
|
|
687
|
-
if (node.kind ===
|
|
711
|
+
if (node.kind === nodeKinds.break) continue;
|
|
688
712
|
const parts = [];
|
|
689
713
|
if ("params" in node && node.params) parts.push(node.params);
|
|
690
714
|
if ("generics" in node && node.generics) parts.push(Array.isArray(node.generics) ? node.generics.join(", ") : node.generics);
|
|
@@ -697,133 +721,550 @@ function extractStringsFromNodes(nodes) {
|
|
|
697
721
|
return collected.join("\n");
|
|
698
722
|
}
|
|
699
723
|
//#endregion
|
|
700
|
-
//#region src/utils/
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
724
|
+
//#region src/utils/combineFileMembers.ts
|
|
725
|
+
const IDENTIFIER_RUN = /[\w$]+/g;
|
|
726
|
+
/**
|
|
727
|
+
* How many imports a file needs before indexing the source beats scanning it once per name.
|
|
728
|
+
*/
|
|
729
|
+
const INDEX_ABOVE_IMPORTS = 128;
|
|
730
|
+
/**
|
|
731
|
+
* Returns the local binding an import name item introduces: the alias when there is one, else the
|
|
732
|
+
* imported name.
|
|
733
|
+
*/
|
|
734
|
+
function importLocalName(item) {
|
|
735
|
+
return typeof item === "string" ? item : item.name ?? item.propertyName;
|
|
704
736
|
}
|
|
705
737
|
/**
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
* then `name`, then the nested `schema.name`.
|
|
709
|
-
*
|
|
710
|
-
* Returns `null` for non-ref nodes or when no name resolves.
|
|
711
|
-
*
|
|
712
|
-
* @example
|
|
713
|
-
* `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Pet' }) // 'Pet'`
|
|
714
|
-
*
|
|
715
|
-
* @example Collision-renamed target
|
|
716
|
-
* `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Order', targetName: 'OrderSchema' }) // 'OrderSchema'`
|
|
738
|
+
* Computes a multi-level sort key for exports and imports:
|
|
739
|
+
* non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
|
|
717
740
|
*/
|
|
718
|
-
function
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
741
|
+
function sortKey(node) {
|
|
742
|
+
const isArray = Array.isArray(node.name) ? "1" : "0";
|
|
743
|
+
const typeOnly = node.isTypeOnly ? "0" : "1";
|
|
744
|
+
const hasName = node.name != null ? "1" : "0";
|
|
745
|
+
const name = Array.isArray(node.name) ? node.name.toSorted().join("\0") : node.name ?? "";
|
|
746
|
+
return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
|
|
723
747
|
}
|
|
724
748
|
/**
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
* @example
|
|
728
|
-
* ```ts
|
|
729
|
-
* const target = resolveRef(refSchema)
|
|
730
|
-
* ```
|
|
749
|
+
* Sorts members by {@link sortKey}. Keys are computed once per node, not per comparison.
|
|
731
750
|
*/
|
|
732
|
-
function
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
if (seen.has(current)) return current;
|
|
738
|
-
seen.add(current);
|
|
739
|
-
current = current.schema;
|
|
740
|
-
}
|
|
741
|
-
return current;
|
|
751
|
+
function sortMembers(nodes) {
|
|
752
|
+
return nodes.map((node) => ({
|
|
753
|
+
node,
|
|
754
|
+
key: sortKey(node)
|
|
755
|
+
})).sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0).map(({ node }) => node);
|
|
742
756
|
}
|
|
743
|
-
/** Fields of a ref node that describe the ref itself, so they never override the resolved schema. */
|
|
744
|
-
const REF_OWN_KEYS = /* @__PURE__ */ new Set([
|
|
745
|
-
"kind",
|
|
746
|
-
"type",
|
|
747
|
-
"name",
|
|
748
|
-
"ref",
|
|
749
|
-
"schema"
|
|
750
|
-
]);
|
|
751
757
|
/**
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
+
* Returns a collector for array-named members. A member whose path and `isTypeOnly` were already
|
|
759
|
+
* seen folds its names into that entry, otherwise it joins `result` as a new entry.
|
|
760
|
+
*/
|
|
761
|
+
function mergeByPath(result) {
|
|
762
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
763
|
+
return ({ node, names }) => {
|
|
764
|
+
const key = `${node.path}:${node.isTypeOnly ?? false}`;
|
|
765
|
+
const existing = byPath.get(key);
|
|
766
|
+
if (existing) {
|
|
767
|
+
for (const name of names) {
|
|
768
|
+
if (existing.seen.has(name)) continue;
|
|
769
|
+
existing.seen.add(name);
|
|
770
|
+
existing.names.push(name);
|
|
771
|
+
}
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const item = {
|
|
775
|
+
...node,
|
|
776
|
+
name: [...names]
|
|
777
|
+
};
|
|
778
|
+
result.push(item);
|
|
779
|
+
byPath.set(key, {
|
|
780
|
+
names: item.name,
|
|
781
|
+
seen: new Set(names)
|
|
782
|
+
});
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Deduplicates `SourceNode` objects by `name + isExportable + isTypeOnly`, keeping the first of each
|
|
787
|
+
* key. An unnamed source only drops when the same node appears twice, so markdown can repeat a
|
|
788
|
+
* paragraph or a table. Returns the deduplicated array in original order.
|
|
789
|
+
*/
|
|
790
|
+
function combineSources(sources) {
|
|
791
|
+
const seen = /* @__PURE__ */ new Set();
|
|
792
|
+
return sources.filter((source) => {
|
|
793
|
+
const key = source.name != null ? `${source.name}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}` : source;
|
|
794
|
+
if (seen.has(key)) return false;
|
|
795
|
+
seen.add(key);
|
|
796
|
+
return true;
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Deduplicates and merges `ExportNode` objects by path and type.
|
|
758
801
|
*
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
* const ref = createSchema({ type: 'ref', ref: '#/components/schemas/Pet', description: 'A pet', schema: petSchema })
|
|
762
|
-
* mergeRefWithSchema(ref) // petSchema's fields with description 'A pet'
|
|
763
|
-
* ```
|
|
802
|
+
* Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
|
|
803
|
+
* Non-array exports are deduplicated by exact identity. Returns a sorted, deduplicated array.
|
|
764
804
|
*/
|
|
765
|
-
function
|
|
766
|
-
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
805
|
+
function combineExports(exports) {
|
|
806
|
+
const result = [];
|
|
807
|
+
const merge = mergeByPath(result);
|
|
808
|
+
const seen = /* @__PURE__ */ new Set();
|
|
809
|
+
for (const curr of sortMembers(exports)) {
|
|
810
|
+
const { name, path, isTypeOnly, asAlias } = curr;
|
|
811
|
+
if (Array.isArray(name)) {
|
|
812
|
+
if (name.length) merge({
|
|
813
|
+
node: curr,
|
|
814
|
+
names: [...new Set(name)]
|
|
815
|
+
});
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
const key = `${path}:${name ?? ""}:${isTypeOnly ?? false}:${asAlias ?? ""}`;
|
|
819
|
+
if (!seen.has(key)) {
|
|
820
|
+
result.push(curr);
|
|
821
|
+
seen.add(key);
|
|
822
|
+
}
|
|
771
823
|
}
|
|
772
|
-
return
|
|
824
|
+
return result;
|
|
773
825
|
}
|
|
774
826
|
/**
|
|
775
|
-
*
|
|
827
|
+
* Deduplicates and merges `ImportNode` objects, filtering out unused imports.
|
|
776
828
|
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
829
|
+
* Retains imports that are referenced in `source` or re-exported. Imports with the same path and
|
|
830
|
+
* `isTypeOnly` flag have their names merged. Returns a sorted, deduplicated, filtered array.
|
|
779
831
|
*/
|
|
780
|
-
function
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
832
|
+
function combineImports(imports, exports, source) {
|
|
833
|
+
const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
|
|
834
|
+
const identifiers = source && imports.length > INDEX_ABOVE_IMPORTS ? new Set(source.match(IDENTIFIER_RUN)) : null;
|
|
835
|
+
const isUsed = (importName) => !source || identifiers?.has(importName) || source.includes(importName) || exportedNames.has(importName);
|
|
836
|
+
const importNameMemo = /* @__PURE__ */ new Map();
|
|
837
|
+
const canonicalizeName = (n) => {
|
|
838
|
+
if (typeof n === "string") return n;
|
|
839
|
+
const key = `${n.propertyName}:${n.name ?? ""}`;
|
|
840
|
+
if (!importNameMemo.has(key)) importNameMemo.set(key, n);
|
|
841
|
+
return importNameMemo.get(key);
|
|
842
|
+
};
|
|
843
|
+
const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
|
|
844
|
+
for (const node of imports) if (Array.isArray(node.name) && node.name.some((item) => isUsed(importLocalName(item)))) pathsWithUsedNamedImport.add(node.path);
|
|
845
|
+
const result = [];
|
|
846
|
+
const merge = mergeByPath(result);
|
|
847
|
+
const seen = /* @__PURE__ */ new Set();
|
|
848
|
+
for (const curr of sortMembers(imports)) {
|
|
849
|
+
if (curr.path === curr.root) continue;
|
|
850
|
+
const { name, path, isTypeOnly } = curr;
|
|
851
|
+
if (Array.isArray(name)) {
|
|
852
|
+
const names = [...new Set(name.map(canonicalizeName))].filter((item) => isUsed(importLocalName(item)));
|
|
853
|
+
if (names.length) merge({
|
|
854
|
+
node: curr,
|
|
855
|
+
names
|
|
856
|
+
});
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
if (!isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
|
|
860
|
+
const key = `${path}:${name}:${isTypeOnly ?? false}`;
|
|
861
|
+
if (!seen.has(key)) {
|
|
862
|
+
result.push(curr);
|
|
863
|
+
seen.add(key);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return result;
|
|
784
867
|
}
|
|
785
868
|
//#endregion
|
|
786
|
-
//#region
|
|
869
|
+
//#region src/nodes/base.ts
|
|
870
|
+
/** Builds a `createX` factory for a node whose only computed field is `kind`. */
|
|
871
|
+
function nodeFactory(kind) {
|
|
872
|
+
return (input) => {
|
|
873
|
+
return {
|
|
874
|
+
...input,
|
|
875
|
+
kind
|
|
876
|
+
};
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
//#endregion
|
|
880
|
+
//#region src/nodes/file.ts
|
|
787
881
|
/**
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
* Pass a `WeakMap` for object keys (results are GC-eligible when the key is
|
|
791
|
-
* collected) or a `Map` for primitive keys. For multi-argument functions,
|
|
792
|
-
* nest two `memoize` calls — the outer keyed by the first argument, the
|
|
793
|
-
* inner (created once per outer miss) keyed by the second.
|
|
794
|
-
*
|
|
795
|
-
* Because the cache is owned by the caller, it can be shared, inspected, or
|
|
796
|
-
* cleared independently of the memoized function.
|
|
882
|
+
* Creates an `ImportNode` representing a language-agnostic import/dependency declaration.
|
|
797
883
|
*
|
|
798
|
-
* @example
|
|
884
|
+
* @example Named import
|
|
799
885
|
* ```ts
|
|
800
|
-
*
|
|
801
|
-
*
|
|
886
|
+
* createImport({ name: ['useState'], path: 'react' })
|
|
887
|
+
* // import { useState } from 'react'
|
|
802
888
|
* ```
|
|
889
|
+
*/
|
|
890
|
+
const createImport = nodeFactory(nodeKinds.import);
|
|
891
|
+
/**
|
|
892
|
+
* Creates an `ExportNode` representing a language-agnostic export/public API declaration.
|
|
803
893
|
*
|
|
804
|
-
* @example
|
|
894
|
+
* @example Named export
|
|
805
895
|
* ```ts
|
|
806
|
-
*
|
|
807
|
-
*
|
|
896
|
+
* createExport({ name: ['Pet'], path: './Pet' })
|
|
897
|
+
* // export { Pet } from './Pet'
|
|
808
898
|
* ```
|
|
899
|
+
*/
|
|
900
|
+
const createExport = nodeFactory(nodeKinds.export);
|
|
901
|
+
/**
|
|
902
|
+
* Creates a `SourceNode` representing a fragment of source code within a file.
|
|
809
903
|
*
|
|
810
|
-
* @example
|
|
904
|
+
* @example
|
|
811
905
|
* ```ts
|
|
812
|
-
*
|
|
813
|
-
* const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
|
|
814
|
-
* fn(params)('camelcase')
|
|
906
|
+
* createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')], isExportable: true })
|
|
815
907
|
* ```
|
|
816
908
|
*/
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
909
|
+
const createSource = nodeFactory(nodeKinds.source);
|
|
910
|
+
/**
|
|
911
|
+
* Creates a fully resolved `FileNode` from a file input descriptor.
|
|
912
|
+
*
|
|
913
|
+
* Computes:
|
|
914
|
+
* - `id` SHA256 hash of the file path
|
|
915
|
+
* - `name` `baseName` without extension
|
|
916
|
+
* - `extname` extension extracted from `baseName`
|
|
917
|
+
*
|
|
918
|
+
* Deduplicates:
|
|
919
|
+
* - `sources` via `combineSources`
|
|
920
|
+
* - `exports` via `combineExports`
|
|
921
|
+
* - `imports` via `combineImports` (also filters unused imports)
|
|
922
|
+
*
|
|
923
|
+
* @throws {Error} when `baseName` has no extension.
|
|
924
|
+
*
|
|
925
|
+
* @example
|
|
926
|
+
* ```ts
|
|
927
|
+
* const file = createFile({
|
|
928
|
+
* baseName: 'petStore.ts',
|
|
929
|
+
* path: 'src/models/petStore.ts',
|
|
930
|
+
* sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],
|
|
931
|
+
* imports: [createImport({ name: ['z'], path: 'zod' })],
|
|
932
|
+
* exports: [createExport({ name: ['Pet'], path: './petStore' })],
|
|
933
|
+
* })
|
|
934
|
+
* // file.id = SHA256 hash of 'src/models/petStore.ts'
|
|
935
|
+
* // file.name = 'petStore'
|
|
936
|
+
* // file.extname = '.ts'
|
|
937
|
+
* ```
|
|
938
|
+
*
|
|
939
|
+
* @example Copy a real file into the output
|
|
940
|
+
* ```ts
|
|
941
|
+
* const file = createFile({
|
|
942
|
+
* baseName: 'client.ts',
|
|
943
|
+
* path: 'src/gen/client.ts',
|
|
944
|
+
* copy: '/abs/path/to/templates/client.ts',
|
|
945
|
+
* })
|
|
946
|
+
* ```
|
|
947
|
+
*/
|
|
948
|
+
function createFile(input) {
|
|
949
|
+
const extname = node_path.default.extname(input.baseName);
|
|
950
|
+
if (!extname) throw new Error(`No extname found for ${input.baseName}`);
|
|
951
|
+
const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
|
|
952
|
+
const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
|
|
953
|
+
const resolvedImports = (() => {
|
|
954
|
+
if (!input.imports?.length) return [];
|
|
955
|
+
const sourceParts = [];
|
|
956
|
+
const localNames = /* @__PURE__ */ new Set();
|
|
957
|
+
for (const item of resolvedSources) {
|
|
958
|
+
const extracted = item.nodes && extractStringsFromNodes(item.nodes);
|
|
959
|
+
if (extracted) sourceParts.push(extracted);
|
|
960
|
+
if (item.name) localNames.add(item.name);
|
|
961
|
+
}
|
|
962
|
+
const source = sourceParts.join("\n") || void 0;
|
|
963
|
+
return combineImports(input.imports, resolvedExports, source).flatMap((imp) => {
|
|
964
|
+
if (imp.path === input.path) return [];
|
|
965
|
+
if (!Array.isArray(imp.name)) return typeof imp.name === "string" && localNames.has(imp.name) ? [] : [imp];
|
|
966
|
+
const kept = imp.name.filter((item) => !localNames.has(importLocalName(item)));
|
|
967
|
+
if (!kept.length) return [];
|
|
968
|
+
return [kept.length === imp.name.length ? imp : {
|
|
969
|
+
...imp,
|
|
970
|
+
name: kept
|
|
971
|
+
}];
|
|
972
|
+
});
|
|
973
|
+
})();
|
|
974
|
+
return {
|
|
975
|
+
...input,
|
|
976
|
+
kind: nodeKinds.file,
|
|
977
|
+
id: (0, node_crypto.hash)("sha256", input.path, "hex"),
|
|
978
|
+
name: node_path.default.basename(input.baseName, extname),
|
|
979
|
+
extname,
|
|
980
|
+
imports: resolvedImports,
|
|
981
|
+
exports: resolvedExports,
|
|
982
|
+
sources: resolvedSources,
|
|
983
|
+
meta: input.meta ?? {}
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
//#endregion
|
|
987
|
+
//#region src/utils/inline.ts
|
|
988
|
+
/**
|
|
989
|
+
* Whether every source of `file` is inline. Other files absorb such a file, so it is never written.
|
|
990
|
+
*/
|
|
991
|
+
function isVirtualFile(file) {
|
|
992
|
+
return file.sources.length > 0 && file.sources.every((source) => source.inline);
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Replaces every name a file imports from a virtual file (see {@link isVirtualFile}) with that name's
|
|
996
|
+
* `inline.expression`, until no inline names are left. The definitions' own imports move to the importing file.
|
|
997
|
+
*/
|
|
998
|
+
function inlineFiles(files, { language }) {
|
|
999
|
+
const virtualFiles = new Map(files.filter(isVirtualFile).map((file) => [file.path, file]));
|
|
1000
|
+
const result = {
|
|
1001
|
+
files: [],
|
|
1002
|
+
virtualPaths: [...virtualFiles.keys()],
|
|
1003
|
+
problems: []
|
|
1004
|
+
};
|
|
1005
|
+
if (virtualFiles.size === 0) return result;
|
|
1006
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
1007
|
+
function define(path, name, from, lang) {
|
|
1008
|
+
const key = `${path}\0${name}`;
|
|
1009
|
+
if (definitions.has(key)) {
|
|
1010
|
+
const definition = definitions.get(key);
|
|
1011
|
+
if (definition === null) result.problems.push({
|
|
1012
|
+
path: from,
|
|
1013
|
+
message: `"${name}" from "${path}" is defined in terms of itself.`
|
|
1014
|
+
});
|
|
1015
|
+
return definition ?? void 0;
|
|
1016
|
+
}
|
|
1017
|
+
const file = virtualFiles.get(path);
|
|
1018
|
+
const inline = file.sources.find((source) => source.name === name)?.inline;
|
|
1019
|
+
if (!inline) {
|
|
1020
|
+
result.problems.push({
|
|
1021
|
+
path: from,
|
|
1022
|
+
message: `"${name}" is imported from "${path}", which never writes it and has no inline definition for it.`
|
|
1023
|
+
});
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
definitions.set(key, null);
|
|
1027
|
+
const imports = [];
|
|
1028
|
+
const { text, used } = replaceNames(inline.expression, file, lang, imports);
|
|
1029
|
+
imports.push(...file.imports.flatMap((imp) => virtualFiles.has(imp.path) ? [] : pickNames(imp, used)));
|
|
1030
|
+
const definition = {
|
|
1031
|
+
expression: text,
|
|
1032
|
+
imports
|
|
1033
|
+
};
|
|
1034
|
+
definitions.set(key, definition);
|
|
1035
|
+
return definition;
|
|
1036
|
+
}
|
|
1037
|
+
function replaceNames(input, file, lang, imports) {
|
|
1038
|
+
const used = /* @__PURE__ */ new Set();
|
|
1039
|
+
return {
|
|
1040
|
+
text: lang.replaceIdentifiers(input, (local) => {
|
|
1041
|
+
const target = findTarget(file, local);
|
|
1042
|
+
const definition = target && define(target.path, target.name, file.path, lang);
|
|
1043
|
+
if (!definition) {
|
|
1044
|
+
used.add(local);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
imports.push(...definition.imports);
|
|
1048
|
+
return lang.wrap(definition.expression);
|
|
1049
|
+
}),
|
|
1050
|
+
used
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
function findTarget(file, local) {
|
|
1054
|
+
if (virtualFiles.has(file.path) && file.sources.some((source) => source.name === local)) return {
|
|
1055
|
+
path: file.path,
|
|
1056
|
+
name: local
|
|
1057
|
+
};
|
|
1058
|
+
for (const imp of file.imports) {
|
|
1059
|
+
const item = virtualFiles.has(imp.path) && [imp.name].flat().find((item) => importLocalName(item) === local);
|
|
1060
|
+
if (item) return {
|
|
1061
|
+
path: imp.path,
|
|
1062
|
+
name: typeof item === "string" ? item : item.propertyName
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
for (const file of files) {
|
|
1067
|
+
if (virtualFiles.has(file.path) || !file.imports.some((imp) => virtualFiles.has(imp.path))) continue;
|
|
1068
|
+
const lang = language(file);
|
|
1069
|
+
if (!lang) {
|
|
1070
|
+
result.problems.push({
|
|
1071
|
+
path: file.path,
|
|
1072
|
+
message: `No parser for "${file.extname}" files supports inline sources.`
|
|
1073
|
+
});
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
const carried = [];
|
|
1077
|
+
const sources = file.sources.map((source) => ({
|
|
1078
|
+
...source,
|
|
1079
|
+
nodes: rewriteNodes(source.nodes, (text) => replaceNames(text, file, lang, carried).text)
|
|
1080
|
+
}));
|
|
1081
|
+
const imports = [...file.imports.filter((imp) => !virtualFiles.has(imp.path)), ...carried.map((imp) => imp.root ? {
|
|
1082
|
+
...imp,
|
|
1083
|
+
root: file.path
|
|
1084
|
+
} : imp)];
|
|
1085
|
+
result.files.push(createFile({
|
|
1086
|
+
...file,
|
|
1087
|
+
sources,
|
|
1088
|
+
imports
|
|
1089
|
+
}));
|
|
1090
|
+
}
|
|
1091
|
+
return result;
|
|
1092
|
+
}
|
|
1093
|
+
function pickNames(imp, used) {
|
|
1094
|
+
if (!Array.isArray(imp.name)) return used.has(imp.name) ? [imp] : [];
|
|
1095
|
+
const name = imp.name.filter((item) => used.has(importLocalName(item)));
|
|
1096
|
+
return name.length > 0 ? [{
|
|
1097
|
+
...imp,
|
|
1098
|
+
name
|
|
1099
|
+
}] : [];
|
|
1100
|
+
}
|
|
1101
|
+
function rewriteNodes(nodes, rewrite) {
|
|
1102
|
+
return nodes?.map((node) => {
|
|
1103
|
+
if (typeof node === "string") return rewrite(node);
|
|
1104
|
+
switch (node.kind) {
|
|
1105
|
+
case nodeKinds.break: return node;
|
|
1106
|
+
case nodeKinds.text:
|
|
1107
|
+
case nodeKinds.jsx: return {
|
|
1108
|
+
...node,
|
|
1109
|
+
value: rewrite(node.value)
|
|
1110
|
+
};
|
|
1111
|
+
case nodeKinds.function:
|
|
1112
|
+
case nodeKinds.arrowFunction: return {
|
|
1113
|
+
...node,
|
|
1114
|
+
params: node.params && rewrite(node.params),
|
|
1115
|
+
generics: Array.isArray(node.generics) ? node.generics.map(rewrite) : node.generics && rewrite(node.generics),
|
|
1116
|
+
returnType: node.returnType && rewrite(node.returnType),
|
|
1117
|
+
nodes: rewriteNodes(node.nodes, rewrite)
|
|
1118
|
+
};
|
|
1119
|
+
case nodeKinds.const: return {
|
|
1120
|
+
...node,
|
|
1121
|
+
type: node.type && rewrite(node.type),
|
|
1122
|
+
nodes: rewriteNodes(node.nodes, rewrite)
|
|
1123
|
+
};
|
|
1124
|
+
default: return {
|
|
1125
|
+
...node,
|
|
1126
|
+
nodes: rewriteNodes(node.nodes, rewrite)
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
//#endregion
|
|
1132
|
+
//#region src/utils/refs.ts
|
|
1133
|
+
/**
|
|
1134
|
+
* Returns the last segment of a `$ref` pointer, for example `'Pet'` for `#/components/schemas/Pet`.
|
|
1135
|
+
* The segment is decoded like the pointer resolver does: percent-escapes first, then `~1` and `~0`
|
|
1136
|
+
* (RFC 6901), so `a~1b~0c` names the component `a/b~c`.
|
|
1137
|
+
*/
|
|
1138
|
+
function refName(ref) {
|
|
1139
|
+
const segment = ref.split("/").at(-1) ?? ref;
|
|
1140
|
+
let decoded = segment;
|
|
1141
|
+
try {
|
|
1142
|
+
decoded = decodeURIComponent(segment);
|
|
1143
|
+
} catch {}
|
|
1144
|
+
return decoded.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Resolves the emitted name of the schema a ref node points at. Prefers `targetName` (set when
|
|
1148
|
+
* the referenced schema was renamed, e.g. to break a collision), then the last segment of `ref`,
|
|
1149
|
+
* then `name`, then the nested `schema.name`.
|
|
1150
|
+
*
|
|
1151
|
+
* Returns `null` for non-ref nodes or when no name resolves.
|
|
1152
|
+
*
|
|
1153
|
+
* @example
|
|
1154
|
+
* `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Pet' }) // 'Pet'`
|
|
1155
|
+
*
|
|
1156
|
+
* @example Collision-renamed target
|
|
1157
|
+
* `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Order', targetName: 'OrderSchema' }) // 'OrderSchema'`
|
|
1158
|
+
*/
|
|
1159
|
+
function resolveRefName(node) {
|
|
1160
|
+
if (!node || node.type !== "ref") return null;
|
|
1161
|
+
if (node.targetName) return node.targetName;
|
|
1162
|
+
if (node.ref) return refName(node.ref);
|
|
1163
|
+
return node.name ?? node.schema?.name ?? null;
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Follows a resolved ref chain to its target schema. Returns an unresolved or cyclic ref as is.
|
|
1167
|
+
*
|
|
1168
|
+
* @example
|
|
1169
|
+
* ```ts
|
|
1170
|
+
* const target = resolveRef(refSchema)
|
|
1171
|
+
* ```
|
|
1172
|
+
*/
|
|
1173
|
+
function resolveRef(node) {
|
|
1174
|
+
if (node.type !== "ref" || !node.schema) return node;
|
|
1175
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1176
|
+
let current = node;
|
|
1177
|
+
while (current.type === "ref" && current.schema) {
|
|
1178
|
+
if (seen.has(current)) return current;
|
|
1179
|
+
seen.add(current);
|
|
1180
|
+
current = current.schema;
|
|
1181
|
+
}
|
|
1182
|
+
return current;
|
|
1183
|
+
}
|
|
1184
|
+
/** Fields of a ref node that describe the ref itself, so they never override the resolved schema. */
|
|
1185
|
+
const REF_OWN_KEYS = /* @__PURE__ */ new Set([
|
|
1186
|
+
"kind",
|
|
1187
|
+
"type",
|
|
1188
|
+
"name",
|
|
1189
|
+
"ref",
|
|
1190
|
+
"schema"
|
|
1191
|
+
]);
|
|
1192
|
+
/**
|
|
1193
|
+
* Merges a ref node with its resolved schema, giving the fields set at the usage site precedence.
|
|
1194
|
+
*
|
|
1195
|
+
* Every field on the ref except `kind`, `type`, `name`, `ref`, and `schema` overrides the same field
|
|
1196
|
+
* of the resolved `schema` (for example `description`, `nullable`, `readOnly`, `deprecated`). Fields
|
|
1197
|
+
* left `undefined` on the ref do not shadow the resolved schema. A ref without a resolved `schema`
|
|
1198
|
+
* is returned as is.
|
|
1199
|
+
*
|
|
1200
|
+
* @example
|
|
1201
|
+
* ```ts
|
|
1202
|
+
* const ref = createSchema({ type: 'ref', ref: '#/components/schemas/Pet', description: 'A pet', schema: petSchema })
|
|
1203
|
+
* mergeRefWithSchema(ref) // petSchema's fields with description 'A pet'
|
|
1204
|
+
* ```
|
|
1205
|
+
*/
|
|
1206
|
+
function mergeRefWithSchema(node) {
|
|
1207
|
+
if (!node.schema) return node;
|
|
1208
|
+
const merged = { ...node.schema };
|
|
1209
|
+
for (const key of Object.keys(node)) {
|
|
1210
|
+
const value = node[key];
|
|
1211
|
+
if (value !== void 0 && !REF_OWN_KEYS.has(key)) merged[key] = value;
|
|
1212
|
+
}
|
|
1213
|
+
return merged;
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Returns `true` when a schema is a bare ref without modifiers, metadata, or omit keys.
|
|
1217
|
+
*
|
|
1218
|
+
* Modifiers inherited from the resolved schema are included in the check because a ref emits the
|
|
1219
|
+
* referenced schema variable with those modifiers already applied.
|
|
1220
|
+
*/
|
|
1221
|
+
function isBareRef(node, keysToOmit) {
|
|
1222
|
+
if (!node || node.type !== "ref") return false;
|
|
1223
|
+
const meta = mergeRefWithSchema(node);
|
|
1224
|
+
return !meta.nullable && !meta.optional && !meta.nullish && meta.default === void 0 && !meta.description && !meta.examples?.length && !keysToOmit?.length;
|
|
1225
|
+
}
|
|
1226
|
+
//#endregion
|
|
1227
|
+
//#region ../../internals/utils/src/promise.ts
|
|
1228
|
+
/**
|
|
1229
|
+
* Wraps `factory` with a keyed cache backed by the provided store.
|
|
1230
|
+
*
|
|
1231
|
+
* Pass a `WeakMap` for object keys (results are GC-eligible when the key is
|
|
1232
|
+
* collected) or a `Map` for primitive keys. For multi-argument functions,
|
|
1233
|
+
* nest two `memoize` calls — the outer keyed by the first argument, the
|
|
1234
|
+
* inner (created once per outer miss) keyed by the second.
|
|
1235
|
+
*
|
|
1236
|
+
* Because the cache is owned by the caller, it can be shared, inspected, or
|
|
1237
|
+
* cleared independently of the memoized function.
|
|
1238
|
+
*
|
|
1239
|
+
* @example Single WeakMap key
|
|
1240
|
+
* ```ts
|
|
1241
|
+
* const cache = new WeakMap<SchemaNode, Set<string>>()
|
|
1242
|
+
* const getRefs = memoize(cache, (node) => collectRefs(node))
|
|
1243
|
+
* ```
|
|
1244
|
+
*
|
|
1245
|
+
* @example Single Map key (primitive)
|
|
1246
|
+
* ```ts
|
|
1247
|
+
* const cache = new Map<string, Resolver>()
|
|
1248
|
+
* const getResolver = memoize(cache, (name) => buildResolver(name))
|
|
1249
|
+
* ```
|
|
1250
|
+
*
|
|
1251
|
+
* @example Two-level (object + primitive)
|
|
1252
|
+
* ```ts
|
|
1253
|
+
* const outer = new WeakMap<Params[], Map<string, Params[]>>()
|
|
1254
|
+
* const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
|
|
1255
|
+
* fn(params)('camelcase')
|
|
1256
|
+
* ```
|
|
1257
|
+
*/
|
|
1258
|
+
function memoize(store, factory) {
|
|
1259
|
+
return (key) => {
|
|
1260
|
+
if (store.has(key)) return store.get(key);
|
|
1261
|
+
const value = factory(key);
|
|
1262
|
+
store.set(key, value);
|
|
1263
|
+
return value;
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
//#endregion
|
|
1267
|
+
//#region src/utils/schemaGraph.ts
|
|
827
1268
|
/**
|
|
828
1269
|
* Collects the names of every schema a node's subtree references, resolved with `resolveRefName`.
|
|
829
1270
|
* Each ref contributes its name only, so the schema it points to is never traversed. The walk is
|
|
@@ -1080,17 +1521,6 @@ function inferDiscriminatorPropertyName(members) {
|
|
|
1080
1521
|
}
|
|
1081
1522
|
}
|
|
1082
1523
|
//#endregion
|
|
1083
|
-
//#region src/nodes/base.ts
|
|
1084
|
-
/** Builds a `createX` factory for a node whose only computed field is `kind`. */
|
|
1085
|
-
function nodeFactory(kind) {
|
|
1086
|
-
return (input) => {
|
|
1087
|
-
return {
|
|
1088
|
-
...input,
|
|
1089
|
-
kind
|
|
1090
|
-
};
|
|
1091
|
-
};
|
|
1092
|
-
}
|
|
1093
|
-
//#endregion
|
|
1094
1524
|
//#region src/nodes/code.ts
|
|
1095
1525
|
/**
|
|
1096
1526
|
* Creates a `ConstNode` representing a TypeScript `const` declaration.
|
|
@@ -1101,7 +1531,7 @@ function nodeFactory(kind) {
|
|
|
1101
1531
|
* // export const pets: Pet[] = ... as const
|
|
1102
1532
|
* ```
|
|
1103
1533
|
*/
|
|
1104
|
-
const createConst = nodeFactory(
|
|
1534
|
+
const createConst = nodeFactory(nodeKinds.const);
|
|
1105
1535
|
/**
|
|
1106
1536
|
* Creates a `TypeNode` representing a TypeScript `type` alias declaration.
|
|
1107
1537
|
*
|
|
@@ -1111,7 +1541,7 @@ const createConst = nodeFactory("Const");
|
|
|
1111
1541
|
* // export type Pet = ...
|
|
1112
1542
|
* ```
|
|
1113
1543
|
*/
|
|
1114
|
-
const createType = nodeFactory(
|
|
1544
|
+
const createType = nodeFactory(nodeKinds.type);
|
|
1115
1545
|
/**
|
|
1116
1546
|
* Creates a `FunctionNode` representing a TypeScript `function` declaration.
|
|
1117
1547
|
*
|
|
@@ -1121,7 +1551,7 @@ const createType = nodeFactory("Type");
|
|
|
1121
1551
|
* // export async function fetchPet(): Promise<Pet> { ... }
|
|
1122
1552
|
* ```
|
|
1123
1553
|
*/
|
|
1124
|
-
const createFunction = nodeFactory(
|
|
1554
|
+
const createFunction = nodeFactory(nodeKinds.function);
|
|
1125
1555
|
/**
|
|
1126
1556
|
* Creates an `ArrowFunctionNode` representing a TypeScript arrow function.
|
|
1127
1557
|
*
|
|
@@ -1131,7 +1561,7 @@ const createFunction = nodeFactory("Function");
|
|
|
1131
1561
|
* // export const double = (n: number) => ...
|
|
1132
1562
|
* ```
|
|
1133
1563
|
*/
|
|
1134
|
-
const createArrowFunction = nodeFactory(
|
|
1564
|
+
const createArrowFunction = nodeFactory(nodeKinds.arrowFunction);
|
|
1135
1565
|
/**
|
|
1136
1566
|
* Creates a {@link TextNode} representing a raw string fragment in the source output.
|
|
1137
1567
|
*
|
|
@@ -1143,7 +1573,7 @@ const createArrowFunction = nodeFactory("ArrowFunction");
|
|
|
1143
1573
|
*/
|
|
1144
1574
|
function createText(value) {
|
|
1145
1575
|
return {
|
|
1146
|
-
kind:
|
|
1576
|
+
kind: nodeKinds.text,
|
|
1147
1577
|
value
|
|
1148
1578
|
};
|
|
1149
1579
|
}
|
|
@@ -1157,7 +1587,7 @@ function createText(value) {
|
|
|
1157
1587
|
* ```
|
|
1158
1588
|
*/
|
|
1159
1589
|
function createBreak() {
|
|
1160
|
-
return { kind:
|
|
1590
|
+
return { kind: nodeKinds.break };
|
|
1161
1591
|
}
|
|
1162
1592
|
/**
|
|
1163
1593
|
* Creates a {@link JsxNode} representing a raw JSX fragment in the source output.
|
|
@@ -1170,7 +1600,7 @@ function createBreak() {
|
|
|
1170
1600
|
*/
|
|
1171
1601
|
function createJsx(value) {
|
|
1172
1602
|
return {
|
|
1173
|
-
kind:
|
|
1603
|
+
kind: nodeKinds.jsx,
|
|
1174
1604
|
value
|
|
1175
1605
|
};
|
|
1176
1606
|
}
|
|
@@ -1179,259 +1609,7 @@ function createJsx(value) {
|
|
|
1179
1609
|
/**
|
|
1180
1610
|
* Creates a `ContentNode` for a single request-body or response content type.
|
|
1181
1611
|
*/
|
|
1182
|
-
const createContent = nodeFactory(
|
|
1183
|
-
//#endregion
|
|
1184
|
-
//#region src/utils/combineFileMembers.ts
|
|
1185
|
-
const IDENTIFIER_RUN = /[\w$]+/g;
|
|
1186
|
-
/**
|
|
1187
|
-
* How many imports a file needs before indexing the source beats scanning it once per name.
|
|
1188
|
-
*/
|
|
1189
|
-
const INDEX_ABOVE_IMPORTS = 128;
|
|
1190
|
-
/**
|
|
1191
|
-
* Returns the local binding an import name item introduces: the alias when there is one, else the
|
|
1192
|
-
* imported name.
|
|
1193
|
-
*/
|
|
1194
|
-
function importLocalName(item) {
|
|
1195
|
-
return typeof item === "string" ? item : item.name ?? item.propertyName;
|
|
1196
|
-
}
|
|
1197
|
-
/**
|
|
1198
|
-
* Computes a multi-level sort key for exports and imports:
|
|
1199
|
-
* non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
|
|
1200
|
-
*/
|
|
1201
|
-
function sortKey(node) {
|
|
1202
|
-
const isArray = Array.isArray(node.name) ? "1" : "0";
|
|
1203
|
-
const typeOnly = node.isTypeOnly ? "0" : "1";
|
|
1204
|
-
const hasName = node.name != null ? "1" : "0";
|
|
1205
|
-
const name = Array.isArray(node.name) ? node.name.toSorted().join("\0") : node.name ?? "";
|
|
1206
|
-
return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
|
|
1207
|
-
}
|
|
1208
|
-
/**
|
|
1209
|
-
* Sorts members by {@link sortKey}. Keys are computed once per node, not per comparison.
|
|
1210
|
-
*/
|
|
1211
|
-
function sortMembers(nodes) {
|
|
1212
|
-
return nodes.map((node) => ({
|
|
1213
|
-
node,
|
|
1214
|
-
key: sortKey(node)
|
|
1215
|
-
})).sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0).map(({ node }) => node);
|
|
1216
|
-
}
|
|
1217
|
-
/**
|
|
1218
|
-
* Returns a collector for array-named members. A member whose path and `isTypeOnly` were already
|
|
1219
|
-
* seen folds its names into that entry, otherwise it joins `result` as a new entry.
|
|
1220
|
-
*/
|
|
1221
|
-
function mergeByPath(result) {
|
|
1222
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
1223
|
-
return ({ node, names }) => {
|
|
1224
|
-
const key = `${node.path}:${node.isTypeOnly ?? false}`;
|
|
1225
|
-
const existing = byPath.get(key);
|
|
1226
|
-
if (existing) {
|
|
1227
|
-
for (const name of names) {
|
|
1228
|
-
if (existing.seen.has(name)) continue;
|
|
1229
|
-
existing.seen.add(name);
|
|
1230
|
-
existing.names.push(name);
|
|
1231
|
-
}
|
|
1232
|
-
return;
|
|
1233
|
-
}
|
|
1234
|
-
const item = {
|
|
1235
|
-
...node,
|
|
1236
|
-
name: [...names]
|
|
1237
|
-
};
|
|
1238
|
-
result.push(item);
|
|
1239
|
-
byPath.set(key, {
|
|
1240
|
-
names: item.name,
|
|
1241
|
-
seen: new Set(names)
|
|
1242
|
-
});
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
/**
|
|
1246
|
-
* Deduplicates `SourceNode` objects by `name + isExportable + isTypeOnly`, keeping the first of each
|
|
1247
|
-
* key. An unnamed source only drops when the same node appears twice, so markdown can repeat a
|
|
1248
|
-
* paragraph or a table. Returns the deduplicated array in original order.
|
|
1249
|
-
*/
|
|
1250
|
-
function combineSources(sources) {
|
|
1251
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1252
|
-
return sources.filter((source) => {
|
|
1253
|
-
const key = source.name != null ? `${source.name}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}` : source;
|
|
1254
|
-
if (seen.has(key)) return false;
|
|
1255
|
-
seen.add(key);
|
|
1256
|
-
return true;
|
|
1257
|
-
});
|
|
1258
|
-
}
|
|
1259
|
-
/**
|
|
1260
|
-
* Deduplicates and merges `ExportNode` objects by path and type.
|
|
1261
|
-
*
|
|
1262
|
-
* Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
|
|
1263
|
-
* Non-array exports are deduplicated by exact identity. Returns a sorted, deduplicated array.
|
|
1264
|
-
*/
|
|
1265
|
-
function combineExports(exports) {
|
|
1266
|
-
const result = [];
|
|
1267
|
-
const merge = mergeByPath(result);
|
|
1268
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1269
|
-
for (const curr of sortMembers(exports)) {
|
|
1270
|
-
const { name, path, isTypeOnly, asAlias } = curr;
|
|
1271
|
-
if (Array.isArray(name)) {
|
|
1272
|
-
if (name.length) merge({
|
|
1273
|
-
node: curr,
|
|
1274
|
-
names: [...new Set(name)]
|
|
1275
|
-
});
|
|
1276
|
-
continue;
|
|
1277
|
-
}
|
|
1278
|
-
const key = `${path}:${name ?? ""}:${isTypeOnly ?? false}:${asAlias ?? ""}`;
|
|
1279
|
-
if (!seen.has(key)) {
|
|
1280
|
-
result.push(curr);
|
|
1281
|
-
seen.add(key);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
return result;
|
|
1285
|
-
}
|
|
1286
|
-
/**
|
|
1287
|
-
* Deduplicates and merges `ImportNode` objects, filtering out unused imports.
|
|
1288
|
-
*
|
|
1289
|
-
* Retains imports that are referenced in `source` or re-exported. Imports with the same path and
|
|
1290
|
-
* `isTypeOnly` flag have their names merged. Returns a sorted, deduplicated, filtered array.
|
|
1291
|
-
*/
|
|
1292
|
-
function combineImports(imports, exports, source) {
|
|
1293
|
-
const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
|
|
1294
|
-
const identifiers = source && imports.length > INDEX_ABOVE_IMPORTS ? new Set(source.match(IDENTIFIER_RUN)) : null;
|
|
1295
|
-
const isUsed = (importName) => !source || identifiers?.has(importName) || source.includes(importName) || exportedNames.has(importName);
|
|
1296
|
-
const importNameMemo = /* @__PURE__ */ new Map();
|
|
1297
|
-
const canonicalizeName = (n) => {
|
|
1298
|
-
if (typeof n === "string") return n;
|
|
1299
|
-
const key = `${n.propertyName}:${n.name ?? ""}`;
|
|
1300
|
-
if (!importNameMemo.has(key)) importNameMemo.set(key, n);
|
|
1301
|
-
return importNameMemo.get(key);
|
|
1302
|
-
};
|
|
1303
|
-
const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
|
|
1304
|
-
for (const node of imports) if (Array.isArray(node.name) && node.name.some((item) => isUsed(importLocalName(item)))) pathsWithUsedNamedImport.add(node.path);
|
|
1305
|
-
const result = [];
|
|
1306
|
-
const merge = mergeByPath(result);
|
|
1307
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1308
|
-
for (const curr of sortMembers(imports)) {
|
|
1309
|
-
if (curr.path === curr.root) continue;
|
|
1310
|
-
const { name, path, isTypeOnly } = curr;
|
|
1311
|
-
if (Array.isArray(name)) {
|
|
1312
|
-
const names = [...new Set(name.map(canonicalizeName))].filter((item) => isUsed(importLocalName(item)));
|
|
1313
|
-
if (names.length) merge({
|
|
1314
|
-
node: curr,
|
|
1315
|
-
names
|
|
1316
|
-
});
|
|
1317
|
-
continue;
|
|
1318
|
-
}
|
|
1319
|
-
if (!isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
|
|
1320
|
-
const key = `${path}:${name}:${isTypeOnly ?? false}`;
|
|
1321
|
-
if (!seen.has(key)) {
|
|
1322
|
-
result.push(curr);
|
|
1323
|
-
seen.add(key);
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
return result;
|
|
1327
|
-
}
|
|
1328
|
-
//#endregion
|
|
1329
|
-
//#region src/nodes/file.ts
|
|
1330
|
-
/**
|
|
1331
|
-
* Creates an `ImportNode` representing a language-agnostic import/dependency declaration.
|
|
1332
|
-
*
|
|
1333
|
-
* @example Named import
|
|
1334
|
-
* ```ts
|
|
1335
|
-
* createImport({ name: ['useState'], path: 'react' })
|
|
1336
|
-
* // import { useState } from 'react'
|
|
1337
|
-
* ```
|
|
1338
|
-
*/
|
|
1339
|
-
const createImport = nodeFactory("Import");
|
|
1340
|
-
/**
|
|
1341
|
-
* Creates an `ExportNode` representing a language-agnostic export/public API declaration.
|
|
1342
|
-
*
|
|
1343
|
-
* @example Named export
|
|
1344
|
-
* ```ts
|
|
1345
|
-
* createExport({ name: ['Pet'], path: './Pet' })
|
|
1346
|
-
* // export { Pet } from './Pet'
|
|
1347
|
-
* ```
|
|
1348
|
-
*/
|
|
1349
|
-
const createExport = nodeFactory("Export");
|
|
1350
|
-
/**
|
|
1351
|
-
* Creates a `SourceNode` representing a fragment of source code within a file.
|
|
1352
|
-
*
|
|
1353
|
-
* @example
|
|
1354
|
-
* ```ts
|
|
1355
|
-
* createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')], isExportable: true })
|
|
1356
|
-
* ```
|
|
1357
|
-
*/
|
|
1358
|
-
const createSource = nodeFactory("Source");
|
|
1359
|
-
/**
|
|
1360
|
-
* Creates a fully resolved `FileNode` from a file input descriptor.
|
|
1361
|
-
*
|
|
1362
|
-
* Computes:
|
|
1363
|
-
* - `id` SHA256 hash of the file path
|
|
1364
|
-
* - `name` `baseName` without extension
|
|
1365
|
-
* - `extname` extension extracted from `baseName`
|
|
1366
|
-
*
|
|
1367
|
-
* Deduplicates:
|
|
1368
|
-
* - `sources` via `combineSources`
|
|
1369
|
-
* - `exports` via `combineExports`
|
|
1370
|
-
* - `imports` via `combineImports` (also filters unused imports)
|
|
1371
|
-
*
|
|
1372
|
-
* @throws {Error} when `baseName` has no extension.
|
|
1373
|
-
*
|
|
1374
|
-
* @example
|
|
1375
|
-
* ```ts
|
|
1376
|
-
* const file = createFile({
|
|
1377
|
-
* baseName: 'petStore.ts',
|
|
1378
|
-
* path: 'src/models/petStore.ts',
|
|
1379
|
-
* sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],
|
|
1380
|
-
* imports: [createImport({ name: ['z'], path: 'zod' })],
|
|
1381
|
-
* exports: [createExport({ name: ['Pet'], path: './petStore' })],
|
|
1382
|
-
* })
|
|
1383
|
-
* // file.id = SHA256 hash of 'src/models/petStore.ts'
|
|
1384
|
-
* // file.name = 'petStore'
|
|
1385
|
-
* // file.extname = '.ts'
|
|
1386
|
-
* ```
|
|
1387
|
-
*
|
|
1388
|
-
* @example Copy a real file into the output
|
|
1389
|
-
* ```ts
|
|
1390
|
-
* const file = createFile({
|
|
1391
|
-
* baseName: 'client.ts',
|
|
1392
|
-
* path: 'src/gen/client.ts',
|
|
1393
|
-
* copy: '/abs/path/to/templates/client.ts',
|
|
1394
|
-
* })
|
|
1395
|
-
* ```
|
|
1396
|
-
*/
|
|
1397
|
-
function createFile(input) {
|
|
1398
|
-
const extname = node_path.default.extname(input.baseName);
|
|
1399
|
-
if (!extname) throw new Error(`No extname found for ${input.baseName}`);
|
|
1400
|
-
const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
|
|
1401
|
-
const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
|
|
1402
|
-
const resolvedImports = (() => {
|
|
1403
|
-
if (!input.imports?.length) return [];
|
|
1404
|
-
const sourceParts = [];
|
|
1405
|
-
const localNames = /* @__PURE__ */ new Set();
|
|
1406
|
-
for (const item of resolvedSources) {
|
|
1407
|
-
const extracted = item.nodes && extractStringsFromNodes(item.nodes);
|
|
1408
|
-
if (extracted) sourceParts.push(extracted);
|
|
1409
|
-
if (item.name) localNames.add(item.name);
|
|
1410
|
-
}
|
|
1411
|
-
const source = sourceParts.join("\n") || void 0;
|
|
1412
|
-
return combineImports(input.imports, resolvedExports, source).flatMap((imp) => {
|
|
1413
|
-
if (imp.path === input.path) return [];
|
|
1414
|
-
if (!Array.isArray(imp.name)) return typeof imp.name === "string" && localNames.has(imp.name) ? [] : [imp];
|
|
1415
|
-
const kept = imp.name.filter((item) => !localNames.has(importLocalName(item)));
|
|
1416
|
-
if (!kept.length) return [];
|
|
1417
|
-
return [kept.length === imp.name.length ? imp : {
|
|
1418
|
-
...imp,
|
|
1419
|
-
name: kept
|
|
1420
|
-
}];
|
|
1421
|
-
});
|
|
1422
|
-
})();
|
|
1423
|
-
return {
|
|
1424
|
-
...input,
|
|
1425
|
-
kind: "File",
|
|
1426
|
-
id: (0, node_crypto.hash)("sha256", input.path, "hex"),
|
|
1427
|
-
name: node_path.default.basename(input.baseName, extname),
|
|
1428
|
-
extname,
|
|
1429
|
-
imports: resolvedImports,
|
|
1430
|
-
exports: resolvedExports,
|
|
1431
|
-
sources: resolvedSources,
|
|
1432
|
-
meta: input.meta ?? {}
|
|
1433
|
-
};
|
|
1434
|
-
}
|
|
1612
|
+
const createContent = nodeFactory(nodeKinds.content);
|
|
1435
1613
|
//#endregion
|
|
1436
1614
|
//#region src/nodes/input.ts
|
|
1437
1615
|
/**
|
|
@@ -1453,7 +1631,7 @@ function createInput(overrides = {}) {
|
|
|
1453
1631
|
enumNames: []
|
|
1454
1632
|
},
|
|
1455
1633
|
...overrides,
|
|
1456
|
-
kind:
|
|
1634
|
+
kind: nodeKinds.input
|
|
1457
1635
|
};
|
|
1458
1636
|
}
|
|
1459
1637
|
//#endregion
|
|
@@ -1461,7 +1639,7 @@ function createInput(overrides = {}) {
|
|
|
1461
1639
|
/**
|
|
1462
1640
|
* Creates a `RequestBodyNode`.
|
|
1463
1641
|
*/
|
|
1464
|
-
const createRequestBody = nodeFactory(
|
|
1642
|
+
const createRequestBody = nodeFactory(nodeKinds.requestBody);
|
|
1465
1643
|
//#endregion
|
|
1466
1644
|
//#region src/nodes/operation.ts
|
|
1467
1645
|
function createOperation(props) {
|
|
@@ -1472,7 +1650,7 @@ function createOperation(props) {
|
|
|
1472
1650
|
parameters: [],
|
|
1473
1651
|
responses: [],
|
|
1474
1652
|
...rest,
|
|
1475
|
-
kind:
|
|
1653
|
+
kind: nodeKinds.operation,
|
|
1476
1654
|
...isHttp ? { protocol: "http" } : {},
|
|
1477
1655
|
requestBody: requestBody ? createRequestBody(requestBody) : void 0
|
|
1478
1656
|
};
|
|
@@ -1499,7 +1677,7 @@ function createParameter(input) {
|
|
|
1499
1677
|
...input,
|
|
1500
1678
|
required,
|
|
1501
1679
|
schema: optionality(input.schema, required),
|
|
1502
|
-
kind:
|
|
1680
|
+
kind: nodeKinds.parameter
|
|
1503
1681
|
};
|
|
1504
1682
|
}
|
|
1505
1683
|
//#endregion
|
|
@@ -1524,7 +1702,7 @@ function createProperty(input) {
|
|
|
1524
1702
|
...input,
|
|
1525
1703
|
required,
|
|
1526
1704
|
schema: optionality(input.schema, required),
|
|
1527
|
-
kind:
|
|
1705
|
+
kind: nodeKinds.property
|
|
1528
1706
|
};
|
|
1529
1707
|
}
|
|
1530
1708
|
//#endregion
|
|
@@ -1551,7 +1729,7 @@ function createResponse(props) {
|
|
|
1551
1729
|
return {
|
|
1552
1730
|
...rest,
|
|
1553
1731
|
content: entries,
|
|
1554
|
-
kind:
|
|
1732
|
+
kind: nodeKinds.response
|
|
1555
1733
|
};
|
|
1556
1734
|
}
|
|
1557
1735
|
//#endregion
|
|
@@ -1586,12 +1764,12 @@ function createSchema(props) {
|
|
|
1586
1764
|
properties: [],
|
|
1587
1765
|
primitive: "object",
|
|
1588
1766
|
...props,
|
|
1589
|
-
kind:
|
|
1767
|
+
kind: nodeKinds.schema
|
|
1590
1768
|
};
|
|
1591
1769
|
return {
|
|
1592
1770
|
primitive: TYPE_TO_PRIMITIVE[props.type],
|
|
1593
1771
|
...props,
|
|
1594
|
-
kind:
|
|
1772
|
+
kind: nodeKinds.schema
|
|
1595
1773
|
};
|
|
1596
1774
|
}
|
|
1597
1775
|
//#endregion
|
|
@@ -1639,10 +1817,13 @@ var src_exports = /* @__PURE__ */ __exportAll({
|
|
|
1639
1817
|
forEachSchema: () => forEachSchema,
|
|
1640
1818
|
getSchemaLiteralValues: () => getSchemaLiteralValues,
|
|
1641
1819
|
inferDiscriminatorPropertyName: () => inferDiscriminatorPropertyName,
|
|
1820
|
+
inlineFiles: () => inlineFiles,
|
|
1642
1821
|
isBareRef: () => isBareRef,
|
|
1643
1822
|
isHttpOperationNode: () => isHttpOperationNode,
|
|
1823
|
+
isVirtualFile: () => isVirtualFile,
|
|
1644
1824
|
mergeRefWithSchema: () => mergeRefWithSchema,
|
|
1645
1825
|
narrowSchema: () => narrowSchema,
|
|
1826
|
+
nodeKinds: () => nodeKinds,
|
|
1646
1827
|
optionality: () => optionality,
|
|
1647
1828
|
refName: () => refName,
|
|
1648
1829
|
resolveRef: () => resolveRef,
|
|
@@ -1681,10 +1862,13 @@ exports.forEachChild = forEachChild;
|
|
|
1681
1862
|
exports.forEachSchema = forEachSchema;
|
|
1682
1863
|
exports.getSchemaLiteralValues = getSchemaLiteralValues;
|
|
1683
1864
|
exports.inferDiscriminatorPropertyName = inferDiscriminatorPropertyName;
|
|
1865
|
+
exports.inlineFiles = inlineFiles;
|
|
1684
1866
|
exports.isBareRef = isBareRef;
|
|
1685
1867
|
exports.isHttpOperationNode = isHttpOperationNode;
|
|
1868
|
+
exports.isVirtualFile = isVirtualFile;
|
|
1686
1869
|
exports.mergeRefWithSchema = mergeRefWithSchema;
|
|
1687
1870
|
exports.narrowSchema = narrowSchema;
|
|
1871
|
+
exports.nodeKinds = nodeKinds;
|
|
1688
1872
|
exports.optionality = optionality;
|
|
1689
1873
|
exports.refName = refName;
|
|
1690
1874
|
exports.resolveRef = resolveRef;
|