@kubb/parser-ts 5.5.6 → 5.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5,6 +5,23 @@ let node_module = require("node:module");
5
5
  let node_path = require("node:path");
6
6
  //#region ../../internals/utils/src/fs.ts
7
7
  /**
8
+ * Converts a filesystem path to use POSIX (`/`) separators.
9
+ *
10
+ * Most of the codebase compares and composes paths as strings (prefix matching, joining for
11
+ * import specifiers, splitting on `/`). On POSIX `path.resolve` already returns `/`-separated
12
+ * paths, but on Windows it returns `\`-separated paths, which breaks every such comparison.
13
+ *
14
+ * Routing every path that crosses a module boundary through `toPosixPath` keeps the rest of the
15
+ * code platform-agnostic. The conversion runs unconditionally so Windows-specific behavior is
16
+ * exercisable from POSIX CI.
17
+ *
18
+ * @example
19
+ * toPosixPath('C:\\repo\\src\\pet.ts') // 'C:/repo/src/pet.ts'
20
+ */
21
+ function toPosixPath(filePath) {
22
+ return filePath.replaceAll("\\", "/");
23
+ }
24
+ /**
8
25
  * Strips the file extension from a path or file name.
9
26
  * Only removes the last `.ext` segment when the dot is not part of a directory name.
10
27
  *
@@ -19,61 +36,42 @@ function trimExtName(text) {
19
36
  if (dotIndex > 0 && !text.includes("/", dotIndex)) return text.slice(0, dotIndex);
20
37
  return text;
21
38
  }
22
- /**
23
- * Indentation unit prepended once per nesting level when pretty-printing.
24
- */
25
- const INDENT = " ".repeat(2);
26
- /**
27
- * Matches only the final `.<ext>` of a path, so a name like `foo.bar.ts` keeps
28
- * `foo.bar` and loses just `.ts`.
29
- */
30
- const FILE_EXTENSION_PATTERN = /\.[^/.]+$/;
31
- /**
32
- * Matches Windows-style backslash path separators.
33
- */
34
- const WINDOWS_PATH_SEPARATOR = /\\/g;
35
- /**
36
- * Matches `*\/` in free-form text so JSDoc bodies can neutralize premature
37
- * comment terminators (`*\/` → `* /`).
38
- */
39
- const JSDOC_TERMINATOR_PATTERN = /\*\//g;
40
- /**
41
- * Matches carriage returns for normalizing CRLF/CR line endings to LF.
42
- */
43
- const CARRIAGE_RETURN_PATTERN = /\r/g;
44
- /**
45
- * Matches CRLF sequences used when normalizing TypeScript printer output.
46
- */
47
- const CRLF_PATTERN = /\r\n/g;
48
- /**
49
- * Matches an identifier that starts with a digit. JavaScript disallows this,
50
- * so the printer replaces the leading digit with `_`.
51
- */
52
- const LEADING_DIGIT_PATTERN = /^\d/;
53
39
  //#endregion
54
40
  //#region src/utils.ts
41
+ let compiler;
55
42
  /**
56
- * Loaded with `require`, not `import`. When ESM imports a CommonJS package, Node keeps a second
57
- * copy of its source to detect named exports, and for TypeScript that copy is about 9 MB.
58
- */
59
- const typescript = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("typescript");
60
- const { factory } = typescript;
43
+ * Loads TypeScript on the first `print` or `copy`; `parse` is string templating and never needs it.
44
+ *
45
+ * Loaded with `require`, not `import`: when ESM imports a CommonJS package, Node keeps a second copy of its source
46
+ * to detect named exports, and for TypeScript that copy is about 9 MB.
47
+ */
48
+ function loadCompiler() {
49
+ if (compiler) return compiler;
50
+ const typescript = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("typescript");
51
+ compiler = {
52
+ typescript,
53
+ printer: typescript.createPrinter({
54
+ omitTrailingSemicolon: true,
55
+ newLine: typescript.NewLineKind.LineFeed,
56
+ removeComments: false,
57
+ noEmitHelpers: true
58
+ }),
59
+ sourceFile: typescript.createSourceFile("print.tsx", "", typescript.ScriptTarget.ES2022, true, typescript.ScriptKind.TSX)
60
+ };
61
+ return compiler;
62
+ }
61
63
  /**
62
- * Resolves `filePath` relative to `rootDir` and returns a POSIX-style path
63
- * prefixed with `./` when the target sits inside the root, or `../` when it escapes it.
64
+ * Module specifier that imports `filePath` from the file at `root`: POSIX, relative to that file's directory, `./`-prefixed unless it climbs with `../`.
64
65
  */
