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

pmcelhaney / counterfact / 7857330850

10 Feb 2024 08:49PM UTC coverage: 86.246% (+0.1%) from 86.138%
7857330850

Pull #764

github

web-flow
Merge branch 'main' into change-context-update-algorithm
Pull Request #764: change context update algorithm

874 of 963 branches covered (0.0%)

Branch coverage included in aggregate %.

6 of 6 new or added lines in 1 file covered. (100.0%)

2763 of 3254 relevant lines covered (84.91%)

40.75 hits per line

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

80.41
/src/server/module-loader.ts
1
import { once } from "node:events";
2✔
2
import { type Dirent, existsSync } from "node:fs";
2✔
3
import fs from "node:fs/promises";
2✔
4
import nodePath from "node:path";
2✔
5
import { pathToFileURL } from "node:url";
2✔
6

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

2✔
10
import { type Context, ContextRegistry } from "./context-registry.js";
2✔
11
import type { Module, Registry } from "./registry.js";
2✔
12

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

2✔
15
interface ContextModule {
2✔
16
  Context: Context;
2✔
17
}
2✔
18

2✔
19
function isContextModule(
8✔
20
  module: ContextModule | Module,
8✔
21
): module is ContextModule {
8✔
22
  return "Context" in module && typeof module.Context === "function";
8✔
23
}
8✔
24

2✔
25
function reportLoadError(error: unknown, fileUrl: string) {
×
26
  if (
×
27
    String(error) ===
×
28
    "SyntaxError: Identifier 'Context' has already been declared"
×
29
  ) {
×
30
    // Not sure why Node throws this error. It doesn't seem to matter.
×
31
    return;
×
32
  }
×
33

×
34
  process.stdout.write(`\nError loading ${fileUrl}:\n~~${String(error)}~~\n`);
×
35
}
×
36

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

12✔
40
  public readonly registry: Registry;
12✔
41

12✔
42
  private watcher: FSWatcher | undefined;
12✔
43

12✔
44
  private readonly contextRegistry: ContextRegistry;
12✔
45

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

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

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

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

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

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

8✔
85
        const fileUrl = `${pathToFileURL(
8✔
86
          pathName,
8✔
87
        ).toString()}?cacheBust=${Date.now()}`;
8✔
88

8✔
89
        debug("importing module: %s", fileUrl);
8✔
90

8✔
91
        // eslint-disable-next-line import/no-dynamic-require, no-unsanitized/method
8✔
92
        import(fileUrl)
8✔
93
          // eslint-disable-next-line promise/prefer-await-to-then
8✔
94
          .then((endpoint: ContextModule | Module) => {
8✔
95
            this.dispatchEvent(new Event(eventName));
8✔
96

8✔
97
            if (pathName.includes("_.context")) {
8!
98
              this.contextRegistry.update(
×
99
                parts.dir,
×
100

×
101
                // @ts-expect-error TS says Context has no constructable signatures but that's not true?
×
102
                // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/consistent-type-assertions
×
103
                new (endpoint as ContextModule).Context(),
×
104
              );
×
105
              return "context";
×
106
            }
×
107

8✔
108
            // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
8✔
109
            this.registry.add(url, endpoint as Module);
8✔
110

8✔
111
            return "path";
8✔
112
          })
8✔
113
          // eslint-disable-next-line promise/prefer-await-to-then
8✔
114
          .catch((error: unknown) => {
8✔
115
            reportLoadError(error, fileUrl);
×
116
          });
×
117
      },
8✔
118
    );
6✔
119
    await once(this.watcher, "ready");
6✔
120
  }
6✔
121

12✔
122
  public async stopWatching(): Promise<void> {
12✔
123
    await this.watcher?.close();
6✔
124
  }
6✔
125

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

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

20✔
140
    const imports = files.flatMap(async (file): Promise<void> => {
20✔
141
      const extension = file.name.split(".").at(-1);
26✔
142

26✔
143
      if (file.isDirectory()) {
26✔
144
        await this.load(
8✔
145
          nodePath.join(directory, file.name).replaceAll("\\", "/"),
8✔
146
        );
8✔
147

8✔
148
        return;
8✔
149
      }
8✔
150

18✔
151
      if (!["js", "mjs", "mts", "ts"].includes(extension ?? "")) {
26✔
152
        return;
2✔
153
      }
2✔
154

16✔
155
      const fullPath = nodePath
16✔
156
        .join(this.basePath, directory, file.name)
16✔
157
        .replaceAll("\\", "/");
16✔
158
      await this.loadEndpoint(fullPath, directory, file);
16✔
159
    });
16✔
160

20✔
161
    await Promise.all(imports);
20✔
162
  }
20✔
163

12✔
164
  // eslint-disable-next-line max-statements
12✔
165
  private async loadEndpoint(
12✔
166
    fullPath: string,
16✔
167
    directory: string,
16✔
168
    file: Dirent,
16✔
169
  ) {
16✔
170
    const fileUrl = `${pathToFileURL(
16✔
171
      fullPath,
16✔
172
    ).toString()}?cacheBust=${Date.now()}`;
16✔
173

16✔
174
    try {
16✔
175
      // eslint-disable-next-line import/no-dynamic-require, no-unsanitized/method, @typescript-eslint/consistent-type-assertions
16✔
176
      const endpoint: ContextModule | Module = (await import(fileUrl)) as
16✔
177
        | ContextModule
16✔
178
        | Module;
16✔
179

16✔
180
      if (file.name.includes("_.context")) {
16✔
181
        if (isContextModule(endpoint)) {
8✔
182
          this.contextRegistry.add(
8✔
183
            `/${directory.replaceAll("\\", "/")}`,
8✔
184

8✔
185
            // @ts-expect-error TS says Context has no constructable signatures but that's not true?
8✔
186
            // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
8✔
187
            new endpoint.Context(),
8✔
188
          );
8✔
189
        }
8✔
190
      } else {
8✔
191
        const url = `/${nodePath.join(
8✔
192
          directory,
8✔
193
          nodePath.parse(file.name).name,
8✔
194
        )}`
8✔
195
          .replaceAll("\\", "/")
8✔
196
          .replaceAll(/\/+/gu, "/");
8✔
197

8✔
198
        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
8✔
199
        this.registry.add(url, endpoint as Module);
8✔
200
      }
8✔
201
    } catch (error: unknown) {
16!
202
      if (
×
203
        String(error) ===
×
204
        "SyntaxError: Identifier 'Context' has already been declared"
×
205
      ) {
×
206
        // Not sure why Node throws this error. It doesn't seem to matter.
×
207
        return;
×
208
      }
×
209

×
210
      process.stdout.write(`\nError loading ${fileUrl}:\n${String(error)}\n`);
×
211
    }
×
212
  }
16✔
213
}
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

© 2025 Coveralls, Inc