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

pmcelhaney / counterfact / 8993935739

08 May 2024 12:05AM UTC coverage: 87.212% (-0.2%) from 87.405%
8993935739

Pull #899

github

pmcelhaney
fix #898, CJS files were getting loaded as modules hence more cache issues
Pull Request #899: fix #898, CJS files were getting loaded as modules hence more cache issues

941 of 1043 branches covered (90.22%)

Branch coverage included in aggregate %.

13 of 18 new or added lines in 2 files covered. (72.22%)

8 existing lines in 1 file now uncovered.

3076 of 3563 relevant lines covered (86.33%)

43.32 hits per line

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

85.65
/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 { 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
interface ContextModule {
2✔
21
  Context: Context;
2✔
22
}
2✔
23

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

2✔
30
export class ModuleLoader extends EventTarget {
2✔
31
  private readonly basePath: string;
10✔
32

10✔
33
  public readonly registry: Registry;
10✔
34

10✔
35
  private watcher: FSWatcher | undefined;
10✔
36

10✔
37
  private readonly contextRegistry: ContextRegistry;
10✔
38

10✔
39
  private readonly dependencyGraph = new ModuleDependencyGraph();
10✔
40

10✔
41
  private uncachedImport: (moduleName: string) => Promise<unknown> =
10✔
42
    // eslint-disable-next-line @typescript-eslint/require-await
10✔
43
    async function (moduleName: string) {
10✔
44
      throw new Error(`uncachedImport not set up; importing ${moduleName}`);
×
45
    };
×
46

10✔
47
  public constructor(
10✔
48
    basePath: string,
10✔
49
    registry: Registry,
10✔
50
    contextRegistry = new ContextRegistry(),
10✔
51
  ) {
10✔
52
    super();
10✔
53
    this.basePath = basePath.replaceAll("\\", "/");
10✔
54
    this.registry = registry;
10✔
55
    this.contextRegistry = contextRegistry;
10✔
56
  }
10✔
57

10✔
58
  public async watch(): Promise<void> {
10✔
59
    this.watcher = watch(
4✔
60
      `${this.basePath}/**/*.{js,mjs,ts,mts,cjs,cts}`,
4✔
61
      CHOKIDAR_OPTIONS,
4✔
62
    ).on(
4✔
63
      "all",
4✔
64
      // eslint-disable-next-line max-statements
4✔
65
      (eventName: string, pathNameOriginal: string) => {
4✔
66
        const pathName = pathNameOriginal.replaceAll("\\", "/");
2✔
67

2✔
68
        if (pathName.includes("$.context") && eventName === "add") {
2!
69
          process.stdout.write(
×
70
            `\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`,
×
71
          );
×
72
          return;
×
73
        }
×
74

2✔
75
        if (!["add", "change", "unlink"].includes(eventName)) {
2!
76
          return;
×
77
        }
×
78

2✔
79
        const parts = nodePath.parse(pathName.replace(this.basePath, ""));
2✔
80
        const url = `/${parts.dir}/${parts.name}`
2✔
81
          .replaceAll("\\", "/")
2✔
82
          .replaceAll(/\/+/gu, "/");
2✔
83

2✔
84
        if (eventName === "unlink") {
2✔
85
          this.registry.remove(url);
2✔
86
          this.dispatchEvent(new Event("remove"));
2✔
87
        }
2✔
88
        const dependencies = this.dependencyGraph.dependentsOf(pathName);
2✔
89

2✔
90
        void this.loadEndpoint(pathName);
2✔
91
        for (const dependency of dependencies) {
2!
92
          void this.loadEndpoint(dependency);
×
93
        }
×
94
      },
2✔
95
    );
4✔
96
    await once(this.watcher, "ready");
4✔
97
  }
4✔
98

10✔
99
  public async stopWatching(): Promise<void> {
10✔
100
    await this.watcher?.close();
4✔
101
  }
4✔
102

10✔
103
  public async load(directory = ""): Promise<void> {
10✔
104
    const moduleKind = await determineModuleKind(this.basePath);
18✔
105

18✔
106
    this.uncachedImport =
18✔
107
      moduleKind === "module" ? uncachedImport : uncachedRequire;
18!
108

18✔
109
    if (
18✔
110
      !existsSync(nodePath.join(this.basePath, directory).replaceAll("\\", "/"))
18✔
111
    ) {
18!
UNCOV
112
      throw new Error(`Directory does not exist ${this.basePath}`);
×
UNCOV
113
    }
×
114

18✔
115
    const files = await fs.readdir(
18✔
116
      nodePath.join(this.basePath, directory).replaceAll("\\", "/"),
18✔
117
      {
18✔
118
        withFileTypes: true,
18✔
119
      },
18✔
120
    );
18✔
121

18✔
122
    const imports = files.flatMap(async (file): Promise<void> => {
18✔
123
      const extension = file.name.split(".").at(-1);
36✔
124

36✔
125
      if (file.isDirectory()) {
36✔
126
        await this.load(
8✔
127
          nodePath.join(directory, file.name).replaceAll("\\", "/"),
8✔
128
        );
8✔
129

8✔
130
        return;
8✔
131
      }
8✔
132

28✔
133
      if (!["cjs", "cts", "js", "mjs", "mts", "ts"].includes(extension ?? "")) {
36✔
134
        return;
12✔
135
      }
12✔
136

16✔
137
      const fullPath = nodePath
16✔
138
        .join(this.basePath, directory, file.name)
16✔
139
        .replaceAll("\\", "/");
16✔
140

16✔
141
      await this.loadEndpoint(fullPath);
16✔
142
    });
16✔
143

18✔
144
    await Promise.all(imports);
18✔
145
  }
18✔
146

10✔
147
  // eslint-disable-next-line max-statements
10✔
148
  private async loadEndpoint(pathName: string) {
10✔
149
    debug("importing module: %s", pathName);
18✔
150

18✔
151
    const directory = dirname(pathName.slice(this.basePath.length)).replaceAll(
18✔
152
      "\\",
18✔
153
      "/",
18✔
154
    );
18✔
155

18✔
156
    const url = `/${nodePath.join(
18✔
157
      directory,
18✔
158
      nodePath.parse(basename(pathName)).name,
18✔
159
    )}`
18✔
160
      .replaceAll("\\", "/")
18✔
161
      .replaceAll(/\/+/gu, "/");
18✔
162

18✔
163
    this.dependencyGraph.load(pathName);
18✔
164

18✔
165
    try {
18✔
166
      const doImport =
18✔
167
        (await determineModuleKind(pathName)) === "commonjs"
18!
NEW
UNCOV
168
          ? uncachedRequire
×
169
          : uncachedImport;
18✔
170

18✔
171
      // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
18✔
172
      const endpoint = (await doImport(pathName)) as ContextModule | Module;
18✔
173

18✔
174
      this.dispatchEvent(new Event("add"));
18✔
175

18✔
176
      if (basename(pathName).startsWith("_.context")) {
18✔
177
        if (isContextModule(endpoint)) {
8✔
178
          this.contextRegistry.update(
8✔
179
            directory,
8✔
180

8✔
181
            // @ts-expect-error TS says Context has no constructable signatures but that's not true?
8✔
182
            // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
8✔
183
            new endpoint.Context(),
8✔
184
          );
8✔
185
        }
8✔
186
      } else {
18✔
187
        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
10✔
188
        this.registry.add(url, endpoint as Module);
10✔
189
      }
10✔
190
    } catch (error: unknown) {
18!
191
      if (
×
192
        String(error) ===
×
193
        "SyntaxError: Identifier 'Context' has already been declared"
×
194
      ) {
×
195
        // Not sure why Node throws this error. It doesn't seem to matter.
×
UNCOV
196
        return;
×
UNCOV
197
      }
×
UNCOV
198

×
UNCOV
199
      process.stdout.write(`\nError loading ${pathName}:\n${String(error)}\n`);
×
UNCOV
200
    }
×
201
  }
18✔
202
}
10✔
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

© 2025 Coveralls, Inc