@stacksjs/zig-dtsx 0.9.10

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.
@@ -0,0 +1,1045 @@
1
+ /// Emitter module — converts declarations to final .d.ts output.
2
+ /// Port of processor/index.ts.
3
+ const std = @import("std");
4
+ const ch = @import("char_utils.zig");
5
+ const types = @import("types.zig");
6
+ const type_inf = @import("type_inference.zig");
7
+ const Declaration = types.Declaration;
8
+ const DeclarationKind = types.DeclarationKind;
9
+
10
+ /// Check if a character is an identifier character (for word boundary checks)
11
+ inline fn isIdentChar(c: u8) bool {
12
+ return (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or c == '_' or c == '$' or c > 127;
13
+ }
14
+
15
+ /// Check if `name` appears as a whole word in `text` (fast indexOf + boundary check)
16
+ pub fn isWordInText(name: []const u8, text: []const u8) bool {
17
+ if (name.len == 0) return false;
18
+ var search_from: usize = 0;
19
+ while (search_from < text.len) {
20
+ const idx = ch.indexOf(text, name, search_from) orelse return false;
21
+ const before: u8 = if (idx > 0) text[idx - 1] else ' ';
22
+ const after: u8 = if (idx + name.len < text.len) text[idx + name.len] else ' ';
23
+ if (!isIdentChar(before) and !isIdentChar(after)) return true;
24
+ search_from = idx + 1;
25
+ }
26
+ return false;
27
+ }
28
+
29
+ /// Extract all identifier words from text into an existing HashMap (single pass, O(n))
30
+ fn extractWords(words: *std.StringHashMap(void), text: []const u8) void {
31
+ var i: usize = 0;
32
+ while (i < text.len) {
33
+ const c = text[i];
34
+ if ((c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') or c == '_' or c == '$' or c > 127) {
35
+ const start = i;
36
+ i += 1;
37
+ while (i < text.len and isIdentChar(text[i])) i += 1;
38
+ words.put(text[start..i], {}) catch {};
39
+ } else {
40
+ i += 1;
41
+ }
42
+ }
43
+ }
44
+
45
+ /// Extract triple-slash directives from source start
46
+ fn extractTripleSlashDirectives(alloc: std.mem.Allocator, source: []const u8) ![][]const u8 {
47
+ var directives = std.array_list.Managed([]const u8).init(alloc);
48
+ var line_start: usize = 0;
49
+
50
+ var i: usize = 0;
51
+ while (i <= source.len) : (i += 1) {
52
+ if (i == source.len or source[i] == '\n') {
53
+ // Extract and trim line
54
+ var start = line_start;
55
+ var end = i;
56
+ while (start < end and (source[start] == ' ' or source[start] == '\t' or source[start] == '\r')) start += 1;
57
+ while (end > start and (source[end - 1] == ' ' or source[end - 1] == '\t' or source[end - 1] == '\r')) end -= 1;
58
+ const trimmed = source[start..end];
59
+ line_start = i + 1;
60
+
61
+ if (trimmed.len >= 3 and trimmed[0] == '/' and trimmed[1] == '/' and trimmed[2] == '/') {
62
+ // Check for /// <reference .../>
63
+ if (ch.contains(trimmed, "<reference ") or ch.contains(trimmed, "<amd-module ") or ch.contains(trimmed, "<amd-dependency ")) {
64
+ try directives.append(trimmed);
65
+ }
66
+ } else if (trimmed.len == 0 or (trimmed.len >= 2 and trimmed[0] == '/' and trimmed[1] == '/')) {
67
+ continue;
68
+ } else {
69
+ break;
70
+ }
71
+ }
72
+ }
73
+
74
+ return directives.toOwnedSlice();
75
+ }
76
+
77
+ /// Format leading comments — direct alloc, output ≤ total comment length + newlines
78
+ fn formatComments(alloc: std.mem.Allocator, comments: ?[]const []const u8, keep_comments: bool) ![]const u8 {
79
+ if (!keep_comments) return "";
80
+ const cmts = comments orelse return "";
81
+ if (cmts.len == 0) return "";
82
+
83
+ // Fast path: single comment (very common)
84
+ if (cmts.len == 1) {
85
+ const t = ch.sliceTrimmed(cmts[0], 0, cmts[0].len);
86
+ const buf = try alloc.alloc(u8, t.len + 1);
87
+ @memcpy(buf[0..t.len], t);
88
+ buf[t.len] = '\n';
89
+ return buf;
90
+ }
91
+
92
+ // Pre-compute exact size
93
+ var total_len: usize = cmts.len; // newlines between + trailing
94
+ for (cmts) |c| {
95
+ const t = ch.sliceTrimmed(c, 0, c.len);
96
+ total_len += t.len;
97
+ }
98
+
99
+ const buf = try alloc.alloc(u8, total_len);
100
+ var pos: usize = 0;
101
+ for (cmts, 0..) |c, idx| {
102
+ if (idx > 0) {
103
+ buf[pos] = '\n';
104
+ pos += 1;
105
+ }
106
+ const t = ch.sliceTrimmed(c, 0, c.len);
107
+ @memcpy(buf[pos..][0..t.len], t);
108
+ pos += t.len;
109
+ }
110
+ buf[pos] = '\n';
111
+ pos += 1;
112
+ return buf[0..pos];
113
+ }
114
+
115
+ const ParsedImportResult = struct {
116
+ default_name: ?[]const u8,
117
+ named_items: []const []const u8,
118
+ source: []const u8,
119
+ is_type_only: bool,
120
+ };
121
+
122
+ /// Get parsed import components from a declaration's cached parsed_import.
123
+ fn getParsedImport(decl: Declaration) ?ParsedImportResult {
124
+ const pi = decl.parsed_import orelse return null;
125
+ return .{
126
+ .default_name = pi.default_name,
127
+ .named_items = pi.named_items,
128
+ .source = pi.source,
129
+ .is_type_only = pi.is_type_only,
130
+ };
131
+ }
132
+
133
+ /// Extract all imported item names from an import declaration (for filtering).
134
+ /// Uses cached parsed_import when available to avoid re-parsing.
135
+ fn extractAllImportedItems(decl: Declaration) []const []const u8 {
136
+ if (decl.parsed_import) |pi| {
137
+ return pi.resolved_items;
138
+ }
139
+ return &.{};
140
+ }
141
+
142
+ /// Process a variable declaration for DTS output
143
+ fn processVariableDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_comments: bool) ![]const u8 {
144
+ const comments = try formatComments(alloc, decl.leading_comments, keep_comments);
145
+
146
+ // Fast path: if we have type annotation and no value needing special inference
147
+ if (decl.type_annotation.len > 0 and decl.value.len == 0) {
148
+ if (comments.len == 0) return decl.text;
149
+ const buf = try alloc.alloc(u8, comments.len + decl.text.len);
150
+ @memcpy(buf[0..comments.len], comments);
151
+ @memcpy(buf[comments.len..][0..decl.text.len], decl.text);
152
+ return buf;
153
+ }
154
+
155
+ // Fast path: type annotation set + value exists but doesn't need special handling.
156
+ // The scanner already built the correct DTS text using the type annotation,
157
+ // so we can return it directly without type inference or value processing.
158
+ if (decl.type_annotation.len > 0 and decl.value.len > 0) {
159
+ const trimmed_val = std.mem.trim(u8, decl.value, " \t\n\r");
160
+ if (!ch.endsWith(trimmed_val, "as const") and
161
+ !ch.contains(decl.value, " satisfies "))
162
+ {
163
+ const kind: []const u8 = if (decl.modifiers) |mods| (if (mods.len > 0) mods[0] else "const") else "const";
164
+ if (!std.mem.eql(u8, kind, "const") or !type_inf.isGenericType(decl.type_annotation)) {
165
+ if (comments.len == 0) return decl.text;
166
+ const buf = try alloc.alloc(u8, comments.len + decl.text.len);
167
+ @memcpy(buf[0..comments.len], comments);
168
+ @memcpy(buf[comments.len..][0..decl.text.len], decl.text);
169
+ return buf;
170
+ }
171
+ }
172
+ }
173
+
174
+ // Variable kind from modifiers
175
+ const kind: []const u8 = if (decl.modifiers) |mods| (if (mods.len > 0) mods[0] else "const") else "const";
176
+
177
+ // Determine type annotation
178
+ var type_annotation: []const u8 = decl.type_annotation;
179
+
180
+ if (decl.value.len > 0 and ch.contains(decl.value, " satisfies ")) {
181
+ if (type_inf.extractSatisfiesType(decl.value)) |sat_type| {
182
+ type_annotation = sat_type;
183
+ }
184
+ } else if (decl.value.len > 0 and ch.endsWith(std.mem.trim(u8, decl.value, " \t\n\r"), "as const")) {
185
+ type_annotation = try type_inf.inferNarrowType(alloc, decl.value, true, false, 0);
186
+ } else if (type_annotation.len == 0 and decl.value.len > 0 and std.mem.eql(u8, kind, "const")) {
187
+ const trimmed_val = std.mem.trim(u8, decl.value, " \t\n\r");
188
+ const is_container = trimmed_val.len > 0 and (trimmed_val[0] == '{' or trimmed_val[0] == '[');
189
+ if (is_container) type_inf.enableCleanDefaultCollection();
190
+ type_annotation = try type_inf.inferNarrowType(alloc, decl.value, !is_container, false, 0);
191
+ } else if (type_annotation.len > 0 and decl.value.len > 0 and std.mem.eql(u8, kind, "const") and type_inf.isGenericType(type_annotation)) {
192
+ const inferred = try type_inf.inferNarrowType(alloc, decl.value, true, false, 0);
193
+ if (!std.mem.eql(u8, inferred, "unknown")) {
194
+ type_annotation = inferred;
195
+ }
196
+ } else if (type_annotation.len == 0 and decl.value.len > 0) {
197
+ type_annotation = try type_inf.inferNarrowType(alloc, decl.value, std.mem.eql(u8, kind, "const"), false, 0);
198
+ }
199
+
200
+ if (type_annotation.len == 0) type_annotation = "unknown";
201
+
202
+ // Add @defaultValue JSDoc for widened declarations
203
+ // Skip when value uses 'as const' — types are already narrow/self-documenting
204
+ const val_trimmed_for_check = std.mem.trim(u8, decl.value, " \t\n\r");
205
+ const has_as_const = ch.endsWith(val_trimmed_for_check, "as const");
206
+ if (decl.value.len > 0 and decl.type_annotation.len == 0 and !has_as_const) {
207
+ var default_val: ?[]const u8 = null;
208
+ var is_container = false;
209
+ const trimmed_val = std.mem.trim(u8, decl.value, " \t\n\r");
210
+
211
+ if (!std.mem.eql(u8, kind, "const")) {
212
+ const is_widened = std.mem.eql(u8, type_annotation, "string") or
213
+ std.mem.eql(u8, type_annotation, "number") or
214
+ std.mem.eql(u8, type_annotation, "boolean");
215
+ if (is_widened and trimmed_val.len > 0) {
216
+ default_val = trimmed_val;
217
+ }
218
+ } else if (trimmed_val.len > 0 and (trimmed_val[0] == '{' or trimmed_val[0] == '[')) {
219
+ default_val = type_inf.consumeCleanDefault();
220
+ is_container = true;
221
+ }
222
+
223
+ // Skip generated @defaultValue if user already has one
224
+ if (std.mem.indexOf(u8, comments, "@defaultValue") != null) {
225
+ default_val = null;
226
+ }
227
+
228
+ if (default_val) |dv| {
229
+ // Build the @defaultValue tag content using direct alloc
230
+ const tag_max = 24 + dv.len * 4 + 16; // generous upper bound for multi-line
231
+ const tag_mem = try alloc.alloc(u8, tag_max);
232
+ var tp: usize = 0;
233
+
234
+ if (std.mem.indexOf(u8, dv, "\n")) |_| {
235
+ const hdr = "@defaultValue\n * ```ts\n";
236
+ @memcpy(tag_mem[tp..][0..hdr.len], hdr); tp += hdr.len;
237
+ var line_iter = std.mem.splitScalar(u8, dv, '\n');
238
+ while (line_iter.next()) |line| {
239
+ @memcpy(tag_mem[tp..][0..3], " * "); tp += 3;
240
+ @memcpy(tag_mem[tp..][0..line.len], line); tp += line.len;
241
+ tag_mem[tp] = '\n'; tp += 1;
242
+ }
243
+ @memcpy(tag_mem[tp..][0..6], " * ```"); tp += 6;
244
+ } else if (is_container) {
245
+ @memcpy(tag_mem[tp..][0..15], "@defaultValue `"); tp += 15;
246
+ @memcpy(tag_mem[tp..][0..dv.len], dv); tp += dv.len;
247
+ tag_mem[tp] = '`'; tp += 1;
248
+ } else {
249
+ @memcpy(tag_mem[tp..][0..14], "@defaultValue "); tp += 14;
250
+ @memcpy(tag_mem[tp..][0..dv.len], dv); tp += dv.len;
251
+ }
252
+ const default_tag = tag_mem[0..tp];
253
+
254
+ // Build rebuilt output using direct alloc
255
+ const export_prefix: []const u8 = if (decl.is_exported) "export " else "";
256
+ const decl_suffix_len = export_prefix.len + 8 + kind.len + 1 + decl.name.len + 2 + type_annotation.len + 1; // "declare " + kind + ' ' + name + ": " + type + ';'
257
+ const rebuilt_max = comments.len + default_tag.len + decl_suffix_len + 64;
258
+ const rbuf = try alloc.alloc(u8, rebuilt_max);
259
+ var rp: usize = 0;
260
+
261
+ // Try to merge into existing JSDoc comment block
262
+ const closing_idx = std.mem.lastIndexOf(u8, comments, "*/");
263
+ if (closing_idx != null and comments.len > 0) {
264
+ const before_raw = comments[0..closing_idx.?];
265
+ var end = before_raw.len;
266
+ while (end > 0 and (before_raw[end - 1] == ' ' or before_raw[end - 1] == '\t' or before_raw[end - 1] == '\n' or before_raw[end - 1] == '\r')) : (end -= 1) {}
267
+ const before = before_raw[0..end];
268
+ if (before.len > 4 and ch.startsWith(before, "/** ") and std.mem.indexOf(u8, before, "\n") == null) {
269
+ @memcpy(rbuf[rp..][0..7], "/**\n * "); rp += 7;
270
+ @memcpy(rbuf[rp..][0..before.len - 4], before[4..]); rp += before.len - 4;
271
+ } else {
272
+ @memcpy(rbuf[rp..][0..before.len], before); rp += before.len;
273
+ }
274
+ @memcpy(rbuf[rp..][0..4], "\n * "); rp += 4;
275
+ @memcpy(rbuf[rp..][0..default_tag.len], default_tag); rp += default_tag.len;
276
+ @memcpy(rbuf[rp..][0..5], "\n */\n"); rp += 5;
277
+ } else if (comments.len > 0) {
278
+ const trimmed_cmt = std.mem.trim(u8, comments, " \t\n\r");
279
+ const cmt_text = if (ch.startsWith(trimmed_cmt, "// "))
280
+ trimmed_cmt[3..]
281
+ else if (ch.startsWith(trimmed_cmt, "//"))
282
+ trimmed_cmt[2..]
283
+ else
284
+ trimmed_cmt;
285
+ @memcpy(rbuf[rp..][0..7], "/**\n * "); rp += 7;
286
+ @memcpy(rbuf[rp..][0..cmt_text.len], cmt_text); rp += cmt_text.len;
287
+ @memcpy(rbuf[rp..][0..4], "\n * "); rp += 4;
288
+ @memcpy(rbuf[rp..][0..default_tag.len], default_tag); rp += default_tag.len;
289
+ @memcpy(rbuf[rp..][0..5], "\n */\n"); rp += 5;
290
+ } else {
291
+ if (std.mem.indexOf(u8, default_tag, "\n") != null) {
292
+ @memcpy(rbuf[rp..][0..7], "/**\n * "); rp += 7;
293
+ @memcpy(rbuf[rp..][0..default_tag.len], default_tag); rp += default_tag.len;
294
+ @memcpy(rbuf[rp..][0..5], "\n */\n"); rp += 5;
295
+ } else {
296
+ @memcpy(rbuf[rp..][0..4], "/** "); rp += 4;
297
+ @memcpy(rbuf[rp..][0..default_tag.len], default_tag); rp += default_tag.len;
298
+ @memcpy(rbuf[rp..][0..4], " */\n"); rp += 4;
299
+ }
300
+ }
301
+
302
+ @memcpy(rbuf[rp..][0..export_prefix.len], export_prefix); rp += export_prefix.len;
303
+ @memcpy(rbuf[rp..][0..8], "declare "); rp += 8;
304
+ @memcpy(rbuf[rp..][0..kind.len], kind); rp += kind.len;
305
+ rbuf[rp] = ' '; rp += 1;
306
+ @memcpy(rbuf[rp..][0..decl.name.len], decl.name); rp += decl.name.len;
307
+ @memcpy(rbuf[rp..][0..2], ": "); rp += 2;
308
+ @memcpy(rbuf[rp..][0..type_annotation.len], type_annotation); rp += type_annotation.len;
309
+ rbuf[rp] = ';'; rp += 1;
310
+ return rbuf[0..rp];
311
+ }
312
+ }
313
+
314
+ // Build final result with direct alloc
315
+ const export_prefix: []const u8 = if (decl.is_exported) "export " else "";
316
+ const total = comments.len + export_prefix.len + 8 + kind.len + 1 + decl.name.len + 2 + type_annotation.len + 1;
317
+ const buf = try alloc.alloc(u8, total);
318
+ var pos: usize = 0;
319
+
320
+ if (comments.len > 0) { @memcpy(buf[pos..][0..comments.len], comments); pos += comments.len; }
321
+ @memcpy(buf[pos..][0..export_prefix.len], export_prefix); pos += export_prefix.len;
322
+ @memcpy(buf[pos..][0..8], "declare "); pos += 8;
323
+ @memcpy(buf[pos..][0..kind.len], kind); pos += kind.len;
324
+ buf[pos] = ' '; pos += 1;
325
+ @memcpy(buf[pos..][0..decl.name.len], decl.name); pos += decl.name.len;
326
+ @memcpy(buf[pos..][0..2], ": "); pos += 2;
327
+ @memcpy(buf[pos..][0..type_annotation.len], type_annotation); pos += type_annotation.len;
328
+ buf[pos] = ';'; pos += 1;
329
+
330
+ return buf[0..pos];
331
+ }
332
+
333
+ /// Process an interface declaration for DTS output
334
+ fn processInterfaceDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_comments: bool) ![]const u8 {
335
+ const comments = try formatComments(alloc, decl.leading_comments, keep_comments);
336
+
337
+ // If the text already starts with proper keywords, use it
338
+ if (ch.startsWith(decl.text, "export declare interface") or ch.startsWith(decl.text, "declare interface")) {
339
+ if (comments.len == 0) return decl.text;
340
+ const buf = try alloc.alloc(u8, comments.len + decl.text.len);
341
+ @memcpy(buf[0..comments.len], comments);
342
+ @memcpy(buf[comments.len..][0..decl.text.len], decl.text);
343
+ return buf;
344
+ }
345
+
346
+ // Direct alloc: compute max size
347
+ const export_prefix: []const u8 = if (decl.is_exported) "export " else "";
348
+ const extends_kw: []const u8 = if (decl.extends_clause.len > 0) " extends " else "";
349
+ const body_start = ch.indexOfChar(decl.text, '{', 0);
350
+ const body = if (body_start) |bi| decl.text[bi..] else "{}";
351
+ const max_len = comments.len + export_prefix.len + "declare interface ".len +
352
+ decl.name.len + decl.generics.len + extends_kw.len + decl.extends_clause.len + 1 + body.len;
353
+
354
+ const buf = try alloc.alloc(u8, max_len);
355
+ var pos: usize = 0;
356
+ @memcpy(buf[pos..][0..comments.len], comments);
357
+ pos += comments.len;
358
+ @memcpy(buf[pos..][0..export_prefix.len], export_prefix);
359
+ pos += export_prefix.len;
360
+ const di = "declare interface ";
361
+ @memcpy(buf[pos..][0..di.len], di);
362
+ pos += di.len;
363
+ @memcpy(buf[pos..][0..decl.name.len], decl.name);
364
+ pos += decl.name.len;
365
+ if (decl.generics.len > 0) {
366
+ @memcpy(buf[pos..][0..decl.generics.len], decl.generics);
367
+ pos += decl.generics.len;
368
+ }
369
+ if (decl.extends_clause.len > 0) {
370
+ @memcpy(buf[pos..][0..extends_kw.len], extends_kw);
371
+ pos += extends_kw.len;
372
+ @memcpy(buf[pos..][0..decl.extends_clause.len], decl.extends_clause);
373
+ pos += decl.extends_clause.len;
374
+ }
375
+ buf[pos] = ' ';
376
+ pos += 1;
377
+ @memcpy(buf[pos..][0..body.len], body);
378
+ pos += body.len;
379
+
380
+ return buf[0..pos];
381
+ }
382
+
383
+ /// Process a type alias declaration for DTS output
384
+ fn processTypeDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_comments: bool) ![]const u8 {
385
+ const comments = try formatComments(alloc, decl.leading_comments, keep_comments);
386
+
387
+ const export_prefix: []const u8 = if (decl.is_exported) "export " else "";
388
+ const declare_prefix: []const u8 = if (!decl.is_exported and !ch.contains(decl.text, " from ")) "declare " else "";
389
+
390
+ // Extract type definition from original text
391
+ var type_def: []const u8 = undefined;
392
+ var fallback = false;
393
+ if (ch.indexOf(decl.text, "type ", 0)) |type_idx| {
394
+ var td = decl.text[type_idx..];
395
+ var end = td.len;
396
+ while (end > 0 and (td[end - 1] == ';' or td[end - 1] == ' ' or td[end - 1] == '\n' or td[end - 1] == '\r')) end -= 1;
397
+ type_def = td[0..end];
398
+ } else {
399
+ fallback = true;
400
+ type_def = ""; // unused, assembled below
401
+ }
402
+
403
+ // Calculate total length
404
+ const needs_semi = if (!fallback)
405
+ (type_def.len > 0 and type_def[type_def.len - 1] != ';' and type_def[type_def.len - 1] != '}')
406
+ else
407
+ true; // fallback " = any" always needs semi
408
+
409
+ const body_len = if (!fallback) type_def.len else 5 + decl.name.len + decl.generics.len + 6; // "type " + name + generics + " = any"
410
+ const total = comments.len + export_prefix.len + declare_prefix.len + body_len + @as(usize, if (needs_semi) 1 else 0);
411
+
412
+ const buf = try alloc.alloc(u8, total);
413
+ var pos: usize = 0;
414
+
415
+ if (comments.len > 0) { @memcpy(buf[pos..][0..comments.len], comments); pos += comments.len; }
416
+ if (export_prefix.len > 0) { @memcpy(buf[pos..][0..export_prefix.len], export_prefix); pos += export_prefix.len; }
417
+ if (declare_prefix.len > 0) { @memcpy(buf[pos..][0..declare_prefix.len], declare_prefix); pos += declare_prefix.len; }
418
+
419
+ if (!fallback) {
420
+ @memcpy(buf[pos..][0..type_def.len], type_def);
421
+ pos += type_def.len;
422
+ } else {
423
+ @memcpy(buf[pos..][0..5], "type ");
424
+ pos += 5;
425
+ @memcpy(buf[pos..][0..decl.name.len], decl.name);
426
+ pos += decl.name.len;
427
+ if (decl.generics.len > 0) { @memcpy(buf[pos..][0..decl.generics.len], decl.generics); pos += decl.generics.len; }
428
+ @memcpy(buf[pos..][0..6], " = any");
429
+ pos += 6;
430
+ }
431
+
432
+ if (needs_semi) { buf[pos] = ';'; pos += 1; }
433
+
434
+ return buf[0..pos];
435
+ }
436
+
437
+ /// Process an enum declaration for DTS output
438
+ fn processEnumDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_comments: bool) ![]const u8 {
439
+ const comments = try formatComments(alloc, decl.leading_comments, keep_comments);
440
+
441
+ const export_prefix: []const u8 = if (decl.is_exported) "export " else "";
442
+ var is_const = false;
443
+ if (decl.modifiers) |mods| {
444
+ for (mods) |m| {
445
+ if (std.mem.eql(u8, m, "const")) {
446
+ is_const = true;
447
+ break;
448
+ }
449
+ }
450
+ }
451
+ const const_kw: []const u8 = if (is_const) "const " else "";
452
+ const body_start = ch.indexOfChar(decl.text, '{', 0);
453
+ const body = if (body_start) |bi| decl.text[bi..] else "{}";
454
+
455
+ const buf = try alloc.alloc(u8, comments.len + export_prefix.len + "declare ".len +
456
+ const_kw.len + "enum ".len + decl.name.len + 1 + body.len);
457
+ var pos: usize = 0;
458
+ @memcpy(buf[pos..][0..comments.len], comments);
459
+ pos += comments.len;
460
+ @memcpy(buf[pos..][0..export_prefix.len], export_prefix);
461
+ pos += export_prefix.len;
462
+ const dec = "declare ";
463
+ @memcpy(buf[pos..][0..dec.len], dec);
464
+ pos += dec.len;
465
+ @memcpy(buf[pos..][0..const_kw.len], const_kw);
466
+ pos += const_kw.len;
467
+ const en = "enum ";
468
+ @memcpy(buf[pos..][0..en.len], en);
469
+ pos += en.len;
470
+ @memcpy(buf[pos..][0..decl.name.len], decl.name);
471
+ pos += decl.name.len;
472
+ buf[pos] = ' ';
473
+ pos += 1;
474
+ @memcpy(buf[pos..][0..body.len], body);
475
+ pos += body.len;
476
+
477
+ return buf[0..pos];
478
+ }
479
+
480
+ /// Process a module/namespace declaration for DTS output
481
+ fn processModuleDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_comments: bool) ![]const u8 {
482
+ const comments = try formatComments(alloc, decl.leading_comments, keep_comments);
483
+
484
+ // Global augmentation
485
+ if (ch.startsWith(decl.text, "declare global")) {
486
+ if (comments.len == 0) return decl.text;
487
+ var result = std.array_list.Managed(u8).init(alloc);
488
+ try result.ensureTotalCapacity(comments.len + decl.text.len);
489
+ try result.appendSlice(comments);
490
+ try result.appendSlice(decl.text);
491
+ return result.toOwnedSlice();
492
+ }
493
+
494
+ // Ambient module (quoted name)
495
+ const is_ambient = decl.source_module.len > 0 or
496
+ (decl.name.len > 0 and (decl.name[0] == '"' or decl.name[0] == '\'' or decl.name[0] == '`'));
497
+
498
+ if (is_ambient) {
499
+ var result = std.array_list.Managed(u8).init(alloc);
500
+ try result.ensureTotalCapacity(comments.len + decl.text.len + 32);
501
+ try result.appendSlice(comments);
502
+ try result.appendSlice("declare module ");
503
+ try result.appendSlice(decl.name);
504
+
505
+ if (ch.indexOfChar(decl.text, '{', 0)) |brace_idx| {
506
+ try result.append(' ');
507
+ try result.appendSlice(decl.text[brace_idx..]);
508
+ } else {
509
+ try result.appendSlice(" {}");
510
+ }
511
+ return result.toOwnedSlice();
512
+ }
513
+
514
+ // Regular namespace
515
+ var result = std.array_list.Managed(u8).init(alloc);
516
+ try result.ensureTotalCapacity(comments.len + decl.text.len + 32);
517
+ try result.appendSlice(comments);
518
+
519
+ if (decl.is_exported) try result.appendSlice("export ");
520
+
521
+ // Check if declare is already in modifiers
522
+ var has_declare = false;
523
+ if (decl.modifiers) |mods| {
524
+ for (mods) |m| {
525
+ if (std.mem.eql(u8, m, "declare")) {
526
+ has_declare = true;
527
+ break;
528
+ }
529
+ }
530
+ }
531
+ if (!has_declare) try result.appendSlice("declare ");
532
+
533
+ try result.appendSlice("namespace ");
534
+ try result.appendSlice(decl.name);
535
+
536
+ if (ch.indexOfChar(decl.text, '{', 0)) |brace_idx| {
537
+ try result.append(' ');
538
+ try result.appendSlice(decl.text[brace_idx..]);
539
+ } else {
540
+ try result.appendSlice(" {}");
541
+ }
542
+
543
+ return result.toOwnedSlice();
544
+ }
545
+
546
+ /// Main entry point: process declarations array into final .d.ts output.
547
+ /// `result_alloc` is used for the final output buffer (may differ from `alloc`
548
+ /// so the result can survive arena reset in FFI mode).
549
+ pub fn processDeclarations(
550
+ alloc: std.mem.Allocator,
551
+ result_alloc: std.mem.Allocator,
552
+ declarations: []const Declaration,
553
+ source_code: []const u8,
554
+ keep_comments: bool,
555
+ import_order: []const []const u8,
556
+ ) ![]const u8 {
557
+ var result = std.array_list.Managed(u8).init(result_alloc);
558
+ try result.ensureTotalCapacity(source_code.len);
559
+
560
+ // Extract triple-slash directives
561
+ // Fast check: skip whitespace
562
+ var si: usize = 0;
563
+ while (si < source_code.len and ch.isWhitespace(source_code[si])) si += 1;
564
+ if (si + 2 < source_code.len and source_code[si] == '/' and source_code[si + 1] == '/' and source_code[si + 2] == '/') {
565
+ const directives = try extractTripleSlashDirectives(alloc, source_code);
566
+ for (directives) |d| {
567
+ if (result.items.len > 0) try result.append('\n');
568
+ try result.appendSlice(d);
569
+ }
570
+ }
571
+
572
+ // Group declarations by type (pre-size to avoid incremental reallocation)
573
+ const group_cap: usize = @max(declarations.len / 4, 4);
574
+ var imports = std.array_list.Managed(Declaration).init(alloc);
575
+ try imports.ensureTotalCapacity(group_cap);
576
+ var functions = std.array_list.Managed(Declaration).init(alloc);
577
+ try functions.ensureTotalCapacity(group_cap);
578
+ var variables = std.array_list.Managed(Declaration).init(alloc);
579
+ try variables.ensureTotalCapacity(group_cap);
580
+ var interfaces = std.array_list.Managed(Declaration).init(alloc);
581
+ try interfaces.ensureTotalCapacity(group_cap);
582
+ var type_decls = std.array_list.Managed(Declaration).init(alloc);
583
+ try type_decls.ensureTotalCapacity(group_cap);
584
+ var classes = std.array_list.Managed(Declaration).init(alloc);
585
+ try classes.ensureTotalCapacity(group_cap);
586
+ var enums = std.array_list.Managed(Declaration).init(alloc);
587
+ try enums.ensureTotalCapacity(group_cap);
588
+ var modules = std.array_list.Managed(Declaration).init(alloc);
589
+ try modules.ensureTotalCapacity(group_cap);
590
+ var exports = std.array_list.Managed(Declaration).init(alloc);
591
+ try exports.ensureTotalCapacity(group_cap);
592
+
593
+ for (declarations) |d| {
594
+ switch (d.kind) {
595
+ .import_decl => try imports.append(d),
596
+ .function_decl => try functions.append(d),
597
+ .variable_decl => try variables.append(d),
598
+ .interface_decl => try interfaces.append(d),
599
+ .type_decl => try type_decls.append(d),
600
+ .class_decl => try classes.append(d),
601
+ .enum_decl => try enums.append(d),
602
+ .module_decl, .namespace_decl => try modules.append(d),
603
+ .export_decl => try exports.append(d),
604
+ .unknown_decl => {},
605
+ }
606
+ }
607
+
608
+ // Parse exports to track exported items
609
+ var exported_items = std.StringHashMap(void).init(alloc);
610
+ try exported_items.ensureTotalCapacity(@intCast(@max(exports.items.len * 4, 8)));
611
+ var type_export_stmts = std.array_list.Managed([]const u8).init(alloc);
612
+ try type_export_stmts.ensureTotalCapacity(@max(exports.items.len / 2, 2));
613
+ var value_export_stmts = std.array_list.Managed([]const u8).init(alloc);
614
+ try value_export_stmts.ensureTotalCapacity(@max(exports.items.len / 2, 2));
615
+ var default_exports = std.array_list.Managed([]const u8).init(alloc);
616
+ var seen_exports = std.StringHashMap(void).init(alloc);
617
+ try seen_exports.ensureTotalCapacity(@intCast(@max(exports.items.len, 4)));
618
+
619
+ // Reusable buffer for building export statements (avoids per-iteration alloc)
620
+ var stmt_buf = std.array_list.Managed(u8).init(alloc);
621
+ try stmt_buf.ensureTotalCapacity(256);
622
+
623
+ for (exports.items) |decl| {
624
+ const comments = try formatComments(alloc, decl.leading_comments, keep_comments);
625
+
626
+ if (ch.startsWith(decl.text, "export default")) {
627
+ stmt_buf.clearRetainingCapacity();
628
+ try stmt_buf.appendSlice(comments);
629
+ try stmt_buf.appendSlice(decl.text);
630
+ if (!ch.endsWith(decl.text, ";")) try stmt_buf.append(';');
631
+ try default_exports.append(try stmt_buf.toOwnedSlice());
632
+ } else {
633
+ var export_text = ch.sliceTrimmed(decl.text, 0, decl.text.len);
634
+ // Ensure semicolon
635
+ if (!ch.endsWith(export_text, ";")) {
636
+ stmt_buf.clearRetainingCapacity();
637
+ try stmt_buf.appendSlice(export_text);
638
+ try stmt_buf.append(';');
639
+ export_text = try stmt_buf.toOwnedSlice();
640
+ }
641
+
642
+ // Extract exported items for tracking
643
+ // Look for export { items } or export type { items }
644
+ if (ch.indexOfChar(export_text, '{', 0)) |brace_s| {
645
+ if (ch.indexOfChar(export_text, '}', brace_s)) |brace_e| {
646
+ const items_str = export_text[brace_s + 1 .. brace_e];
647
+ var iter = std.mem.splitSequence(u8, items_str, ",");
648
+ while (iter.next()) |item| {
649
+ const trimmed_item = ch.sliceTrimmed(item, 0, item.len);
650
+ if (trimmed_item.len > 0) {
651
+ try exported_items.put(trimmed_item, {});
652
+ }
653
+ }
654
+ }
655
+ }
656
+
657
+ stmt_buf.clearRetainingCapacity();
658
+ try stmt_buf.appendSlice(comments);
659
+ try stmt_buf.appendSlice(export_text);
660
+ const full_text = try stmt_buf.toOwnedSlice();
661
+
662
+ const gop = try seen_exports.getOrPut(full_text);
663
+ if (!gop.found_existing) {
664
+ if (ch.contains(full_text, "export type")) {
665
+ try type_export_stmts.append(full_text);
666
+ } else {
667
+ try value_export_stmts.append(full_text);
668
+ }
669
+ }
670
+ }
671
+ }
672
+
673
+ // Short-circuit: skip combined_words building and import map when there are no imports.
674
+ // For import-free code (e.g. synthetic benchmarks), this avoids O(n) word
675
+ // extraction across all declarations — a significant saving on large inputs.
676
+ var interface_references = std.StringHashMap(void).init(alloc);
677
+ var processed_imports = std.array_list.Managed([]const u8).init(alloc);
678
+
679
+ if (imports.items.len > 0) {
680
+ // Build import-item-to-declaration map (only when imports exist)
681
+ const ImportDeclMap = std.StringHashMap(Declaration);
682
+ var all_imported_items_map = ImportDeclMap.init(alloc);
683
+ try all_imported_items_map.ensureTotalCapacity(@intCast(@max(imports.items.len * 4, 8)));
684
+ for (imports.items) |imp| {
685
+ const items = extractAllImportedItems(imp);
686
+ for (items) |item| {
687
+ try all_imported_items_map.put(item, imp);
688
+ }
689
+ }
690
+ // Build combined word set for interface reference detection AND import filtering.
691
+ // Single pass over all declarations instead of 7 separate loops per group.
692
+ var combined_words = std.StringHashMap(void).init(alloc);
693
+ try combined_words.ensureTotalCapacity(@intCast(@max(declarations.len * 4, 128)));
694
+ for (declarations) |d| {
695
+ switch (d.kind) {
696
+ .function_decl => {
697
+ if (d.is_exported) extractWords(&combined_words, d.text);
698
+ },
699
+ .variable_decl => {
700
+ if (d.is_exported) {
701
+ extractWords(&combined_words, d.text);
702
+ if (d.type_annotation.len > 0)
703
+ extractWords(&combined_words, d.type_annotation);
704
+ }
705
+ },
706
+ .type_decl, .class_decl, .enum_decl, .module_decl, .namespace_decl, .export_decl => {
707
+ extractWords(&combined_words, d.text);
708
+ },
709
+ .interface_decl, .import_decl, .unknown_decl => {},
710
+ }
711
+ }
712
+
713
+ // Interface reference detection
714
+ try interface_references.ensureTotalCapacity(@intCast(@max(interfaces.items.len, 4)));
715
+ if (interfaces.items.len > 0 and combined_words.count() > 0) {
716
+ for (interfaces.items) |iface| {
717
+ if (combined_words.contains(iface.name)) {
718
+ try interface_references.put(iface.name, {});
719
+ }
720
+ }
721
+ }
722
+
723
+ // Add interface text to combined_words (after ref detection, before import filtering)
724
+ for (interfaces.items) |iface| {
725
+ if (iface.is_exported or interface_references.contains(iface.name)) {
726
+ extractWords(&combined_words, iface.text);
727
+ }
728
+ }
729
+ var used_import_items = std.StringHashMap(void).init(alloc);
730
+ try used_import_items.ensureTotalCapacity(@intCast(@max(imports.items.len * 4, 8)));
731
+
732
+ if (combined_words.count() > 0) {
733
+ var map_iter = all_imported_items_map.keyIterator();
734
+ while (map_iter.next()) |key_ptr| {
735
+ if (combined_words.contains(key_ptr.*)) {
736
+ try used_import_items.put(key_ptr.*, {});
737
+ }
738
+ }
739
+ }
740
+
741
+ // Check re-exports
742
+ var exp_iter = exported_items.keyIterator();
743
+ while (exp_iter.next()) |key_ptr| {
744
+ if (all_imported_items_map.contains(key_ptr.*)) {
745
+ try used_import_items.put(key_ptr.*, {});
746
+ }
747
+ }
748
+
749
+ // Filter and rebuild imports
750
+ try processed_imports.ensureTotalCapacity(imports.items.len);
751
+ // Reusable buffer for building import statements
752
+ var import_buf = std.array_list.Managed(u8).init(alloc);
753
+ try import_buf.ensureTotalCapacity(256);
754
+ var used_named = std.array_list.Managed([]const u8).init(alloc);
755
+ try used_named.ensureTotalCapacity(16);
756
+
757
+ for (imports.items) |imp| {
758
+ // Preserve side-effect imports
759
+ if (imp.is_side_effect) {
760
+ const trimmed_imp = ch.sliceTrimmed(imp.text, 0, imp.text.len);
761
+ import_buf.clearRetainingCapacity();
762
+ try import_buf.appendSlice(trimmed_imp);
763
+ if (!ch.endsWith(trimmed_imp, ";")) try import_buf.append(';');
764
+ try processed_imports.append(try import_buf.toOwnedSlice());
765
+ continue;
766
+ }
767
+
768
+ const parsed = getParsedImport(imp) orelse continue;
769
+
770
+ const used_default = if (parsed.default_name) |dn| used_import_items.contains(dn) else false;
771
+ used_named.clearRetainingCapacity();
772
+
773
+ for (parsed.named_items) |item| {
774
+ var clean_item = item;
775
+ if (ch.startsWith(clean_item, "type ")) {
776
+ clean_item = ch.sliceTrimmed(clean_item, 5, clean_item.len);
777
+ }
778
+ if (ch.indexOf(clean_item, " as ", 0)) |as_idx| {
779
+ clean_item = ch.sliceTrimmed(clean_item, as_idx + 4, clean_item.len);
780
+ }
781
+ if (used_import_items.contains(clean_item)) {
782
+ try used_named.append(item);
783
+ }
784
+ }
785
+
786
+ if (used_default or used_named.items.len > 0) {
787
+ import_buf.clearRetainingCapacity();
788
+ if (parsed.is_type_only) {
789
+ try import_buf.appendSlice("import type ");
790
+ } else {
791
+ try import_buf.appendSlice("import ");
792
+ }
793
+
794
+ if (used_default) {
795
+ if (parsed.default_name) |dn| try import_buf.appendSlice(dn);
796
+ if (used_named.items.len > 0) {
797
+ try import_buf.appendSlice(", { ");
798
+ for (used_named.items, 0..) |ni, idx| {
799
+ if (idx > 0) try import_buf.appendSlice(", ");
800
+ try import_buf.appendSlice(ni);
801
+ }
802
+ try import_buf.appendSlice(" }");
803
+ }
804
+ } else if (used_named.items.len > 0) {
805
+ try import_buf.appendSlice("{ ");
806
+ for (used_named.items, 0..) |ni, idx| {
807
+ if (idx > 0) try import_buf.appendSlice(", ");
808
+ try import_buf.appendSlice(ni);
809
+ }
810
+ try import_buf.appendSlice(" }");
811
+ }
812
+
813
+ try import_buf.appendSlice(" from '");
814
+ try import_buf.appendSlice(parsed.source);
815
+ try import_buf.appendSlice("';");
816
+
817
+ try processed_imports.append(try import_buf.toOwnedSlice());
818
+ }
819
+ }
820
+
821
+ // Sort imports by priority then locale-aware alphabetical
822
+ if (processed_imports.items.len > 1) {
823
+ const SortCtx = struct {
824
+ import_order_items: []const []const u8,
825
+ default_priority: usize,
826
+
827
+ pub fn priority(self: @This(), imp: []const u8) usize {
828
+ for (self.import_order_items, 0..) |p, idx| {
829
+ var found = false;
830
+ if (ch.indexOf(imp, "from '", 0)) |fi| {
831
+ if (ch.indexOf(imp, p, fi + 6) != null) found = true;
832
+ }
833
+ if (!found) {
834
+ if (ch.indexOf(imp, "from \"", 0)) |fi| {
835
+ if (ch.indexOf(imp, p, fi + 6) != null) found = true;
836
+ }
837
+ }
838
+ if (found) return idx;
839
+ }
840
+ return self.default_priority;
841
+ }
842
+
843
+ /// Locale-aware char sort key: symbols < digits < letters
844
+ /// Matches JavaScript's localeCompare behavior
845
+ fn charSortKey(c_val: u8) u32 {
846
+ if (c_val >= 'a' and c_val <= 'z') return @as(u32, c_val - 'a') * 4 + 1000;
847
+ if (c_val >= 'A' and c_val <= 'Z') return @as(u32, c_val - 'A') * 4 + 1001;
848
+ if (c_val >= '0' and c_val <= '9') return @as(u32, c_val - '0') + 500;
849
+ return @as(u32, c_val);
850
+ }
851
+
852
+ pub fn localeCompare(_: @This(), a: []const u8, b: []const u8) bool {
853
+ const min_len = @min(a.len, b.len);
854
+ for (0..min_len) |i| {
855
+ const ak = charSortKey(a[i]);
856
+ const bk = charSortKey(b[i]);
857
+ if (ak != bk) return ak < bk;
858
+ }
859
+ return a.len < b.len;
860
+ }
861
+ };
862
+
863
+ const ctx = SortCtx{
864
+ .import_order_items = import_order,
865
+ .default_priority = import_order.len,
866
+ };
867
+
868
+ std.mem.sort([]const u8, processed_imports.items, ctx, struct {
869
+ fn lessThan(c_ctx: SortCtx, a: []const u8, b: []const u8) bool {
870
+ const ap = c_ctx.priority(a);
871
+ const bp = c_ctx.priority(b);
872
+ if (ap != bp) return ap < bp;
873
+ return c_ctx.localeCompare(a, b);
874
+ }
875
+ }.lessThan);
876
+ }
877
+ }
878
+
879
+ // Emit imports
880
+ for (processed_imports.items) |imp| {
881
+ if (result.items.len > 0) try result.append('\n');
882
+ try result.appendSlice(imp);
883
+ }
884
+
885
+ // Emit type exports
886
+ for (type_export_stmts.items) |stmt| {
887
+ if (result.items.len > 0) try result.append('\n');
888
+ try result.appendSlice(stmt);
889
+ }
890
+
891
+ // Emit declaration groups: functions, variables, interfaces, types, classes, enums, modules
892
+ const decl_groups = [_]struct { items: []const Declaration, kind_tag: u8 }{
893
+ .{ .items = functions.items, .kind_tag = 'f' },
894
+ .{ .items = variables.items, .kind_tag = 'v' },
895
+ .{ .items = interfaces.items, .kind_tag = 'i' },
896
+ .{ .items = type_decls.items, .kind_tag = 't' },
897
+ .{ .items = classes.items, .kind_tag = 'c' },
898
+ .{ .items = enums.items, .kind_tag = 'e' },
899
+ .{ .items = modules.items, .kind_tag = 'm' },
900
+ };
901
+
902
+ for (decl_groups) |group| {
903
+ for (group.items) |decl| {
904
+ switch (decl.kind) {
905
+ .function_decl, .class_decl => {
906
+ // Direct emit: write comments + text straight to result buffer
907
+ // avoiding intermediate allocation + double copy
908
+ if (decl.text.len == 0) continue;
909
+ if (result.items.len > 0) try result.append('\n');
910
+ if (keep_comments) {
911
+ if (decl.leading_comments) |cmts| {
912
+ if (cmts.len > 0) {
913
+ const comments = try formatComments(alloc, cmts, true);
914
+ if (comments.len > 0) try result.appendSlice(comments);
915
+ }
916
+ }
917
+ }
918
+ try result.appendSlice(decl.text);
919
+ },
920
+ .variable_decl => {
921
+ const processed = try processVariableDeclaration(alloc, decl, keep_comments);
922
+ if (processed.len > 0) {
923
+ if (result.items.len > 0) try result.append('\n');
924
+ try result.appendSlice(processed);
925
+ }
926
+ },
927
+ .interface_decl => {
928
+ // Fast path: text already has correct keywords → direct emit
929
+ if (decl.text.len > 0 and (ch.startsWith(decl.text, "export declare interface") or
930
+ ch.startsWith(decl.text, "declare interface")))
931
+ {
932
+ if (result.items.len > 0) try result.append('\n');
933
+ if (keep_comments) {
934
+ if (decl.leading_comments) |cmts| {
935
+ if (cmts.len > 0) {
936
+ const comments = try formatComments(alloc, cmts, true);
937
+ if (comments.len > 0) try result.appendSlice(comments);
938
+ }
939
+ }
940
+ }
941
+ try result.appendSlice(decl.text);
942
+ } else {
943
+ const processed = try processInterfaceDeclaration(alloc, decl, keep_comments);
944
+ if (processed.len > 0) {
945
+ if (result.items.len > 0) try result.append('\n');
946
+ try result.appendSlice(processed);
947
+ }
948
+ }
949
+ },
950
+ .type_decl => {
951
+ const processed = try processTypeDeclaration(alloc, decl, keep_comments);
952
+ if (processed.len > 0) {
953
+ if (result.items.len > 0) try result.append('\n');
954
+ try result.appendSlice(processed);
955
+ }
956
+ },
957
+ .enum_decl => {
958
+ // Fast path: text already has correct keywords → direct emit
959
+ if (decl.text.len > 0 and ch.startsWith(decl.text, "declare ")) {
960
+ if (result.items.len > 0) try result.append('\n');
961
+ if (keep_comments) {
962
+ if (decl.leading_comments) |cmts| {
963
+ if (cmts.len > 0) {
964
+ const comments = try formatComments(alloc, cmts, true);
965
+ if (comments.len > 0) try result.appendSlice(comments);
966
+ }
967
+ }
968
+ }
969
+ if (decl.is_exported) try result.appendSlice("export ");
970
+ try result.appendSlice(decl.text);
971
+ } else {
972
+ const processed = try processEnumDeclaration(alloc, decl, keep_comments);
973
+ if (processed.len > 0) {
974
+ if (result.items.len > 0) try result.append('\n');
975
+ try result.appendSlice(processed);
976
+ }
977
+ }
978
+ },
979
+ .module_decl, .namespace_decl => {
980
+ // Fast path: text already has correct keywords → direct emit
981
+ if (decl.text.len > 0 and (ch.startsWith(decl.text, "export declare namespace") or
982
+ ch.startsWith(decl.text, "declare namespace") or
983
+ ch.startsWith(decl.text, "declare module")))
984
+ {
985
+ if (result.items.len > 0) try result.append('\n');
986
+ if (keep_comments) {
987
+ if (decl.leading_comments) |cmts| {
988
+ if (cmts.len > 0) {
989
+ const comments = try formatComments(alloc, cmts, true);
990
+ if (comments.len > 0) try result.appendSlice(comments);
991
+ }
992
+ }
993
+ }
994
+ try result.appendSlice(decl.text);
995
+ } else {
996
+ const processed = try processModuleDeclaration(alloc, decl, keep_comments);
997
+ if (processed.len > 0) {
998
+ if (result.items.len > 0) try result.append('\n');
999
+ try result.appendSlice(processed);
1000
+ }
1001
+ }
1002
+ },
1003
+ else => {},
1004
+ }
1005
+ }
1006
+ }
1007
+
1008
+ // Emit value exports
1009
+ for (value_export_stmts.items) |stmt| {
1010
+ if (result.items.len > 0) try result.append('\n');
1011
+ try result.appendSlice(stmt);
1012
+ }
1013
+
1014
+ // Emit default exports last
1015
+ for (default_exports.items) |stmt| {
1016
+ if (result.items.len > 0) try result.append('\n');
1017
+ try result.appendSlice(stmt);
1018
+ }
1019
+
1020
+ // Append null terminator so FFI callers get a C string without extra copy.
1021
+ // Returned slice length does NOT include the null byte.
1022
+ const content_len = result.items.len;
1023
+ try result.append(0);
1024
+ const owned = try result.toOwnedSlice();
1025
+ return owned[0..content_len];
1026
+ }
1027
+
1028
+ // --- Tests ---
1029
+ test "isWordInText" {
1030
+ try std.testing.expect(isWordInText("Foo", "const x: Foo = 1"));
1031
+ try std.testing.expect(!isWordInText("Foo", "const x: FooBar = 1"));
1032
+ try std.testing.expect(isWordInText("Bar", "type X = Bar | Baz"));
1033
+ try std.testing.expect(!isWordInText("ar", "type X = Bar | Baz"));
1034
+ }
1035
+
1036
+ test "extractTripleSlashDirectives" {
1037
+ const alloc = std.testing.allocator;
1038
+ {
1039
+ const source = "/// <reference types=\"node\" />\nimport { foo } from 'bar';";
1040
+ const directives = try extractTripleSlashDirectives(alloc, source);
1041
+ defer alloc.free(directives);
1042
+ try std.testing.expectEqual(@as(usize, 1), directives.len);
1043
+ try std.testing.expectEqualStrings("/// <reference types=\"node\" />", directives[0]);
1044
+ }
1045
+ }