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

pmcelhaney / counterfact / 17450255946

04 Sep 2025 01:08AM UTC coverage: 84.299% (+0.03%) from 84.272%
17450255946

Pull #1386

github

pmcelhaney
to catch an error
Pull Request #1386: windows fix for a path that contains a colon (resolves #1381)

1173 of 1303 branches covered (90.02%)

Branch coverage included in aggregate %.

38 of 40 new or added lines in 4 files covered. (95.0%)

6 existing lines in 1 file now uncovered.

3541 of 4289 relevant lines covered (82.56%)

62.74 hits per line

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

85.44
/src/server/module-loader.ts
1
/* eslint-disable n/no-sync */
2✔
2
import { once } from "node:events";
2✔
3
import { existsSync } from "node:fs";
2✔
4
import fs from "node:fs/promises";
2✔
5
import nodePath, { basename, dirname } from "node:path";
2✔
6

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

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

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

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

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

2✔
27
interface ContextModule {
2✔
28
  Context?: Context;
2✔
29
}
2✔
30

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

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

2✔
47
export class ModuleLoader extends EventTarget {
2✔
48
  private readonly basePath: string;
16✔
49

16✔
50
  public readonly registry: Registry;
16✔
51

16✔
52
  private watcher: FSWatcher | undefined;
16✔
53

16✔
54
  private readonly contextRegistry: ContextRegistry;
16✔
55

16✔
56
  private readonly dependencyGraph = new ModuleDependencyGraph();
16✔
57

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

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

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

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

4✔
81
        if (
4✔
82
          !JS_EXTENSIONS.some((extension) =>
4✔
83
            pathNameOriginal.endsWith(`.${extension}`),
14✔
84
          )
4✔
85
        )
4✔
86
          return;
4✔
87

2✔
88
        const pathName = pathNameOriginal.replaceAll("\\", "/");
2✔
89

2✔
90
        if (pathName.includes("$.context") && eventName === "add") {
4!
91
          process.stdout.write(
×
92
            `\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`,
×
93
          );
×
94

×
95
          return;
×
96
        }
✔
97

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

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

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

2✔
114
        const dependencies = this.dependencyGraph.dependentsOf(pathName);
2✔
115

2✔
116
        void this.loadEndpoint(pathName);
2✔
117

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

16✔
126
  public async stopWatching(): Promise<void> {
16✔
127
    await this.watcher?.close();
4✔
128
  }
4✔
129

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

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

28✔
144
    const imports = files.flatMap(async (file): Promise<void> => {
28✔
145
      const extension = file.name.split(".").at(-1);
56✔
146

56✔
147
      if (file.isDirectory()) {
56✔
148
        await this.load(
12✔
149
          nodePath.join(directory, file.name).replaceAll("\\", "/"),
12✔
150
        );
12✔
151

12✔
152
        return;
12✔
153
      }
12✔
154

44✔
155
      if (!["cjs", "cts", "js", "mjs", "mts", "ts"].includes(extension ?? "")) {
56✔
156
        return;
18✔
157
      }
18✔
158

26✔
159
      const fullPath = nodePath
26✔
160
        .join(this.basePath, directory, file.name)
26✔
161
        .replaceAll("\\", "/");
26✔
162

26✔
163
      await this.loadEndpoint(escapePathForWindows(fullPath));
26✔
164
    });
26✔
165

28✔
166
    await Promise.all(imports);
28✔
167
  }
28✔
168

16✔
169
  private async loadEndpoint(pathName: string) {
16✔
170
    debug("importing module: %s", pathName);
28✔
171

28✔
172
    const directory = dirname(pathName.slice(this.basePath.length)).replaceAll(
28✔
173
      "\\",
28✔
174
      "/",
28✔
175
    );
28✔
176

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

28✔
183
    debug(`loading pathName from dependencyGraph: ${pathName}`);
28✔
184

28✔
185
    this.dependencyGraph.load(pathName);
28✔
186

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

28✔
193
      const endpoint = (await doImport(pathName).catch((err) => {
28✔
194
        console.log("ERROR");
×
195
      })) as ContextModule | Module;
×
196

28✔
197
      if (endpoint === undefined) {
28!
NEW
UNCOV
198
        throw new Error(`Failed to load endpoint: ${pathName}`);
×
NEW
UNCOV
199
      }
×
200

28✔
201
      this.dispatchEvent(new Event("add"));
28✔
202

28✔
203
      if (
28✔
204
        basename(pathName).startsWith("_.context.") &&
28✔
205
        isContextModule(endpoint)
12✔
206
      ) {
28✔
207
        const loadContext = (path: string) => this.contextRegistry.find(path);
12✔
208

12✔
209
        this.contextRegistry.update(
12✔
210
          directory,
12✔
211

12✔
212
          // @ts-expect-error TS says Context has no constructable signatures but that's not true?
12✔
213

12✔
214
          new endpoint.Context({
12✔
215
            loadContext,
12✔
216
          }),
12✔
217
        );
12✔
218
        return;
12✔
219
      }
12✔
220

16✔
221
      if (
16✔
222
        basename(pathName).startsWith("_.middleware.") &&
16✔
223
        isMiddlewareModule(endpoint)
4✔
224
      ) {
28✔
225
        this.registry.addMiddleware(
4✔
226
          url.slice(0, url.lastIndexOf("/")) || "/",
4✔
227
          endpoint.middleware,
4✔
228
        );
4✔
229
      }
4✔
230

16✔
231
      if (url === "/index") this.registry.add("/", endpoint as Module);
28✔
232

16✔
233
      debug(`adding "${url}" to registry`);
16✔
234
      this.registry.add(url, endpoint as Module);
16✔
235
    } catch (error: unknown) {
28!
236
      if (
×
237
        String(error) ===
×
238
        "SyntaxError: Identifier 'Context' has already been declared"
×
239
      ) {
×
240
        // Not sure why Node throws this error. It doesn't seem to matter.
×
241
        return;
×
242
      }
×
243

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

×
UNCOV
246
      throw error;
×
UNCOV
247
    }
×
248
  }
28✔
249
}
16✔
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