@kubb/ast 5.0.0-beta.57 → 5.0.0-beta.59

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.
Files changed (69) hide show
  1. package/README.md +21 -7
  2. package/dist/casing-BE2R1RXg.cjs +88 -0
  3. package/dist/casing-BE2R1RXg.cjs.map +1 -0
  4. package/dist/chunk-CNktS9qV.js +17 -0
  5. package/dist/extractStringsFromNodes-WMYJ8nQL.d.ts +82 -0
  6. package/dist/factory-BmcGBdeg.cjs +1251 -0
  7. package/dist/factory-BmcGBdeg.cjs.map +1 -0
  8. package/dist/factory-Du7nEP4B.js +282 -0
  9. package/dist/factory-Du7nEP4B.js.map +1 -0
  10. package/dist/factory.cjs +28 -0
  11. package/dist/factory.d.ts +62 -0
  12. package/dist/factory.js +3 -0
  13. package/dist/{types-C5aVnRE1.d.ts → index-BzjwdK2M.d.ts} +94 -1146
  14. package/dist/index.cjs +220 -1481
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.ts +96 -58
  17. package/dist/index.js +182 -1920
  18. package/dist/index.js.map +1 -1
  19. package/dist/operationParams-BZ07xDm0.d.ts +204 -0
  20. package/dist/response-DKxTr522.js +683 -0
  21. package/dist/response-DKxTr522.js.map +1 -0
  22. package/dist/types-olVl9v5p.d.ts +764 -0
  23. package/dist/types.d.ts +5 -2
  24. package/dist/utils-BCtRXfhI.cjs +275 -0
  25. package/dist/utils-BCtRXfhI.cjs.map +1 -0
  26. package/dist/utils-SdZU0F3H.js +1336 -0
  27. package/dist/utils-SdZU0F3H.js.map +1 -0
  28. package/dist/utils.cjs +129 -13
  29. package/dist/utils.d.ts +127 -76
  30. package/dist/utils.js +3 -2
  31. package/package.json +5 -1
  32. package/src/constants.ts +8 -14
  33. package/src/dedupe.ts +8 -0
  34. package/src/factory.ts +18 -1
  35. package/src/guards.ts +1 -1
  36. package/src/index.ts +7 -50
  37. package/src/node.ts +1 -1
  38. package/src/nodes/base.ts +0 -10
  39. package/src/nodes/code.ts +29 -47
  40. package/src/nodes/content.ts +2 -2
  41. package/src/nodes/file.ts +28 -23
  42. package/src/nodes/function.ts +2 -17
  43. package/src/nodes/index.ts +1 -1
  44. package/src/nodes/input.ts +20 -19
  45. package/src/nodes/operation.ts +1 -1
  46. package/src/nodes/parameter.ts +0 -3
  47. package/src/nodes/property.ts +0 -3
  48. package/src/nodes/requestBody.ts +3 -6
  49. package/src/nodes/schema.ts +3 -2
  50. package/src/printer.ts +4 -4
  51. package/src/registry.ts +31 -26
  52. package/src/signature.ts +3 -3
  53. package/src/transformers.ts +28 -1
  54. package/src/types.ts +2 -53
  55. package/src/utils/codegen.ts +104 -0
  56. package/src/utils/extractStringsFromNodes.ts +34 -0
  57. package/src/utils/fileMerge.ts +184 -0
  58. package/src/utils/index.ts +8 -296
  59. package/src/utils/operationParams.ts +353 -0
  60. package/src/utils/refs.ts +112 -0
  61. package/src/utils/schemaGraph.ts +169 -0
  62. package/src/utils/strings.ts +139 -0
  63. package/src/visitor.ts +43 -19
  64. package/dist/chunk-C0LytTxp.js +0 -8
  65. package/dist/utils-0p8ZO287.js +0 -570
  66. package/dist/utils-0p8ZO287.js.map +0 -1
  67. package/dist/utils-cdQ6Pzyi.cjs +0 -726
  68. package/dist/utils-cdQ6Pzyi.cjs.map +0 -1
  69. package/src/utils/ast.ts +0 -879
