• 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

83.18
/src/server/module-loader.ts
1
import { once } from "node:events";
2✔
2
import { existsSync } from "node:fs";
2✔
3
import fs from "node:fs/promises";
2✔
4
import nodePath, { basename, dirname } from "node:path";
2✔
5

2✔
6
import { type FSWatcher, watch } from "chokidar";
2✔
7
import createDebug from "debug";
2✔
8

2✔
9
import { CHOKIDAR_OPTIONS } from "./constants.js";
2✔
10
import { type Context, ContextRegistry } from "./context-registry.js";
2✔
11
import { determineModuleKind } from "./determine-module-kind.js";
2✔
12
import { ModuleDependencyGraph } from "./module-dependency-graph.js";
2✔
13
import type { MiddlewareFunction, Module, Registry } from "./registry.js";
2✔
14
import { uncachedImport } from "./uncached-import.js";
2✔
15

2✔
16
const { uncachedRequire } = await import("./uncached-require.cjs");
2✔
17

2✔
18
const debug = createDebug("counterfact:server:module-loader");
2✔
19

2✔
20
import {
2✔
21
  escapePathForWindows,
2✔
22
  unescapePathForWindows,
2✔
23
} from "../util/windows-escape.js";
2✔
24

2✔
25
interface ContextModule {
2✔
26
  Context?: Context;
2✔
27
}
2✔
28

2✔
29
function isContextModule(
16✔
30
  module: ContextModule | Module,
16✔
31
): module is ContextModule {
16✔
32
  return "Context" in module && typeof module.Context === "function";
16✔
33
}
16✔
34

2✔
35
function isMiddlewareModule(
4✔
36
  module: ContextModule | Module,
4✔
37
): module is ContextModule & { middleware: MiddlewareFunction } {
4✔
38
  return (
4✔
39
    "middleware" in module &&
4✔
40
    typeof Object.getOwnPropertyDescriptor(module, "middleware")?.value ===
4✔
41
      "function"
4✔
42
  );
4✔
43
}
4✔
44

