@kubb/ast 5.0.0-beta.6 → 5.0.0-beta.60
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/LICENSE +17 -10
- package/README.md +50 -27
- package/dist/chunk-CNktS9qV.js +17 -0
- package/dist/extractStringsFromNodes-WMYJ8nQL.d.ts +82 -0
- package/dist/factory-Cl8Z7mcc.cjs +299 -0
- package/dist/factory-Cl8Z7mcc.cjs.map +1 -0
- package/dist/factory-Du7nEP4B.js +282 -0
- package/dist/factory-Du7nEP4B.js.map +1 -0
- package/dist/factory.cjs +29 -0
- package/dist/factory.d.ts +62 -0
- package/dist/factory.js +3 -0
- package/dist/index-BzjwdK2M.d.ts +2433 -0
- package/dist/index.cjs +442 -2180
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +93 -3408
- package/dist/index.js +392 -2101
- package/dist/index.js.map +1 -1
- package/dist/operationParams-BZ07xDm0.d.ts +204 -0
- package/dist/response-DKxTr522.js +683 -0
- package/dist/response-DKxTr522.js.map +1 -0
- package/dist/response-DS5S3IG4.cjs +1058 -0
- package/dist/response-DS5S3IG4.cjs.map +1 -0
- package/dist/types-olVl9v5p.d.ts +764 -0
- package/dist/types.cjs +0 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.js +1 -0
- package/dist/utils-D83JA6Xx.cjs +1645 -0
- package/dist/utils-D83JA6Xx.cjs.map +1 -0
- package/dist/utils-Dj_KoXMv.js +1389 -0
- package/dist/utils-Dj_KoXMv.js.map +1 -0
- package/dist/utils.cjs +34 -0
- package/dist/utils.d.ts +332 -0
- package/dist/utils.js +3 -0
- package/package.json +17 -6
- package/src/constants.ts +19 -64
- package/src/dedupe.ts +239 -0
- package/src/dialect.ts +53 -0
- package/src/factory.ts +67 -678
- package/src/guards.ts +10 -92
- package/src/index.ts +16 -43
- package/src/infer.ts +16 -14
- package/src/mocks.ts +7 -127
- package/src/node.ts +128 -0
- package/src/nodes/base.ts +5 -12
- package/src/nodes/code.ts +165 -74
- package/src/nodes/content.ts +56 -0
- package/src/nodes/file.ts +97 -36
- package/src/nodes/function.ts +216 -156
- package/src/nodes/http.ts +1 -35
- package/src/nodes/index.ts +23 -15
- package/src/nodes/input.ts +140 -0
- package/src/nodes/operation.ts +122 -68
- package/src/nodes/output.ts +23 -0
- package/src/nodes/parameter.ts +33 -3
- package/src/nodes/property.ts +36 -3
- package/src/nodes/requestBody.ts +61 -0
- package/src/nodes/response.ts +58 -13
- package/src/nodes/schema.ts +93 -17
- package/src/printer.ts +48 -42
- package/src/registry.ts +75 -0
- package/src/signature.ts +207 -0
- package/src/transformers.ts +50 -18
- package/src/types.ts +7 -68
- package/src/utils/codegen.ts +104 -0
- package/src/utils/extractStringsFromNodes.ts +34 -0
- package/src/utils/fileMerge.ts +184 -0
- package/src/utils/index.ts +11 -0
- package/src/utils/operationParams.ts +353 -0
- package/src/utils/refs.ts +112 -0
- package/src/utils/schemaGraph.ts +169 -0
- package/src/utils/schemaTraversal.ts +86 -0
- package/src/utils/strings.ts +139 -0
- package/src/visitor.ts +227 -289
- package/dist/chunk--u3MIqq1.js +0 -8
- package/src/nodes/root.ts +0 -64
- package/src/refs.ts +0 -67
- package/src/resolvers.ts +0 -45
- package/src/utils.ts +0 -915
package/LICENSE
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
This repository contains software under two licenses:
|
|
1
|
+
MIT License
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
MIT License — see licenses/LICENSE-MIT for the full license text.
|
|
3
|
+
Copyright (c) 2026 Stijn Van Hulle
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<h1>@kubb/ast</h1>
|
|
3
2
|
<a href="https://kubb.dev" target="_blank" rel="noopener noreferrer">
|
|
4
|
-
<img
|
|
3
|
+
<img src="https://kubb.dev/og.png" alt="Kubb banner">
|
|
5
4
|
</a>
|
|
6
5
|
|
|
7
6
|
[![npm version][npm-version-src]][npm-version-href]
|
|
8
7
|
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
9
|
-
[![
|
|
8
|
+
[![Stars][stars-src]][stars-href]
|
|
10
9
|
[![License][license-src]][license-href]
|
|
11
|
-
[![
|
|
10
|
+
[![Node][node-src]][node-href]
|
|
12
11
|
|
|
13
12
|
<h4>
|
|
14
|
-
<a href="https://kubb.dev
|
|
13
|
+
<a href="https://kubb.dev" target="_blank">Documentation</a>
|
|
15
14
|
<span> · </span>
|
|
16
15
|
<a href="https://github.com/kubb-labs/kubb/issues/" target="_blank">Report Bug</a>
|
|
17
16
|
<span> · </span>
|
|
@@ -19,14 +18,22 @@
|
|
|
19
18
|
</h4>
|
|
20
19
|
</div>
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
<br />
|
|
22
|
+
|
|
23
|
+
# @kubb/ast
|
|
24
|
+
|
|
25
|
+
### Spec-agnostic AST layer for Kubb
|
|
26
|
+
|
|
27
|
+
Defines the node tree, visitor pattern, factory functions, and type guards used across every Kubb code generation plugin.
|
|
23
28
|
|
|
24
29
|
## Imports
|
|
25
30
|
|
|
26
|
-
| Path
|
|
27
|
-
|
|
|
28
|
-
| `@kubb/ast`
|
|
29
|
-
| `@kubb/ast/
|
|
31
|
+
| Path | Contents |
|
|
32
|
+
| ------------------- | ---------------------------------------------------------------------------------------- |
|
|
33
|
+
| `@kubb/ast` | Runtime: node definitions, guards, visitor, transformers, constants |
|
|
34
|
+
| `@kubb/ast/factory` | Node constructors (`createSchema`, `createFile`, and friends), the `ts.factory` analogue |
|
|
35
|
+
| `@kubb/ast/types` | Types only: all node interfaces, type aliases, visitor types |
|
|
36
|
+
| `@kubb/ast/utils` | Spec-agnostic string and identifier helpers, ref helpers |
|
|
30
37
|
|
|
31
38
|
## Node tree
|
|
32
39
|
|
|
@@ -51,10 +58,12 @@ SchemaNode (discriminated union)
|
|
|
51
58
|
|
|
52
59
|
### Factory
|
|
53
60
|
|
|
61
|
+
Constructors live on the `@kubb/ast/factory` subpath, mirroring `ts.factory.createX`. Through `@kubb/core` the same set is reachable as `ast.factory.createSchema(...)`.
|
|
62
|
+
|
|
54
63
|
```ts
|
|
55
|
-
import {
|
|
64
|
+
import { createInput, createSchema, createProperty } from '@kubb/ast/factory'
|
|
56
65
|
|
|
57
|
-
const root =
|
|
66
|
+
const root = createInput({
|
|
58
67
|
schemas: [
|
|
59
68
|
createSchema({
|
|
60
69
|
name: 'Pet',
|
|
@@ -106,11 +115,11 @@ const types = collect<string>(root, {
|
|
|
106
115
|
### Guards
|
|
107
116
|
|
|
108
117
|
```ts
|
|
109
|
-
import {
|
|
118
|
+
import { narrowSchema, schemaDef } from '@kubb/ast'
|
|
110
119
|
import type { Node } from '@kubb/ast/types'
|
|
111
120
|
|
|
112
121
|
function process(node: Node) {
|
|
113
|
-
if (
|
|
122
|
+
if (schemaDef.is(node)) {
|
|
114
123
|
const obj = narrowSchema(node, 'object')
|
|
115
124
|
obj?.properties?.forEach((p) => console.log(p.name))
|
|
116
125
|
}
|
|
@@ -120,17 +129,27 @@ function process(node: Node) {
|
|
|
120
129
|
### Refs
|
|
121
130
|
|
|
122
131
|
```ts
|
|
123
|
-
import {
|
|
132
|
+
import { extractRefName } from '@kubb/ast/utils'
|
|
124
133
|
|
|
125
|
-
|
|
126
|
-
const pet = resolveRef(refMap, 'Pet')
|
|
134
|
+
extractRefName('#/components/schemas/Pet') // 'Pet'
|
|
127
135
|
```
|
|
128
136
|
|
|
137
|
+
## Adding a node
|
|
138
|
+
|
|
139
|
+
Adding a node touches three files. The barrels and visitor tables derive the rest.
|
|
140
|
+
|
|
141
|
+
1. Define the node in its own `src/nodes/*.ts` file. Call `defineNode` and export the resulting `fooDef`, the `createFoo` constructor, and the node's type.
|
|
142
|
+
2. Add `fooDef` to the `nodeDefs` array in `src/registry.ts`.
|
|
143
|
+
3. Re-export `createFoo` from `src/factory.ts`.
|
|
144
|
+
|
|
145
|
+
Everything else follows from there. `@kubb/ast/types` picks up the node type through `export type *`, the `@kubb/ast` barrel picks up `fooDef` through `export * from './registry.ts'`, and the visitor tables (`VISITOR_KEYS`, `VISITOR_KEY_BY_KIND`, `nodeRebuilders`) come from the def's `children`, `visitorKey`, and `rebuild` fields. `registry.test.ts` fails when a def has no matching `factory.create*`, so missing wiring is caught in CI.
|
|
146
|
+
|
|
129
147
|
## Supporting Kubb
|
|
130
148
|
|
|
131
|
-
Kubb
|
|
149
|
+
Kubb is an open source project, and its development is funded entirely by sponsors. If you would like to become a sponsor, please consider:
|
|
132
150
|
|
|
133
151
|
- [Become a Sponsor on GitHub](https://github.com/sponsors/stijnvanhulle)
|
|
152
|
+
- [See sponsorship tiers and our sponsors](https://kubb.dev/sponsors)
|
|
134
153
|
|
|
135
154
|
<p align="center">
|
|
136
155
|
<a href="https://github.com/sponsors/stijnvanhulle">
|
|
@@ -138,15 +157,19 @@ Kubb uses an MIT-licensed open source project with its ongoing development made
|
|
|
138
157
|
</a>
|
|
139
158
|
</p>
|
|
140
159
|
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
[MIT](https://github.com/kubb-labs/kubb/blob/main/licenses/LICENSE-MIT)
|
|
163
|
+
|
|
141
164
|
<!-- Badges -->
|
|
142
165
|
|
|
143
|
-
[npm-version-src]: https://
|
|
144
|
-
[npm-version-href]: https://
|
|
145
|
-
[npm-downloads-src]: https://
|
|
146
|
-
[npm-downloads-href]: https://
|
|
147
|
-
[
|
|
166
|
+
[npm-version-src]: https://shieldcn.dev/npm/v/@kubb/ast.svg?variant=secondary&size=xs&theme=zinc&mode=dark
|
|
167
|
+
[npm-version-href]: https://npmx.dev/package/@kubb/ast
|
|
168
|
+
[npm-downloads-src]: https://shieldcn.dev/npm/dm/@kubb/ast.svg?variant=secondary&size=xs&theme=zinc&mode=dark
|
|
169
|
+
[npm-downloads-href]: https://npmx.dev/package/@kubb/ast
|
|
170
|
+
[stars-src]: https://shieldcn.dev/github/stars/kubb-labs/kubb.svg?variant=secondary&size=xs&theme=zinc&mode=dark
|
|
171
|
+
[stars-href]: https://github.com/kubb-labs/kubb
|
|
172
|
+
[license-src]: https://shieldcn.dev/npm/license/@kubb/ast.svg?variant=secondary&size=xs&theme=zinc
|
|
148
173
|
[license-href]: https://github.com/kubb-labs/kubb/blob/main/LICENSE
|
|
149
|
-
[
|
|
150
|
-
[
|
|
151
|
-
[sponsors-src]: https://img.shields.io/github/sponsors/stijnvanhulle?style=flat&colorA=18181B&colorB=f58517
|
|
152
|
-
[sponsors-href]: https://github.com/sponsors/stijnvanhulle/
|
|
174
|
+
[node-src]: https://shieldcn.dev/npm/node/@kubb/ast.svg?variant=secondary&size=xs&theme=zinc&mode=dark
|
|
175
|
+
[node-href]: https://npmx.dev/package/@kubb/ast
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __name = (target, value) => __defProp(target, "name", {
|
|
4
|
+
value,
|
|
5
|
+
configurable: true
|
|
6
|
+
});
|
|
7
|
+
var __exportAll = (all, no_symbols) => {
|
|
8
|
+
let target = {};
|
|
9
|
+
for (var name in all) __defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true
|
|
12
|
+
});
|
|
13
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
14
|
+
return target;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { __name as n, __exportAll as t };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { n as __name } from "./chunk-CNktS9qV.js";
|
|
2
|
+
import { Pt as CodeNode, _t as SchemaNode } from "./index-BzjwdK2M.js";
|
|
3
|
+
|
|
4
|
+
//#region src/transformers.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Replaces a discriminator property's schema with a string enum of allowed values.
|
|
7
|
+
*
|
|
8
|
+
* If `node` is not an object schema, or if the property does not exist, the input
|
|
9
|
+
* node is returned as-is.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const schema = createSchema({
|
|
14
|
+
* type: 'object',
|
|
15
|
+
* properties: [createProperty({ name: 'type', required: true, schema: createSchema({ type: 'string' }) })],
|
|
16
|
+
* })
|
|
17
|
+
* const result = setDiscriminatorEnum({ node: schema, propertyName: 'type', values: ['dog', 'cat'] })
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
declare function setDiscriminatorEnum({
|
|
21
|
+
node,
|
|
22
|
+
propertyName,
|
|
23
|
+
values,
|
|
24
|
+
enumName
|
|
25
|
+
}: {
|
|
26
|
+
node: SchemaNode;
|
|
27
|
+
propertyName: string;
|
|
28
|
+
values: Array<string>;
|
|
29
|
+
enumName?: string;
|
|
30
|
+
}): SchemaNode;
|
|
31
|
+
/**
|
|
32
|
+
* Merges adjacent anonymous object members into a single anonymous object member.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const merged = mergeAdjacentObjects([
|
|
37
|
+
* createSchema({ type: 'object', properties: [createProperty({ name: 'a', schema: createSchema({ type: 'string' }) })] }),
|
|
38
|
+
* createSchema({ type: 'object', properties: [createProperty({ name: 'b', schema: createSchema({ type: 'number' }) })] }),
|
|
39
|
+
* ])
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
declare function mergeAdjacentObjectsLazy(members: Iterable<SchemaNode>): Generator<SchemaNode, void, undefined>;
|
|
43
|
+
/**
|
|
44
|
+
* Removes enum members that are covered by broader scalar primitives in the same union.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* const simplified = simplifyUnion([
|
|
49
|
+
* createSchema({ type: 'enum', primitive: 'string', enumValues: ['active'] }),
|
|
50
|
+
* createSchema({ type: 'string' }),
|
|
51
|
+
* ])
|
|
52
|
+
* // keeps only string member
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
declare function simplifyUnion(members: Array<SchemaNode>): Array<SchemaNode>;
|
|
56
|
+
declare function setEnumName(propNode: SchemaNode, parentName: string | null | undefined, propName: string, enumSuffix: string): SchemaNode;
|
|
57
|
+
/**
|
|
58
|
+
* Merges a ref node with its resolved schema, giving usage-site fields precedence.
|
|
59
|
+
*
|
|
60
|
+
* Usage-site fields (`description`, `readOnly`, `nullable`, `deprecated`) on the ref node override
|
|
61
|
+
* the same fields in the resolved `node.schema`. Non-ref nodes are returned unchanged.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* // Ref with description override
|
|
66
|
+
* const ref = createSchema({ type: 'ref', ref: '#/components/schemas/Pet', description: 'A cute pet' })
|
|
67
|
+
* const merged = syncSchemaRef(ref) // merges with resolved Pet schema
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
declare function syncSchemaRef(node: SchemaNode): SchemaNode;
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/utils/extractStringsFromNodes.d.ts
|
|
73
|
+
/**
|
|
74
|
+
* Extracts all string content from a `CodeNode` tree recursively.
|
|
75
|
+
*
|
|
76
|
+
* Collects text node values, identifier references in string fields (`params`, `generics`, `returnType`, `type`),
|
|
77
|
+
* and nested node content. Used to build the full source string for import filtering.
|
|
78
|
+
*/
|
|
79
|
+
declare function extractStringsFromNodes(nodes: Array<CodeNode> | undefined): string;
|
|
80
|
+
//#endregion
|
|
81
|
+
export { simplifyUnion as a, setEnumName as i, mergeAdjacentObjectsLazy as n, syncSchemaRef as o, setDiscriminatorEnum as r, extractStringsFromNodes as t };
|
|
82
|
+
//# sourceMappingURL=extractStringsFromNodes-WMYJ8nQL.d.ts.map
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
const require_response = require("./response-DS5S3IG4.cjs");
|
|
2
|
+
let node_crypto = require("node:crypto");
|
|
3
|
+
let node_path = require("node:path");
|
|
4
|
+
node_path = require_response.__toESM(node_path, 1);
|
|
5
|
+
//#region ../../internals/utils/src/fs.ts
|
|
6
|
+
/**
|
|
7
|
+
* Strips the file extension from a path or file name.
|
|
8
|
+
* Only removes the last `.ext` segment when the dot is not part of a directory name.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* trimExtName('petStore.ts') // 'petStore'
|
|
12
|
+
* trimExtName('/src/models/pet.ts') // '/src/models/pet'
|
|
13
|
+
* trimExtName('/project.v2/gen/pet.ts') // '/project.v2/gen/pet'
|
|
14
|
+
* trimExtName('noExtension') // 'noExtension'
|
|
15
|
+
*/
|
|
16
|
+
function trimExtName(text) {
|
|
17
|
+
const dotIndex = text.lastIndexOf(".");
|
|
18
|
+
if (dotIndex > 0 && !text.includes("/", dotIndex)) return text.slice(0, dotIndex);
|
|
19
|
+
return text;
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/utils/fileMerge.ts
|
|
23
|
+
function sourceKey(source) {
|
|
24
|
+
return `${source.name ?? require_response.extractStringsFromNodes(source.nodes)}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`;
|
|
25
|
+
}
|
|
26
|
+
function pathTypeKey(path, isTypeOnly) {
|
|
27
|
+
return `${path}:${isTypeOnly ?? false}`;
|
|
28
|
+
}
|
|
29
|
+
function exportKey(path, name, isTypeOnly, asAlias) {
|
|
30
|
+
return `${path}:${name ?? ""}:${isTypeOnly ?? false}:${asAlias ?? ""}`;
|
|
31
|
+
}
|
|
32
|
+
function importKey(path, name, isTypeOnly) {
|
|
33
|
+
return `${path}:${name ?? ""}:${isTypeOnly ?? false}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Computes a multi-level sort key for exports and imports:
|
|
37
|
+
* non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
|
|
38
|
+
*/
|
|
39
|
+
function sortKey(node) {
|
|
40
|
+
const isArray = Array.isArray(node.name) ? "1" : "0";
|
|
41
|
+
const typeOnly = node.isTypeOnly ? "0" : "1";
|
|
42
|
+
const hasName = node.name != null ? "1" : "0";
|
|
43
|
+
const name = Array.isArray(node.name) ? node.name.toSorted().join("\0") : node.name ?? "";
|
|
44
|
+
return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Deduplicates and merges `SourceNode` objects by `name + isExportable + isTypeOnly`.
|
|
48
|
+
*
|
|
49
|
+
* Unnamed sources are deduplicated by object reference. Returns a deduplicated array in original order.
|
|
50
|
+
*/
|
|
51
|
+
function combineSources(sources) {
|
|
52
|
+
const seen = /* @__PURE__ */ new Map();
|
|
53
|
+
for (const source of sources) {
|
|
54
|
+
const key = sourceKey(source);
|
|
55
|
+
if (!seen.has(key)) seen.set(key, source);
|
|
56
|
+
}
|
|
57
|
+
return [...seen.values()];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Merges `incoming` names into `existing`, preserving order and dropping duplicates.
|
|
61
|
+
*
|
|
62
|
+
* Shared by `combineExports` and `combineImports` for the same-path name-merge case.
|
|
63
|
+
*/
|
|
64
|
+
function mergeNameArrays(existing, incoming) {
|
|
65
|
+
const merged = new Set(existing);
|
|
66
|
+
for (const name of incoming) merged.add(name);
|
|
67
|
+
return [...merged];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Deduplicates and merges `ExportNode` objects by path and type.
|
|
71
|
+
*
|
|
72
|
+
* Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
|
|
73
|
+
* Non-array exports are deduplicated by exact identity. Returns a sorted, deduplicated array.
|
|
74
|
+
*/
|
|
75
|
+
function combineExports(exports) {
|
|
76
|
+
const result = [];
|
|
77
|
+
const namedByPath = /* @__PURE__ */ new Map();
|
|
78
|
+
const seen = /* @__PURE__ */ new Set();
|
|
79
|
+
const keyed = exports.map((node) => ({
|
|
80
|
+
node,
|
|
81
|
+
key: sortKey(node)
|
|
82
|
+
}));
|
|
83
|
+
keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
84
|
+
for (const { node: curr } of keyed) {
|
|
85
|
+
const { name, path, isTypeOnly, asAlias } = curr;
|
|
86
|
+
if (Array.isArray(name)) {
|
|
87
|
+
if (!name.length) continue;
|
|
88
|
+
const key = pathTypeKey(path, isTypeOnly);
|
|
89
|
+
const existing = namedByPath.get(key);
|
|
90
|
+
if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
|
|
91
|
+
else {
|
|
92
|
+
const newItem = {
|
|
93
|
+
...curr,
|
|
94
|
+
name: [...new Set(name)]
|
|
95
|
+
};
|
|
96
|
+
result.push(newItem);
|
|
97
|
+
namedByPath.set(key, newItem);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
const key = exportKey(path, name, isTypeOnly, asAlias);
|
|
101
|
+
if (!seen.has(key)) {
|
|
102
|
+
result.push(curr);
|
|
103
|
+
seen.add(key);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Deduplicates and merges `ImportNode` objects, filtering out unused imports.
|
|
111
|
+
*
|
|
112
|
+
* Retains imports that are referenced in `source` or re-exported. Imports with the same path and
|
|
113
|
+
* `isTypeOnly` flag have their names merged. Returns a sorted, deduplicated, filtered array.
|
|
114
|
+
*/
|
|
115
|
+
function combineImports(imports, exports, source) {
|
|
116
|
+
const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
|
|
117
|
+
const isUsed = (importName) => !source || source.includes(importName) || exportedNames.has(importName);
|
|
118
|
+
const importNameMemo = /* @__PURE__ */ new Map();
|
|
119
|
+
const canonicalizeName = (n) => {
|
|
120
|
+
if (typeof n === "string") return n;
|
|
121
|
+
const key = `${n.propertyName}:${n.name ?? ""}`;
|
|
122
|
+
if (!importNameMemo.has(key)) importNameMemo.set(key, n);
|
|
123
|
+
return importNameMemo.get(key);
|
|
124
|
+
};
|
|
125
|
+
const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
|
|
126
|
+
for (const node of imports) {
|
|
127
|
+
if (!Array.isArray(node.name)) continue;
|
|
128
|
+
if (node.name.some((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName))) pathsWithUsedNamedImport.add(node.path);
|
|
129
|
+
}
|
|
130
|
+
const result = [];
|
|
131
|
+
const namedByPath = /* @__PURE__ */ new Map();
|
|
132
|
+
const seen = /* @__PURE__ */ new Set();
|
|
133
|
+
const keyed = imports.map((node) => ({
|
|
134
|
+
node,
|
|
135
|
+
key: sortKey(node)
|
|
136
|
+
}));
|
|
137
|
+
keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
138
|
+
for (const { node: curr } of keyed) {
|
|
139
|
+
if (curr.path === curr.root) continue;
|
|
140
|
+
const { path, isTypeOnly } = curr;
|
|
141
|
+
let { name } = curr;
|
|
142
|
+
if (Array.isArray(name)) {
|
|
143
|
+
name = [...new Set(name.map(canonicalizeName))].filter((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName));
|
|
144
|
+
if (!name.length) continue;
|
|
145
|
+
const key = pathTypeKey(path, isTypeOnly);
|
|
146
|
+
const existing = namedByPath.get(key);
|
|
147
|
+
if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
|
|
148
|
+
else {
|
|
149
|
+
const newItem = {
|
|
150
|
+
...curr,
|
|
151
|
+
name
|
|
152
|
+
};
|
|
153
|
+
result.push(newItem);
|
|
154
|
+
namedByPath.set(key, newItem);
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
if (name && !isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
|
|
158
|
+
const key = importKey(path, name, isTypeOnly);
|
|
159
|
+
if (!seen.has(key)) {
|
|
160
|
+
result.push(curr);
|
|
161
|
+
seen.add(key);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/factory.ts
|
|
169
|
+
var factory_exports = /* @__PURE__ */ require_response.__exportAll({
|
|
170
|
+
createArrowFunction: () => require_response.createArrowFunction,
|
|
171
|
+
createBreak: () => require_response.createBreak,
|
|
172
|
+
createConst: () => require_response.createConst,
|
|
173
|
+
createContent: () => require_response.createContent,
|
|
174
|
+
createExport: () => require_response.createExport,
|
|
175
|
+
createFile: () => createFile,
|
|
176
|
+
createFunction: () => require_response.createFunction,
|
|
177
|
+
createFunctionParameter: () => require_response.createFunctionParameter,
|
|
178
|
+
createFunctionParameters: () => require_response.createFunctionParameters,
|
|
179
|
+
createImport: () => require_response.createImport,
|
|
180
|
+
createIndexedAccessType: () => require_response.createIndexedAccessType,
|
|
181
|
+
createInput: () => require_response.createInput,
|
|
182
|
+
createJsx: () => require_response.createJsx,
|
|
183
|
+
createObjectBindingPattern: () => require_response.createObjectBindingPattern,
|
|
184
|
+
createOperation: () => require_response.createOperation,
|
|
185
|
+
createOutput: () => require_response.createOutput,
|
|
186
|
+
createParameter: () => require_response.createParameter,
|
|
187
|
+
createProperty: () => require_response.createProperty,
|
|
188
|
+
createRequestBody: () => require_response.createRequestBody,
|
|
189
|
+
createResponse: () => require_response.createResponse,
|
|
190
|
+
createSchema: () => require_response.createSchema,
|
|
191
|
+
createSource: () => require_response.createSource,
|
|
192
|
+
createText: () => require_response.createText,
|
|
193
|
+
createType: () => require_response.createType,
|
|
194
|
+
createTypeLiteral: () => require_response.createTypeLiteral,
|
|
195
|
+
update: () => update
|
|
196
|
+
});
|
|
197
|
+
/**
|
|
198
|
+
* Identity-preserving node update: returns `node` unchanged when every field in
|
|
199
|
+
* `changes` already equals (by reference) the current value, otherwise a new node
|
|
200
|
+
* with the changes applied.
|
|
201
|
+
*
|
|
202
|
+
* Mirrors the TypeScript compiler's `factory.updateX` contract, pair it with the
|
|
203
|
+
* structural sharing in {@link transform} so a no-op rewrite doesn't allocate and
|
|
204
|
+
* downstream passes can detect "nothing changed" by identity. Comparison is
|
|
205
|
+
* shallow: a structurally-equal but newly-allocated array/object counts as a change.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```ts
|
|
209
|
+
* update(node, { name: node.name }) // -> same `node` reference
|
|
210
|
+
* update(node, { name: 'renamed' }) // -> new node, `name` replaced
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
function update(node, changes) {
|
|
214
|
+
for (const key in changes) if (changes[key] !== node[key]) return {
|
|
215
|
+
...node,
|
|
216
|
+
...changes
|
|
217
|
+
};
|
|
218
|
+
return node;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Creates a fully resolved `FileNode` from a file input descriptor.
|
|
222
|
+
*
|
|
223
|
+
* Computes:
|
|
224
|
+
* - `id` SHA256 hash of the file path
|
|
225
|
+
* - `name` `baseName` without extension
|
|
226
|
+
* - `extname` extension extracted from `baseName`
|
|
227
|
+
*
|
|
228
|
+
* Deduplicates:
|
|
229
|
+
* - `sources` via `combineSources`
|
|
230
|
+
* - `exports` via `combineExports`
|
|
231
|
+
* - `imports` via `combineImports` (also filters unused imports)
|
|
232
|
+
*
|
|
233
|
+
* @throws {Error} when `baseName` has no extension.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```ts
|
|
237
|
+
* const file = createFile({
|
|
238
|
+
* baseName: 'petStore.ts',
|
|
239
|
+
* path: 'src/models/petStore.ts',
|
|
240
|
+
* sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],
|
|
241
|
+
* imports: [createImport({ name: ['z'], path: 'zod' })],
|
|
242
|
+
* exports: [createExport({ name: ['Pet'], path: './petStore' })],
|
|
243
|
+
* })
|
|
244
|
+
* // file.id = SHA256 hash of 'src/models/petStore.ts'
|
|
245
|
+
* // file.name = 'petStore'
|
|
246
|
+
* // file.extname = '.ts'
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
function createFile(input) {
|
|
250
|
+
const extname = node_path.default.extname(input.baseName) || (input.baseName.startsWith(".") ? input.baseName : "");
|
|
251
|
+
if (!extname) throw new Error(`No extname found for ${input.baseName}`);
|
|
252
|
+
const source = (input.sources ?? []).flatMap((item) => item.nodes ?? []).map((node) => require_response.extractStringsFromNodes([node])).filter(Boolean).join("\n\n");
|
|
253
|
+
const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
|
|
254
|
+
const combinedImports = input.imports?.length ? combineImports(input.imports, resolvedExports, source || void 0) : [];
|
|
255
|
+
const localNames = new Set((input.sources ?? []).map((item) => item.name).filter((name) => Boolean(name)));
|
|
256
|
+
const nameOf = (item) => typeof item === "string" ? item : item.name ?? item.propertyName;
|
|
257
|
+
const resolvedImports = combinedImports.filter((imp) => imp.path !== input.path).flatMap((imp) => {
|
|
258
|
+
if (!Array.isArray(imp.name)) return typeof imp.name === "string" && localNames.has(imp.name) ? [] : [imp];
|
|
259
|
+
const kept = imp.name.filter((item) => !localNames.has(nameOf(item)));
|
|
260
|
+
if (!kept.length) return [];
|
|
261
|
+
return [kept.length === imp.name.length ? imp : {
|
|
262
|
+
...imp,
|
|
263
|
+
name: kept
|
|
264
|
+
}];
|
|
265
|
+
});
|
|
266
|
+
const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
|
|
267
|
+
return {
|
|
268
|
+
kind: "File",
|
|
269
|
+
...input,
|
|
270
|
+
id: (0, node_crypto.hash)("sha256", input.path, "hex"),
|
|
271
|
+
name: trimExtName(input.baseName),
|
|
272
|
+
extname,
|
|
273
|
+
imports: resolvedImports,
|
|
274
|
+
exports: resolvedExports,
|
|
275
|
+
sources: resolvedSources,
|
|
276
|
+
meta: input.meta ?? {}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
Object.defineProperty(exports, "createFile", {
|
|
281
|
+
enumerable: true,
|
|
282
|
+
get: function() {
|
|
283
|
+
return createFile;
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
Object.defineProperty(exports, "factory_exports", {
|
|
287
|
+
enumerable: true,
|
|
288
|
+
get: function() {
|
|
289
|
+
return factory_exports;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
Object.defineProperty(exports, "update", {
|
|
293
|
+
enumerable: true,
|
|
294
|
+
get: function() {
|
|
295
|
+
return update;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
//# sourceMappingURL=factory-Cl8Z7mcc.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory-Cl8Z7mcc.cjs","names":["extractStringsFromNodes","path","extractStringsFromNodes"],"sources":["../../../internals/utils/src/fs.ts","../src/utils/fileMerge.ts","../src/factory.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises'\nimport { dirname, join, posix, resolve } from 'node:path'\nimport { camelCase } from './casing.ts'\nimport { runtime } from './runtime.ts'\n\n/**\n * Walks up the directory tree from `cwd` (defaults to `process.cwd()`) and\n * returns the absolute path of the nearest `package.json`, or `null` when none\n * is found before reaching the filesystem root.\n *\n * @example\n * ```ts\n * const pkgPath = findPackageJSON('/home/user/project/src') // '/home/user/project/package.json'\n * ```\n */\nexport function findPackageJSON(cwd?: string): string | null {\n let dir = cwd ? resolve(cwd) : process.cwd()\n while (true) {\n const pkgPath = join(dir, 'package.json')\n if (existsSync(pkgPath)) return pkgPath\n const parent = dirname(dir)\n if (parent === dir) return null\n dir = parent\n }\n}\n\n/**\n * Converts all backslashes to forward slashes.\n * Extended-length Windows paths (`\\\\?\\...`) are left unchanged.\n */\nfunction toSlash(p: string): string {\n if (p.startsWith('\\\\\\\\?\\\\')) return p\n\n return p.replaceAll('\\\\', '/')\n}\n\n/**\n * Returns the relative path from `rootDir` to `filePath`, always using forward slashes\n * and prefixed with `./` when not already traversing upward.\n *\n * @example\n * ```ts\n * getRelativePath('/src/components', '/src/components/Button.tsx') // './Button.tsx'\n * getRelativePath('/src/components', '/src/utils/helpers.ts') // '../utils/helpers.ts'\n * ```\n */\nexport function getRelativePath(rootDir?: string | null, filePath?: string | null): string {\n if (!rootDir || !filePath) {\n throw new Error(`Root and file should be filled in when retrieving the relativePath, ${rootDir || ''} ${filePath || ''}`)\n }\n\n const relativePath = posix.relative(toSlash(rootDir), toSlash(filePath))\n\n return relativePath.startsWith('../') ? relativePath : `./${relativePath}`\n}\n\n/**\n * Resolves to `true` when the file or directory at `path` exists.\n * Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.\n *\n * @example\n * ```ts\n * if (await exists('./kubb.config.ts')) {\n * const content = await read('./kubb.config.ts')\n * }\n * ```\n */\nexport async function exists(path: string): Promise<boolean> {\n if (runtime.isBun) {\n return Bun.file(path).exists()\n }\n return access(path).then(\n () => true,\n () => false,\n )\n}\n\n/**\n * Reads the file at `path` as a UTF-8 string.\n * Uses `Bun.file().text()` when running under Bun, `fs.readFile` otherwise.\n *\n * @example\n * ```ts\n * const source = await read('./src/Pet.ts')\n * ```\n */\nexport async function read(path: string): Promise<string> {\n if (runtime.isBun) {\n return Bun.file(path).text()\n }\n return readFile(path, { encoding: 'utf8' })\n}\n\ntype WriteOptions = {\n /**\n * When `true`, re-reads the file immediately after writing and throws if the\n * content does not match — useful for catching write failures on unreliable file systems.\n */\n sanity?: boolean\n}\n\n/**\n * Writes `data` to `path`, trimming leading/trailing whitespace before saving.\n * Skips the write when the trimmed content is empty or identical to what is already on disk.\n * Creates any missing parent directories automatically.\n * When `sanity` is `true`, re-reads the file after writing and throws if the content does not match.\n *\n * @example\n * ```ts\n * await write('./src/Pet.ts', source) // writes and returns trimmed content\n * await write('./src/Pet.ts', source) // null — file unchanged\n * await write('./src/Pet.ts', ' ') // null — empty content skipped\n * ```\n */\nexport async function write(path: string, data: string, options: WriteOptions = {}): Promise<string | null> {\n const trimmed = data.trim()\n if (trimmed === '') return null\n\n const resolved = resolve(path)\n\n if (runtime.isBun) {\n const file = Bun.file(resolved)\n const oldContent = (await file.exists()) ? await file.text() : null\n if (oldContent === trimmed) return null\n await Bun.write(resolved, trimmed)\n return trimmed\n }\n\n try {\n const oldContent = await readFile(resolved, { encoding: 'utf-8' })\n if (oldContent === trimmed) return null\n } catch {\n /* file doesn't exist yet */\n }\n\n await mkdir(dirname(resolved), { recursive: true })\n await writeFile(resolved, trimmed, { encoding: 'utf-8' })\n\n if (options.sanity) {\n const savedData = await readFile(resolved, { encoding: 'utf-8' })\n if (savedData !== trimmed) {\n throw new Error(`Sanity check failed for ${path}\\n\\nData[${data.length}]:\\n${data}\\n\\nSaved[${savedData.length}]:\\n${savedData}\\n`)\n }\n return savedData\n }\n\n return trimmed\n}\n\n/**\n * Recursively removes `path`. Silently succeeds when `path` does not exist.\n *\n * @example\n * ```ts\n * await clean('./dist')\n * ```\n */\nexport async function clean(path: string): Promise<void> {\n return rm(path, { recursive: true, force: true })\n}\n\n/**\n * Converts a filesystem path to use POSIX (`/`) separators.\n *\n * Most of the codebase compares and composes paths as strings (prefix matching, joining for\n * import specifiers, splitting on `/`). On POSIX `path.resolve` already returns `/`-separated\n * paths, but on Windows it returns `\\`-separated paths, which breaks every such comparison.\n *\n * Routing every path that crosses a module boundary through `toPosixPath` keeps the rest of the\n * code platform-agnostic. The conversion runs unconditionally so Windows-specific behavior is\n * exercisable from POSIX CI.\n *\n * @example\n * toPosixPath('C:\\\\repo\\\\src\\\\pet.ts') // 'C:/repo/src/pet.ts'\n */\nexport function toPosixPath(filePath: string): string {\n return filePath.replaceAll('\\\\', '/')\n}\n\n/**\n * Strips the file extension from a path or file name.\n * Only removes the last `.ext` segment when the dot is not part of a directory name.\n *\n * @example\n * trimExtName('petStore.ts') // 'petStore'\n * trimExtName('/src/models/pet.ts') // '/src/models/pet'\n * trimExtName('/project.v2/gen/pet.ts') // '/project.v2/gen/pet'\n * trimExtName('noExtension') // 'noExtension'\n */\nexport function trimExtName(text: string): string {\n const dotIndex = text.lastIndexOf('.')\n if (dotIndex > 0 && !text.includes('/', dotIndex)) {\n return text.slice(0, dotIndex)\n }\n return text\n}\n\n/**\n * Builds a nested file path from a dotted name. Splits on dots that precede a letter\n * (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases\n * every earlier segment, applies `caseLast` to the final segment, and joins with `/`.\n *\n * Empty segments are dropped before joining. They arise when the name starts with a dot\n * followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to\n * an empty string). Without this a leading `/` would form, which `path.resolve` reads as an\n * absolute path, letting generated files escape the configured output directory.\n *\n * @example Nested path from a dotted name\n * `toFilePath('pet.petId') // 'pet/petId'`\n *\n * @example PascalCase the final segment\n * `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'`\n *\n * @example Suffix applied to the final segment only\n * `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`\n */\nexport function toFilePath(name: string, caseLast: (part: string) => string = camelCase): string {\n const parts = name.split(/\\.(?=[a-zA-Z])/)\n return parts\n .map((part, i) => (i === parts.length - 1 ? caseLast(part) : camelCase(part)))\n .filter(Boolean)\n .join('/')\n}\n","/**\n * File-member merging. `combineImports`, `combineExports`, and `combineSources` deduplicate and sort\n * the import, export, and source entries of one file, and drop imports nothing references. This works\n * on a file's members, not on schema content.\n *\n * For collapsing duplicate schema shapes by structural signature, see `dedupe.ts`.\n */\nimport type { ExportNode, ImportNode, SourceNode } from '../nodes/index.ts'\nimport { extractStringsFromNodes } from './extractStringsFromNodes.ts'\n\nfunction sourceKey(source: SourceNode): string {\n const nameKey = source.name ?? extractStringsFromNodes(source.nodes)\n return `${nameKey}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`\n}\n\nfunction pathTypeKey(path: string, isTypeOnly: boolean | null | undefined): string {\n return `${path}:${isTypeOnly ?? false}`\n}\n\nfunction exportKey(path: string, name: string | null | undefined, isTypeOnly: boolean | null | undefined, asAlias: boolean | null | undefined): string {\n return `${path}:${name ?? ''}:${isTypeOnly ?? false}:${asAlias ?? ''}`\n}\n\nfunction importKey(path: string, name: string | null | undefined, isTypeOnly: boolean | null | undefined): string {\n return `${path}:${name ?? ''}:${isTypeOnly ?? false}`\n}\n\n/**\n * Computes a multi-level sort key for exports and imports:\n * non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.\n */\nfunction sortKey(node: { name?: string | Array<unknown> | null; isTypeOnly?: boolean | null; path: string }): string {\n const isArray = Array.isArray(node.name) ? '1' : '0'\n const typeOnly = node.isTypeOnly ? '0' : '1'\n const hasName = node.name != null ? '1' : '0'\n const name = Array.isArray(node.name) ? node.name.toSorted().join('\\0') : (node.name ?? '')\n return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`\n}\n\n/**\n * Deduplicates and merges `SourceNode` objects by `name + isExportable + isTypeOnly`.\n *\n * Unnamed sources are deduplicated by object reference. Returns a deduplicated array in original order.\n */\nexport function combineSources(sources: Array<SourceNode>): Array<SourceNode> {\n const seen = new Map<string, SourceNode>()\n for (const source of sources) {\n const key = sourceKey(source)\n if (!seen.has(key)) seen.set(key, source)\n }\n return [...seen.values()]\n}\n\n/**\n * Merges `incoming` names into `existing`, preserving order and dropping duplicates.\n *\n * Shared by `combineExports` and `combineImports` for the same-path name-merge case.\n */\nfunction mergeNameArrays<TName>(existing: Array<TName>, incoming: Array<TName>): Array<TName> {\n const merged = new Set(existing)\n for (const name of incoming) merged.add(name)\n return [...merged]\n}\n\n/**\n * Deduplicates and merges `ExportNode` objects by path and type.\n *\n * Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.\n * Non-array exports are deduplicated by exact identity. Returns a sorted, deduplicated array.\n */\nexport function combineExports(exports: Array<ExportNode>): Array<ExportNode> {\n const result: Array<ExportNode> = []\n // Accumulates array-named exports keyed by `path:isTypeOnly` for name-merging\n const namedByPath = new Map<string, ExportNode>()\n // Deduplicates non-array exports by their exact identity\n const seen = new Set<string>()\n\n // Precompute sort keys once, avoids recomputing per comparison.\n const keyed = exports.map((node) => ({ node, key: sortKey(node) }))\n keyed.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))\n\n for (const { node: curr } of keyed) {\n const { name, path, isTypeOnly, asAlias } = curr\n\n if (Array.isArray(name)) {\n if (!name.length) continue\n\n const key = pathTypeKey(path, isTypeOnly)\n const existing = namedByPath.get(key)\n\n if (existing && Array.isArray(existing.name)) {\n existing.name = mergeNameArrays(existing.name, name)\n } else {\n const newItem: ExportNode = { ...curr, name: [...new Set(name)] }\n result.push(newItem)\n namedByPath.set(key, newItem)\n }\n } else {\n const key = exportKey(path, name, isTypeOnly, asAlias)\n if (!seen.has(key)) {\n result.push(curr)\n seen.add(key)\n }\n }\n }\n\n return result\n}\n\n/**\n * Deduplicates and merges `ImportNode` objects, filtering out unused imports.\n *\n * Retains imports that are referenced in `source` or re-exported. Imports with the same path and\n * `isTypeOnly` flag have their names merged. Returns a sorted, deduplicated, filtered array.\n */\nexport function combineImports(imports: Array<ImportNode>, exports: Array<ExportNode>, source?: string): Array<ImportNode> {\n // Build a lookup of all exported names to retain imports that are re-exported\n const exportedNames = new Set(exports.flatMap((e) => (Array.isArray(e.name) ? e.name : e.name ? [e.name] : [])))\n const isUsed = (importName: string): boolean => !source || source.includes(importName) || exportedNames.has(importName)\n\n // Memoize object import names so the same logical (propertyName, name) pair always\n // reuses the same object reference. Set-based deduplication then works correctly.\n const importNameMemo = new Map<string, { propertyName: string; name?: string }>()\n const canonicalizeName = (n: string | { propertyName: string; name?: string }): string | { propertyName: string; name?: string } => {\n if (typeof n === 'string') return n\n const key = `${n.propertyName}:${n.name ?? ''}`\n if (!importNameMemo.has(key)) importNameMemo.set(key, n)\n return importNameMemo.get(key)!\n }\n\n // Paths that keep at least one used named import. A default import from such a path is retained\n // even when its binding can't be found in `source` e.g. a generated `client` default import\n // alongside `import type { Client } from <same path>`, where merged grouped output omits the body.\n const pathsWithUsedNamedImport = new Set<string>()\n for (const node of imports) {\n if (!Array.isArray(node.name)) continue\n if (node.name.some((item) => (typeof item === 'string' ? isUsed(item) : isUsed(item.name ?? item.propertyName)))) {\n pathsWithUsedNamedImport.add(node.path)\n }\n }\n\n const result: Array<ImportNode> = []\n // Accumulates array-named imports keyed by `path:isTypeOnly` for name-merging\n const namedByPath = new Map<string, ImportNode>()\n // Deduplicates non-array imports by their exact identity\n const seen = new Set<string>()\n\n // Precompute sort keys once, avoids recomputing per comparison.\n const keyed = imports.map((node) => ({ node, key: sortKey(node) }))\n keyed.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))\n\n for (const { node: curr } of keyed) {\n if (curr.path === curr.root) continue\n\n const { path, isTypeOnly } = curr\n let { name } = curr\n\n if (Array.isArray(name)) {\n name = [...new Set(name.map(canonicalizeName))].filter((item) => (typeof item === 'string' ? isUsed(item) : isUsed(item.name ?? item.propertyName)))\n if (!name.length) continue\n\n const key = pathTypeKey(path, isTypeOnly)\n const existing = namedByPath.get(key)\n\n if (existing && Array.isArray(existing.name)) {\n existing.name = mergeNameArrays(existing.name, name)\n } else {\n const newItem: ImportNode = { ...curr, name }\n result.push(newItem)\n namedByPath.set(key, newItem)\n }\n } else {\n if (name && !isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue\n\n const key = importKey(path, name, isTypeOnly)\n if (!seen.has(key)) {\n result.push(curr)\n seen.add(key)\n }\n }\n }\n\n return result\n}\n","import { hash } from 'node:crypto'\nimport path from 'node:path'\nimport { trimExtName } from '@internals/utils'\nimport type { FileNode, Node } from './nodes/index.ts'\nimport { extractStringsFromNodes } from './utils/extractStringsFromNodes.ts'\nimport { combineExports, combineImports, combineSources } from './utils/fileMerge.ts'\n\n// Node constructors, grouped under the `factory` namespace the way the TypeScript compiler exposes\n// `ts.factory.createX`. Aggregating them here lets `export * as factory from './factory.ts'` in the\n// barrel surface every `createX` alongside the `createFile`/`update` helpers from a single module.\nexport { createArrowFunction, createBreak, createConst, createFunction, createJsx, createText, createType } from './nodes/code.ts'\nexport { createContent } from './nodes/content.ts'\nexport { createExport, createImport, createSource } from './nodes/file.ts'\nexport { createFunctionParameter, createFunctionParameters, createIndexedAccessType, createObjectBindingPattern, createTypeLiteral } from './nodes/function.ts'\nexport { createInput } from './nodes/input.ts'\nexport { createOperation } from './nodes/operation.ts'\nexport { createOutput } from './nodes/output.ts'\nexport { createParameter } from './nodes/parameter.ts'\nexport { createProperty } from './nodes/property.ts'\nexport { createRequestBody } from './nodes/requestBody.ts'\nexport { createResponse } from './nodes/response.ts'\nexport { createSchema } from './nodes/schema.ts'\n\n/**\n * Identity-preserving node update: returns `node` unchanged when every field in\n * `changes` already equals (by reference) the current value, otherwise a new node\n * with the changes applied.\n *\n * Mirrors the TypeScript compiler's `factory.updateX` contract, pair it with the\n * structural sharing in {@link transform} so a no-op rewrite doesn't allocate and\n * downstream passes can detect \"nothing changed\" by identity. Comparison is\n * shallow: a structurally-equal but newly-allocated array/object counts as a change.\n *\n * @example\n * ```ts\n * update(node, { name: node.name }) // -> same `node` reference\n * update(node, { name: 'renamed' }) // -> new node, `name` replaced\n * ```\n */\nexport function update<T extends Node>(node: T, changes: Partial<T>): T {\n for (const key in changes) {\n if (changes[key] !== node[key as keyof T]) {\n return { ...node, ...changes }\n }\n }\n\n return node\n}\n\n/**\n * Input descriptor for {@link createFile}, before `id`, `name`, and `extname` are computed\n * and `imports`/`exports`/`sources` are deduplicated.\n */\nexport type UserFileNode<TMeta extends object = object> = Omit<FileNode<TMeta>, 'kind' | 'id' | 'name' | 'extname' | 'imports' | 'exports' | 'sources'> &\n Pick<Partial<FileNode<TMeta>>, 'imports' | 'exports' | 'sources'>\n\n/**\n * Creates a fully resolved `FileNode` from a file input descriptor.\n *\n * Computes:\n * - `id` SHA256 hash of the file path\n * - `name` `baseName` without extension\n * - `extname` extension extracted from `baseName`\n *\n * Deduplicates:\n * - `sources` via `combineSources`\n * - `exports` via `combineExports`\n * - `imports` via `combineImports` (also filters unused imports)\n *\n * @throws {Error} when `baseName` has no extension.\n *\n * @example\n * ```ts\n * const file = createFile({\n * baseName: 'petStore.ts',\n * path: 'src/models/petStore.ts',\n * sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],\n * imports: [createImport({ name: ['z'], path: 'zod' })],\n * exports: [createExport({ name: ['Pet'], path: './petStore' })],\n * })\n * // file.id = SHA256 hash of 'src/models/petStore.ts'\n * // file.name = 'petStore'\n * // file.extname = '.ts'\n * ```\n */\nexport function createFile<TMeta extends object = object>(input: UserFileNode<TMeta>): FileNode<TMeta> {\n const rawExtname = path.extname(input.baseName)\n // Handle dotfile basename like '.ts' where path.extname returns ''\n const extname = (rawExtname || (input.baseName.startsWith('.') ? input.baseName : '')) as `.${string}`\n if (!extname) {\n throw new Error(`No extname found for ${input.baseName}`)\n }\n\n const source = (input.sources ?? [])\n .flatMap((item) => item.nodes ?? [])\n .map((node) => extractStringsFromNodes([node]))\n .filter(Boolean)\n .join('\\n\\n')\n const resolvedExports = input.exports?.length ? combineExports(input.exports) : []\n const combinedImports = input.imports?.length ? combineImports(input.imports, resolvedExports, source || undefined) : []\n const localNames = new Set((input.sources ?? []).map((item) => item.name).filter((name): name is string => Boolean(name)))\n const nameOf = (item: string | { propertyName: string; name?: string }): string => (typeof item === 'string' ? item : (item.name ?? item.propertyName))\n // Drop self-imports. Consolidating output (`mode: 'file'`) can place a symbol's\n // definition and a cross-file import of it in the same file. The first pass catches imports that\n // resolve to this file's own path. The second drops imports of names the file already defines,\n // the case consolidation produces when the import path no longer matches `input.path`. Sources\n // stay intact, so the local definition remains. Bare specifiers like `'zod'` never match a path.\n const resolvedImports = combinedImports\n .filter((imp) => imp.path !== input.path)\n .flatMap((imp) => {\n if (!Array.isArray(imp.name)) {\n return typeof imp.name === 'string' && localNames.has(imp.name) ? [] : [imp]\n }\n const kept = imp.name.filter((item) => !localNames.has(nameOf(item)))\n if (!kept.length) return []\n return [kept.length === imp.name.length ? imp : { ...imp, name: kept }]\n })\n const resolvedSources = input.sources?.length ? combineSources(input.sources) : []\n\n return {\n kind: 'File',\n ...input,\n id: hash('sha256', input.path, 'hex'),\n name: trimExtName(input.baseName),\n extname,\n imports: resolvedImports,\n exports: resolvedExports,\n sources: resolvedSources,\n meta: input.meta ?? ({} as TMeta),\n }\n}\n"],"mappings":"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"}
|