• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pmcelhaney / counterfact / 24042444760

06 Apr 2026 05:28PM UTC coverage: 87.991% (+0.2%) from 87.811%
24042444760

Pull #1644

github

web-flow
Merge branch 'main' into copilot/add-jsdoc-generation-for-types
Pull Request #1644: feat: Add JSDoc generation for types from OpenAPI metadata

1905 of 2157 branches covered (88.32%)

Branch coverage included in aggregate %.

97 of 98 new or added lines in 6 files covered. (98.98%)

5840 of 6645 relevant lines covered (87.89%)

71.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.31
/src/typescript-generator/script.ts
1
import nodePath from "node:path";
2✔
2

2✔
3
import createDebugger from "debug";
2✔
4
import { format } from "prettier";
2✔
5

2✔
6
import { escapePathForWindows } from "../util/windows-escape.js";
2✔
7
import type { Coder, ExportStatement } from "./coder.js";
2✔
8
import type { Repository } from "./repository.js";
2✔
9

2✔
10
const debug = createDebugger("counterfact:typescript-generator:script");
2✔
11

2✔
12
interface ImportEntry {
2✔
13
  isDefault: boolean;
2✔
14
  isType: boolean;
2✔
15
  name: string;
2✔
16
  script: Script;
2✔
17
}
2✔
18

2✔
19
interface ExternalImportEntry {
2✔
20
  isDefault?: boolean;
2✔
21
  isType: boolean;
2✔
22
  modulePath: string;
2✔
23
}
2✔
24