2✔
45
export class ModuleLoader extends EventTarget {
2✔
46
  private readonly basePath: string;
2✔
47

26✔
48
  public readonly registry: Registry;
26✔
49

26✔
50
  private watcher: FSWatcher | undefined;
26✔
51

26✔
52
  private readonly contextRegistry: ContextRegistry;
26✔
53

26✔
54
  private readonly dependencyGraph = new ModuleDependencyGraph();
26✔
55

26✔
56
  private readonly uncachedImport: (moduleName: string) => Promise<unknown> =
26✔
57
    async function (moduleName: string) {
26✔
58
      throw new Error(`uncachedImport not set up; importing ${moduleName}`);
×
59
    };
×
60

2✔
61
  public constructor(
2✔
62
    basePath: string,
26✔
63
    registry: Registry,
26✔
64
    contextRegistry = new ContextRegistry(),
26✔
65
  ) {
26✔
66
    super();
26✔
67
    this.basePath = basePath.replaceAll("\\", "/");
26✔
68
    this.registry = registry;
26✔
69
    this.contextRegistry = contextRegistry;
26✔
70
  }
26✔
71

2✔
72
  public async watch(): Promise<void> {
2✔
73
    this.watcher = watch(this.basePath, CHOKIDAR_OPTIONS).on(
4✔
74
      "all",
4✔
75

4✔
76
      (eventName: string, pathNameOriginal: string) => {
4✔
77
        const JS_EXTENSIONS = ["js", "mjs", "cjs", "ts", "mts", "cts"];
2✔
78

2✔
79
        if (
2✔
80
          !JS_EXTENSIONS.some((extension) =>
2✔
81
            pathNameOriginal.endsWith(`.${extension}`),
2✔
82
          )
2✔
83
        )
2✔
84
          return;
2!
85

2✔
86
        const pathName = pathNameOriginal.replaceAll("\\", "/");
2✔
87

2✔
88
        if (pathName.includes("$.context") && eventName === "add") {
2!
89
          process.stdout.write(
×
90
            `\n\n!!! The file at ${pathName} needs a minor update.\n    See https://github.com/pmcelhaney/counterfact/blob/main/docs/context-change.md\n\n\n`,
×
91
          );
×
92

×
93
          return;
×
94
        }
×
95

2✔
96
        if (!["add", "change", "unlink"].includes(eventName)) {
2!
97
          return;
×
98
        }
×
99

2✔
100
        const parts = nodePath.parse(pathName.replace(this.basePath, ""));
2✔
101
        const url = unescapePathForWindows(
2✔
102
          `/${parts.dir}/${parts.name}`
2✔
103
            .replaceAll("\\", "/")
2✔
104
            .replaceAll(/\/+/gu, "/"),
2✔
105
        );
2✔
106

2✔
107
        if (eventName === "unlink") {
2✔
108
          this.registry.remove(url);
2✔
109
          this.dispatchEvent(new Event("remove"));
2✔
110
        }
2✔
111

2✔
112
        const dependencies = this.dependencyGraph.dependentsOf(pathName);
2✔
113

2✔
114
        void this.loadEndpoint(pathName);
2✔
115

2✔
116
        for (const dependency of dependencies) {
2!
117
          void this.loadEndpoint(dependency);
×
118
        }
×
119
      },
2✔
120
    );
4✔
121
    await once(this.watcher, "ready");
4✔
122
  }
4✔
123

2✔
124
  public async stopWatching(): Promise<void> {
2✔
125
    await this.watcher?.close();
6✔
126
  }
6✔
127

2✔
128
  public async load(directory = ""): Promise<void> {
2✔
129
    if (
36✔
130
      !existsSync(nodePath.join(this.basePath, directory).replaceAll("\\", "/"))
36✔
131
    ) {
36!
132
      throw new Error(`Directory does not exist ${this.basePath}`);
×
133
    }
×
134

36✔
135
    const files = await fs.readdir(
36✔
136
      nodePath.join(this.basePath, directory).replaceAll("\\", "/"),
36✔
137
      {
36✔
138
        withFileTypes: true,
36✔
139
      },
36✔
140
    );
36✔
141

36✔
142
    const imports = files.flatMap(async (file): Promise<void> => {
36✔
143
      const extension = file.name.split(".").at(-1);
72✔
144

72✔
145
      if (file.isDirectory()) {
72✔
146
        await this.load(
16✔
147
          nodePath.join(directory, file.name).replaceAll("\\", "/"),
16✔
148
        );
16✔
149

16✔
150
        return;
16✔
151
      }
16✔
152

56✔
153
      if (!["cjs", "cts", "js", "mjs", "mts", "ts"].includes(extension ?? "")) {
72✔
154
        return;
26✔
155
      }
26✔
156

30✔
157
      const fullPath = nodePath
30✔
158
        .join(this.basePath, directory, file.name)
30✔
159
        .replaceAll("\\", "/");
30✔
160

30✔
161
      await this.loadEndpoint(escapePathForWindows(fullPath));
30✔
162
    });
30✔
163

36✔
164
    await Promise.all(imports);
36✔
165
  }
36✔
166

2✔
167
  private async loadEndpoint(pathName: string) {
2✔
168
    debug("importing module: %s", pathName);
32✔
169

32✔
170
    const directory = dirname(pathName.slice(this.basePath.length)).replaceAll(
32✔
171
      "\\",
32✔
172
      "/",
32✔
173
    );
32✔
174

32✔
175
    const url = unescapePathForWindows(
32✔
176
      `/${nodePath.join(directory, nodePath.parse(basename(pathName)).name)}`
32✔
177
        .replaceAll("\\", "/")
32✔
178
        .replaceAll(/\/+/gu, "/"),
32✔
179
    );
32✔
180

32✔
181
    debug(`loading pathName from dependencyGraph: ${pathName}`);
32✔
182

32✔
183
    this.dependencyGraph.load(pathName);
32✔
184

32✔
185
    try {
32✔
186
      const doImport =
32✔
187
        (await determineModuleKind(pathName)) === "commonjs"
32!
188
          ? uncachedRequire
×
189
          : uncachedImport;
32✔
190

32✔
191
      const endpoint = (await doImport(pathName).catch(() => {
32✔
192
        console.log("ERROR");
×
193
      })) as ContextModule | Module;
×
194

32✔
195
      this.dispatchEvent(new Event("add"));
32✔
196

32✔
197
      if (
32✔
198
        basename(pathName).startsWith("_.context.") &&
32✔
199
        isContextModule(endpoint)
16✔
200
      ) {
32✔
201
        const loadContext = (path: string) => this.contextRegistry.find(path);
16✔
202

16✔
203
        const contextDir = nodePath.dirname(unescapePathForWindows(pathName));
16✔
204
        const readJson = async (relativePath: string): Promise<unknown> => {
16✔
205
          const absolutePath = nodePath.resolve(contextDir, relativePath);
4✔
206
          let content: string;
4✔
207
          try {
4✔
208
            content = await fs.readFile(absolutePath, "utf8");
4✔
209
          } catch {
4!
NEW
210
            throw new Error(
×
NEW
211
              `readJson: could not read file at "${absolutePath}" (resolved from "${relativePath}" relative to "${contextDir}")`,
×
NEW
212
            );
×
NEW
213
          }
×
214
          try {
4✔
215
            return JSON.parse(content) as unknown;
4✔
216
          } catch {
4!
NEW
217
            throw new Error(
×
NEW
218
              `readJson: file at "${absolutePath}" does not contain valid JSON`,
×
NEW
219
            );
×
NEW
220
          }
×
221
        };
4✔
222

16✔
223
        this.contextRegistry.update(
16✔
224
          directory,
16✔
225

16✔
226
          // @ts-expect-error TS says Context has no constructable signatures but that's not true?
16✔
227

16✔
228
          new endpoint.Context({
16✔
229
            loadContext,
16✔
230
            readJson,
16✔
231
          }),
16✔
232
        );
16✔
233
        return;
16✔
234
      }
16✔
235

16✔
236
      if (
16✔
237
        basename(pathName).startsWith("_.middleware.") &&
16✔
238
        isMiddlewareModule(endpoint)
4✔
239
      ) {
32✔
240
        this.registry.addMiddleware(
4✔
241
          url.slice(0, url.lastIndexOf("/")) || "/",
4✔
242
          endpoint.middleware,
4✔
243
        );
4✔
244
      }
4✔
245

16✔
246
      if (url === "/index") this.registry.add("/", endpoint as Module);
32✔
247

16✔
248
      debug(`adding "${url}" to registry`);
16✔
249
      this.registry.add(url, endpoint as Module);
16✔
250
    } catch (error: unknown) {
32!
251
      if (
×
252
        String(error) ===
×
253
        "SyntaxError: Identifier 'Context' has already been declared"
×
254
      ) {
×
255
        // Not sure why Node throws this error. It doesn't seem to matter.
×
256
        return;
×
257
      }
×
258

×
259
      process.stdout.write(`\nError loading ${pathName}:\n${String(error)}\n`);
×
260

×
261
      throw error;
×
262
    }
×
263
  }
32✔
264
}
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