@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.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { hash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
//#region \0rolldown/runtime.js
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
5
5
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -14,6 +14,30 @@ var __exportAll = (all, no_symbols) => {
|
|
|
14
14
|
//#endregion
|
|
15
15
|
//#region src/constants.ts
|
|
16
16
|
/**
|
|
17
|
+
* Node kind discriminators used by every AST node (for example `node.kind === nodeKinds.text`).
|
|
18
|
+
*/
|
|
19
|
+
const nodeKinds = {
|
|
20
|
+
input: "Input",
|
|
21
|
+
operation: "Operation",
|
|
22
|
+
schema: "Schema",
|
|
23
|
+
property: "Property",
|
|
24
|
+
parameter: "Parameter",
|
|
25
|
+
response: "Response",
|
|
26
|
+
requestBody: "RequestBody",
|
|
27
|
+
content: "Content",
|
|
28
|
+
type: "Type",
|
|
29
|
+
file: "File",
|
|
30
|
+
import: "Import",
|
|
31
|
+
export: "Export",
|
|
32
|
+
source: "Source",
|
|
33
|
+
const: "Const",
|
|
34
|
+
function: "Function",
|
|
35
|
+
arrowFunction: "ArrowFunction",
|
|
36
|
+
text: "Text",
|
|
37
|
+
break: "Break",
|
|
38
|
+
jsx: "Jsx"
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
17
41
|
* Schema type discriminators used by all AST schema nodes.
|
|
18
42
|
*
|
|
19
43
|
* Each value is a stable discriminator across the AST (for example `schema.type === schemaTypes.object`).
|
|
@@ -222,7 +246,7 @@ function isNodeRecord(value) {
|
|
|
222
246
|
* ```
|
|
223
247
|
*/
|
|
224
248
|
function getChildren(node, recurse) {
|
|
225
|
-
if (node.kind ===
|
|
249
|
+
if (node.kind === nodeKinds.schema && !recurse) return [];
|
|
226
250
|
const children = [];
|
|
227
251
|
visitChildNodes(node, (child) => {
|
|
228
252
|
children.push(child);
|
|
@@ -280,7 +304,7 @@ function visitChildNodes(node, visit) {
|
|
|
280
304
|
*/
|
|
281
305
|
function forEachSchema(node, visit) {
|
|
282
306
|
const walk = (current) => {
|
|
283
|
-
if (current.kind ===
|
|
307
|
+
if (current.kind === nodeKinds.schema) visit(current);
|
|
284
308
|
forEachChild(current, walk);
|
|
285
309
|
};
|
|
286
310
|
walk(node);
|
|
@@ -298,7 +322,7 @@ function forEachSchema(node, visit) {
|
|
|
298
322
|
function findSchema(node, { predicate, followRefs = false }) {
|
|
299
323
|
const seen = followRefs ? /* @__PURE__ */ new WeakSet() : void 0;
|
|
300
324
|
const walk = (current) => {
|
|
301
|
-
if (current.kind ===
|
|
325
|
+
if (current.kind === nodeKinds.schema) {
|
|
302
326
|
if (predicate(current)) return current;
|
|
303
327
|
if (seen) {
|
|
304
328
|
if (seen.has(current)) return void 0;
|
|
@@ -374,7 +398,7 @@ function transformNode(node, visitor, recurse, parent) {
|
|
|
374
398
|
* (e.g. `additionalProperties: true`) intact.
|
|
375
399
|
*/
|
|
376
400
|
function transformChildren(node, visitor, recurse) {
|
|
377
|
-
const keys = node.kind ===
|
|
401
|
+
const keys = node.kind === nodeKinds.schema && !recurse ? void 0 : VISITOR_KEYS[node.kind];
|
|
378
402
|
if (!keys) return node;
|
|
379
403
|
const record = node;
|
|
380
404
|
let updates;
|
|
@@ -659,11 +683,11 @@ function extractStringsFromNodes(nodes) {
|
|
|
659
683
|
if (node) collected.push(node);
|
|
660
684
|
continue;
|
|
661
685
|
}
|
|
662
|
-
if (node.kind ===
|
|
686
|
+
if (node.kind === nodeKinds.text || node.kind === nodeKinds.jsx) {
|
|
663
687
|
if (node.value) collected.push(node.value);
|
|
664
688
|
continue;
|
|
665
689
|
}
|
|
666
|
-
if (node.kind ===
|
|
690
|
+
if (node.kind === nodeKinds.break) continue;
|
|
667
691
|
const parts = [];
|
|
668
692
|
if ("params" in node && node.params) parts.push(node.params);
|
|
669
693
|
if ("generics" in node && node.generics) parts.push(Array.isArray(node.generics) ? node.generics.join(", ") : node.generics);
|
|
@@ -676,133 +700,550 @@ function extractStringsFromNodes(nodes) {
|
|
|
676
700
|
return collected.join("\n");
|
|
677
701
|
}
|
|
678
702
|
//#endregion
|
|
679
|
-
//#region src/utils/
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
703
|
+
//#region src/utils/combineFileMembers.ts
|
|
704
|
+
const IDENTIFIER_RUN = /[\w$]+/g;
|
|
705
|
+
/**
|
|
706
|
+
* How many imports a file needs before indexing the source beats scanning it once per name.
|
|
707
|
+
*/
|
|
708
|
+
const INDEX_ABOVE_IMPORTS = 128;
|
|
709
|
+
/**
|
|
710
|
+
* Returns the local binding an import name item introduces: the alias when there is one, else the
|
|
711
|
+
* imported name.
|
|
712
|
+
*/
|
|
713
|
+
function importLocalName(item) {
|
|
714
|
+
return typeof item === "string" ? item : item.name ?? item.propertyName;
|
|
683
715
|
}
|
|
684
716
|
/**
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
* then `name`, then the nested `schema.name`.
|
|
688
|
-
*
|
|
689
|
-
* Returns `null` for non-ref nodes or when no name resolves.
|
|
690
|
-
*
|
|
691
|
-
* @example
|
|
692
|
-
* `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Pet' }) // 'Pet'`
|
|
693
|
-
*
|
|
694
|
-
* @example Collision-renamed target
|
|
695
|
-
* `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Order', targetName: 'OrderSchema' }) // 'OrderSchema'`
|
|
717
|
+
* Computes a multi-level sort key for exports and imports:
|
|
718
|
+
* non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
|
|
696
719
|
*/
|
|
697
|
-
function
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
720
|
+
function sortKey(node) {
|
|
721
|
+
const isArray = Array.isArray(node.name) ? "1" : "0";
|
|
722
|
+
const typeOnly = node.isTypeOnly ? "0" : "1";
|
|
723
|
+
const hasName = node.name != null ? "1" : "0";
|
|
724
|
+
const name = Array.isArray(node.name) ? node.name.toSorted().join("\0") : node.name ?? "";
|
|
725
|
+
return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
|
|
702
726
|
}
|
|
703
727
|
/**
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
* @example
|
|
707
|
-
* ```ts
|
|
708
|
-
* const target = resolveRef(refSchema)
|
|
709
|
-
* ```
|
|
728
|
+
* Sorts members by {@link sortKey}. Keys are computed once per node, not per comparison.
|
|
710
729
|
*/
|
|
711
|
-
function
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
if (seen.has(current)) return current;
|
|
717
|
-
seen.add(current);
|
|
718
|
-
current = current.schema;
|
|
719
|
-
}
|
|
720
|
-
return current;
|
|
730
|
+
function sortMembers(nodes) {
|
|
731
|
+
return nodes.map((node) => ({
|
|
732
|
+
node,
|
|
733
|
+
key: sortKey(node)
|
|
734
|
+
})).sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0).map(({ node }) => node);
|
|
721
735
|
}
|
|
722
|
-
/** Fields of a ref node that describe the ref itself, so they never override the resolved schema. */
|
|
723
|
-
const REF_OWN_KEYS = /* @__PURE__ */ new Set([
|
|
724
|
-
"kind",
|
|
725
|
-
"type",
|
|
726
|
-
"name",
|
|
727
|
-
"ref",
|
|
728
|
-
"schema"
|
|
729
|
-
]);
|
|
730
736
|
/**
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
+
* Returns a collector for array-named members. A member whose path and `isTypeOnly` were already
|
|
738
|
+
* seen folds its names into that entry, otherwise it joins `result` as a new entry.
|
|
739
|
+
*/
|
|
740
|
+
function mergeByPath(result) {
|
|
741
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
742
|
+
return ({ node, names }) => {
|
|
743
|
+
const key = `${node.path}:${node.isTypeOnly ?? false}`;
|
|
744
|
+
const existing = byPath.get(key);
|
|
745
|
+
if (existing) {
|
|
746
|
+
for (const name of names) {
|
|
747
|
+
if (existing.seen.has(name)) continue;
|
|
748
|
+
existing.seen.add(name);
|
|
749
|
+
existing.names.push(name);
|
|
750
|
+
}
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const item = {
|
|
754
|
+
...node,
|
|
755
|
+
name: [...names]
|
|
756
|
+
};
|
|
757
|
+
result.push(item);
|
|
758
|
+
byPath.set(key, {
|
|
759
|
+
names: item.name,
|
|
760
|
+
seen: new Set(names)
|
|
761
|
+
});
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Deduplicates `SourceNode` objects by `name + isExportable + isTypeOnly`, keeping the first of each
|
|
766
|
+
* key. An unnamed source only drops when the same node appears twice, so markdown can repeat a
|
|
767
|
+
* paragraph or a table. Returns the deduplicated array in original order.
|
|
768
|
+
*/
|
|
769
|
+
function combineSources(sources) {
|
|
770
|
+
const seen = /* @__PURE__ */ new Set();
|
|
771
|
+
return sources.filter((source) => {
|
|
772
|
+
const key = source.name != null ? `${source.name}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}` : source;
|
|
773
|
+
if (seen.has(key)) return false;
|
|
774
|
+
seen.add(key);
|
|
775
|
+
return true;
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Deduplicates and merges `ExportNode` objects by path and type.
|
|
737
780
|
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
* const ref = createSchema({ type: 'ref', ref: '#/components/schemas/Pet', description: 'A pet', schema: petSchema })
|
|
741
|
-
* mergeRefWithSchema(ref) // petSchema's fields with description 'A pet'
|
|
742
|
-
* ```
|
|
781
|
+
* Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
|
|
782
|
+
* Non-array exports are deduplicated by exact identity. Returns a sorted, deduplicated array.
|
|
743
783
|
*/
|
|
744
|
-
function
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
784
|
+
function combineExports(exports) {
|
|
785
|
+
const result = [];
|
|
786
|
+
const merge = mergeByPath(result);
|
|
787
|
+
const seen = /* @__PURE__ */ new Set();
|
|
788
|
+
for (const curr of sortMembers(exports)) {
|
|
789
|
+
const { name, path, isTypeOnly, asAlias } = curr;
|
|
790
|
+
if (Array.isArray(name)) {
|
|
791
|
+
if (name.length) merge({
|
|
792
|
+
node: curr,
|
|
793
|
+
names: [...new Set(name)]
|
|
794
|
+
});
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
const key = `${path}:${name ?? ""}:${isTypeOnly ?? false}:${asAlias ?? ""}`;
|
|
798
|
+
if (!seen.has(key)) {
|
|
799
|
+
result.push(curr);
|
|
800
|
+
seen.add(key);
|
|
801
|
+
}
|
|
750
802
|
}
|
|
751
|
-
return
|
|
803
|
+
return result;
|
|
752
804
|
}
|
|
753
805
|
/**
|
|
754
|
-
*
|
|
806
|
+
* Deduplicates and merges `ImportNode` objects, filtering out unused imports.
|
|
755
807
|
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
808
|
+
* Retains imports that are referenced in `source` or re-exported. Imports with the same path and
|
|
809
|
+
* `isTypeOnly` flag have their names merged. Returns a sorted, deduplicated, filtered array.
|
|
758
810
|
*/
|
|
759
|
-
function
|
|
760
|
-
|
|
761
|
-
const
|
|
762
|
-
|
|
811
|
+
function combineImports(imports, exports, source) {
|
|
812
|
+
const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
|
|
813
|
+
const identifiers = source && imports.length > INDEX_ABOVE_IMPORTS ? new Set(source.match(IDENTIFIER_RUN)) : null;
|
|
814
|
+
const isUsed = (importName) => !source || identifiers?.has(importName) || source.includes(importName) || exportedNames.has(importName);
|
|
815
|
+
const importNameMemo = /* @__PURE__ */ new Map();
|
|
816
|
+
const canonicalizeName = (n) => {
|
|
817
|
+
if (typeof n === "string") return n;
|
|
818
|
+
const key = `${n.propertyName}:${n.name ?? ""}`;
|
|
819
|
+
if (!importNameMemo.has(key)) importNameMemo.set(key, n);
|
|
820
|
+
return importNameMemo.get(key);
|
|
821
|
+
};
|
|
822
|
+
const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
|
|
823
|
+
for (const node of imports) if (Array.isArray(node.name) && node.name.some((item) => isUsed(importLocalName(item)))) pathsWithUsedNamedImport.add(node.path);
|
|
824
|
+
const result = [];
|
|
825
|
+
const merge = mergeByPath(result);
|
|
826
|
+
const seen = /* @__PURE__ */ new Set();
|
|
827
|
+
for (const curr of sortMembers(imports)) {
|
|
828
|
+
if (curr.path === curr.root) continue;
|
|
829
|
+
const { name, path, isTypeOnly } = curr;
|
|
830
|
+
if (Array.isArray(name)) {
|
|
831
|
+
const names = [...new Set(name.map(canonicalizeName))].filter((item) => isUsed(importLocalName(item)));
|
|
832
|
+
if (names.length) merge({
|
|
833
|
+
node: curr,
|
|
834
|
+
names
|
|
835
|
+
});
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
if (!isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
|
|
839
|
+
const key = `${path}:${name}:${isTypeOnly ?? false}`;
|
|
840
|
+
if (!seen.has(key)) {
|
|
841
|
+
result.push(curr);
|
|
842
|
+
seen.add(key);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return result;
|
|
763
846
|
}
|
|
764
847
|
//#endregion
|
|
765
|
-
//#region
|
|
848
|
+
//#region src/nodes/base.ts
|
|
849
|
+
/** Builds a `createX` factory for a node whose only computed field is `kind`. */
|
|
850
|
+
function nodeFactory(kind) {
|
|
851
|
+
return (input) => {
|
|
852
|
+
return {
|
|
853
|
+
...input,
|
|
854
|
+
kind
|
|
855
|
+
};
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
//#endregion
|
|
859
|
+
//#region src/nodes/file.ts
|
|
766
860
|
/**
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
* Pass a `WeakMap` for object keys (results are GC-eligible when the key is
|
|
770
|
-
* collected) or a `Map` for primitive keys. For multi-argument functions,
|
|
771
|
-
* nest two `memoize` calls — the outer keyed by the first argument, the
|
|
772
|
-
* inner (created once per outer miss) keyed by the second.
|
|
773
|
-
*
|
|
774
|
-
* Because the cache is owned by the caller, it can be shared, inspected, or
|
|
775
|
-
* cleared independently of the memoized function.
|
|
861
|
+
* Creates an `ImportNode` representing a language-agnostic import/dependency declaration.
|
|
776
862
|
*
|
|
777
|
-
* @example
|
|
863
|
+
* @example Named import
|
|
778
864
|
* ```ts
|
|
779
|
-
*
|
|
780
|
-
*
|
|
865
|
+
* createImport({ name: ['useState'], path: 'react' })
|
|
866
|
+
* // import { useState } from 'react'
|
|
781
867
|
* ```
|
|
868
|
+
*/
|
|
869
|
+
const createImport = nodeFactory(nodeKinds.import);
|
|
870
|
+
/**
|
|
871
|
+
* Creates an `ExportNode` representing a language-agnostic export/public API declaration.
|
|
782
872
|
*
|
|
783
|
-
* @example
|
|
873
|
+
* @example Named export
|
|
784
874
|
* ```ts
|
|
785
|
-
*
|
|
786
|
-
*
|
|
875
|
+
* createExport({ name: ['Pet'], path: './Pet' })
|
|
876
|
+
* // export { Pet } from './Pet'
|
|
787
877
|
* ```
|
|
878
|
+
*/
|
|
879
|
+
const createExport = nodeFactory(nodeKinds.export);
|
|
880
|
+
/**
|
|
881
|
+
* Creates a `SourceNode` representing a fragment of source code within a file.
|
|
788
882
|
*
|
|
789
|
-
* @example
|
|
883
|
+
* @example
|
|
790
884
|
* ```ts
|
|
791
|
-
*
|
|
792
|
-
* const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
|
|
793
|
-
* fn(params)('camelcase')
|
|
885
|
+
* createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')], isExportable: true })
|
|
794
886
|
* ```
|
|
795
887
|
*/
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
888
|
+
const createSource = nodeFactory(nodeKinds.source);
|
|
889
|
+
/**
|
|
890
|
+
* Creates a fully resolved `FileNode` from a file input descriptor.
|
|
891
|
+
*
|
|
892
|
+
* Computes:
|
|
893
|
+
* - `id` SHA256 hash of the file path
|
|
894
|
+
* - `name` `baseName` without extension
|
|
895
|
+
* - `extname` extension extracted from `baseName`
|
|
896
|
+
*
|
|
897
|
+
* Deduplicates:
|
|
898
|
+
* - `sources` via `combineSources`
|
|
899
|
+
* - `exports` via `combineExports`
|
|
900
|
+
* - `imports` via `combineImports` (also filters unused imports)
|
|
901
|
+
*
|
|
902
|
+
* @throws {Error} when `baseName` has no extension.
|
|
903
|
+
*
|
|
904
|
+
* @example
|
|
905
|
+
* ```ts
|
|
906
|
+
* const file = createFile({
|
|
907
|
+
* baseName: 'petStore.ts',
|
|
908
|
+
* path: 'src/models/petStore.ts',
|
|
909
|
+
* sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],
|
|
910
|
+
* imports: [createImport({ name: ['z'], path: 'zod' })],
|
|
911
|
+
* exports: [createExport({ name: ['Pet'], path: './petStore' })],
|
|
912
|
+
* })
|
|
913
|
+
* // file.id = SHA256 hash of 'src/models/petStore.ts'
|
|
914
|
+
* // file.name = 'petStore'
|
|
915
|
+
* // file.extname = '.ts'
|
|
916
|
+
* ```
|
|
917
|
+
*
|
|
918
|
+
* @example Copy a real file into the output
|
|
919
|
+
* ```ts
|
|
920
|
+
* const file = createFile({
|
|
921
|
+
* baseName: 'client.ts',
|
|
922
|
+
* path: 'src/gen/client.ts',
|
|
923
|
+
* copy: '/abs/path/to/templates/client.ts',
|
|
924
|
+
* })
|
|
925
|
+
* ```
|
|
926
|
+
*/
|
|
927
|
+
function createFile(input) {
|
|
928
|
+
const extname = path.extname(input.baseName);
|
|
929
|
+
if (!extname) throw new Error(`No extname found for ${input.baseName}`);
|
|
930
|
+
const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
|
|
931
|
+
const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
|
|
932
|
+
const resolvedImports = (() => {
|
|
933
|
+
if (!input.imports?.length) return [];
|
|
934
|
+
const sourceParts = [];
|
|
935
|
+
const localNames = /* @__PURE__ */ new Set();
|
|
936
|
+
for (const item of resolvedSources) {
|
|
937
|
+
const extracted = item.nodes && extractStringsFromNodes(item.nodes);
|
|
938
|
+
if (extracted) sourceParts.push(extracted);
|
|
939
|
+
if (item.name) localNames.add(item.name);
|
|
940
|
+
}
|
|
941
|
+
const source = sourceParts.join("\n") || void 0;
|
|
942
|
+
return combineImports(input.imports, resolvedExports, source).flatMap((imp) => {
|
|
943
|
+
if (imp.path === input.path) return [];
|
|
944
|
+
if (!Array.isArray(imp.name)) return typeof imp.name === "string" && localNames.has(imp.name) ? [] : [imp];
|
|
945
|
+
const kept = imp.name.filter((item) => !localNames.has(importLocalName(item)));
|
|
946
|
+
if (!kept.length) return [];
|
|
947
|
+
return [kept.length === imp.name.length ? imp : {
|
|
948
|
+
...imp,
|
|
949
|
+
name: kept
|
|
950
|
+
}];
|
|
951
|
+
});
|
|
952
|
+
})();
|
|
953
|
+
return {
|
|
954
|
+
...input,
|
|
955
|
+
kind: nodeKinds.file,
|
|
956
|
+
id: hash("sha256", input.path, "hex"),
|
|
957
|
+
name: path.basename(input.baseName, extname),
|
|
958
|
+
extname,
|
|
959
|
+
imports: resolvedImports,
|
|
960
|
+
exports: resolvedExports,
|
|
961
|
+
sources: resolvedSources,
|
|
962
|
+
meta: input.meta ?? {}
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
//#endregion
|
|
966
|
+
//#region src/utils/inline.ts
|
|
967
|
+
/**
|
|
968
|
+
* Whether every source of `file` is inline. Other files absorb such a file, so it is never written.
|
|
969
|
+
*/
|
|
970
|
+
function isVirtualFile(file) {
|
|
971
|
+
return file.sources.length > 0 && file.sources.every((source) => source.inline);
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Replaces every name a file imports from a virtual file (see {@link isVirtualFile}) with that name's
|
|
975
|
+
* `inline.expression`, until no inline names are left. The definitions' own imports move to the importing file.
|
|
976
|
+
*/
|
|
977
|
+
function inlineFiles(files, { language }) {
|
|
978
|
+
const virtualFiles = new Map(files.filter(isVirtualFile).map((file) => [file.path, file]));
|
|
979
|
+
const result = {
|
|
980
|
+
files: [],
|
|
981
|
+
virtualPaths: [...virtualFiles.keys()],
|
|
982
|
+
problems: []
|
|
983
|
+
};
|
|
984
|
+
if (virtualFiles.size === 0) return result;
|
|
985
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
986
|
+
function define(path, name, from, lang) {
|
|
987
|
+
const key = `${path}\0${name}`;
|
|
988
|
+
if (definitions.has(key)) {
|
|
989
|
+
const definition = definitions.get(key);
|
|
990
|
+
if (definition === null) result.problems.push({
|
|
991
|
+
path: from,
|
|
992
|
+
message: `"${name}" from "${path}" is defined in terms of itself.`
|
|
993
|
+
});
|
|
994
|
+
return definition ?? void 0;
|
|
995
|
+
}
|
|
996
|
+
const file = virtualFiles.get(path);
|
|
997
|
+
const inline = file.sources.find((source) => source.name === name)?.inline;
|
|
998
|
+
if (!inline) {
|
|
999
|
+
result.problems.push({
|
|
1000
|
+
path: from,
|
|
1001
|
+
message: `"${name}" is imported from "${path}", which never writes it and has no inline definition for it.`
|
|
1002
|
+
});
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
definitions.set(key, null);
|
|
1006
|
+
const imports = [];
|
|
1007
|
+
const { text, used } = replaceNames(inline.expression, file, lang, imports);
|
|
1008
|
+
imports.push(...file.imports.flatMap((imp) => virtualFiles.has(imp.path) ? [] : pickNames(imp, used)));
|
|
1009
|
+
const definition = {
|
|
1010
|
+
expression: text,
|
|
1011
|
+
imports
|
|
1012
|
+
};
|
|
1013
|
+
definitions.set(key, definition);
|
|
1014
|
+
return definition;
|
|
1015
|
+
}
|
|
1016
|
+
function replaceNames(input, file, lang, imports) {
|
|
1017
|
+
const used = /* @__PURE__ */ new Set();
|
|
1018
|
+
return {
|
|
1019
|
+
text: lang.replaceIdentifiers(input, (local) => {
|
|
1020
|
+
const target = findTarget(file, local);
|
|
1021
|
+
const definition = target && define(target.path, target.name, file.path, lang);
|
|
1022
|
+
if (!definition) {
|
|
1023
|
+
used.add(local);
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
imports.push(...definition.imports);
|
|
1027
|
+
return lang.wrap(definition.expression);
|
|
1028
|
+
}),
|
|
1029
|
+
used
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
function findTarget(file, local) {
|
|
1033
|
+
if (virtualFiles.has(file.path) && file.sources.some((source) => source.name === local)) return {
|
|
1034
|
+
path: file.path,
|
|
1035
|
+
name: local
|
|
1036
|
+
};
|
|
1037
|
+
for (const imp of file.imports) {
|
|
1038
|
+
const item = virtualFiles.has(imp.path) && [imp.name].flat().find((item) => importLocalName(item) === local);
|
|
1039
|
+
if (item) return {
|
|
1040
|
+
path: imp.path,
|
|
1041
|
+
name: typeof item === "string" ? item : item.propertyName
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
for (const file of files) {
|
|
1046
|
+
if (virtualFiles.has(file.path) || !file.imports.some((imp) => virtualFiles.has(imp.path))) continue;
|
|
1047
|
+
const lang = language(file);
|
|
1048
|
+
if (!lang) {
|
|
1049
|
+
result.problems.push({
|
|
1050
|
+
path: file.path,
|
|
1051
|
+
message: `No parser for "${file.extname}" files supports inline sources.`
|
|
1052
|
+
});
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
const carried = [];
|
|
1056
|
+
const sources = file.sources.map((source) => ({
|
|
1057
|
+
...source,
|
|
1058
|
+
nodes: rewriteNodes(source.nodes, (text) => replaceNames(text, file, lang, carried).text)
|
|
1059
|
+
}));
|
|
1060
|
+
const imports = [...file.imports.filter((imp) => !virtualFiles.has(imp.path)), ...carried.map((imp) => imp.root ? {
|
|
1061
|
+
...imp,
|
|
1062
|
+
root: file.path
|
|
1063
|
+
} : imp)];
|
|
1064
|
+
result.files.push(createFile({
|
|
1065
|
+
...file,
|
|
1066
|
+
sources,
|
|
1067
|
+
imports
|
|
1068
|
+
}));
|
|
1069
|
+
}
|
|
1070
|
+
return result;
|
|
1071
|
+
}
|
|
1072
|
+
function pickNames(imp, used) {
|
|
1073
|
+
if (!Array.isArray(imp.name)) return used.has(imp.name) ? [imp] : [];
|
|
1074
|
+
const name = imp.name.filter((item) => used.has(importLocalName(item)));
|
|
1075
|
+
return name.length > 0 ? [{
|
|
1076
|
+
...imp,
|
|
1077
|
+
name
|
|
1078
|
+
}] : [];
|
|
1079
|
+
}
|
|
1080
|
+
function rewriteNodes(nodes, rewrite) {
|
|
1081
|
+
return nodes?.map((node) => {
|
|
1082
|
+
if (typeof node === "string") return rewrite(node);
|
|
1083
|
+
switch (node.kind) {
|
|
1084
|
+
case nodeKinds.break: return node;
|
|
1085
|
+
case nodeKinds.text:
|
|
1086
|
+
case nodeKinds.jsx: return {
|
|
1087
|
+
...node,
|
|
1088
|
+
value: rewrite(node.value)
|
|
1089
|
+
};
|
|
1090
|
+
case nodeKinds.function:
|
|
1091
|
+
case nodeKinds.arrowFunction: return {
|
|
1092
|
+
...node,
|
|
1093
|
+
params: node.params && rewrite(node.params),
|
|
1094
|
+
generics: Array.isArray(node.generics) ? node.generics.map(rewrite) : node.generics && rewrite(node.generics),
|
|
1095
|
+
returnType: node.returnType && rewrite(node.returnType),
|
|
1096
|
+
nodes: rewriteNodes(node.nodes, rewrite)
|
|
1097
|
+
};
|
|
1098
|
+
case nodeKinds.const: return {
|
|
1099
|
+
...node,
|
|
1100
|
+
type: node.type && rewrite(node.type),
|
|
1101
|
+
nodes: rewriteNodes(node.nodes, rewrite)
|
|
1102
|
+
};
|
|
1103
|
+
default: return {
|
|
1104
|
+
...node,
|
|
1105
|
+
nodes: rewriteNodes(node.nodes, rewrite)
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
//#endregion
|
|
1111
|
+
//#region src/utils/refs.ts
|
|
1112
|
+
/**
|
|
1113
|
+
* Returns the last segment of a `$ref` pointer, for example `'Pet'` for `#/components/schemas/Pet`.
|
|
1114
|
+
* The segment is decoded like the pointer resolver does: percent-escapes first, then `~1` and `~0`
|
|
1115
|
+
* (RFC 6901), so `a~1b~0c` names the component `a/b~c`.
|
|
1116
|
+
*/
|
|
1117
|
+
function refName(ref) {
|
|
1118
|
+
const segment = ref.split("/").at(-1) ?? ref;
|
|
1119
|
+
let decoded = segment;
|
|
1120
|
+
try {
|
|
1121
|
+
decoded = decodeURIComponent(segment);
|
|
1122
|
+
} catch {}
|
|
1123
|
+
return decoded.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Resolves the emitted name of the schema a ref node points at. Prefers `targetName` (set when
|
|
1127
|
+
* the referenced schema was renamed, e.g. to break a collision), then the last segment of `ref`,
|
|
1128
|
+
* then `name`, then the nested `schema.name`.
|
|
1129
|
+
*
|
|
1130
|
+
* Returns `null` for non-ref nodes or when no name resolves.
|
|
1131
|
+
*
|
|
1132
|
+
* @example
|
|
1133
|
+
* `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Pet' }) // 'Pet'`
|
|
1134
|
+
*
|
|
1135
|
+
* @example Collision-renamed target
|
|
1136
|
+
* `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Order', targetName: 'OrderSchema' }) // 'OrderSchema'`
|
|
1137
|
+
*/
|
|
1138
|
+
function resolveRefName(node) {
|
|
1139
|
+
if (!node || node.type !== "ref") return null;
|
|
1140
|
+
if (node.targetName) return node.targetName;
|
|
1141
|
+
if (node.ref) return refName(node.ref);
|
|
1142
|
+
return node.name ?? node.schema?.name ?? null;
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Follows a resolved ref chain to its target schema. Returns an unresolved or cyclic ref as is.
|
|
1146
|
+
*
|
|
1147
|
+
* @example
|
|
1148
|
+
* ```ts
|
|
1149
|
+
* const target = resolveRef(refSchema)
|
|
1150
|
+
* ```
|
|
1151
|
+
*/
|
|
1152
|
+
function resolveRef(node) {
|
|
1153
|
+
if (node.type !== "ref" || !node.schema) return node;
|
|
1154
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1155
|
+
let current = node;
|
|
1156
|
+
while (current.type === "ref" && current.schema) {
|
|
1157
|
+
if (seen.has(current)) return current;
|
|
1158
|
+
seen.add(current);
|
|
1159
|
+
current = current.schema;
|
|
1160
|
+
}
|
|
1161
|
+
return current;
|
|
1162
|
+
}
|
|
1163
|
+
/** Fields of a ref node that describe the ref itself, so they never override the resolved schema. */
|
|
1164
|
+
const REF_OWN_KEYS = /* @__PURE__ */ new Set([
|
|
1165
|
+
"kind",
|
|
1166
|
+
"type",
|
|
1167
|
+
"name",
|
|
1168
|
+
"ref",
|
|
1169
|
+
"schema"
|
|
1170
|
+
]);
|
|
1171
|
+
/**
|
|
1172
|
+
* Merges a ref node with its resolved schema, giving the fields set at the usage site precedence.
|
|
1173
|
+
*
|
|
1174
|
+
* Every field on the ref except `kind`, `type`, `name`, `ref`, and `schema` overrides the same field
|
|
1175
|
+
* of the resolved `schema` (for example `description`, `nullable`, `readOnly`, `deprecated`). Fields
|
|
1176
|
+
* left `undefined` on the ref do not shadow the resolved schema. A ref without a resolved `schema`
|
|
1177
|
+
* is returned as is.
|
|
1178
|
+
*
|
|
1179
|
+
* @example
|
|
1180
|
+
* ```ts
|
|
1181
|
+
* const ref = createSchema({ type: 'ref', ref: '#/components/schemas/Pet', description: 'A pet', schema: petSchema })
|
|
1182
|
+
* mergeRefWithSchema(ref) // petSchema's fields with description 'A pet'
|
|
1183
|
+
* ```
|
|
1184
|
+
*/
|
|
1185
|
+
function mergeRefWithSchema(node) {
|
|
1186
|
+
if (!node.schema) return node;
|
|
1187
|
+
const merged = { ...node.schema };
|
|
1188
|
+
for (const key of Object.keys(node)) {
|
|
1189
|
+
const value = node[key];
|
|
1190
|
+
if (value !== void 0 && !REF_OWN_KEYS.has(key)) merged[key] = value;
|
|
1191
|
+
}
|
|
1192
|
+
return merged;
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Returns `true` when a schema is a bare ref without modifiers, metadata, or omit keys.
|
|
1196
|
+
*
|
|
1197
|
+
* Modifiers inherited from the resolved schema are included in the check because a ref emits the
|
|
1198
|
+
* referenced schema variable with those modifiers already applied.
|
|
1199
|
+
*/
|
|
1200
|
+
function isBareRef(node, keysToOmit) {
|
|
1201
|
+
if (!node || node.type !== "ref") return false;
|
|
1202
|
+
const meta = mergeRefWithSchema(node);
|
|
1203
|
+
return !meta.nullable && !meta.optional && !meta.nullish && meta.default === void 0 && !meta.description && !meta.examples?.length && !keysToOmit?.length;
|
|
1204
|
+
}
|
|
1205
|
+
//#endregion
|
|
1206
|
+
//#region ../../internals/utils/src/promise.ts
|
|
1207
|
+
/**
|
|
1208
|
+
* Wraps `factory` with a keyed cache backed by the provided store.
|
|
1209
|
+
*
|
|
1210
|
+
* Pass a `WeakMap` for object keys (results are GC-eligible when the key is
|
|
1211
|
+
* collected) or a `Map` for primitive keys. For multi-argument functions,
|
|
1212
|
+
* nest two `memoize` calls — the outer keyed by the first argument, the
|
|
1213
|
+
* inner (created once per outer miss) keyed by the second.
|
|
1214
|
+
*
|
|
1215
|
+
* Because the cache is owned by the caller, it can be shared, inspected, or
|
|
1216
|
+
* cleared independently of the memoized function.
|
|
1217
|
+
*
|
|
1218
|
+
* @example Single WeakMap key
|
|
1219
|
+
* ```ts
|
|
1220
|
+
* const cache = new WeakMap<SchemaNode, Set<string>>()
|
|
1221
|
+
* const getRefs = memoize(cache, (node) => collectRefs(node))
|
|
1222
|
+
* ```
|
|
1223
|
+
*
|
|
1224
|
+
* @example Single Map key (primitive)
|
|
1225
|
+
* ```ts
|
|
1226
|
+
* const cache = new Map<string, Resolver>()
|
|
1227
|
+
* const getResolver = memoize(cache, (name) => buildResolver(name))
|
|
1228
|
+
* ```
|
|
1229
|
+
*
|
|
1230
|
+
* @example Two-level (object + primitive)
|
|
1231
|
+
* ```ts
|
|
1232
|
+
* const outer = new WeakMap<Params[], Map<string, Params[]>>()
|
|
1233
|
+
* const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
|
|
1234
|
+
* fn(params)('camelcase')
|
|
1235
|
+
* ```
|
|
1236
|
+
*/
|
|
1237
|
+
function memoize(store, factory) {
|
|
1238
|
+
return (key) => {
|
|
1239
|
+
if (store.has(key)) return store.get(key);
|
|
1240
|
+
const value = factory(key);
|
|
1241
|
+
store.set(key, value);
|
|
1242
|
+
return value;
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
//#endregion
|
|
1246
|
+
//#region src/utils/schemaGraph.ts
|
|
806
1247
|
/**
|
|
807
1248
|
* Collects the names of every schema a node's subtree references, resolved with `resolveRefName`.
|
|
808
1249
|
* Each ref contributes its name only, so the schema it points to is never traversed. The walk is
|
|
@@ -1059,17 +1500,6 @@ function inferDiscriminatorPropertyName(members) {
|
|
|
1059
1500
|
}
|
|
1060
1501
|
}
|
|
1061
1502
|
//#endregion
|
|
1062
|
-
//#region src/nodes/base.ts
|
|
1063
|
-
/** Builds a `createX` factory for a node whose only computed field is `kind`. */
|
|
1064
|
-
function nodeFactory(kind) {
|
|
1065
|
-
return (input) => {
|
|
1066
|
-
return {
|
|
1067
|
-
...input,
|
|
1068
|
-
kind
|
|
1069
|
-
};
|
|
1070
|
-
};
|
|
1071
|
-
}
|
|
1072
|
-
//#endregion
|
|
1073
1503
|
//#region src/nodes/code.ts
|
|
1074
1504
|
/**
|
|
1075
1505
|
* Creates a `ConstNode` representing a TypeScript `const` declaration.
|
|
@@ -1080,7 +1510,7 @@ function nodeFactory(kind) {
|
|
|
1080
1510
|
* // export const pets: Pet[] = ... as const
|
|
1081
1511
|
* ```
|
|
1082
1512
|
*/
|
|
1083
|
-
const createConst = nodeFactory(
|
|
1513
|
+
const createConst = nodeFactory(nodeKinds.const);
|
|
1084
1514
|
/**
|
|
1085
1515
|
* Creates a `TypeNode` representing a TypeScript `type` alias declaration.
|
|
1086
1516
|
*
|
|
@@ -1090,7 +1520,7 @@ const createConst = nodeFactory("Const");
|
|
|
1090
1520
|
* // export type Pet = ...
|
|
1091
1521
|
* ```
|
|
1092
1522
|
*/
|
|
1093
|
-
const createType = nodeFactory(
|
|
1523
|
+
const createType = nodeFactory(nodeKinds.type);
|
|
1094
1524
|
/**
|
|
1095
1525
|
* Creates a `FunctionNode` representing a TypeScript `function` declaration.
|
|
1096
1526
|
*
|
|
@@ -1100,7 +1530,7 @@ const createType = nodeFactory("Type");
|
|
|
1100
1530
|
* // export async function fetchPet(): Promise<Pet> { ... }
|
|
1101
1531
|
* ```
|
|
1102
1532
|
*/
|
|
1103
|
-
const createFunction = nodeFactory(
|
|
1533
|
+
const createFunction = nodeFactory(nodeKinds.function);
|
|
1104
1534
|
/**
|
|
1105
1535
|
* Creates an `ArrowFunctionNode` representing a TypeScript arrow function.
|
|
1106
1536
|
*
|
|
@@ -1110,7 +1540,7 @@ const createFunction = nodeFactory("Function");
|
|
|
1110
1540
|
* // export const double = (n: number) => ...
|
|
1111
1541
|
* ```
|
|
1112
1542
|
*/
|
|
1113
|
-
const createArrowFunction = nodeFactory(
|
|
1543
|
+
const createArrowFunction = nodeFactory(nodeKinds.arrowFunction);
|
|
1114
1544
|
/**
|
|
1115
1545
|
* Creates a {@link TextNode} representing a raw string fragment in the source output.
|
|
1116
1546
|
*
|
|
@@ -1122,7 +1552,7 @@ const createArrowFunction = nodeFactory("ArrowFunction");
|
|
|
1122
1552
|
*/
|
|
1123
1553
|
function createText(value) {
|
|
1124
1554
|
return {
|
|
1125
|
-
kind:
|
|
1555
|
+
kind: nodeKinds.text,
|
|
1126
1556
|
value
|
|
1127
1557
|
};
|
|
1128
1558
|
}
|
|
@@ -1136,7 +1566,7 @@ function createText(value) {
|
|
|
1136
1566
|
* ```
|
|
1137
1567
|
*/
|
|
1138
1568
|
function createBreak() {
|
|
1139
|
-
return { kind:
|
|
1569
|
+
return { kind: nodeKinds.break };
|
|
1140
1570
|
}
|
|
1141
1571
|
/**
|
|
1142
1572
|
* Creates a {@link JsxNode} representing a raw JSX fragment in the source output.
|
|
@@ -1149,7 +1579,7 @@ function createBreak() {
|
|
|
1149
1579
|
*/
|
|
1150
1580
|
function createJsx(value) {
|
|
1151
1581
|
return {
|
|
1152
|
-
kind:
|
|
1582
|
+
kind: nodeKinds.jsx,
|
|
1153
1583
|
value
|
|
1154
1584
|
};
|
|
1155
1585
|
}
|
|
@@ -1158,259 +1588,7 @@ function createJsx(value) {
|
|
|
1158
1588
|
/**
|
|
1159
1589
|
* Creates a `ContentNode` for a single request-body or response content type.
|
|
1160
1590
|
*/
|
|
1161
|
-
const createContent = nodeFactory(
|
|
1162
|
-
//#endregion
|
|
1163
|
-
//#region src/utils/combineFileMembers.ts
|
|
1164
|
-
const IDENTIFIER_RUN = /[\w$]+/g;
|
|
1165
|
-
/**
|
|
1166
|
-
* How many imports a file needs before indexing the source beats scanning it once per name.
|
|
1167
|
-
*/
|
|
1168
|
-
const INDEX_ABOVE_IMPORTS = 128;
|
|
1169
|
-
/**
|
|
1170
|
-
* Returns the local binding an import name item introduces: the alias when there is one, else the
|
|
1171
|
-
* imported name.
|
|
1172
|
-
*/
|
|
1173
|
-
function importLocalName(item) {
|
|
1174
|
-
return typeof item === "string" ? item : item.name ?? item.propertyName;
|
|
1175
|
-
}
|
|
1176
|
-
/**
|
|
1177
|
-
* Computes a multi-level sort key for exports and imports:
|
|
1178
|
-
* non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
|
|
1179
|
-
*/
|
|
1180
|
-
function sortKey(node) {
|
|
1181
|
-
const isArray = Array.isArray(node.name) ? "1" : "0";
|
|
1182
|
-
const typeOnly = node.isTypeOnly ? "0" : "1";
|
|
1183
|
-
const hasName = node.name != null ? "1" : "0";
|
|
1184
|
-
const name = Array.isArray(node.name) ? node.name.toSorted().join("\0") : node.name ?? "";
|
|
1185
|
-
return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
|
|
1186
|
-
}
|
|
1187
|
-
/**
|
|
1188
|
-
* Sorts members by {@link sortKey}. Keys are computed once per node, not per comparison.
|
|
1189
|
-
*/
|
|
1190
|
-
function sortMembers(nodes) {
|
|
1191
|
-
return nodes.map((node) => ({
|
|
1192
|
-
node,
|
|
1193
|
-
key: sortKey(node)
|
|
1194
|
-
})).sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0).map(({ node }) => node);
|
|
1195
|
-
}
|
|
1196
|
-
/**
|
|
1197
|
-
* Returns a collector for array-named members. A member whose path and `isTypeOnly` were already
|
|
1198
|
-
* seen folds its names into that entry, otherwise it joins `result` as a new entry.
|
|
1199
|
-
*/
|
|
1200
|
-
function mergeByPath(result) {
|
|
1201
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
1202
|
-
return ({ node, names }) => {
|
|
1203
|
-
const key = `${node.path}:${node.isTypeOnly ?? false}`;
|
|
1204
|
-
const existing = byPath.get(key);
|
|
1205
|
-
if (existing) {
|
|
1206
|
-
for (const name of names) {
|
|
1207
|
-
if (existing.seen.has(name)) continue;
|
|
1208
|
-
existing.seen.add(name);
|
|
1209
|
-
existing.names.push(name);
|
|
1210
|
-
}
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
const item = {
|
|
1214
|
-
...node,
|
|
1215
|
-
name: [...names]
|
|
1216
|
-
};
|
|
1217
|
-
result.push(item);
|
|
1218
|
-
byPath.set(key, {
|
|
1219
|
-
names: item.name,
|
|
1220
|
-
seen: new Set(names)
|
|
1221
|
-
});
|
|
1222
|
-
};
|
|
1223
|
-
}
|
|
1224
|
-
/**
|
|
1225
|
-
* Deduplicates `SourceNode` objects by `name + isExportable + isTypeOnly`, keeping the first of each
|
|
1226
|
-
* key. An unnamed source only drops when the same node appears twice, so markdown can repeat a
|
|
1227
|
-
* paragraph or a table. Returns the deduplicated array in original order.
|
|
1228
|
-
*/
|
|
1229
|
-
function combineSources(sources) {
|
|
1230
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1231
|
-
return sources.filter((source) => {
|
|
1232
|
-
const key = source.name != null ? `${source.name}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}` : source;
|
|
1233
|
-
if (seen.has(key)) return false;
|
|
1234
|
-
seen.add(key);
|
|
1235
|
-
return true;
|
|
1236
|
-
});
|
|
1237
|
-
}
|
|
1238
|
-
/**
|
|
1239
|
-
* Deduplicates and merges `ExportNode` objects by path and type.
|
|
1240
|
-
*
|
|
1241
|
-
* Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
|
|
1242
|
-
* Non-array exports are deduplicated by exact identity. Returns a sorted, deduplicated array.
|
|
1243
|
-
*/
|
|
1244
|
-
function combineExports(exports) {
|
|
1245
|
-
const result = [];
|
|
1246
|
-
const merge = mergeByPath(result);
|
|
1247
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1248
|
-
for (const curr of sortMembers(exports)) {
|
|
1249
|
-
const { name, path, isTypeOnly, asAlias } = curr;
|
|
1250
|
-
if (Array.isArray(name)) {
|
|
1251
|
-
if (name.length) merge({
|
|
1252
|
-
node: curr,
|
|
1253
|
-
names: [...new Set(name)]
|
|
1254
|
-
});
|
|
1255
|
-
continue;
|
|
1256
|
-
}
|
|
1257
|
-
const key = `${path}:${name ?? ""}:${isTypeOnly ?? false}:${asAlias ?? ""}`;
|
|
1258
|
-
if (!seen.has(key)) {
|
|
1259
|
-
result.push(curr);
|
|
1260
|
-
seen.add(key);
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
return result;
|
|
1264
|
-
}
|
|
1265
|
-
/**
|
|
1266
|
-
* Deduplicates and merges `ImportNode` objects, filtering out unused imports.
|
|
1267
|
-
*
|
|
1268
|
-
* Retains imports that are referenced in `source` or re-exported. Imports with the same path and
|
|
1269
|
-
* `isTypeOnly` flag have their names merged. Returns a sorted, deduplicated, filtered array.
|
|
1270
|
-
*/
|
|
1271
|
-
function combineImports(imports, exports, source) {
|
|
1272
|
-
const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
|
|
1273
|
-
const identifiers = source && imports.length > INDEX_ABOVE_IMPORTS ? new Set(source.match(IDENTIFIER_RUN)) : null;
|
|
1274
|
-
const isUsed = (importName) => !source || identifiers?.has(importName) || source.includes(importName) || exportedNames.has(importName);
|
|
1275
|
-
const importNameMemo = /* @__PURE__ */ new Map();
|
|
1276
|
-
const canonicalizeName = (n) => {
|
|
1277
|
-
if (typeof n === "string") return n;
|
|
1278
|
-
const key = `${n.propertyName}:${n.name ?? ""}`;
|
|
1279
|
-
if (!importNameMemo.has(key)) importNameMemo.set(key, n);
|
|
1280
|
-
return importNameMemo.get(key);
|
|
1281
|
-
};
|
|
1282
|
-
const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
|
|
1283
|
-
for (const node of imports) if (Array.isArray(node.name) && node.name.some((item) => isUsed(importLocalName(item)))) pathsWithUsedNamedImport.add(node.path);
|
|
1284
|
-
const result = [];
|
|
1285
|
-
const merge = mergeByPath(result);
|
|
1286
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1287
|
-
for (const curr of sortMembers(imports)) {
|
|
1288
|
-
if (curr.path === curr.root) continue;
|
|
1289
|
-
const { name, path, isTypeOnly } = curr;
|
|
1290
|
-
if (Array.isArray(name)) {
|
|
1291
|
-
const names = [...new Set(name.map(canonicalizeName))].filter((item) => isUsed(importLocalName(item)));
|
|
1292
|
-
if (names.length) merge({
|
|
1293
|
-
node: curr,
|
|
1294
|
-
names
|
|
1295
|
-
});
|
|
1296
|
-
continue;
|
|
1297
|
-
}
|
|
1298
|
-
if (!isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
|
|
1299
|
-
const key = `${path}:${name}:${isTypeOnly ?? false}`;
|
|
1300
|
-
if (!seen.has(key)) {
|
|
1301
|
-
result.push(curr);
|
|
1302
|
-
seen.add(key);
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
return result;
|
|
1306
|
-
}
|
|
1307
|
-
//#endregion
|
|
1308
|
-
//#region src/nodes/file.ts
|
|
1309
|
-
/**
|
|
1310
|
-
* Creates an `ImportNode` representing a language-agnostic import/dependency declaration.
|
|
1311
|
-
*
|
|
1312
|
-
* @example Named import
|
|
1313
|
-
* ```ts
|
|
1314
|
-
* createImport({ name: ['useState'], path: 'react' })
|
|
1315
|
-
* // import { useState } from 'react'
|
|
1316
|
-
* ```
|
|
1317
|
-
*/
|
|
1318
|
-
const createImport = nodeFactory("Import");
|
|
1319
|
-
/**
|
|
1320
|
-
* Creates an `ExportNode` representing a language-agnostic export/public API declaration.
|
|
1321
|
-
*
|
|
1322
|
-
* @example Named export
|
|
1323
|
-
* ```ts
|
|
1324
|
-
* createExport({ name: ['Pet'], path: './Pet' })
|
|
1325
|
-
* // export { Pet } from './Pet'
|
|
1326
|
-
* ```
|
|
1327
|
-
*/
|
|
1328
|
-
const createExport = nodeFactory("Export");
|
|
1329
|
-
/**
|
|
1330
|
-
* Creates a `SourceNode` representing a fragment of source code within a file.
|
|
1331
|
-
*
|
|
1332
|
-
* @example
|
|
1333
|
-
* ```ts
|
|
1334
|
-
* createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')], isExportable: true })
|
|
1335
|
-
* ```
|
|
1336
|
-
*/
|
|
1337
|
-
const createSource = nodeFactory("Source");
|
|
1338
|
-
/**
|
|
1339
|
-
* Creates a fully resolved `FileNode` from a file input descriptor.
|
|
1340
|
-
*
|
|
1341
|
-
* Computes:
|
|
1342
|
-
* - `id` SHA256 hash of the file path
|
|
1343
|
-
* - `name` `baseName` without extension
|
|
1344
|
-
* - `extname` extension extracted from `baseName`
|
|
1345
|
-
*
|
|
1346
|
-
* Deduplicates:
|
|
1347
|
-
* - `sources` via `combineSources`
|
|
1348
|
-
* - `exports` via `combineExports`
|
|
1349
|
-
* - `imports` via `combineImports` (also filters unused imports)
|
|
1350
|
-
*
|
|
1351
|
-
* @throws {Error} when `baseName` has no extension.
|
|
1352
|
-
*
|
|
1353
|
-
* @example
|
|
1354
|
-
* ```ts
|
|
1355
|
-
* const file = createFile({
|
|
1356
|
-
* baseName: 'petStore.ts',
|
|
1357
|
-
* path: 'src/models/petStore.ts',
|
|
1358
|
-
* sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],
|
|
1359
|
-
* imports: [createImport({ name: ['z'], path: 'zod' })],
|
|
1360
|
-
* exports: [createExport({ name: ['Pet'], path: './petStore' })],
|
|
1361
|
-
* })
|
|
1362
|
-
* // file.id = SHA256 hash of 'src/models/petStore.ts'
|
|
1363
|
-
* // file.name = 'petStore'
|
|
1364
|
-
* // file.extname = '.ts'
|
|
1365
|
-
* ```
|
|
1366
|
-
*
|
|
1367
|
-
* @example Copy a real file into the output
|
|
1368
|
-
* ```ts
|
|
1369
|
-
* const file = createFile({
|
|
1370
|
-
* baseName: 'client.ts',
|
|
1371
|
-
* path: 'src/gen/client.ts',
|
|
1372
|
-
* copy: '/abs/path/to/templates/client.ts',
|
|
1373
|
-
* })
|
|
1374
|
-
* ```
|
|
1375
|
-
*/
|
|
1376
|
-
function createFile(input) {
|
|
1377
|
-
const extname = path.extname(input.baseName);
|
|
1378
|
-
if (!extname) throw new Error(`No extname found for ${input.baseName}`);
|
|
1379
|
-
const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
|
|
1380
|
-
const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
|
|
1381
|
-
const resolvedImports = (() => {
|
|
1382
|
-
if (!input.imports?.length) return [];
|
|
1383
|
-
const sourceParts = [];
|
|
1384
|
-
const localNames = /* @__PURE__ */ new Set();
|
|
1385
|
-
for (const item of resolvedSources) {
|
|
1386
|
-
const extracted = item.nodes && extractStringsFromNodes(item.nodes);
|
|
1387
|
-
if (extracted) sourceParts.push(extracted);
|
|
1388
|
-
if (item.name) localNames.add(item.name);
|
|
1389
|
-
}
|
|
1390
|
-
const source = sourceParts.join("\n") || void 0;
|
|
1391
|
-
return combineImports(input.imports, resolvedExports, source).flatMap((imp) => {
|
|
1392
|
-
if (imp.path === input.path) return [];
|
|
1393
|
-
if (!Array.isArray(imp.name)) return typeof imp.name === "string" && localNames.has(imp.name) ? [] : [imp];
|
|
1394
|
-
const kept = imp.name.filter((item) => !localNames.has(importLocalName(item)));
|
|
1395
|
-
if (!kept.length) return [];
|
|
1396
|
-
return [kept.length === imp.name.length ? imp : {
|
|
1397
|
-
...imp,
|
|
1398
|
-
name: kept
|
|
1399
|
-
}];
|
|
1400
|
-
});
|
|
1401
|
-
})();
|
|
1402
|
-
return {
|
|
1403
|
-
...input,
|
|
1404
|
-
kind: "File",
|
|
1405
|
-
id: hash("sha256", input.path, "hex"),
|
|
1406
|
-
name: path.basename(input.baseName, extname),
|
|
1407
|
-
extname,
|
|
1408
|
-
imports: resolvedImports,
|
|
1409
|
-
exports: resolvedExports,
|
|
1410
|
-
sources: resolvedSources,
|
|
1411
|
-
meta: input.meta ?? {}
|
|
1412
|
-
};
|
|
1413
|
-
}
|
|
1591
|
+
const createContent = nodeFactory(nodeKinds.content);
|
|
1414
1592
|
//#endregion
|
|
1415
1593
|
//#region src/nodes/input.ts
|
|
1416
1594
|
/**
|
|
@@ -1432,7 +1610,7 @@ function createInput(overrides = {}) {
|
|
|
1432
1610
|
enumNames: []
|
|
1433
1611
|
},
|
|
1434
1612
|
...overrides,
|
|
1435
|
-
kind:
|
|
1613
|
+
kind: nodeKinds.input
|
|
1436
1614
|
};
|
|
1437
1615
|
}
|
|
1438
1616
|
//#endregion
|
|
@@ -1440,7 +1618,7 @@ function createInput(overrides = {}) {
|
|
|
1440
1618
|
/**
|
|
1441
1619
|
* Creates a `RequestBodyNode`.
|
|
1442
1620
|
*/
|
|
1443
|
-
const createRequestBody = nodeFactory(
|
|
1621
|
+
const createRequestBody = nodeFactory(nodeKinds.requestBody);
|
|
1444
1622
|
//#endregion
|
|
1445
1623
|
//#region src/nodes/operation.ts
|
|
1446
1624
|
function createOperation(props) {
|
|
@@ -1451,7 +1629,7 @@ function createOperation(props) {
|
|
|
1451
1629
|
parameters: [],
|
|
1452
1630
|
responses: [],
|
|
1453
1631
|
...rest,
|
|
1454
|
-
kind:
|
|
1632
|
+
kind: nodeKinds.operation,
|
|
1455
1633
|
...isHttp ? { protocol: "http" } : {},
|
|
1456
1634
|
requestBody: requestBody ? createRequestBody(requestBody) : void 0
|
|
1457
1635
|
};
|
|
@@ -1478,7 +1656,7 @@ function createParameter(input) {
|
|
|
1478
1656
|
...input,
|
|
1479
1657
|
required,
|
|
1480
1658
|
schema: optionality(input.schema, required),
|
|
1481
|
-
kind:
|
|
1659
|
+
kind: nodeKinds.parameter
|
|
1482
1660
|
};
|
|
1483
1661
|
}
|
|
1484
1662
|
//#endregion
|
|
@@ -1503,7 +1681,7 @@ function createProperty(input) {
|
|
|
1503
1681
|
...input,
|
|
1504
1682
|
required,
|
|
1505
1683
|
schema: optionality(input.schema, required),
|
|
1506
|
-
kind:
|
|
1684
|
+
kind: nodeKinds.property
|
|
1507
1685
|
};
|
|
1508
1686
|
}
|
|
1509
1687
|
//#endregion
|
|
@@ -1530,7 +1708,7 @@ function createResponse(props) {
|
|
|
1530
1708
|
return {
|
|
1531
1709
|
...rest,
|
|
1532
1710
|
content: entries,
|
|
1533
|
-
kind:
|
|
1711
|
+
kind: nodeKinds.response
|
|
1534
1712
|
};
|
|
1535
1713
|
}
|
|
1536
1714
|
//#endregion
|
|
@@ -1565,12 +1743,12 @@ function createSchema(props) {
|
|
|
1565
1743
|
properties: [],
|
|
1566
1744
|
primitive: "object",
|
|
1567
1745
|
...props,
|
|
1568
|
-
kind:
|
|
1746
|
+
kind: nodeKinds.schema
|
|
1569
1747
|
};
|
|
1570
1748
|
return {
|
|
1571
1749
|
primitive: TYPE_TO_PRIMITIVE[props.type],
|
|
1572
1750
|
...props,
|
|
1573
|
-
kind:
|
|
1751
|
+
kind: nodeKinds.schema
|
|
1574
1752
|
};
|
|
1575
1753
|
}
|
|
1576
1754
|
//#endregion
|
|
@@ -1618,10 +1796,13 @@ var src_exports = /* @__PURE__ */ __exportAll({
|
|
|
1618
1796
|
forEachSchema: () => forEachSchema,
|
|
1619
1797
|
getSchemaLiteralValues: () => getSchemaLiteralValues,
|
|
1620
1798
|
inferDiscriminatorPropertyName: () => inferDiscriminatorPropertyName,
|
|
1799
|
+
inlineFiles: () => inlineFiles,
|
|
1621
1800
|
isBareRef: () => isBareRef,
|
|
1622
1801
|
isHttpOperationNode: () => isHttpOperationNode,
|
|
1802
|
+
isVirtualFile: () => isVirtualFile,
|
|
1623
1803
|
mergeRefWithSchema: () => mergeRefWithSchema,
|
|
1624
1804
|
narrowSchema: () => narrowSchema,
|
|
1805
|
+
nodeKinds: () => nodeKinds,
|
|
1625
1806
|
optionality: () => optionality,
|
|
1626
1807
|
refName: () => refName,
|
|
1627
1808
|
resolveRef: () => resolveRef,
|
|
@@ -1631,6 +1812,6 @@ var src_exports = /* @__PURE__ */ __exportAll({
|
|
|
1631
1812
|
transform: () => transform
|
|
1632
1813
|
});
|
|
1633
1814
|
//#endregion
|
|
1634
|
-
export { applyMacros, src_exports as ast, collect, collectImportedRefNames, collectSchemaRefs, collectSync, collectUsedSchemaNames, composeMacros, createPrinter, defineMacro, extractStringsFromNodes, nodes_exports as factory, findCircularSchemas, findCircularSchemasFromGraph, findSchema, forEachChild, forEachSchema, getSchemaLiteralValues, inferDiscriminatorPropertyName, isBareRef, isHttpOperationNode, mergeRefWithSchema, narrowSchema, optionality, refName, resolveRef, resolveRefName, resolveSchemaProperties, schemaTypes, transform };
|
|
1815
|
+
export { applyMacros, src_exports as ast, collect, collectImportedRefNames, collectSchemaRefs, collectSync, collectUsedSchemaNames, composeMacros, createPrinter, defineMacro, extractStringsFromNodes, nodes_exports as factory, findCircularSchemas, findCircularSchemasFromGraph, findSchema, forEachChild, forEachSchema, getSchemaLiteralValues, inferDiscriminatorPropertyName, inlineFiles, isBareRef, isHttpOperationNode, isVirtualFile, mergeRefWithSchema, narrowSchema, nodeKinds, optionality, refName, resolveRef, resolveRefName, resolveSchemaProperties, schemaTypes, transform };
|
|
1635
1816
|
|
|
1636
1817
|
//# sourceMappingURL=index.js.map
|