@@ -35,11 +35,11 @@ export type InputMeta = {
35
35
  baseURL?: string | null
36
36
  /**
37
37
  * Names of schemas that participate in a circular reference chain.
38
- * Computed once during the adapter pre-scan, use this instead of calling
39
- * `findCircularSchemas` per generator call.
38
+ * Computed once during the adapter pre-scan, so a generator never has to
39
+ * call `findCircularSchemas` itself.
40
40
  *
41
41
  * Convert to a `Set` once at the start of a generator, not per-schema,
42
- * to keep lookup O(1) without repeated allocations.
42
+ * so lookups stay O(1) without repeated allocations.
43
43
  *
44
44
  * @example Wrap a circular schema in z.lazy()
45
45
  * ```ts
@@ -50,11 +50,11 @@ export type InputMeta = {
50
50
  circularNames: ReadonlyArray<string>
51
51
  /**
52
52
  * Names of schemas whose type is `enum`.
53
- * Computed once during the adapter pre-scan, use this instead of filtering
54
- * schemas per generator call.
53
+ * Computed once during the adapter pre-scan, so a generator never has to
54
+ * filter the schema list itself.
55
55
  *
56
56
  * Convert to a `Set` once at the start of a generator when you need repeated
57
- * membership checks, rather than calling `.includes()` per schema.
57
+ * membership checks, so each check stays O(1) instead of an array scan.
58
58
  *
59
59
  * @example Check if a referenced schema is an enum
60
60
  * `const enums = new Set(meta.enumNames)`
@@ -114,26 +114,27 @@ export const inputDef = defineNode<InputNode, Partial<Omit<InputNode, 'kind'>>>(
114
114
  })
115
115
 
116
116
  /**
117
- * Creates an `InputNode` with stable defaults for `schemas` and `operations`.
117
+ * Creates an `InputNode`. Pass `stream: true` for the streaming variant whose `schemas` and
118
+ * `operations` are `AsyncIterable` sources and whose `meta` is optional. Otherwise it builds the
119
+ * eager variant with array `schemas`/`operations` and the defaulted `meta`.
118
120
  *
119
- * @example
121
+ * @example Eager
120
122
  * ```ts
121
123
  * const input = createInput()
122
124
  * // { kind: 'Input', schemas: [], operations: [] }
123
125
  * ```
124
- */
125
- export function createInput(overrides: Partial<Omit<InputNode, 'kind'>> = {}): InputNode {
126
- return inputDef.create(overrides)
127
- }
128
-
129
- /**
130
- * Creates a streaming `InputNode<true>` from pre-built `AsyncIterable` sources.
131
126
  *
132
- * @example
127
+ * @example Streaming
133
128
  * ```ts
134
- * const node = createStreamInput(schemasIterable, operationsIterable, { title: 'My API' })
129
+ * const node = createInput({ stream: true, schemas: schemasIterable, operations: operationsIterable, meta: { title: 'My API' } })
135
130
  * ```
136
131
  */
137
- export function createStreamInput(schemas: AsyncIterable<SchemaNode>, operations: AsyncIterable<OperationNode>, meta?: InputMeta): InputNode<true> {
138
- return { kind: 'Input', schemas, operations, meta }
132
+ export function createInput<Stream extends boolean = false>(options: Partial<Omit<InputNode<Stream>, 'kind'>> & { stream?: Stream } = {}): InputNode<Stream> {
133
+ const { stream, ...overrides } = options
134
+ // Streaming inputs carry AsyncIterable sources, so skip the array/meta defaults that
135
+ // inputDef.create applies for the eager variant.
136
+ if (stream) {
137
+ return { kind: 'Input', ...overrides } as InputNode<Stream>
138
+ }
139
+ return inputDef.create(overrides as Partial<Omit<InputNode, 'kind'>>) as InputNode<Stream>
139
140
  }
@@ -41,7 +41,7 @@ type OperationNodeBase = BaseNode & {
41
41
  */
42
42
  deprecated?: boolean
43
43
  /**
44
- * Parameters that could be used, we have QueryParams, PathParams, HeaderParams and CookieParams
44
+ * Query, path, header, and cookie parameters for the operation.
45
45
  */
46
46
  parameters: Array<ParameterNode>
47
47
  /**
@@ -19,9 +19,6 @@ export type ParameterLocation = 'path' | 'query' | 'header' | 'cookie'
19
19
  * ```
20
20
  */
21
21
  export type ParameterNode = BaseNode & {
22
- /**
23
- * Node kind.
24
- */
25
22
  kind: 'Parameter'
26
23
  /**
27
24
  * Parameter name.
@@ -16,9 +16,6 @@ import type { SchemaNode } from './schema.ts'
16
16
  * ```
17
17
  */
18
18
  export type PropertyNode = BaseNode & {
19
- /**
20
- * Node kind.
21
- */
22
19
  kind: 'Property'
23
20
  /**
24
21
  * Property key.
@@ -18,12 +18,9 @@ import { type ContentNode, createContent, type UserContent } from './content.ts'
18
18
  * ```
19
19
  */
20
20
  export type RequestBodyNode = BaseNode & {
21
- /**
22
- * Node kind.
23
- */
24
21
  kind: 'RequestBody'
25
22
  /**
26
- * Human-readable request body description.
23
+ * Request body description carried over from the spec.
27
24
  */
28
25
  description?: string
29
26
  /**
@@ -32,11 +29,11 @@ export type RequestBodyNode = BaseNode & {
32
29
  */
33
30
  required?: boolean
34
31
  /**
35
- * All available content type entries for this request body.
32
+ * Content type entries for this request body.
36
33
  *
37
34
  * When the adapter `contentType` option is set, this array contains exactly one entry for
38
35
  * that content type. Otherwise it contains one entry per content type declared in the spec,
39
- * so that plugins can generate code for every variant (e.g. separate hooks for
36
+ * so plugins can generate code for every variant (for example, separate hooks for
40
37
  * `application/json` and `multipart/form-data`).
41
38
  */
42
39
  content?: Array<ContentNode>
@@ -280,8 +280,9 @@ export type UnionSchemaNode = CompositeSchemaNodeBase & {
280
280
  */
281
281
  discriminatorPropertyName?: string
282
282
  /**
283
- * Logical strategy applied to union members: 'one' means exactly one member must be valid (from `oneOf`),
284
- * 'any' means any number of members can be valid (from `anyOf`).
283
+ * How many union members must be valid.
284
+ * - `'one'`: exactly one member, from `oneOf`
285
+ * - `'any'`: any number of members, from `anyOf`
285
286
  */
286
287
  strategy?: 'one' | 'any'
287
288
  }
package/src/printer.ts CHANGED
@@ -106,7 +106,7 @@ export type Printer<T extends PrinterFactoryOptions = PrinterFactoryOptions> = {
106
106
  /**
107
107
  * Node-level dispatcher, converts a `SchemaNode` directly to `TOutput` using the `nodes` handlers.
108
108
  * Always dispatches through the `nodes` map. Never calls the `print` override.
109
- * Use this when you need the raw output (e.g. `ts.TypeNode`) without declaration wrapping.
109
+ * Reach for it when you need the raw output (e.g. `ts.TypeNode`) without declaration wrapping.
110
110
  */
111
111
  transform: (node: SchemaNode) => T['output'] | null
112
112
  /**
@@ -192,11 +192,11 @@ export function definePrinter<T extends PrinterFactoryOptions = PrinterFactoryOp
192
192
 
193
193
  /**
194
194
  * Generic printer-factory function used by `definePrinter` and `defineFunctionPrinter`.
195
- **
195
+ *
196
196
  * @example
197
197
  * ```ts
198
- * export const defineFunctionPrinter = createPrinterFactory<FunctionNode, FunctionNodeType, FunctionNodeByType>(
199
- * (node) => kindToHandlerKey[node.kind],
198
+ * export const defineFunctionPrinter = createPrinterFactory<FunctionParamNode, FunctionParamKind, Partial<Record<FunctionParamKind, FunctionParamNode>>>(
199
+ * (node) => node.kind,
200
200
  * )
201
201
  * ```
202
202
  */
package/src/registry.ts CHANGED
@@ -3,7 +3,6 @@ import { arrowFunctionDef, breakDef, constDef, functionDef, jsxDef, textDef, typ
3
3
  import { contentDef } from './nodes/content.ts'
4
4
  import { exportDef, fileDef, importDef, sourceDef } from './nodes/file.ts'
5
5
  import { functionParameterDef, functionParametersDef, indexedAccessTypeDef, objectBindingPatternDef, typeLiteralDef } from './nodes/function.ts'
6
- import type { Node, NodeKind } from './nodes/index.ts'
7
6
  import { inputDef } from './nodes/input.ts'
8
7
  import { operationDef } from './nodes/operation.ts'
9
8
  import { outputDef } from './nodes/output.ts'
@@ -13,9 +12,39 @@ import { requestBodyDef } from './nodes/requestBody.ts'
13
12
  import { responseDef } from './nodes/response.ts'
14
13
  import { schemaDef } from './nodes/schema.ts'
15
14
 
15
+ // Surface every def from one place so the package barrel re-exports them with `export * from './registry.ts'`.
16
+ // Adding a node means adding its `defineNode` to a `nodes/*.ts` file and listing it in `nodeDefs` below, nothing else.
17
+ export {
18
+ arrowFunctionDef,
19
+ breakDef,
20
+ constDef,
21
+ contentDef,
22
+ exportDef,
23
+ fileDef,
24
+ functionDef,
25
+ functionParameterDef,
26
+ functionParametersDef,
27
+ importDef,
28
+ indexedAccessTypeDef,
29
+ inputDef,
30
+ jsxDef,
31
+ objectBindingPatternDef,
32
+ operationDef,
33
+ outputDef,
34
+ parameterDef,
35
+ propertyDef,
36
+ requestBodyDef,
37
+ responseDef,
38
+ schemaDef,
39
+ sourceDef,
40
+ textDef,
41
+ typeDef,
42
+ typeLiteralDef,
43
+ }
44
+
16
45
  /**
17
46
  * Every node definition. Adding a node means adding its `defineNode` to one
18
- * `nodes/*.ts` file and listing it here. The visitor tables below derive from it.
47
+ * `nodes/*.ts` file and listing it here. The visitor tables in `visitor.ts` derive from it.
19
48
  */
20
49
  export const nodeDefs = [
21
50
  inputDef,
@@ -44,27 +73,3 @@ export const nodeDefs = [
44
73
  sourceDef,
45
74
  fileDef,
46
75
  ] satisfies ReadonlyArray<NodeDef>
47
-
48
- /**
49
- * Child node fields per node kind, in traversal order (Babel's `VISITOR_KEYS`).
50
- * Derived from each definition's `children`.
51
- */
52
- export const VISITOR_KEYS = Object.fromEntries(nodeDefs.flatMap((def) => (def.children ? [[def.kind, def.children] as const] : []))) as Partial<
53
- Record<NodeKind, ReadonlyArray<string>>
54
- >
55
-
56
- /**
57
- * Maps a node kind to the matching visitor callback name. Derived from each
58
- * definition's `visitorKey`.
59
- */
60
- export const VISITOR_KEY_BY_KIND = Object.fromEntries(nodeDefs.flatMap((def) => (def.visitorKey ? [[def.kind, def.visitorKey] as const] : []))) as Partial<
61
- Record<NodeKind, NonNullable<NodeDef['visitorKey']>>
62
- >
63
-
64
- /**
65
- * Per-kind builders rerun after children are rebuilt. Derived from each
66
- * definition's `rebuild` flag.
67
- */
68
- export const nodeRebuilders = Object.fromEntries(
69
- nodeDefs.flatMap((def) => (def.rebuild ? [[def.kind, def.create as unknown as (node: Node) => Node] as const] : [])),
70
- ) as Partial<Record<NodeKind, (node: Node) => Node>>
package/src/signature.ts CHANGED
@@ -170,9 +170,9 @@ function describeShape(node: SchemaNode): string {
170
170
  }
171
171
 
172
172
  /**
173
- * Node digest cache, keyed by identity. A `WeakMap` so entries die with the node, and so a tree
174
- * hashed during dedupe planning is not walked again when it is rewritten during streaming. Reuse
175
- * is safe because a signature depends only on content, and nodes are immutable once created.
173
+ * Caches the digest per node, keyed by identity. A `WeakMap` so entries die with the node, and so
174
+ * a tree hashed during dedupe planning is not walked again when it is rewritten during streaming.
175
+ * Reuse is safe because a signature depends only on content, and nodes are immutable once created.
176
176
  */
177
177
  const signatureCache = new WeakMap<SchemaNode, string>()
178
178
 
@@ -2,7 +2,7 @@ import { isScalarPrimitive } from './constants.ts'
2
2
  import { narrowSchema } from './guards.ts'
3
3
  import { createProperty } from './nodes/property.ts'
4
4
  import { createSchema, type SchemaNode } from './nodes/schema.ts'
5
- import { enumPropName } from './utils/index.ts'
5
+ import { enumPropName } from './utils/refs.ts'
6
6
 
7
7
  /**
8
8
  * Replaces a discriminator property's schema with a string enum of allowed values.
@@ -162,3 +162,30 @@ export function setEnumName(propNode: SchemaNode, parentName: string | null | un
162
162
 
163
163
  return propNode
164
164
  }
165
+
166
+ /**
167
+ * Merges a ref node with its resolved schema, giving usage-site fields precedence.
168
+ *
169
+ * Usage-site fields (`description`, `readOnly`, `nullable`, `deprecated`) on the ref node override
170
+ * the same fields in the resolved `node.schema`. Non-ref nodes are returned unchanged.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * // Ref with description override
175
+ * const ref = createSchema({ type: 'ref', ref: '#/components/schemas/Pet', description: 'A cute pet' })
176
+ * const merged = syncSchemaRef(ref) // merges with resolved Pet schema
177
+ * ```
178
+ */
179
+ export function syncSchemaRef(node: SchemaNode): SchemaNode {
180
+ const ref = narrowSchema(node, 'ref')
181
+
182
+ if (!ref) return node
183
+ if (!ref.schema) return node
184
+
185
+ const { kind: _kind, type: _type, name: _name, ref: _ref, schema: _schema, ...overrides } = ref
186
+
187
+ // Filter out undefined override values so they don't shadow the resolved schema's fields.
188
+ const definedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined))
189
+
190
+ return createSchema({ ...ref.schema, ...definedOverrides })
191
+ }
package/src/types.ts CHANGED
@@ -2,59 +2,8 @@ export type { DedupeCanonical, DedupeLookups, DedupePlan } from './dedupe.ts'
2
2
  export type { SchemaDialect } from './dialect.ts'
3
3
  export type { DistributiveOmit } from './node.ts'
4
4
  export type { InferSchemaNode, ParserOptions } from './infer.ts'
5
- export type {
6
- ArraySchemaNode,
7
- ArrowFunctionNode,
8
- CodeNode,
9
- ConstNode,
10
- DateSchemaNode,
11
- DatetimeSchemaNode,
12
- EnumSchemaNode,
13
- ExportNode,
14
- FileNode,
15
- FunctionNode,
16
- FunctionNodeType,
17
- FunctionParameterNode,
18
- FunctionParametersNode,
19
- FunctionParamNode,
20
- HttpMethod,
21
- HttpOperationNode,
22
- ImportNode,
23
- IndexedAccessTypeNode,
24
- InputMeta,
25
- InputNode,
26
- IntersectionSchemaNode,
27
- JSDocNode,
28
- JsxNode,
29
- Node,
30
- NodeKind,
31
- NumberSchemaNode,
32
- ObjectBindingPatternNode,
33
- ObjectSchemaNode,
34
- OperationNode,
35
- OutputNode,
36
- ParameterLocation,
37
- ParameterNode,
38
- PrimitiveSchemaType,
39
- PropertyNode,
40
- RefSchemaNode,
41
- ResponseNode,
42
- ScalarSchemaType,
43
- SchemaNode,
44
- SchemaNodeByType,
45
- SchemaType,
46
- SourceNode,
47
- StatusCode,
48
- StringSchemaNode,
49
- TextNode,
50
- TimeSchemaNode,
51
- TypeExpression,
52
- TypeLiteralNode,
53
- TypeNode,
54
- UnionSchemaNode,
55
- UrlSchemaNode,
56
- } from './nodes/index.ts'
5
+ export type * from './nodes/index.ts'
57
6
  export type { ParentOf, Visitor, VisitorContext } from './visitor.ts'
58
7
  export type { Printer, PrinterFactoryOptions, PrinterPartial } from './printer.ts'
59
- export type { OperationParamsResolver } from './utils/ast.ts'
8
+ export type { OperationParamsResolver } from './utils/operationParams.ts'
60
9
  export type { UserFileNode } from './factory.ts'
@@ -0,0 +1,104 @@
1
+ import { isIdentifier, singleQuote } from '@internals/utils'
2
+ import { INDENT } from '../constants.ts'
3
+
4
+ /**
5
+ * Builds a JSDoc comment block from an array of lines, returning `fallback` when there are no
6
+ * comments so callers always get a usable string.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * buildJSDoc(['@type string', '@example hello'])
11
+ * // '/**\n * @type string\n * @example hello\n *\/\n '
12
+ * ```
13
+ */
14
+ export function buildJSDoc(
15
+ comments: Array<string>,
16
+ options: {
17
+ /**
18
+ * String used to indent each comment line.
19
+ * @default ' * '
20
+ */
21
+ indent?: string
22
+ /**
23
+ * String appended after the closing tag.
24
+ * @default '\n '
25
+ */
26
+ suffix?: string
27
+ /**
28
+ * Returned as-is when `comments` is empty.
29
+ * @default ' '
30
+ */
31
+ fallback?: string
32
+ } = {},
33
+ ): string {
34
+ const { indent = ' * ', suffix = '\n ', fallback = ' ' } = options
35
+
36
+ if (comments.length === 0) return fallback
37
+
38
+ return `/**\n${comments.map((c) => `${indent}${c}`).join('\n')}\n */${suffix}`
39
+ }
40
+
41
+ /**
42
+ * Indents every non-empty line of `text` by one indent level, leaving blank lines empty.
43
+ */
44
+ function indentLines(text: string): string {
45
+ if (!text) return ''
46
+ return text
47
+ .split('\n')
48
+ .map((line) => (line.trim() ? `${INDENT}${line}` : ''))
49
+ .join('\n')
50
+ }
51
+
52
+ /**
53
+ * Renders an object key, quoting it with single quotes only when it is not a valid identifier.
54
+ * Reserved words and globals (`name`, `class`, …) are valid bare keys and stay unquoted.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * objectKey('name') // 'name'
59
+ * objectKey('x-total') // "'x-total'"
60
+ * ```
61
+ */
62
+ export function objectKey(name: string): string {
63
+ return isIdentifier(name) ? name : singleQuote(name)
64
+ }
65
+
66
+ /**
67
+ * Assembles a multi-line object literal from already-rendered `entries`, indenting each entry one
68
+ * level and closing the brace at column zero. Nested objects built the same way indent cumulatively,
69
+ * so callers never re-parse the generated code. A trailing comma is added per entry to match the
70
+ * formatter's multi-line style.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * buildObject(['id: z.number()', 'name: z.string()'])
75
+ * // '{\n id: z.number(),\n name: z.string(),\n}'
76
+ * ```
77
+ */
78
+ export function buildObject(entries: Array<string>): string {
79
+ if (entries.length === 0) return '{}'
80
+ const body = entries.map((entry) => `${indentLines(entry)},`).join('\n')
81
+
82
+ return `{\n${body}\n}`
83
+ }
84
+
85
+ /**
86
+ * Assembles a bracketed list (array by default) from already-rendered `items`. Keeps everything on
87
+ * one line when no item spans multiple lines, and otherwise puts each item on its own line, indented
88
+ * one level with a trailing comma and the closing bracket at column zero. Use it for `z.union([…])`,
89
+ * `z.array([…])`, and similar member lists so objects inside them nest correctly.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * buildList(['z.string()', 'z.number()'])
94
+ * // '[z.string(), z.number()]'
95
+ * ```
96
+ */
97
+ export function buildList(items: Array<string>, brackets: [open: string, close: string] = ['[', ']']): string {
98
+ const [open, close] = brackets
99
+ if (items.length === 0) return `${open}${close}`
100
+ if (!items.some((item) => item.includes('\n'))) return `${open}${items.join(', ')}${close}`
101
+ const body = items.map((item) => `${indentLines(item)},`).join('\n')
102
+
103
+ return `${open}\n${body}\n${close}`
104
+ }
@@ -0,0 +1,34 @@
1
+ import type { CodeNode } from '../nodes/code.ts'
2
+
3
+ /**
4
+ * Extracts all string content from a `CodeNode` tree recursively.
5
+ *
6
+ * Collects text node values, identifier references in string fields (`params`, `generics`, `returnType`, `type`),
7
+ * and nested node content. Used to build the full source string for import filtering.
8
+ */
9
+ export function extractStringsFromNodes(nodes: Array<CodeNode> | undefined): string {
10
+ if (!nodes?.length) return ''
11
+ return nodes
12
+ .map((node) => {
13
+ // Backward-compat: compiled plugins may still pass bare strings at runtime
14
+ if (typeof node === 'string') return node as string
15
+ if (node.kind === 'Text') return node.value
16
+ if (node.kind === 'Break') return ''
17
+ if (node.kind === 'Jsx') return node.value
18
+
19
+ const parts: Array<string> = []
20
+
21
+ if ('params' in node && node.params) parts.push(node.params)
22
+ if ('generics' in node && node.generics) parts.push(Array.isArray(node.generics) ? node.generics.join(', ') : node.generics)
23
+ if ('returnType' in node && node.returnType) parts.push(node.returnType)
24
+ if ('type' in node && typeof node.type === 'string') parts.push(node.type)
25
+
26
+ const nested = extractStringsFromNodes(node.nodes)
27
+
28
+ if (nested) parts.push(nested)
29
+
30
+ return parts.join('\n')
31
+ })
32
+ .filter(Boolean)
33
+ .join('\n')
34
+ }