@kubb/parser-ts 5.5.5 → 5.6.0

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