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

pmcelhaney / counterfact / 8697996524

16 Apr 2024 12:29AM UTC coverage: 86.607% (+0.09%) from 86.517%
8697996524

Pull #852

github

web-flow
Merge branch 'main' into refactor-path-coder
Pull Request #852: Support OpenAPI reusable responses in global components object (handle $ref properly everywhere)

891 of 984 branches covered (90.55%)

Branch coverage included in aggregate %.

101 of 103 new or added lines in 8 files covered. (98.06%)

2 existing lines in 1 file now uncovered.

2879 of 3369 relevant lines covered (85.46%)

43.44 hits per line

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

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

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

2✔
9
import { type Context, ContextRegistry } from "./context-registry.js";
2✔
10
import { determineModuleKind } from "./determine-module-kind.js";
2✔
11
import type { Module, Registry } from "./registry.js";
2✔
12
import { uncachedImport } from "./uncached-import.js";
2✔
13

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

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

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

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

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

×
37
  process.stdout.write(`\nError loading ${fileUrl}:\n~~${String(error)}~~\n`);
×
38
}
×
39

2✔
40
export class ModuleLoader extends EventTarget {
2✔
41
  private readonly basePath: string;
12✔
42

12✔
43
  public readonly registry: Registry;
12✔
44

12✔
45
  private watcher: FSWatcher | undefined;
12✔
46

12✔
47
  private readonly contextRegistry: ContextRegistry;
12✔
48

12✔
49
  private uncachedImport: (moduleName: string) => Promise<unknown> =
12✔
50
    // eslint-disable-next-line @typescript-eslint/require-await
12✔
51
    async function (moduleName: string) {
12✔
52
      throw new Error(`uncachedImport not set up; importing ${moduleName}`);
×
53
    };
×
54

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

12✔
66
  public async watch(): Promise<void> {
12✔
67
    this.watcher = watch(`${this.basePath}/**/*.{js,mjs,ts,mts}`).on(
6✔
68
      "all",
6✔
69
      // eslint-disable-next-line max-statements
6✔
70
      (eventName: string, pathNameOriginal: string) => {
6✔
71
        const pathName = pathNameOriginal.replaceAll("\\", "/");
8✔
72

8✔
73
        if (pathName.includes("$.context") && eventName === "add") {
8!
74
          process.stdout.write(
×
75
            `\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`,
×
76
          );
×
77
          return;
×
78
        }
×
79

8✔
80
        if (!["add", "change", "unlink"].includes(eventName)) {
8!
81
          return;
×
82
        }
×
83

8✔
84
        const parts = nodePath.parse(pathName.replace(this.basePath, ""));
8✔
85
        const url = `/${parts.dir}/${parts.name}`
8✔
86
          .replaceAll("\\", "/")
8✔
87
          .replaceAll(/\/+/gu, "/");
8✔
88

8✔
89
        if (eventName === "unlink") {
8✔
90
          this.registry.remove(url);
2✔
91
          this.dispatchEvent(new Event("remove"));
2✔
92
        }
2✔
93

8✔
94
        debug("importing module: %s", pathName);
8✔
95
        this.uncachedImport(pathName)
8✔
96
          // eslint-disable-next-line promise/prefer-await-to-then
8✔
97
          .then((endpoint) => {
8✔
98
            this.dispatchEvent(new Event(eventName));
8✔
99

8✔
100
            if (pathName.includes("_.context")) {
8!
101
              this.contextRegistry.update(
×
102
                parts.dir,
×
103

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

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

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

12✔
125
  public async stopWatching(): Promise<void> {
12✔
126
    await this.watcher?.close();
6✔
127
  }
6✔
128

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

20✔
132
    this.uncachedImport =
20✔
133
      moduleKind === "module" ? uncachedImport : uncachedRequire;
20!
134

20✔
135
    if (
20✔
136
      !existsSync(nodePath.join(this.basePath, directory).replaceAll("\\", "/"))
20✔
137
    ) {
20!
138
      throw new Error(`Directory does not exist ${this.basePath}`);
×
139
    }
×
140

20✔
141
    const files = await fs.readdir(
20✔
142
      nodePath.join(this.basePath, directory).replaceAll("\\", "/"),
20✔
143
      {
20✔
144
        withFileTypes: true,
20✔
145
      },
20✔
146
    );
20✔
147

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

38✔
151
      if (file.isDirectory()) {
38✔
152
        await this.load(
8✔
153
          nodePath.join(directory, file.name).replaceAll("\\", "/"),
8✔
154
        );
8✔
155

8✔
156
        return;
8✔
157
      }
8✔
158

30✔
159
      if (!["js", "mjs", "mts", "ts"].includes(extension ?? "")) {
38✔
160
        return;
14✔
161
      }
14✔
162

16✔
163
      const fullPath = nodePath
16✔
164
        .join(this.basePath, directory, file.name)
16✔
165
        .replaceAll("\\", "/");
16✔
166
      await this.loadEndpoint(fullPath, directory, file);
16✔
167
    });
16✔
168

20✔
169
    await Promise.all(imports);
20✔
170
  }
20✔
171

12✔
172
  private async loadEndpoint(
12✔
173
    fullPath: string,
16✔
174
    directory: string,
16✔
175
    file: Dirent,
16✔
176
  ) {
16✔
177
    try {
16✔
178
      // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
16✔
179
      const endpoint: ContextModule | Module = (await this.uncachedImport(
16✔
180
        fullPath,
16✔
181
      )) as ContextModule | Module;
16✔
182

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

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

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

×
213
      process.stdout.write(`\nError loading ${fullPath}:\n${String(error)}\n`);
×
214
    }
×
215
  }
16✔
216
}
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