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

pmcelhaney / counterfact / 23657345706

27 Mar 2026 04:44PM UTC coverage: 86.081% (+2.2%) from 83.89%
23657345706

push

github

Copilot
Merge main into branch and regenerate yarn.lock to fix conflicts

1553 of 1750 branches covered (88.74%)

Branch coverage included in aggregate %.

279 of 311 new or added lines in 16 files covered. (89.71%)

10 existing lines in 2 files now uncovered.

4860 of 5700 relevant lines covered (85.26%)

60.13 hits per line

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

91.08
/src/server/module-tree.ts
1
import type { Module, MiddlewareFunction } from "./registry.js";
2✔
2

2✔
3
interface Route {
2✔
4
  methods: { [key: string]: string };
2✔
5
  path: string;
2✔
6
}
2✔
7

2✔
8
interface File {
2✔
9
  isWildcard: boolean;
2✔
10
  module: Module;
2✔
11
  name: string;
2✔
12
  rawName: string;
2✔
13
}
2✔
14

2✔
15
interface Directory {
2✔
16
  directories: { [key: string]: Directory };
2✔
17
  files: { [key: string]: File };
2✔
18
  isWildcard: boolean;
2✔
19
  name: string;
2✔
20
  rawName: string;
2✔
21
  middleware?: MiddlewareFunction;
2✔
22
}
2✔
23

2✔
24
interface Match {
2✔
25
  ambiguous?: boolean;
2✔
26
  matchedPath: string;
2✔
27
  module: Module;
2✔
28
  pathVariables: { [key: string]: string };
2✔
29
}
2✔
30

2✔
31
function isDirectory(test: Directory | undefined): test is Directory {
242✔
32
  return test !== undefined;
242✔
33
}
242✔
34