2✔
25
export class Script {
2✔
26
  public repository: Repository;
2✔
27
  public comments: string[];
160✔
28
  public exports: Map<string, ExportStatement>;
160✔
29
  public imports: Map<string, ImportEntry>;
160✔
30
  public externalImport: Map<string, ExternalImportEntry>;
160✔
31
  public cache: Map<string, string>;
160✔
32
  public typeCache: Map<string, string>;
160✔
33
  public path: string;
160✔
34

2✔
35
  public constructor(repository: Repository, path: string) {
2✔
36
    this.repository = repository;
160✔
37
    this.comments = [];
160✔
38
    this.exports = new Map();
160✔
39
    this.imports = new Map();
160✔
40
    this.externalImport = new Map();
160✔
41
    this.cache = new Map();
160✔
42
    this.typeCache = new Map();
160✔
43
    this.path = path;
160✔
44
  }
160✔
45

2✔
46
  public get relativePathToBase(): string {
2✔
47
    return this.path
318✔
48
      .split("/")
318✔
49
      .slice(0, -1)
318✔
50
      .map(() => "..")
318✔
51
      .join("/");
318✔
52
  }
318✔
53

2✔
54
  public firstUniqueName(coder: Coder): string {
2✔
55
    for (const name of coder.names()) {
334✔
56
      if (!this.imports.has(name)) {
352✔
57
        return name;
334✔
58
      }
334✔
59
    }
352!
60

×
61
    throw new Error(`could not find a unique name for ${coder.id}`);
×
62
  }
×
63

2✔
64
  public export(coder: Coder, isType = false, isDefault = false): string {
2✔
65
    const cacheKey = isDefault ? "default" : `${coder.id}:${isType}`;
226✔
66

226✔
67
    if (this.cache.has(cacheKey)) {
226✔
68
      return this.cache.get(cacheKey)!;
16✔
69
    }
16✔
70

210✔
71
    const name = this.firstUniqueName(coder);
210✔
72

210✔
73
    this.cache.set(cacheKey, name);
210✔
74

210✔
75
    const exportStatement: ExportStatement = {
210✔
76
      beforeExport: coder.beforeExport(this.path),
210✔
77
      done: false,
210✔
78
      id: coder.id,
210✔
79
      isDefault,
210✔
80
      isType,
210✔
81
      jsdoc: "",
210✔
82
      typeDeclaration: coder.typeDeclaration(this.exports, this),
210✔
83
    };
210✔
84

210✔
85
    exportStatement.promise = coder
210✔
86
      .delegate()
210✔
87

210✔
88
      .then((availableCoder) => {
210✔
89
        exportStatement.name = name;
210✔
90
        exportStatement.code = availableCoder.write(this);
210✔
91
        exportStatement.jsdoc = availableCoder.jsdoc();
210✔
92

210✔
93
        return availableCoder;
210✔
94
      })
210✔
95

210✔
96
      .catch((error: Error) => {
210✔
97
        exportStatement.code = `{/* error creating export "${name}" for ${this.path}: ${error.stack} */}`;
24✔
98
        exportStatement.error = error;
24✔
99
        return undefined;
24✔
100
      })
24✔
101

210✔
102
      .finally(() => {
210✔
103
        exportStatement.done = true;
210✔
104
      });
210✔
105

210✔
106
    this.exports.set(name, exportStatement);
210✔
107

210✔
108
    return name;
210✔
109
  }
210✔
110

2✔
111
  public exportDefault(coder: Coder, isType = false): void {
2✔
112
    this.export(coder, isType, true);
×
113
  }
×
114

2✔
115
  public import(coder: Coder, isType = false, isDefault = false): string {
2✔
116
    debug("import coder: %s", coder.id);
210✔
117

210✔
118
    const modulePath = coder.modulePath();
210✔
119

210✔
120
    const cacheKey = `${coder.id}@${modulePath}:${isType}:${isDefault}`;
210✔
121

210✔
122
    debug("cache key: %s", cacheKey);
210✔
123

210✔
124
    if (this.cache.has(cacheKey)) {
210✔
125
      debug("cache hit: %s", cacheKey);
86✔
126

86✔
127
      return this.cache.get(cacheKey)!;
86✔
128
    }
86✔
129

124✔
130
    debug("cache miss: %s", cacheKey);
124✔
131

124✔
132
    const name = this.firstUniqueName(coder);
124✔
133

124✔
134
    this.cache.set(cacheKey, name);
124✔
135

124✔
136
    const scriptFromWhichToExport = this.repository.get(modulePath);
124✔
137

124✔
138
    const exportedName = scriptFromWhichToExport.export(
124✔
139
      coder,
124✔
140
      isType,
124✔
141
      isDefault,
124✔
142
    );
124✔
143

124✔
144
    this.imports.set(name, {
124✔
145
      isDefault,
124✔
146
      isType,
124✔
147
      name: exportedName,
124✔
148
      script: scriptFromWhichToExport,
124✔
149
    });
124✔
150

124✔
151
    return name;
124✔
152
  }
124✔
153

2✔
154
  public importType(coder: Coder): string {
2✔
155
    return this.import(coder, true);
188✔
156
  }
188✔
157

2✔
158
  public importDefault(coder: Coder, isType = false): string {
2✔
159
    return this.import(coder, isType, true);
4✔
160
  }
4✔
161

2✔
162
  public importExternal(
2✔
163
    name: string,
382✔
164
    modulePath: string,
382✔
165
    isType = false,
382✔
166
  ): string {
382✔
167
    this.externalImport.set(name, { isType, modulePath });
382✔
168

382✔
169
    return name;
382✔
170
  }
382✔
171

2✔
172
  public importExternalType(name: string, modulePath: string): string {
2✔
173
    return this.importExternal(name, modulePath, true);
62✔
174
  }
62✔
175

2✔
176
  public importSharedType(name: string): string {
2✔
177
    return this.importExternal(
318✔
178
      name,
318✔
179
      nodePath
318✔
180
        .join(this.relativePathToBase, "counterfact-types/index.ts")
318✔
181
        .replaceAll("\\", "/"),
318✔
182
      true,
318✔
183
    );
318✔
184
  }
318✔
185

2✔
186
  public exportType(coder: Coder): string {
2✔
187
    return this.export(coder, true);
2✔
188
  }
2✔
189

2✔
190
  public isInProgress(): boolean {
2✔
191
    return Array.from(this.exports.values()).some(
150✔
192
      (exportStatement) => !exportStatement.done,
150✔
193
    );
150✔
194
  }
150✔
195

2✔
196
  public finished(): Promise<(Coder | undefined)[]> {
2✔
197
    return Promise.all(
114✔
198
      Array.from(this.exports.values(), (value) => value.promise!),
114✔
199
    );
114✔
200
  }
114✔
201

2✔
202
  public externalImportStatements(): string[] {
2✔
203
    return Array.from(
130✔
204
      this.externalImport,
130✔
205
      ([name, { isDefault, isType, modulePath }]) =>
130✔
206
        `import${isType ? " type" : ""} ${
308✔
207
          isDefault ? name : `{ ${name} }`
308!
208
        } from "${modulePath}";`,
130✔
209
    );
130✔
210
  }
130✔
211

2✔
212
  public importStatements(): string[] {
2✔
213
    return Array.from(this.imports, ([name, { isDefault, isType, script }]) => {
128✔
214
      const resolvedPath = escapePathForWindows(
114✔
215
        nodePath
114✔
216
          .relative(
114✔
217
            nodePath.dirname(this.path).replaceAll("\\", "/"),
114✔
218
            script.path.replace(/\.ts$/u, ".js"),
114✔
219
          )
114✔
220
          .replaceAll("\\", "/"),
114✔
221
      );
114✔
222

114✔
223
      return `import${isType ? " type" : ""} ${
114✔
224
        isDefault ? name : `{ ${name} }`
114✔
225
      } from "${resolvedPath.includes("../") ? "" : "./"}${resolvedPath}";`;
114✔
226
    });
114✔
227
  }
128✔
228

2✔
229
  public exportStatements(): string[] {
2✔
230
    return Array.from(
124✔
231
      this.exports.values(),
124✔
232
      ({
124✔
233
        beforeExport,
186✔
234
        code,
186✔
235
        isDefault,
186✔
236
        isType,
186✔
237
        jsdoc,
186✔
238
        name,
186✔
239
        typeDeclaration,
186✔
240
      }) => {
186✔
241
        if (typeof code === "object" && code !== null && "raw" in code) {
186!
242
          return code.raw;
×
243
        }
×
244

186✔
245
        if (isDefault) {
186!
NEW
246
          return `${jsdoc}${beforeExport}export default ${code as string};`;
×
247
        }
×
248

186✔
249
        const keyword = isType ? "type" : "const";
186✔
250
        const typeAnnotation =
186✔
251
          (typeDeclaration ?? "").length === 0
186!
252
            ? ""
128✔
253
            : `:${typeDeclaration ?? ""}`;
186!
254

186✔
255
        return `${jsdoc}${beforeExport}export ${keyword} ${name ?? ""}${typeAnnotation} = ${code as string};`;
186!
256
      },
186✔
257
    );
124✔
258
  }
124✔
259

2✔
260
  public contents(): Promise<string> {
2✔
261
    return format(
124✔
262
      [
124✔
263
        this.comments.map((comment) => `// ${comment}`).join("\n"),
124✔
264
        this.comments.length > 0 ? "\n\n" : "",
124✔
265
        this.externalImportStatements().join("\n"),
124✔
266
        this.importStatements().join("\n"),
124✔
267
        "\n\n",
124✔
268
        this.exportStatements().join("\n\n"),
124✔
269
      ].join(""),
124✔
270
      { parser: "typescript" },
124✔
271
    );
124✔
272
  }
124✔
273
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc