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

pmcelhaney / counterfact / 13740927970

08 Mar 2025 07:51PM UTC coverage: 82.347% (-0.09%) from 82.436%
13740927970

push

github

pmcelhaney
rename interceptor to middleware

1110 of 1241 branches covered (89.44%)

Branch coverage included in aggregate %.

14 of 17 new or added lines in 3 files covered. (82.35%)

9 existing lines in 1 file now uncovered.

3303 of 4118 relevant lines covered (80.21%)

63.1 hits per line

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

82.5
/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

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

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

2✔
21
interface ContextModule {
2✔
22
  Context?: Context;
2✔
23
  middleware?: MiddlewareFunction;
2✔
24
}
2✔
25

2✔
26
function isContextModule(
12✔
27
  module: ContextModule | Module,
12✔
28
): module is ContextModule {
12✔
29
  return "Context" in module && typeof module.Context === "function";
12✔
30
}
12✔
31

2✔
UNCOV
32
function isInterceptModule(
×
UNCOV
33
  module: ContextModule | Module,
×
NEW
34
): module is ContextModule & { middleware: MiddlewareFunction } {
×
NEW
35
  return "middleware" in module && typeof module.middleware === "function";
×
UNCOV
36
}
×
37

2✔
38
export class ModuleLoader extends EventTarget {
2✔
39
  private readonly basePath: string;
14✔
40

14✔
41
  public readonly registry: Registry;
14✔
42

14✔
43
  private watcher: FSWatcher | undefined;
14✔
44

14✔
45
  private readonly contextRegistry: ContextRegistry;
14✔
46

14✔
47
  private readonly dependencyGraph = new ModuleDependencyGraph();
14✔
48

14✔
49
  private readonly uncachedImport: (moduleName: string) => Promise<unknown> =
14✔
50
    async function (moduleName: string) {
14✔
51
      throw new Error(`uncachedImport not set up; importing ${moduleName}`);
×
52
    };
×
53

14✔
54
  public constructor(
14✔
55
    basePath: string,
14✔
56
    registry: Registry,
14✔
57
    contextRegistry = new ContextRegistry(),
14✔
58
  ) {
14✔
59
    super();
14✔
60
    this.basePath = basePath.replaceAll("\\", "/");
14✔
61
    this.registry = registry;
14✔
62
    this.contextRegistry = contextRegistry;
14✔
63
  }
14✔
64

14✔
65
  public async watch(): Promise<void> {
14✔
66
    this.watcher = watch(this.basePath, CHOKIDAR_OPTIONS).on(
4✔
67
      "all",
4✔
68

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

4✔
72
        if (
4✔
73
          !JS_EXTENSIONS.some((extension) =>
4✔
74
            pathNameOriginal.endsWith(`.${extension}`),
14✔
75
          )
4✔
76
        )
4✔
77
          return;
4✔
78

2✔
79
        const pathName = pathNameOriginal.replaceAll("\\", "/");
2✔
80

2✔
81
        if (pathName.includes("$.context") && eventName === "add") {
4!
82
          process.stdout.write(
×
83
            `\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`,
×
84
          );
×
85

×
86
          return;
×
87
        }
✔
88

2✔
89
        if (!["add", "change", "unlink"].includes(eventName)) {
4!
90
          return;
×
91
        }
✔
92

2✔
93
        const parts = nodePath.parse(pathName.replace(this.basePath, ""));
2✔
94
        const url = `/${parts.dir}/${parts.name}`
2✔
95
          .replaceAll("\\", "/")
2✔
96
          .replaceAll(/\/+/gu, "/");
2✔
97

2✔
98
        if (eventName === "unlink") {
2✔
99
          this.registry.remove(url);
2✔
100
          this.dispatchEvent(new Event("remove"));
2✔
101
        }
2✔
102

2✔
103
        const dependencies = this.dependencyGraph.dependentsOf(pathName);
2✔
104

2✔
105
        void this.loadEndpoint(pathName);
2✔
106

2✔
107
        for (const dependency of dependencies) {
4!
108
          void this.loadEndpoint(dependency);
×
109
        }
✔
110
      },
2✔
111
    );
4✔
112
    await once(this.watcher, "ready");
4✔
113
  }
4✔
114

14✔
115
  public async stopWatching(): Promise<void> {
14✔
116
    await this.watcher?.close();
4✔
117
  }
4✔
118

14✔
119
  public async load(directory = ""): Promise<void> {
14✔
120
    if (
24✔
121
      !existsSync(nodePath.join(this.basePath, directory).replaceAll("\\", "/"))
24✔
122
    ) {
24!
123
      throw new Error(`Directory does not exist ${this.basePath}`);
×
124
    }
×
125

24✔
126
    const files = await fs.readdir(
24✔
127
      nodePath.join(this.basePath, directory).replaceAll("\\", "/"),
24✔
128
      {
24✔
129
        withFileTypes: true,
24✔
130
      },
24✔
131
    );
24✔
132

24✔
133
    const imports = files.flatMap(async (file): Promise<void> => {
24✔
134
      const extension = file.name.split(".").at(-1);
48✔
135

48✔
136
      if (file.isDirectory()) {
48✔
137
        await this.load(
10✔
138
          nodePath.join(directory, file.name).replaceAll("\\", "/"),
10✔
139
        );
10✔
140

10✔
141
        return;
10✔
142
      }
10✔
143

38✔
144
      if (!["cjs", "cts", "js", "mjs", "mts", "ts"].includes(extension ?? "")) {
48✔
145
        return;
16✔
146
      }
16✔
147

22✔
148
      const fullPath = nodePath
22✔
149
        .join(this.basePath, directory, file.name)
22✔
150
        .replaceAll("\\", "/");
22✔
151

22✔
152
      await this.loadEndpoint(fullPath);
22✔
153
    });
22✔
154

24✔
155
    await Promise.all(imports);
24✔
156
  }
24✔
157

14✔
158
  private async loadEndpoint(pathName: string) {
14✔
159
    debug("importing module: %s", pathName);
24✔
160

24✔
161
    const directory = dirname(pathName.slice(this.basePath.length)).replaceAll(
24✔
162
      "\\",
24✔
163
      "/",
24✔
164
    );
24✔
165

24✔
166
    const url = `/${nodePath.join(
24✔
167
      directory,
24✔
168
      nodePath.parse(basename(pathName)).name,
24✔
169
    )}`
24✔
170
      .replaceAll("\\", "/")
24✔
171
      .replaceAll(/\/+/gu, "/");
24✔
172

24✔
173
    this.dependencyGraph.load(pathName);
24✔
174

24✔
175
    try {
24✔
176
      const doImport =
24✔
177
        (await determineModuleKind(pathName)) === "commonjs"
24!
178
          ? uncachedRequire
×
179
          : uncachedImport;
24✔
180

24✔
181
      const endpoint = (await doImport(pathName)) as ContextModule | Module;
24✔
182

24✔
183
      this.dispatchEvent(new Event("add"));
24✔
184

24✔
185
      if (
24✔
186
        basename(pathName).startsWith("_.context.") &&
24✔
187
        isContextModule(endpoint)
12✔
188
      ) {
24✔
189
        const loadContext = (path: string) => this.contextRegistry.find(path);
12✔
190

12✔
191
        this.contextRegistry.update(
12✔
192
          directory,
12✔
193

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

12✔
196
          new endpoint.Context({
12✔
197
            loadContext,
12✔
198
          }),
12✔
199
        );
12✔
200
        return;
12✔
201
      }
12✔
202

12✔
203
      if (
12✔
204
        basename(pathName).startsWith("_.middleware.") &&
12!
UNCOV
205
        isInterceptModule(endpoint)
×
206
      ) {
24!
NEW
207
        this.registry.addMiddleware(url, endpoint.middleware);
×
208
      }
✔
209

12✔
210
      if (url === "/index") this.registry.add("/", endpoint as Module);
24✔
211
      this.registry.add(url, endpoint as Module);
12✔
212
    } catch (error: unknown) {
24!
213
      if (
×
214
        String(error) ===
×
215
        "SyntaxError: Identifier 'Context' has already been declared"
×
216
      ) {
×
217
        // Not sure why Node throws this error. It doesn't seem to matter.
×
218
        return;
×
219
      }
×
UNCOV
220

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

×
UNCOV
223
      throw error;
×
UNCOV
224
    }
×
225
  }
24✔
226
}
14✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc