@stacksjs/zig-dtsx 0.9.17 → 0.9.20

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/zig-dtsx",
3
3
  "type": "module",
4
- "version": "0.9.17",
4
+ "version": "0.9.20",
5
5
  "description": "High-performance DTS emitter written in Zig",
6
6
  "exports": {
7
7
  ".": {
@@ -18,6 +18,6 @@
18
18
  "benchmark": "bun run test/benchmark.ts"
19
19
  },
20
20
  "dependencies": {
21
- "@stacksjs/dtsx": "0.9.17"
21
+ "@stacksjs/dtsx": "0.9.20"
22
22
  }
23
23
  }
@@ -555,11 +555,21 @@ pub fn buildDtsParams(s: *Scanner, raw_params: []const u8) []const u8 {
555
555
  var str_ch: u8 = 0;
556
556
  var skip_next = false;
557
557
 
558
+ var in_block_comment = false;
559
+ var skip_to_eol = false;
558
560
  for (inner, 0..) |c, i| {
559
561
  if (skip_next) {
560
562
  skip_next = false;
561
563
  continue;
562
564
  }
565
+ if (skip_to_eol) {
566
+ if (c == '\n') skip_to_eol = false;
567
+ continue;
568
+ }
569
+ if (in_block_comment) {
570
+ if (c == '/' and i > 0 and inner[i - 1] == '*') in_block_comment = false;
571
+ continue;
572
+ }
563
573
  if (in_str) {
564
574
  if (c == ch.CH_BACKSLASH) {
565
575
  skip_next = true; // skip the escaped character
@@ -568,6 +578,20 @@ pub fn buildDtsParams(s: *Scanner, raw_params: []const u8) []const u8 {
568
578
  if (c == str_ch) in_str = false;
569
579
  continue;
570
580
  }
581
+ // Skip block and line comments — JSDoc prose can contain unmatched
582
+ // quote chars (e.g. apostrophe in "error's") that would otherwise
583
+ // trip the scanner into a string-literal mode it never escapes.
584
+ if (c == '/' and i + 1 < inner.len) {
585
+ const nc = inner[i + 1];
586
+ if (nc == '*') {
587
+ in_block_comment = true;
588
+ continue;
589
+ }
590
+ if (nc == '/') {
591
+ skip_to_eol = true;
592
+ continue;
593
+ }
594
+ }
571
595
  if (c == ch.CH_SQUOTE or c == ch.CH_DQUOTE or c == ch.CH_BACKTICK) {
572
596
  in_str = true;
573
597
  str_ch = c;
@@ -655,11 +679,21 @@ pub fn buildSingleDtsParam(s: *Scanner, raw: []const u8) []const u8 {
655
679
  var str_ch2: u8 = 0;
656
680
  var skip_next2 = false;
657
681
 
682
+ var in_block_comment2 = false;
683
+ var skip_to_eol2 = false;
658
684
  for (p, 0..) |c, i| {
659
685
  if (skip_next2) {
660
686
  skip_next2 = false;
661
687
  continue;
662
688
  }
689
+ if (skip_to_eol2) {
690
+ if (c == '\n') skip_to_eol2 = false;
691
+ continue;
692
+ }
693
+ if (in_block_comment2) {
694
+ if (c == '/' and i > 0 and p[i - 1] == '*') in_block_comment2 = false;
695
+ continue;
696
+ }
663
697
  if (in_str2) {
664
698
  if (c == ch.CH_BACKSLASH) {
665
699
  skip_next2 = true;
@@ -668,6 +702,19 @@ pub fn buildSingleDtsParam(s: *Scanner, raw: []const u8) []const u8 {
668
702
  if (c == str_ch2) in_str2 = false;
669
703
  continue;
670
704
  }
705
+ // Skip block and line comments before string-mode detection so JSDoc
706
+ // apostrophes (e.g. "error's") don't trigger an unclosed string.
707
+ if (c == '/' and i + 1 < p.len) {
708
+ const nc = p[i + 1];
709
+ if (nc == '*') {
710
+ in_block_comment2 = true;
711
+ continue;
712
+ }
713
+ if (nc == '/') {
714
+ skip_to_eol2 = true;
715
+ continue;
716
+ }
717
+ }
671
718
  if (c == ch.CH_SQUOTE or c == ch.CH_DQUOTE or c == ch.CH_BACKTICK) {
672
719
  in_str2 = true;
673
720
  str_ch2 = c;
@@ -763,6 +810,30 @@ fn cleanDestructuredPattern(alloc: std.mem.Allocator, pattern: []const u8) []con
763
810
  while (i < pattern.len) : (i += 1) {
764
811
  const c = pattern[i];
765
812
 
813
+ // Preserve block and line comments verbatim (useful JSDoc) but skip
814
+ // parsing inside them so apostrophes in prose (e.g. "error's") don't
815
+ // put us in an inescapable string mode.
816
+ if (!in_str and c == '/' and i + 1 < pattern.len) {
817
+ const nc = pattern[i + 1];
818
+ if (nc == '*') {
819
+ const start = i;
820
+ var j = i + 2;
821
+ while (j + 1 < pattern.len and !(pattern[j] == '*' and pattern[j + 1] == '/')) : (j += 1) {}
822
+ const end = if (j + 1 < pattern.len) j + 2 else pattern.len;
823
+ result.appendSlice(pattern[start..end]) catch {};
824
+ i = end - 1; // loop's += 1 advances past
825
+ continue;
826
+ }
827
+ if (nc == '/') {
828
+ const start = i;
829
+ var j = i;
830
+ while (j < pattern.len and pattern[j] != '\n') : (j += 1) {}
831
+ result.appendSlice(pattern[start..j]) catch {};
832
+ i = if (j == 0) 0 else j - 1; // loop's += 1 lands on the newline
833
+ continue;
834
+ }
835
+ }
836
+
766
837
  // String tracking
767
838
  if (!in_str and (c == '\'' or c == '"' or c == '`')) {
768
839
  in_str = true;
@@ -934,16 +1005,18 @@ pub fn extractFunction(s: *Scanner, decl_start: usize, is_exported: bool, is_asy
934
1005
 
935
1006
  // Build DTS text — single alloc + memcpy is cheaper than ArrayList init +
936
1007
  // multiple appendSlice + toOwnedSlice for the fixed-shape function header.
937
- const export_prefix: []const u8 = if (is_exported) "export " else "";
938
- const declare_kw = "declare function ";
1008
+ // `export default function foo()` keeps its `default` keyword in DTS:
1009
+ // `declare` is not allowed alongside `default`, and emitting
1010
+ // `export declare function ...` would silently drop the default-export
1011
+ // signal that consumers (and the dtsx TS reference impl) preserve.
1012
+ const export_prefix: []const u8 = if (is_default) "export default function " else if (is_exported) "export declare function " else "declare function ";
939
1013
  const colon_sep = ": ";
940
- const total = export_prefix.len + declare_kw.len + func_name.len + generics.len +
1014
+ const total = export_prefix.len + func_name.len + generics.len +
941
1015
  dts_params.len + colon_sep.len + return_type.len + 1; // +1 for ';'
942
1016
  const dts_text = blk: {
943
1017
  const buf = s.allocator.alloc(u8, total) catch break :blk @as([]const u8, "");
944
1018
  var tp: usize = 0;
945
1019
  @memcpy(buf[tp..][0..export_prefix.len], export_prefix); tp += export_prefix.len;
946
- @memcpy(buf[tp..][0..declare_kw.len], declare_kw); tp += declare_kw.len;
947
1020
  @memcpy(buf[tp..][0..func_name.len], func_name); tp += func_name.len;
948
1021
  @memcpy(buf[tp..][0..generics.len], generics); tp += generics.len;
949
1022
  @memcpy(buf[tp..][0..dts_params.len], dts_params); tp += dts_params.len;
@@ -963,7 +963,9 @@ export function useFoo(f: Foo): void {}
963
963
 
964
964
  test('default export function', () => {
965
965
  const result = dts(`export default function main(): void {}`)
966
- expect(result).toContain('declare function main(): void')
966
+ // `export default function` keeps the `default` keyword in DTS —
967
+ // `export declare function` would lose the default-export signal.
968
+ expect(result).toContain('export default function main(): void')
967
969
  })
968
970
 
969
971
  test('default export class', () => {
@@ -977,7 +979,7 @@ export const version = '1.0.0'
977
979
  export default function init(): void {}
978
980
  `)
979
981
  expect(result).toContain('declare const version')
980
- expect(result).toContain('declare function init')
982
+ expect(result).toContain('export default function init')
981
983
  })
982
984
  })
983
985
 
@@ -1173,6 +1175,29 @@ export function merge<T extends Record<string, unknown>, U extends Record<string
1173
1175
  expect(result).toContain('declare function parse')
1174
1176
  })
1175
1177
 
1178
+ test('issue 3095 — destructured param with JSDoc apostrophe', () => {
1179
+ // JSDoc prose containing an apostrophe (e.g. "error's") used to put the
1180
+ // param scanner into an inescapable string-literal mode, producing
1181
+ // `,: unknown):` that TypeScript rejects with TS1138.
1182
+ const result = dts(`export class CLI {
1183
+ async parse(
1184
+ argv: string[] = [],
1185
+ {
1186
+ /** Whether to run the action for matched command */
1187
+ run = true,
1188
+ /**
1189
+ * Defaults to \`false\`. When set, render the error's message and exit.
1190
+ */
1191
+ exitOnError = false,
1192
+ }: { run?: boolean, exitOnError?: boolean } = {},
1193
+ ): Promise<void> {}
1194
+ }`)
1195
+ expect(result).not.toMatch(/,\s*:\s*unknown\)/)
1196
+ expect(result).not.toContain('= {}')
1197
+ expect(result).toContain('run?: boolean')
1198
+ expect(result).toContain('exitOnError?: boolean')
1199
+ })
1200
+
1176
1201
  test('export declare (already declared)', () => {
1177
1202
  const result = dts(`export declare const VERSION: string`)
1178
1203
  expect(result).toContain('export declare const VERSION: string')
Binary file
Binary file