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

pmcelhaney / counterfact / 8887027088

29 Apr 2024 11:42PM UTC coverage: 87.472% (+0.7%) from 86.763%
8887027088

Pull #875

github

pmcelhaney
fixed a bug where context wasn't updating... with some kludges
Pull Request #875: reload dependent modules

927 of 1020 branches covered (90.88%)

Branch coverage included in aggregate %.

94 of 95 new or added lines in 3 files covered. (98.95%)

5 existing lines in 1 file now uncovered.

3004 of 3474 relevant lines covered (86.47%)

43.65 hits per line

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

87.16
/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 } 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 type { Module, Registry } from "./registry.js";
2✔
13
import { uncachedImport } from "./uncached-import.js";
2✔
14

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

2✔
17
const debug = createDebug("counterfact:typescript-generator:module-loader");
2✔
18

2✔
19
interface ContextModule {
2✔
20
  Context: Context;
2✔
21
}
2✔
22

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

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

12✔
32
  public readonly registry: Registry;
12✔
33

12✔
34
  private watcher: FSWatcher | undefined;
12✔
35

12✔
36
  private readonly contextRegistry: ContextRegistry;
12✔
37

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

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

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

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

4✔
72
        if (!["add", "change", "unlink"].includes(eventName)) {
4!
73
          return;
×
74
        }
×
75

4✔
76
        const parts = nodePath.parse(pathName.replace(this.basePath, ""));
4✔
77
        const url = `/${parts.dir}/${parts.name}`
4✔
78
          .replaceAll("\\", "/")
4✔
79
          .replaceAll(/\/+/gu, "/");
4✔
80

4✔
81
        if (eventName === "unlink") {
4✔
82
          this.registry.remove(url);
2✔
83
          this.dispatchEvent(new Event("remove"));
2✔
84
        }
2✔
85

4✔
86
        void this.loadEndpoint(pathName, parts.dir, url);
4✔
87
      },
4✔
88
    );
6✔
89
    await once(this.watcher, "ready");
6✔
90
  }
6✔
91

12✔
92
  public async stopWatching(): Promise<void> {
12✔
93
    await this.watcher?.close();
6✔
94
  }
6✔
95

12✔
96
  public async load(directory = ""): Promise<void> {
12✔
97
    const moduleKind = await determineModuleKind(this.basePath);
20✔
98

20✔
99
    this.uncachedImport =
20✔
100
      moduleKind === "module" ? uncachedImport : uncachedRequire;
20!
101

20✔
102
    if (
20✔
103
      !existsSync(nodePath.join(this.basePath, directory).replaceAll("\\", "/"))
20✔
104
    ) {
20!
105
      throw new Error(`Directory does not exist ${this.basePath}`);
×
106
    }
×
107

20✔
108
    const files = await fs.readdir(
20✔
109
      nodePath.join(this.basePath, directory).replaceAll("\\", "/"),
20✔
110
      {
20✔
111
        withFileTypes: true,
20✔
112
      },
20✔
113
    );
20✔
114

20✔
115
    const imports = files.flatMap(async (file): Promise<void> => {
20✔
116
      const extension = file.name.split(".").at(-1);
38✔
117

38✔
118
      if (file.isDirectory()) {
38✔
119
        await this.load(
8✔
120
          nodePath.join(directory, file.name).replaceAll("\\", "/"),
8✔
121
        );
8✔
122

8✔
123
        return;
8✔
124
      }
8✔
125

30✔
126
      if (!["js", "mjs", "mts", "ts"].includes(extension ?? "")) {
38✔
127
        return;
14✔
128
      }
14✔
129

16✔
130
      const fullPath = nodePath
16✔
131
        .join(this.basePath, directory, file.name)
16✔
132
        .replaceAll("\\", "/");
16✔
133

16✔
134
      const url = `/${nodePath.join(
16✔
135
        directory,
16✔
136
        nodePath.parse(basename(fullPath)).name,
16✔
137
      )}`
16✔
138
        .replaceAll("\\", "/")
16✔
139
        .replaceAll(/\/+/gu, "/");
16✔
140

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

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

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

20✔
151
    try {
20✔
152
      // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
20✔
153
      const endpoint = (await this.uncachedImport(pathName)) as
20✔
154
        | ContextModule
20✔
155
        | Module;
20✔
156

20✔
157
      this.dispatchEvent(new Event("add"));
20✔
158

20✔
159
      if (basename(pathName).startsWith("_.context")) {
20✔
160
        if (isContextModule(endpoint)) {
8✔
161
          this.contextRegistry.update(
8✔
162
            `/${directory.replaceAll("\\", "/")}`.replaceAll(/\/+/gu, "/"),
8✔
163

8✔
164
            // @ts-expect-error TS says Context has no constructable signatures but that's not true?
8✔
165
            // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
8✔
166
            new endpoint.Context(),
8✔
167
          );
8✔
168
        }
8✔
169
      } else {
20✔
170
        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
12✔
171
        this.registry.add(url, endpoint as Module);
12✔
172
      }
12✔
173
    } catch (error: unknown) {
20!
UNCOV
174
      if (
×
UNCOV
175
        String(error) ===
×
UNCOV
176
        "SyntaxError: Identifier 'Context' has already been declared"
×
UNCOV
177
      ) {
×
UNCOV
178
        // Not sure why Node throws this error. It doesn't seem to matter.
×
179
        return;
×
180
      }
×
181

×
NEW
182
      process.stdout.write(`\nError loading ${pathName}:\n${String(error)}\n`);
×
183
    }
×
184
  }
20✔
185
}
12✔
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