@kubb/ast 5.13.1 → 5.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +558 -383
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -3
- package/dist/index.d.ts +81 -3
- package/dist/index.js +556 -384
- 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,131 +721,539 @@ 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
|
-
|
|
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
|
+
/** Returns the last segment of a `$ref` pointer, for example `'Pet'` for `#/components/schemas/Pet`. */
|
|
1134
|
+
function refName(ref) {
|
|
1135
|
+
return ref.split("/").at(-1) ?? ref;
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Resolves the emitted name of the schema a ref node points at. Prefers `targetName` (set when
|
|
1139
|
+
* the referenced schema was renamed, e.g. to break a collision), then the last segment of `ref`,
|
|
1140
|
+
* then `name`, then the nested `schema.name`.
|
|
1141
|
+
*
|
|
1142
|
+
* Returns `null` for non-ref nodes or when no name resolves.
|
|
1143
|
+
*
|
|
1144
|
+
* @example
|
|
1145
|
+
* `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Pet' }) // 'Pet'`
|
|
1146
|
+
*
|
|
1147
|
+
* @example Collision-renamed target
|
|
1148
|
+
* `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Order', targetName: 'OrderSchema' }) // 'OrderSchema'`
|
|
1149
|
+
*/
|
|
1150
|
+
function resolveRefName(node) {
|
|
1151
|
+
if (!node || node.type !== "ref") return null;
|
|
1152
|
+
if (node.targetName) return node.targetName;
|
|
1153
|
+
if (node.ref) return refName(node.ref);
|
|
1154
|
+
return node.name ?? node.schema?.name ?? null;
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Follows a resolved ref chain to its target schema. Returns an unresolved or cyclic ref as is.
|
|
1158
|
+
*
|
|
1159
|
+
* @example
|
|
1160
|
+
* ```ts
|
|
1161
|
+
* const target = resolveRef(refSchema)
|
|
1162
|
+
* ```
|
|
1163
|
+
*/
|
|
1164
|
+
function resolveRef(node) {
|
|
1165
|
+
if (node.type !== "ref" || !node.schema) return node;
|
|
1166
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1167
|
+
let current = node;
|
|
1168
|
+
while (current.type === "ref" && current.schema) {
|
|
1169
|
+
if (seen.has(current)) return current;
|
|
1170
|
+
seen.add(current);
|
|
1171
|
+
current = current.schema;
|
|
1172
|
+
}
|
|
1173
|
+
return current;
|
|
1174
|
+
}
|
|
1175
|
+
/** Fields of a ref node that describe the ref itself, so they never override the resolved schema. */
|
|
1176
|
+
const REF_OWN_KEYS = /* @__PURE__ */ new Set([
|
|
1177
|
+
"kind",
|
|
1178
|
+
"type",
|
|
1179
|
+
"name",
|
|
1180
|
+
"ref",
|
|
1181
|
+
"schema"
|
|
1182
|
+
]);
|
|
1183
|
+
/**
|
|
1184
|
+
* Merges a ref node with its resolved schema, giving the fields set at the usage site precedence.
|
|
1185
|
+
*
|
|
1186
|
+
* Every field on the ref except `kind`, `type`, `name`, `ref`, and `schema` overrides the same field
|
|
1187
|
+
* of the resolved `schema` (for example `description`, `nullable`, `readOnly`, `deprecated`). Fields
|
|
1188
|
+
* left `undefined` on the ref do not shadow the resolved schema. A ref without a resolved `schema`
|
|
1189
|
+
* is returned as is.
|
|
1190
|
+
*
|
|
1191
|
+
* @example
|
|
1192
|
+
* ```ts
|
|
1193
|
+
* const ref = createSchema({ type: 'ref', ref: '#/components/schemas/Pet', description: 'A pet', schema: petSchema })
|
|
1194
|
+
* mergeRefWithSchema(ref) // petSchema's fields with description 'A pet'
|
|
1195
|
+
* ```
|
|
1196
|
+
*/
|
|
1197
|
+
function mergeRefWithSchema(node) {
|
|
1198
|
+
if (!node.schema) return node;
|
|
1199
|
+
const merged = { ...node.schema };
|
|
1200
|
+
for (const key of Object.keys(node)) {
|
|
1201
|
+
const value = node[key];
|
|
1202
|
+
if (value !== void 0 && !REF_OWN_KEYS.has(key)) merged[key] = value;
|
|
1203
|
+
}
|
|
1204
|
+
return merged;
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Returns `true` when a schema is a bare ref without modifiers, metadata, or omit keys.
|
|
1208
|
+
*
|
|
1209
|
+
* Modifiers inherited from the resolved schema are included in the check because a ref emits the
|
|
1210
|
+
* referenced schema variable with those modifiers already applied.
|
|
1211
|
+
*/
|
|
1212
|
+
function isBareRef(node, keysToOmit) {
|
|
1213
|
+
if (!node || node.type !== "ref") return false;
|
|
1214
|
+
const meta = mergeRefWithSchema(node);
|
|
1215
|
+
return !meta.nullable && !meta.optional && !meta.nullish && meta.default === void 0 && !meta.description && !meta.examples?.length && !keysToOmit?.length;
|
|
1216
|
+
}
|
|
1217
|
+
//#endregion
|
|
1218
|
+
//#region ../../internals/utils/src/promise.ts
|
|
1219
|
+
/**
|
|
1220
|
+
* Wraps `factory` with a keyed cache backed by the provided store.
|
|
1221
|
+
*
|
|
1222
|
+
* Pass a `WeakMap` for object keys (results are GC-eligible when the key is
|
|
1223
|
+
* collected) or a `Map` for primitive keys. For multi-argument functions,
|
|
1224
|
+
* nest two `memoize` calls — the outer keyed by the first argument, the
|
|
1225
|
+
* inner (created once per outer miss) keyed by the second.
|
|
1226
|
+
*
|
|
1227
|
+
* Because the cache is owned by the caller, it can be shared, inspected, or
|
|
1228
|
+
* cleared independently of the memoized function.
|
|
1229
|
+
*
|
|
1230
|
+
* @example Single WeakMap key
|
|
1231
|
+
* ```ts
|
|
1232
|
+
* const cache = new WeakMap<SchemaNode, Set<string>>()
|
|
1233
|
+
* const getRefs = memoize(cache, (node) => collectRefs(node))
|
|
1234
|
+
* ```
|
|
1235
|
+
*
|
|
1236
|
+
* @example Single Map key (primitive)
|
|
1237
|
+
* ```ts
|
|
1238
|
+
* const cache = new Map<string, Resolver>()
|
|
1239
|
+
* const getResolver = memoize(cache, (name) => buildResolver(name))
|
|
1240
|
+
* ```
|
|
1241
|
+
*
|
|
1242
|
+
* @example Two-level (object + primitive)
|
|
1243
|
+
* ```ts
|
|
1244
|
+
* const outer = new WeakMap<Params[], Map<string, Params[]>>()
|
|
1245
|
+
* const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
|
|
1246
|
+
* fn(params)('camelcase')
|
|
1247
|
+
* ```
|
|
1248
|
+
*/
|
|
1249
|
+
function memoize(store, factory) {
|
|
1250
|
+
return (key) => {
|
|
1251
|
+
if (store.has(key)) return store.get(key);
|
|
1252
|
+
const value = factory(key);
|
|
1253
|
+
store.set(key, value);
|
|
1254
|
+
return value;
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
825
1257
|
//#endregion
|
|
826
1258
|
//#region src/utils/schemaGraph.ts
|
|
827
1259
|
/**
|
|
@@ -1080,17 +1512,6 @@ function inferDiscriminatorPropertyName(members) {
|
|
|
1080
1512
|
}
|
|
1081
1513
|
}
|
|
1082
1514
|
//#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
1515
|
//#region src/nodes/code.ts
|
|
1095
1516
|
/**
|
|
1096
1517
|
* Creates a `ConstNode` representing a TypeScript `const` declaration.
|
|
@@ -1101,7 +1522,7 @@ function nodeFactory(kind) {
|
|
|
1101
1522
|
* // export const pets: Pet[] = ... as const
|
|
1102
1523
|
* ```
|
|
1103
1524
|
*/
|
|
1104
|
-
const createConst = nodeFactory(
|
|
1525
|
+
const createConst = nodeFactory(nodeKinds.const);
|
|
1105
1526
|
/**
|
|
1106
1527
|
* Creates a `TypeNode` representing a TypeScript `type` alias declaration.
|
|
1107
1528
|
*
|
|
@@ -1111,7 +1532,7 @@ const createConst = nodeFactory("Const");
|
|
|
1111
1532
|
* // export type Pet = ...
|
|
1112
1533
|
* ```
|
|
1113
1534
|
*/
|
|
1114
|
-
const createType = nodeFactory(
|
|
1535
|
+
const createType = nodeFactory(nodeKinds.type);
|
|
1115
1536
|
/**
|
|
1116
1537
|
* Creates a `FunctionNode` representing a TypeScript `function` declaration.
|
|
1117
1538
|
*
|
|
@@ -1121,7 +1542,7 @@ const createType = nodeFactory("Type");
|
|
|
1121
1542
|
* // export async function fetchPet(): Promise<Pet> { ... }
|
|
1122
1543
|
* ```
|
|
1123
1544
|
*/
|
|
1124
|
-
const createFunction = nodeFactory(
|
|
1545
|
+
const createFunction = nodeFactory(nodeKinds.function);
|
|
1125
1546
|
/**
|
|
1126
1547
|
* Creates an `ArrowFunctionNode` representing a TypeScript arrow function.
|
|
1127
1548
|
*
|
|
@@ -1131,7 +1552,7 @@ const createFunction = nodeFactory("Function");
|
|
|
1131
1552
|
* // export const double = (n: number) => ...
|
|
1132
1553
|
* ```
|
|
1133
1554
|
*/
|
|
1134
|
-
const createArrowFunction = nodeFactory(
|
|
1555
|
+
const createArrowFunction = nodeFactory(nodeKinds.arrowFunction);
|
|
1135
1556
|
/**
|
|
1136
1557
|
* Creates a {@link TextNode} representing a raw string fragment in the source output.
|
|
1137
1558
|
*
|
|
@@ -1143,7 +1564,7 @@ const createArrowFunction = nodeFactory("ArrowFunction");
|
|
|
1143
1564
|
*/
|
|
1144
1565
|
function createText(value) {
|
|
1145
1566
|
return {
|
|
1146
|
-
kind:
|
|
1567
|
+
kind: nodeKinds.text,
|
|
1147
1568
|
value
|
|
1148
1569
|
};
|
|
1149
1570
|
}
|
|
@@ -1157,7 +1578,7 @@ function createText(value) {
|
|
|
1157
1578
|
* ```
|
|
1158
1579
|
*/
|
|
1159
1580
|
function createBreak() {
|
|
1160
|
-
return { kind:
|
|
1581
|
+
return { kind: nodeKinds.break };
|
|
1161
1582
|
}
|
|
1162
1583
|
/**
|
|
1163
1584
|
* Creates a {@link JsxNode} representing a raw JSX fragment in the source output.
|
|
@@ -1170,7 +1591,7 @@ function createBreak() {
|
|
|
1170
1591
|
*/
|
|
1171
1592
|
function createJsx(value) {
|
|
1172
1593
|
return {
|
|
1173
|
-
kind:
|
|
1594
|
+
kind: nodeKinds.jsx,
|
|
1174
1595
|
value
|
|
1175
1596
|
};
|
|
1176
1597
|
}
|
|
@@ -1179,259 +1600,7 @@ function createJsx(value) {
|
|
|
1179
1600
|
/**
|
|
1180
1601
|
* Creates a `ContentNode` for a single request-body or response content type.
|
|
1181
1602
|
*/
|
|
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
|
-
}
|
|
1603
|
+
const createContent = nodeFactory(nodeKinds.content);
|
|
1435
1604
|
//#endregion
|
|
1436
1605
|
//#region src/nodes/input.ts
|
|
1437
1606
|
/**
|
|
@@ -1453,7 +1622,7 @@ function createInput(overrides = {}) {
|
|
|
1453
1622
|
enumNames: []
|
|
1454
1623
|
},
|
|
1455
1624
|
...overrides,
|
|
1456
|
-
kind:
|
|
1625
|
+
kind: nodeKinds.input
|
|
1457
1626
|
};
|
|
1458
1627
|
}
|
|
1459
1628
|
//#endregion
|
|
@@ -1461,7 +1630,7 @@ function createInput(overrides = {}) {
|
|
|
1461
1630
|
/**
|
|
1462
1631
|
* Creates a `RequestBodyNode`.
|
|
1463
1632
|
*/
|
|
1464
|
-
const createRequestBody = nodeFactory(
|
|
1633
|
+
const createRequestBody = nodeFactory(nodeKinds.requestBody);
|
|
1465
1634
|
//#endregion
|
|
1466
1635
|
//#region src/nodes/operation.ts
|
|
1467
1636
|
function createOperation(props) {
|
|
@@ -1472,7 +1641,7 @@ function createOperation(props) {
|
|
|
1472
1641
|
parameters: [],
|
|
1473
1642
|
responses: [],
|
|
1474
1643
|
...rest,
|
|
1475
|
-
kind:
|
|
1644
|
+
kind: nodeKinds.operation,
|
|
1476
1645
|
...isHttp ? { protocol: "http" } : {},
|
|
1477
1646
|
requestBody: requestBody ? createRequestBody(requestBody) : void 0
|
|
1478
1647
|
};
|
|
@@ -1499,7 +1668,7 @@ function createParameter(input) {
|
|
|
1499
1668
|
...input,
|
|
1500
1669
|
required,
|
|
1501
1670
|
schema: optionality(input.schema, required),
|
|
1502
|
-
kind:
|
|
1671
|
+
kind: nodeKinds.parameter
|
|
1503
1672
|
};
|
|
1504
1673
|
}
|
|
1505
1674
|
//#endregion
|
|
@@ -1524,7 +1693,7 @@ function createProperty(input) {
|
|
|
1524
1693
|
...input,
|
|
1525
1694
|
required,
|
|
1526
1695
|
schema: optionality(input.schema, required),
|
|
1527
|
-
kind:
|
|
1696
|
+
kind: nodeKinds.property
|
|
1528
1697
|
};
|
|
1529
1698
|
}
|
|
1530
1699
|
//#endregion
|
|
@@ -1551,7 +1720,7 @@ function createResponse(props) {
|
|
|
1551
1720
|
return {
|
|
1552
1721
|
...rest,
|
|
1553
1722
|
content: entries,
|
|
1554
|
-
kind:
|
|
1723
|
+
kind: nodeKinds.response
|
|
1555
1724
|
};
|
|
1556
1725
|
}
|
|
1557
1726
|
//#endregion
|
|
@@ -1586,12 +1755,12 @@ function createSchema(props) {
|
|
|
1586
1755
|
properties: [],
|
|
1587
1756
|
primitive: "object",
|
|
1588
1757
|
...props,
|
|
1589
|
-
kind:
|
|
1758
|
+
kind: nodeKinds.schema
|
|
1590
1759
|
};
|
|
1591
1760
|
return {
|
|
1592
1761
|
primitive: TYPE_TO_PRIMITIVE[props.type],
|
|
1593
1762
|
...props,
|
|
1594
|
-
kind:
|
|
1763
|
+
kind: nodeKinds.schema
|
|
1595
1764
|
};
|
|
1596
1765
|
}
|
|
1597
1766
|
//#endregion
|
|
@@ -1639,10 +1808,13 @@ var src_exports = /* @__PURE__ */ __exportAll({
|
|
|
1639
1808
|
forEachSchema: () => forEachSchema,
|
|
1640
1809
|
getSchemaLiteralValues: () => getSchemaLiteralValues,
|
|
1641
1810
|
inferDiscriminatorPropertyName: () => inferDiscriminatorPropertyName,
|
|
1811
|
+
inlineFiles: () => inlineFiles,
|
|
1642
1812
|
isBareRef: () => isBareRef,
|
|
1643
1813
|
isHttpOperationNode: () => isHttpOperationNode,
|
|
1814
|
+
isVirtualFile: () => isVirtualFile,
|
|
1644
1815
|
mergeRefWithSchema: () => mergeRefWithSchema,
|
|
1645
1816
|
narrowSchema: () => narrowSchema,
|
|
1817
|
+
nodeKinds: () => nodeKinds,
|
|
1646
1818
|
optionality: () => optionality,
|
|
1647
1819
|
refName: () => refName,
|
|
1648
1820
|
resolveRef: () => resolveRef,
|
|
@@ -1681,10 +1853,13 @@ exports.forEachChild = forEachChild;
|
|
|
1681
1853
|
exports.forEachSchema = forEachSchema;
|
|
1682
1854
|
exports.getSchemaLiteralValues = getSchemaLiteralValues;
|
|
1683
1855
|
exports.inferDiscriminatorPropertyName = inferDiscriminatorPropertyName;
|
|
1856
|
+
exports.inlineFiles = inlineFiles;
|
|
1684
1857
|
exports.isBareRef = isBareRef;
|
|
1685
1858
|
exports.isHttpOperationNode = isHttpOperationNode;
|
|
1859
|
+
exports.isVirtualFile = isVirtualFile;
|
|
1686
1860
|
exports.mergeRefWithSchema = mergeRefWithSchema;
|
|
1687
1861
|
exports.narrowSchema = narrowSchema;
|
|
1862
|
+
exports.nodeKinds = nodeKinds;
|
|
1688
1863
|
exports.optionality = optionality;
|
|
1689
1864
|
exports.refName = refName;
|
|
1690
1865
|
exports.resolveRef = resolveRef;
|