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

pmcelhaney / counterfact / 10645385797

31 Aug 2024 12:51PM UTC coverage: 82.599%. Remained the same
10645385797

push

github

web-flow
chore(deps): update dependency @swc/core to v1.7.21

1001 of 1114 branches covered (89.86%)

Branch coverage included in aggregate %.

3233 of 4012 relevant lines covered (80.58%)

42.37 hits per line

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

85.23
/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 { 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
}
2✔
24

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

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

12✔
34
  public readonly registry: Registry;
12✔
35

12✔
36
  private watcher: FSWatcher | undefined;
12✔
37

12✔
38
  private readonly contextRegistry: ContextRegistry;
12✔
39

12✔
40
  private readonly dependencyGraph = new ModuleDependencyGraph();
12✔
41

12✔
42
  private readonly uncachedImport: (moduleName: string) => Promise<unknown> =
12✔
43
    async function (moduleName: string) {
12✔
44
      throw new Error(`uncachedImport not set up; importing ${moduleName}`);
×
45
    };
×
46

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

12✔
58
  public async watch(): Promise<void> {
12✔
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

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

×
73
          return;
×
74
        }
×
75

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

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

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

2✔
90
        const dependencies = this.dependencyGraph.dependentsOf(pathName);
2✔
91

2✔
92
        void this.loadEndpoint(pathName);
2✔
93

2✔
94
        for (const dependency of dependencies) {
2!
95
          void this.loadEndpoint(dependency);
×
96
        }
×
97
      },
2✔
98
    );
4✔
99
    await once(this.watcher, "ready");
4✔
100
  }
4✔
101

12✔
102
  public async stopWatching(): Promise<void> {
12✔
103
    await this.watcher?.close();
4✔
104
  }
4✔
105

12✔
106
  public async load(directory = ""): Promise<void> {
12✔
107
    if (
22✔
108
      !existsSync(nodePath.join(this.basePath, directory).replaceAll("\\", "/"))
22✔
109
    ) {
22!
110
      throw new Error(`Directory does not exist ${this.basePath}`);
×
111
    }
×
112

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

22✔
120
    const imports = files.flatMap(async (file): Promise<void> => {
22✔
121
      const extension = file.name.split(".").at(-1);
44✔
122

44✔
123
      if (file.isDirectory()) {
44✔
124
        await this.load(
10✔
125
          nodePath.join(directory, file.name).replaceAll("\\", "/"),
10✔
126
        );
10✔
127

10✔
128
        return;
10✔
129
      }
10✔
130

34✔
131
      if (!["cjs", "cts", "js", "mjs", "mts", "ts"].includes(extension ?? "")) {
44✔
132
        return;
14✔
133
      }
14✔
134

20✔
135
      const fullPath = nodePath
20✔
136
        .join(this.basePath, directory, file.name)
20✔
137
        .replaceAll("\\", "/");
20✔
138

20✔
139
      await this.loadEndpoint(fullPath);
20✔
140
    });
20✔
141

22✔
142
    await Promise.all(imports);
22✔
143
  }
22✔
144

12✔
145
  private async loadEndpoint(pathName: string) {
12✔
146
    debug("importing module: %s", pathName);
22✔
147

22✔
148
    const directory = dirname(pathName.slice(this.basePath.length)).replaceAll(
22✔
149
      "\\",
22✔
150
      "/",
22✔
151
    );
22✔
152

22✔
153
    const url = `/${nodePath.join(
22✔
154
      directory,
22✔
155
      nodePath.parse(basename(pathName)).name,
22✔
156
    )}`
22✔
157
      .replaceAll("\\", "/")
22✔
158
      .replaceAll(/\/+/gu, "/");
22✔
159

22✔
160
    this.dependencyGraph.load(pathName);
22✔
161

22✔
162
    try {
22✔
163
      const doImport =
22✔
164
        (await determineModuleKind(pathName)) === "commonjs"
22!
165
          ? uncachedRequire
×
166
          : uncachedImport;
22✔
167

22✔
168
      const endpoint = (await doImport(pathName)) as ContextModule | Module;
22✔
169

22✔
170
      this.dispatchEvent(new Event("add"));
22✔
171

22✔
172
      if (basename(pathName).startsWith("_.context")) {
22✔
173
        if (isContextModule(endpoint)) {
12✔
174
          const loadContext = (path: string) => this.contextRegistry.find(path);
12✔
175

12✔
176
          this.contextRegistry.update(
12✔
177
            directory,
12✔
178

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

12✔
181
            new endpoint.Context({
12✔
182
              loadContext,
12✔
183
            }),
12✔
184
          );
12✔
185
        }
12✔
186
      } else {
22✔
187
        this.registry.add(url, endpoint as Module);
10✔
188
      }
10✔
189
    } catch (error: unknown) {
22!
190
      if (
×
191
        String(error) ===
×
192
        "SyntaxError: Identifier 'Context' has already been declared"
×
193
      ) {
×
194
        // Not sure why Node throws this error. It doesn't seem to matter.
×
195
        return;
×
196
      }
×
197

×
198
      throw error;
×
199

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