2✔
35
export class ModuleTree {
2✔
36
  public readonly root: Directory = {
2✔
37
    directories: {},
202✔
38
    files: {},
202✔
39
    isWildcard: false,
202✔
40
    name: "",
202✔
41
    rawName: "",
202✔
42
  };
202✔
43

2✔
44
  private putDirectory(directory: Directory, segments: string[]): Directory {
2✔
45
    const [segment, ...remainingSegments] = segments;
336✔
46

336✔
47
    if (segment === undefined) {
336!
48
      throw new Error("segments array is empty");
×
49
    }
×
50

336✔
51
    if (remainingSegments.length === 0) {
336✔
52
      return directory;
204✔
53
    }
204✔
54

132✔
55
    const isNewDirectory =
132✔
56
      directory.directories[segment.toLowerCase()] === undefined;
132✔
57

132✔
58
    const nextDirectory = (directory.directories[segment.toLowerCase()] ??= {
132✔
59
      directories: {},
132✔
60
      files: {},
132✔
61
      isWildcard: segment.startsWith("{"),
132✔
62
      name: segment.replace(/^\{(?<name>.*)\}$/u, "$<name>"),
132✔
63
      rawName: segment,
132✔
64
    });
132✔
65

132✔
66
    if (isNewDirectory && segment.startsWith("{")) {
336✔
67
      const ambiguousWildcardDirectories = Object.values(
30✔
68
        directory.directories,
30✔
69
      ).filter((subdirectory) => subdirectory.isWildcard);
30✔
70

30✔
71
      if (ambiguousWildcardDirectories.length > 1) {
30✔
72
        process.stderr.write(
6✔
73
          `[counterfact] ERROR: Ambiguous wildcard paths detected. Multiple wildcard directories exist at the same level: ${ambiguousWildcardDirectories.map((d) => d.rawName).join(", ")}. Requests may be routed unpredictably.\n`,
6✔
74
        );
6✔
75
      }
6✔
76
    }
30✔
77

132✔
78
    return this.putDirectory(nextDirectory, remainingSegments);
132✔
79
  }
132✔
80

2✔
81
  private addModuleToDirectory(
2✔
82
    directory: Directory | undefined,
204✔
83
    segments: string[],
204✔
84
    module: Module,
204✔
85
  ) {
204✔
86
    if (directory === undefined) {
204!
87
      return;
×
88
    }
×
89

204✔
90
    const targetDirectory = this.putDirectory(directory, segments);
204✔
91

204✔
92
    const filename = segments.at(-1);
204✔
93

204✔
94
    if (filename === undefined) {
204!
95
      throw new Error(
×
96
        "The file name (the last segment of the URL) is undefined. This is theoretically impossible but TypeScript can't enforce it.",
×
97
      );
×
98
    }
×
99

204✔
100
    targetDirectory.files[filename] = {
204✔
101
      isWildcard: filename.startsWith("{"),
204✔
102
      module,
204✔
103
      name: filename.replace(/^\{(?<name>.*)\}$/u, "$<name>"),
204✔
104
      rawName: filename,
204✔
105
    };
204✔
106

204✔
107
    if (filename.startsWith("{")) {
204✔
108
      const ambiguousWildcardFiles = Object.values(
44✔
109
        targetDirectory.files,
44✔
110
      ).filter((file) => file.isWildcard);
44✔
111

44✔
112
      if (ambiguousWildcardFiles.length > 1) {
44✔
113
        process.stderr.write(
10✔
114
          `[counterfact] ERROR: Ambiguous wildcard paths detected. Multiple wildcard files exist at the same path level: ${ambiguousWildcardFiles.map((f) => f.rawName).join(", ")}. Requests may be routed unpredictably.\n`,
10✔
115
        );
10✔
116
      }
10✔
117
    }
44✔
118
  }
204✔
119

2✔
120
  public add(url: string, module: Module) {
2✔
121
    this.addModuleToDirectory(this.root, url.split("/").slice(1), module);
204✔
122
  }
204✔
123

2✔
124
  private removeModuleFromDirectory(
2✔
125
    directory: Directory | undefined,
8✔
126
    segments: string[],
8✔
127
  ) {
8✔
128
    if (!isDirectory(directory)) {
8!
129
      return;
×
130
    }
×
131

8✔
132
    const [segment, ...remainingSegments] = segments;
8✔
133

8✔
134
    if (segment === undefined) {
8!
135
      return;
×
136
    }
×
137

8✔
138
    if (remainingSegments.length === 0) {
8✔
139
      delete directory.files[segment.toLowerCase()];
6✔
140

6✔
141
      return;
6✔
142
    }
6✔
143

2✔
144
    this.removeModuleFromDirectory(
2✔
145
      directory.directories[segment.toLowerCase()],
2✔
146
      remainingSegments,
2✔
147
    );
2✔
148
  }
2✔
149

2✔
150
  public remove(url: string) {
2✔
151
    const segments = url.split("/").slice(1);
6✔
152

6✔
153
    this.removeModuleFromDirectory(this.root, segments);
6✔
154
  }
6✔
155

2✔
156
  private fileModuleDefined(file: File, method: string) {
2✔
157
    return (file.module as { [key: string]: unknown })[method] !== undefined;
78✔
158
  }
78✔
159

2✔
160
  private buildMatch(
2✔
161
    directory: Directory,
344✔
162
    segment: string,
344✔
163
    pathVariables: { [key: string]: string },
344✔
164
    matchedPath: string,
344✔
165
    method: string,
344✔
166
  ): Match | undefined {
344✔
167
    function normalizedSegment(segment: string, directory: Directory) {
344✔
168
      for (const file in directory.files) {
344✔
169
        if (file.toLowerCase() === segment.toLowerCase()) {
358✔
170
          return file;
256✔
171
        }
256✔
172
      }
358✔
173
      return "";
88✔
174
    }
88✔
175

344✔
176
    const exactMatchFile =
344✔
177
      directory.files[normalizedSegment(segment, directory)];
344✔
178

344✔
179
    // If the URL segment literally matches a file key (e.g., requesting "/{x}"
344✔
180
    // as a literal URL value), exactMatchFile may be a wildcard file. In that
344✔
181
    // case, fall through to wildcard matching below.
344✔
182
    if (exactMatchFile !== undefined && !exactMatchFile.isWildcard) {
344✔
183
      return {
256✔
184
        ...exactMatchFile,
256✔
185
        matchedPath: `${matchedPath}/${exactMatchFile.rawName}`,
256✔
186
        pathVariables,
256✔
187
      };
256✔
188
    }
256✔
189

88✔
190
    const wildcardFiles = Object.values(directory.files).filter(
88✔
191
      (file) => file.isWildcard && this.fileModuleDefined(file, method),
88✔
192
    );
88✔
193

88✔
194
    if (wildcardFiles.length > 1) {
284✔
195
      const firstWildcard = wildcardFiles[0] as File;
4✔
196

4✔
197
      return {
4✔
198
        ...firstWildcard,
4✔
199
        ambiguous: true,
4✔
200
        matchedPath: `${matchedPath}/${firstWildcard.rawName}`,
4✔
201
        pathVariables: {
4✔
202
          ...pathVariables,
4✔
203
          [firstWildcard.name]: segment,
4✔
204
        },
4✔
205
      };
4✔
206
    }
4✔
207

84✔
208
    const match = exactMatchFile ?? wildcardFiles[0];
344✔
209

344✔
210
    if (match === undefined) {
344✔
211
      return undefined;
40✔
212
    }
40✔
213

44✔
214
    if (match.isWildcard) {
44✔
215
      return {
44✔
216
        ...match,
44✔
217

44✔
218
        matchedPath: `${matchedPath}/${match.rawName}`,
44✔
219

44✔
220
        pathVariables: {
44✔
221
          ...pathVariables,
44✔
222
          [match.name]: segment,
44✔
223
        },
44✔
224
      };
44✔
225
    }
44!
UNCOV
226

×
UNCOV
227
    return {
×
UNCOV
228
      ...match,
×
UNCOV
229

×
UNCOV
230
      matchedPath: `${matchedPath}/${match.rawName}`,
×
UNCOV
231

×
UNCOV
232
      pathVariables,
×
UNCOV
233
    };
×
UNCOV
234
  }
×
235

2✔
236
  private matchWithinDirectory(
2✔
237
    directory: Directory,
578✔
238
    segments: string[],
578✔
239
    pathVariables: { [key: string]: string },
578✔
240
    matchedPath: string,
578✔
241
    method: string,
578✔
242
  ): Match | undefined {
578✔
243
    if (segments.length === 0) {
578!
244
      return undefined;
×
245
    }
×
246

578✔
247
    const [segment, ...remainingSegments] = segments;
578✔
248

578✔
249
    if (segment === undefined) {
578!
250
      throw new Error(
×
251
        "segment cannot be undefined but TypeScript doesn't know that",
×
252
      );
×
253
    }
×
254

578✔
255
    if (
578✔
256
      remainingSegments.length === 0 ||
578✔
257
      (remainingSegments.length === 1 && remainingSegments[0] === "")
236✔
258
    ) {
578✔
259
      return this.buildMatch(
344✔
260
        directory,
344✔
261
        segment,
344✔
262
        pathVariables,
344✔
263
        matchedPath,
344✔
264
        method,
344✔
265
      );
344✔
266
    }
344✔
267

234✔
268
    const exactMatch = directory.directories[segment.toLowerCase()];
234✔
269

234✔
270
    if (isDirectory(exactMatch)) {
494✔
271
      return this.matchWithinDirectory(
150✔
272
        exactMatch,
150✔
273
        remainingSegments,
150✔
274
        pathVariables,
150✔
275
        `${matchedPath}/${segment}`,
150✔
276
        method,
150✔
277
      );
150✔
278
    }
150✔
279

84✔
280
    const wildcardDirectories = Object.values(directory.directories).filter(
84✔
281
      (subdirectory) => subdirectory.isWildcard,
84✔
282
    );
84✔
283

84✔
284
    const wildcardMatches: Match[] = [];
84✔
285

84✔
286
    for (const wildcardDirectory of wildcardDirectories) {
450✔
287
      const wildcardMatch = this.matchWithinDirectory(
70✔
288
        wildcardDirectory,
70✔
289
        remainingSegments,
70✔
290
        {
70✔
291
          ...pathVariables,
70✔
292
          [wildcardDirectory.name]: segment,
70✔
293
        },
70✔
294
        `${matchedPath}/${wildcardDirectory.rawName}`,
70✔
295
        method,
70✔
296
      );
70✔
297

70✔
298
      if (wildcardMatch !== undefined) {
70✔
299
        wildcardMatches.push(wildcardMatch);
26✔
300
      }
26✔
301
    }
70✔
302

84✔
303
    if (wildcardMatches.length > 1) {
490✔
304
      const firstMatch = wildcardMatches[0] as Match;
2✔
305

2✔
306
      return { ...firstMatch, ambiguous: true };
2✔
307
    }
2✔
308

82✔
309
    return wildcardMatches[0];
82✔
310
  }
82✔
311

2✔
312
  public match(url: string, method: string) {
2✔
313
    return this.matchWithinDirectory(
358✔
314
      this.root,
358✔
315
      url.split("/").slice(1),
358✔
316
      {},
358✔
317
      "",
358✔
318
      method,
358✔
319
    );
358✔
320
  }
358✔
321

2✔
322
  public get routes(): Route[] {
2✔
323
    const routes: Route[] = [];
16✔
324

16✔
325
    function traverse(directory: Directory, path: string) {
16✔
326
      Object.values(directory.directories).forEach((subdirectory) => {
30✔
327
        traverse(subdirectory, `${path}/${subdirectory.rawName}`);
14✔
328
      });
14✔
329

30✔
330
      Object.values(directory.files).forEach((file) => {
30✔
331
        const methods: [string, string][] = Object.entries(file.module).map(
34✔
332
          ([method, implementation]) => [method, String(implementation)],
34✔
333
        );
34✔
334

34✔
335
        routes.push({
34✔
336
          methods: Object.fromEntries(methods),
34✔
337
          path: `${path}/${file.rawName}`,
34✔
338
        });
34✔
339
      });
34✔
340
    }
30✔
341

16✔
342
    function stripBrackets(string: string) {
16✔
343
      return string.replaceAll(/\{|\}/gu, "");
104✔
344
    }
104✔
345

16✔
346
    traverse(this.root, "");
16✔
347

16✔
348
    return routes.sort((first, second) =>
16✔
349
      stripBrackets(first.path).localeCompare(stripBrackets(second.path)),
52✔
350
    );
16✔
351
  }
16✔
352
}
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