65
- function getRelativePath(rootDir, filePath) {
66
- const rel = (0, node_path.relative)(rootDir, filePath);
67
- const slashed = (0, node_path.normalize)(rel).replaceAll(WINDOWS_PATH_SEPARATOR, "/").replace("../", "");
66
+ function getRelativePath(root, filePath) {
67
+ const slashed = toPosixPath((0, node_path.relative)((0, node_path.dirname)(root), filePath));
68
68
  return slashed.startsWith("../") ? slashed : `./${slashed}`;
69
69
  }
70
70
  /**
71
- * Rewrites an import/export path so its extension matches the caller-supplied
72
- * `options.extname`. When the source path has no extension the original is kept,
73
- * so virtual/module-only paths flow through unchanged.
71
+ * Swaps the extension of a relative import/export path for `options.extname`; package specifiers and extension-less paths stay as-is.
74
72
  */
75
73
  function resolveOutputPath(path, options, rootAware) {
76
- const hasExtname = FILE_EXTENSION_PATTERN.test(path);
74
+ const hasExtname = /\.[^/.]+$/.test(path);
77
75
  if (rootAware && options?.extname && hasExtname) return `${trimExtName(path)}${options.extname}`;
78
76
  return rootAware ? trimExtName(path) : path;
79
77
  }
@@ -82,6 +80,7 @@ function resolveOutputPath(path, options, rootAware) {
82
80
  */
83
81
  function toImportName(element) {
84
82
  if (!element.propertyName) return element.name.text;
83
+ const { typescript } = loadCompiler();
85
84
  const { propertyName } = element;
86
85
  return {
87
86
  propertyName: typescript.isStringLiteral(propertyName) ? quoteModulePath(propertyName.text) : propertyName.text,
@@ -92,6 +91,7 @@ function toImportName(element) {
92
91
  * Converts an `import` declaration into `ImportNode`s. Side-effect imports and imports with attributes stay as written.
93
92
  */
94
93
  function toImportNodes(statement, filePath) {
94
+ const { typescript } = loadCompiler();
95
95
  if (!typescript.isImportDeclaration(statement) || !statement.importClause || !typescript.isStringLiteral(statement.moduleSpecifier) || statement.attributes) return [];
96
96
  const { name, namedBindings, phaseModifier } = statement.importClause;
97
97
  const specifier = statement.moduleSpecifier.text;
@@ -132,6 +132,7 @@ function toImportNodes(statement, filePath) {
132
132
  * Converts an `export … from` declaration into `ExportNode`s. Forms `printExport` cannot print stay as written.
133
133
  */
134
134
  function toExportNodes(statement) {
135
+ const { typescript } = loadCompiler();
135
136
  if (!typescript.isExportDeclaration(statement) || !statement.moduleSpecifier || !typescript.isStringLiteral(statement.moduleSpecifier) || statement.attributes) return [];
136
137
  const { exportClause, isTypeOnly } = statement;
137
138
  const path = statement.moduleSpecifier.text;
@@ -152,14 +153,11 @@ function toExportNodes(statement) {
152
153
  isTypeOnly
153
154
  })];
154
155
  }
155
- function isModuleDeclaration(statement) {
156
- return typescript.isImportDeclaration(statement) || typescript.isExportDeclaration(statement) && Boolean(statement.moduleSpecifier);
157
- }
158
156
  /**
159
- * Splits `source` into its top-level `import`/`export … from` declarations, as nodes, the `header` above the first of them
160
- * (shebang, directives, comments) and the remaining `body`.
157
+ * Splits `source` into its lifted `import`/`export … from` nodes, the `header` above the first one (shebang, directives, comments) and the rest as `body`.
161
158
  */
162
159
  function splitModuleDeclarations(source, filePath) {
160
+ const { typescript } = loadCompiler();
163
161
  const sourceFile = typescript.createSourceFile(filePath, source, typescript.ScriptTarget.Latest);
164
162
  const imports = [];
165
163
  const exports = [];
@@ -170,7 +168,7 @@ function splitModuleDeclarations(source, filePath) {
170
168
  const importNodes = toImportNodes(statement, filePath);
171
169
  const exportNodes = toExportNodes(statement);
172
170
  if (!importNodes.length && !exportNodes.length) {
173
- if (isModuleDeclaration(statement)) break;
171
+ if (typescript.isImportDeclaration(statement) || typescript.isExportDeclaration(statement) && statement.moduleSpecifier) break;
174
172
  continue;
175
173
  }
176
174
  const text = source.slice(cursor, statement.getStart(sourceFile));
@@ -188,50 +186,35 @@ function splitModuleDeclarations(source, filePath) {
188
186
  };
189
187
  }
190
188
  /**
191
- * Serializes a `nodes` array into source text. Each entry is rendered via {@link printCodeNode}
192
- * and joined with a single newline. A `Break` node (`<br/>`) inserts one blank line between
193
- * statements. Consecutive breaks, and breaks at the very start or end, are folded into the
194
- * separator, so a double `<br/>` never emits more than one blank line.
189
+ * Joins the printed `nodes` with `separator`; a `Break` widens the next separator to a blank line, and leading, trailing and repeated breaks fold into one.
190
+ *
191
+ * Imperative on purpose: this runs once per source fragment and `map().filter().join()` showed up in deopt traces.
195
192
  */
196
- function printNodes(nodes) {
197
- if (!nodes || nodes.length === 0) return "";
193
+ function printNodes({ nodes, separator }) {
198
194
  let result = "";
199
- let hasContent = false;
200
195
  let pendingBreak = false;
201
- for (const node of nodes) {
196
+ for (const node of nodes ?? []) {
202
197
  if (node.kind === "Break") {
203
- if (hasContent) pendingBreak = true;
198
+ pendingBreak = result !== "";
204
199
  continue;
205
200
  }
206
201
  const text = printCodeNode(node);
207
202
  if (!text) continue;
208
- if (hasContent) result += pendingBreak ? "\n\n" : "\n";
203
+ if (result) result += pendingBreak ? "\n\n" : separator;
209
204
  result += text;
210
- hasContent = true;
211
205
  pendingBreak = false;
212
206
  }
213
207
  return result;
214
208
  }
215
209
  /**
216
- * Indents every non-empty line of `text` by one indent unit. Pass a number to repeat
217
- * {@link INDENT_CHAR} that many times, or a string to use as the indent verbatim.
210
+ * Indents every non-empty line of `text` by two spaces.
218
211
  */
219
- function indentLines(text, indent = INDENT) {
212
+ function indentLines(text) {
220
213
  if (!text) return "";
221
- const pad = typeof indent === "string" ? indent : " ".repeat(indent);
222
- return text.split("\n").map((line) => line.trim() ? `${pad}${line}` : "").join("\n");
214
+ return text.split("\n").map((line) => line.trim() ? ` ${line}` : "").join("\n");
223
215
  }
224
216
  /**
225
- * Removes the common leading whitespace shared by every non-blank line and trims
226
- * surrounding blank lines, so multi-line content authored inside an indented template
227
- * literal lines up at a column-zero baseline. Leading whitespace is counted by
228
- * character, so N tabs and N spaces are treated as the same depth.
229
- *
230
- * @example
231
- * ```ts
232
- * dedent('\n foo\n bar\n ')
233
- * // 'foo\n bar'
234
- * ```
217
+ * Strips the common leading whitespace and the surrounding blank lines, counting tabs and spaces alike, so indented template literals start at column zero.
235
218
  */
236
219
  function dedent(text) {
237
220
  if (!text) return "";
@@ -246,61 +229,37 @@ function dedent(text) {
246
229
  return trimmed.map((line) => isBlank(line) ? "" : line.slice(min)).join("\n");
247
230
  }
248
231
  /**
249
- * Renders the generic clause (`<T, U>`) shared by function and arrow-function nodes.
250
- * Accepts either a raw string (rendered verbatim) or an array of type-parameter names.
232
+ * Renders `<T>(params): ReturnType`; an async function wraps its return type in `Promise<>`.
251
233
  */
252
- function formatGenerics(generics) {
253
- if (!generics) return "";
254
- return `<${Array.isArray(generics) ? generics.join(", ") : generics}>`;
234
+ function signature({ async: isAsync, generics, params, returnType }) {
235
+ const genericsStr = generics ? `<${Array.isArray(generics) ? generics.join(", ") : generics}>` : "";
236
+ const wrapped = isAsync ? `Promise<${returnType}>` : returnType;
237
+ const returnTypeStr = returnType ? `: ${wrapped}` : "";
238
+ return `${genericsStr}(${params ?? ""})${returnTypeStr}`;
255
239
  }
256
240
  /**
257
- * Renders the return-type suffix (`: T` or `: Promise<T>` when `isAsync` is true).
258
- * Returns an empty string when no return type is provided.
241
+ * Puts the JSDoc block, when it renders to something, above `declaration`.
259
242
  */
260
- function formatReturnType(returnType, isAsync) {
261
- if (!returnType) return "";
262
- return isAsync ? `: Promise<${returnType}>` : `: ${returnType}`;
243
+ function withJSDoc({ jsDoc, declaration }) {
244
+ const jsDocStr = jsDoc ? printJSDoc(jsDoc) : "";
245
+ return jsDocStr ? `${jsDocStr}\n${declaration}` : declaration;
263
246
  }
264
247
  /**
265
- * Module-scoped TypeScript printer instance. A printer does not mutate the source file, so one
266
- * instance is reused across every `print()` call instead of constructing a new printer each time.
267
- */
268
- const TS_PRINTER = typescript.createPrinter({
269
- omitTrailingSemicolon: true,
270
- newLine: typescript.NewLineKind.LineFeed,
271
- removeComments: false,
272
- noEmitHelpers: true
273
- });
274
- /**
275
- * Module-scoped source file used as the print target. `printList` only reads the source
276
- * file's compiler options / language version. It never mutates it.
277
- */
278
- const PRINT_SOURCE_FILE = typescript.createSourceFile("print.tsx", "", typescript.ScriptTarget.ES2022, true, typescript.ScriptKind.TSX);
279
- TS_PRINTER.printList(typescript.ListFormat.MultiLine, factory.createNodeArray([]), PRINT_SOURCE_FILE);
280
- /**
281
248
  * Converts TypeScript/TSX AST nodes to a string using the TypeScript printer.
282
249
  */
283
250
  function print(...elements) {
284
251
  const filtered = elements.filter(Boolean);
285
252
  if (filtered.length === 0) return "";
286
- return TS_PRINTER.printList(typescript.ListFormat.MultiLine, factory.createNodeArray(filtered), PRINT_SOURCE_FILE).replace(CRLF_PATTERN, "\n");
253
+ const { typescript, printer, sourceFile } = loadCompiler();
254
+ return printer.printList(typescript.ListFormat.MultiLine, typescript.factory.createNodeArray(filtered), sourceFile).replace(/\r\n/g, "\n");
287
255
  }
288
256
  /**
289
- * Converts a {@link ast.JSDocNode} to a JSDoc comment block string.
290
- *
291
- * @example
292
- * ```ts
293
- * printJSDoc({ comments: ['@description A pet', '@deprecated'] })
294
- * // /**
295
- * // * @description A pet
296
- * // * @deprecated
297
- * // *\/
298
- * ```
257
+ * Converts a {@link ast.JSDocNode} to a `/** … *\/` block, dropping blank comments.
299
258
  */
300
259
  function printJSDoc(jsDoc) {
301
260
  const comments = (jsDoc.comments ?? []).filter((c) => c != null);
302
261
  if (comments.length === 0) return "";
303
- const lines = comments.flatMap((c) => c.split(/\r?\n/)).map((l) => l.replace(JSDOC_TERMINATOR_PATTERN, "* /").replace(CARRIAGE_RETURN_PATTERN, "")).filter((l) => l.trim().length > 0);
262
+ const lines = comments.flatMap((c) => c.split(/\r?\n/)).map((l) => l.replace(/\*\//g, "* /").replace(/\r/g, "")).filter((l) => l.trim().length > 0);
304
263
  if (lines.length === 0) return "";
305
264
  return [
306
265
  "/**",
@@ -309,194 +268,101 @@ function printJSDoc(jsDoc) {
309
268
  ].join("\n");
310
269
  }
311
270
  /**
312
- * Converts a {@link ast.ConstNode} to a TypeScript `const` declaration string.
313
- *
314
- * Mirrors the `Const` component from `@kubb/renderer-jsx`.
315
- *
316
- * @example
317
- * ```ts
318
- * printConst(factory.createConst({ name: 'pet', export: true, nodes: ['{}'] }))
319
- * // 'export const pet = {}'
320
- * ```
321
- *
322
- * @example With type and `as const`
323
- * ```ts
324
- * printConst(factory.createConst({ name: 'pets', export: true, type: 'Pet[]', asConst: true, nodes: ['[]'] }))
325
- * // 'export const pets: Pet[] = [] as const'
326
- * ```
271
+ * Converts a {@link ast.ConstNode} to a `const` declaration, mirroring the `Const` component from `@kubb/renderer-jsx`.
327
272
  */
328
273
  function printConst(node) {
329
274
  const { name, export: canExport, type, JSDoc, asConst, nodes } = node;
330
- const jsDocStr = JSDoc ? printJSDoc(JSDoc) : "";
331
- const body = printNodes(nodes);
332
- const parts = [];
333
- if (canExport) parts.push("export ");
334
- parts.push("const ");
335
- parts.push(name);
336
- if (type) parts.push(`: ${type}`);
337
- parts.push(" = ");
338
- parts.push(body);
339
- if (asConst) parts.push(" as const");
340
- return [jsDocStr, parts.join("")].filter(Boolean).join("\n");
275
+ return withJSDoc({
276
+ jsDoc: JSDoc,
277
+ declaration: `${canExport ? "export " : ""}const ${name}${type ? `: ${type}` : ""} = ${printNodes({
278
+ nodes,
279
+ separator: "\n"
280
+ })}${asConst ? " as const" : ""}`
281
+ });
341
282
  }
342
283
  /**
343
- * Converts a {@link ast.TypeNode} to a TypeScript `type` alias declaration string.
344
- *
345
- * Mirrors the `Type` component from `@kubb/renderer-jsx`.
346
- *
347
- * @example
348
- * ```ts
349
- * printType(factory.createType({ name: 'Pet', export: true, nodes: ['{ id: number }'] }))
350
- * // 'export type Pet = { id: number }'
351
- * ```
284
+ * Converts a {@link ast.TypeNode} to a `type` alias, mirroring the `Type` component from `@kubb/renderer-jsx`.
352
285
  */
353
286
  function printType(node) {
354
287
  const { name, export: canExport, JSDoc, nodes } = node;
355
- const jsDocStr = JSDoc ? printJSDoc(JSDoc) : "";
356
- const body = printNodes(nodes);
357
- const parts = [];
358
- if (canExport) parts.push("export ");
359
- parts.push("type ");
360
- parts.push(name);
361
- parts.push(" = ");
362
- parts.push(body);
363
- return [jsDocStr, parts.join("")].filter(Boolean).join("\n");
288
+ return withJSDoc({
289
+ jsDoc: JSDoc,
290
+ declaration: `${canExport ? "export " : ""}type ${name} = ${printNodes({
291
+ nodes,
292
+ separator: "\n"
293
+ })}`
294
+ });
364
295
  }
365
296
  /**
366
- * Converts a {@link ast.FunctionNode} to a TypeScript `function` declaration string.
367
- *
368
- * Mirrors the `Function` component from `@kubb/renderer-jsx`.
369
- *
370
- * @example
371
- * ```ts
372
- * printFunction(factory.createFunction({ name: 'getPet', export: true, params: 'id: string', returnType: 'Pet', nodes: ['return fetch(id)'] }))
373
- * // 'export function getPet(id: string): Pet {\n return fetch(id)\n}'
374
- * ```
375
- *
376
- * @example Async with generics
377
- * ```ts
378
- * printFunction(factory.createFunction({ name: 'fetchPet', export: true, async: true, generics: ['T'], params: 'id: string', returnType: 'T' }))
379
- * // 'export async function fetchPet<T>(id: string): Promise<T> {\n}'
380
- * ```
297
+ * Converts a {@link ast.FunctionNode} to a `function` declaration, mirroring the `Function` component from `@kubb/renderer-jsx`.
381
298
  */
382
299
  function printFunction(node) {
383
300
  const { name, default: isDefault, export: canExport, async: isAsync, generics, params, returnType, JSDoc, nodes } = node;
384
- const jsDocStr = JSDoc ? printJSDoc(JSDoc) : "";
385
- const body = printNodes(nodes);
386
- const indented = body ? indentLines(body) : "";
387
- const parts = [];
388
- if (canExport) parts.push("export ");
389
- if (isDefault) parts.push("default ");
390
- if (isAsync) parts.push("async ");
391
- parts.push("function ");
392
- parts.push(name);
393
- parts.push(formatGenerics(generics));
394
- parts.push(`(${params ?? ""})`);
395
- parts.push(formatReturnType(returnType, isAsync));
396
- parts.push(" {");
397
- if (indented) parts.push(`\n${indented}\n`);
398
- parts.push("}");
399
- return [jsDocStr, parts.join("")].filter(Boolean).join("\n");
301
+ const body = indentLines(printNodes({
302
+ nodes,
303
+ separator: "\n"
304
+ }));
305
+ return withJSDoc({
306
+ jsDoc: JSDoc,
307
+ declaration: `${`${canExport ? "export " : ""}${isDefault ? "default " : ""}${isAsync ? "async " : ""}`}function ${name}${signature({
308
+ async: isAsync,
309
+ generics,
310
+ params,
311
+ returnType
312
+ })} {${body ? `\n${body}\n` : ""}}`
313
+ });
400
314
  }
401
315
  /**
402
- * Converts an {@link ast.ArrowFunctionNode} to a TypeScript arrow function declaration string.
403
- *
404
- * Mirrors the `Function.Arrow` component from `@kubb/renderer-jsx`.
405
- *
406
- * @example Multi-line arrow function
407
- * ```ts
408
- * printArrowFunction(factory.createArrowFunction({ name: 'getPet', export: true, params: 'id: string', nodes: ['return fetch(id)'] }))
409
- * // 'export const getPet = (id: string) => {\n return fetch(id)\n}'
410
- * ```
411
- *
412
- * @example Single-line arrow function
413
- * ```ts
414
- * printArrowFunction(factory.createArrowFunction({ name: 'double', params: 'n: number', singleLine: true, nodes: ['n * 2'] }))
415
- * // 'const double = (n: number) => n * 2'
416
- * ```
316
+ * Converts an {@link ast.ArrowFunctionNode} to a `const` arrow function, mirroring the `Function.Arrow` component from `@kubb/renderer-jsx`.
417
317
  */
418
318
  function printArrowFunction(node) {
419
319
  const { name, default: isDefault, export: canExport, async: isAsync, generics, params, returnType, JSDoc, nodes, singleLine } = node;
420
- const jsDocStr = JSDoc ? printJSDoc(JSDoc) : "";
421
- const body = printNodes(nodes);
320
+ const body = printNodes({
321
+ nodes,
322
+ separator: "\n"
323
+ });
422
324
  const arrowBody = singleLine ? ` => ${body}` : body ? ` => {\n${indentLines(body)}\n}` : " => {}";
423
- const parts = [];
424
- if (canExport) parts.push("export ");
425
- if (isDefault) parts.push("default ");
426
- parts.push("const ");
427
- parts.push(name);
428
- parts.push(" = ");
429
- if (isAsync) parts.push("async ");
430
- parts.push(formatGenerics(generics));
431
- parts.push(`(${params ?? ""})`);
432
- parts.push(formatReturnType(returnType, isAsync));
433
- parts.push(arrowBody);
434
- return [jsDocStr, parts.join("")].filter(Boolean).join("\n");
325
+ return withJSDoc({
326
+ jsDoc: JSDoc,
327
+ declaration: `${`${canExport ? "export " : ""}${isDefault ? "default " : ""}`}const ${name} = ${isAsync ? "async " : ""}${signature({
328
+ async: isAsync,
329
+ generics,
330
+ params,
331
+ returnType
332
+ })}${arrowBody}`
333
+ });
435
334
  }
436
335
  /**
437
- * Converts a {@link ast.CodeNode} to its TypeScript string representation.
438
- *
439
- * Dispatches to the appropriate printer based on the node's `kind`.
440
- *
441
- * @example
442
- * ```ts
443
- * printCodeNode(factory.createConst({ name: 'x', nodes: ['1'] }))
444
- * // 'const x = 1'
445
- * ```
336
+ * Dispatches a {@link ast.CodeNode} to the printer for its `kind`.
446
337
  */
447
338
  function printCodeNode(node) {
448
- if (node.kind === "Break") return "";
449
- if (node.kind === "Text") return dedent(node.value);
450
- if (node.kind === "Jsx") return dedent(node.value);
451
- if (node.kind === "Const") return printConst(node);
452
- if (node.kind === "Type") return printType(node);
453
- if (node.kind === "Function") return printFunction(node);
454
- if (node.kind === "ArrowFunction") return printArrowFunction(node);
455
- return "";
339
+ switch (node.kind) {
340
+ case "Break": return "";
341
+ case "Text":
342
+ case "Jsx": return dedent(node.value);
343
+ case "Const": return printConst(node);
344
+ case "Type": return printType(node);
345
+ case "Function": return printFunction(node);
346
+ case "ArrowFunction": return printArrowFunction(node);
347
+ }
456
348
  }
457
349
  /**
458
- * Converts a {@link ast.SourceNode} to its TypeScript string representation.
459
- *
460
- * Iterates `nodes` in DOM order, rendering each {@link ast.CodeNode} via
461
- * {@link printCodeNode}.
462
- *
463
- * Top-level declarations are separated by a blank line so the source reads
464
- * cleanly without an external formatter.
465
- *
466
- * @example From nodes
467
- * ```ts
468
- * printSource({ kind: 'Source', nodes: [factory.createConst({ name: 'x', nodes: [factory.createText('1')] }), factory.createText('x.toString()')] })
469
- * // 'const x = 1\n\nx.toString()'
470
- * ```
350
+ * Prints a {@link ast.SourceNode} with a blank line between its top-level nodes.
471
351
  */
472
352
  function printSource(node) {
473
- const nodes = node.nodes;
474
- if (!nodes || nodes.length === 0) return "";
475
- let result = "";
476
- for (const child of nodes) {
477
- const text = printCodeNode(child);
478
- if (!text) continue;
479
- result = result ? `${result}\n\n${text}` : text;
480
- }
481
- return result;
353
+ return printNodes({
354
+ nodes: node.nodes,
355
+ separator: "\n\n"
356
+ });
482
357
  }
483
358
  /**
484
- * Wraps a module specifier in single quotes, escaping any embedded backslash or quote so the emitted
485
- * statement stays valid even for unusual paths.
359
+ * Wraps a module specifier in single quotes, escaping embedded backslashes and quotes.
486
360
  */
487
361
  function quoteModulePath(path) {
488
362
  return `'${path.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
489
363
  }
490
364
  /**
491
- * Renders an import declaration string in the repo style (single quotes, no semicolons), covering
492
- * default, namespace (`* as`), and named imports with `{ a as b }` aliases, each optionally
493
- * `type`-only. `path` is used verbatim, so resolve it first.
494
- *
495
- * @example
496
- * ```ts
497
- * printImport({ name: ['z'], path: './zod.ts' })
498
- * // "import { z } from './zod.ts'"
499
- * ```
365
+ * Renders an `import` declaration (default, `* as`, or named with `{ a as b }`, each optionally `type`-only). `path` is used verbatim, so resolve it first.
500
366
  */
501
367
  function printImport({ name, path, isTypeOnly = false, isNameSpace = false }) {
502
368
  const typePrefix = isTypeOnly ? "type " : "";
@@ -511,21 +377,13 @@ function printImport({ name, path, isTypeOnly = false, isNameSpace = false }) {
511
377
  }).join(", ")} } from ${from}`;
512
378
  }
513
379
  /**
514
- * Renders an export declaration string in the repo style (single quotes, no semicolons), covering
515
- * named re-exports, namespace alias (`* as name`), and wildcard, each optionally `type`-only.
516
- * `path` is used verbatim, so resolve it first.
517
- *
518
- * @example
519
- * ```ts
520
- * printExport({ name: ['Pet', 'Order'], path: './models.ts' })
521
- * // "export { Pet, Order } from './models.ts'"
522
- * ```
380
+ * Renders an `export … from` declaration (named, `* as name`, or wildcard, each optionally `type`-only). `path` is used verbatim, so resolve it first.
523
381
  */
524
382
  function printExport({ path, name, isTypeOnly = false, asAlias = false }) {
525
383
  const typePrefix = isTypeOnly ? "type " : "";
526
384
  const from = quoteModulePath(path);
527
- if (Array.isArray(name)) return `export ${typePrefix}{ ${name.map((item) => typeof item === "string" ? item : item.text).join(", ")} } from ${from}`;
528
- if (asAlias && name) return `export ${typePrefix}* as ${LEADING_DIGIT_PATTERN.test(name) ? `_${name.slice(1)}` : name} from ${from}`;
385
+ if (Array.isArray(name)) return `export ${typePrefix}{ ${name.join(", ")} } from ${from}`;
386
+ if (asAlias && name) return `export ${typePrefix}* as ${/^\d/.test(name) ? `_${name.slice(1)}` : name} from ${from}`;
529
387
  if (name) return `export ${typePrefix}{ ${name} } from ${from}`;
530
388
  return `export ${typePrefix}* from ${from}`;
531
389
  }
@@ -560,33 +418,17 @@ const parserTs = (0, _kubb_kit.defineParser)(({ extension = DEFAULT_EXTENSION }
560
418
  return {
561
419
  name: "typescript",
562
420
  extNames: [".ts", ".js"],
563
- print(...nodes) {
564
- return print(...nodes);
565
- },
421
+ print,
566
422
  parse(file) {
567
423
  const extname = extension[file.extname] || void 0;
568
- const sourceParts = [];
569
- for (const item of file.sources) {
570
- const sourceStr = printSource(item);
571
- if (sourceStr) sourceParts.push(sourceStr.trimEnd());
572
- }
573
- const source = sourceParts.join("\n\n");
574
- const importLines = [];
575
- for (const item of file.imports) {
576
- const importPath = item.root ? getRelativePath(item.root, item.path) : item.path;
577
- importLines.push(printImport({
578
- name: item.name,
579
- path: resolveOutputPath(importPath, { extname }, Boolean(item.root)),
580
- isTypeOnly: item.isTypeOnly,
581
- isNameSpace: item.isNameSpace
582
- }));
583
- }
584
- const exportLines = [];
585
- for (const item of file.exports) exportLines.push(printExport({
586
- name: item.name,
587
- path: resolveOutputPath(item.path, { extname }, item.path.startsWith(".")),
588
- isTypeOnly: item.isTypeOnly,
589
- asAlias: item.asAlias
424
+ const source = file.sources.map((item) => printSource(item)).filter(Boolean).map((text) => text.trimEnd()).join("\n\n");
425
+ const importLines = file.imports.map((item) => printImport({
426
+ ...item,
427
+ path: resolveOutputPath(item.root ? getRelativePath(item.root, item.path) : item.path, { extname }, Boolean(item.root))
428
+ }));
429
+ const exportLines = file.exports.map((item) => printExport({
430
+ ...item,
431
+ path: resolveOutputPath(item.path, { extname }, item.path.startsWith("."))
590
432
  }));
591
433
  const importExportBlock = [...importLines, ...exportLines].join("\n");
592
434
  return [
@@ -633,20 +475,11 @@ const parserTs = (0, _kubb_kit.defineParser)(({ extension = DEFAULT_EXTENSION }
633
475
  * })
634
476
  * ```
635
477
  */
636
- const parserTsx = (0, _kubb_kit.defineParser)((options = {}) => {
637
- const parser = parserTs(options);
638
- return {
639
- name: "tsx",
640
- extNames: [".tsx", ".jsx"],
641
- print(...nodes) {
642
- return print(...nodes);
643
- },
644
- parse(file) {
645
- return parser.parse(file);
646
- },
647
- copy: parser.copy
648
- };
649
- });
478
+ const parserTsx = (0, _kubb_kit.defineParser)((options) => ({
479
+ ...parserTs(options),
480
+ name: "tsx",
481
+ extNames: [".tsx", ".jsx"]
482
+ }));
650
483
  //#endregion
651
484
  exports.parserTs = parserTs;
652
485
  exports.parserTsx = parserTsx;