Skip to content

Commit 145c7f4

Browse files
authored
Add ForwardSlashPath branded type and toForwardSlashPath utility function
Agent-Logs-Url: https://github.com/counterfact/api-simulator/sessions/1369f6f6-b129-4be5-b4c7-7337eefb6eaa
1 parent 294319d commit 145c7f4

17 files changed

+182
-106
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"counterfact": patch
3+
---
4+
5+
Add `toForwardSlashPath` utility function and `ForwardSlashPath` branded type. All path normalization that previously used inline `.replaceAll("\\", "/")` now goes through this single, centralized function, making Windows path handling easier to find and reason about.

bin/counterfact.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,14 @@ const { loadConfigFile } = await import(
162162
)
163163
);
164164

165+
const { toForwardSlashPath } = await import(
166+
resolve(
167+
nativeTs
168+
? "../src/util/forward-slash-path.js"
169+
: "../dist/util/forward-slash-path.js",
170+
)
171+
);
172+
165173
const DEFAULT_PORT = 3100;
166174

167175
const debug = createDebug("counterfact:bin:counterfact");
@@ -314,9 +322,9 @@ async function main(source, destination) {
314322
source = options.spec;
315323
}
316324

317-
const destinationPath = nodePath.resolve(destination).replaceAll("\\", "/");
325+
const destinationPath = toForwardSlashPath(nodePath.resolve(destination));
318326

319-
const basePath = nodePath.resolve(destinationPath).replaceAll("\\", "/");
327+
const basePath = toForwardSlashPath(nodePath.resolve(destinationPath));
320328

321329
// If no action-related option is provided, default to all options
322330

src/app.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { Transpiler } from "./server/transpiler.js";
1919
import { CodeGenerator } from "./typescript-generator/code-generator.js";
2020
import { writeScenarioContextType } from "./typescript-generator/generate.js";
2121
import { runtimeCanExecuteErasableTs } from "./util/runtime-can-execute-erasable-ts.js";
22+
import { toForwardSlashPath } from "./util/forward-slash-path.js";
2223

2324
export { loadOpenApiDocument } from "./server/load-openapi-document.js";
2425

@@ -113,9 +114,9 @@ export async function createMswHandlers(
113114
await fs.readFile(config.openApiPath);
114115
const openApiDocument = await loadOpenApiDocument(config.openApiPath);
115116
const modulesPath = config.basePath;
116-
const compiledPathsDirectory = nodePath
117-
.join(modulesPath, ".cache")
118-
.replaceAll("\\", "/");
117+
const compiledPathsDirectory = toForwardSlashPath(
118+
nodePath.join(modulesPath, ".cache"),
119+
);
119120

120121
const registry = new Registry();
121122
const contextRegistry = new ContextRegistry();
@@ -175,9 +176,9 @@ export async function counterfact(config: Config) {
175176

176177
const nativeTs = await runtimeCanExecuteErasableTs();
177178

178-
const compiledPathsDirectory = nodePath
179-
.join(modulesPath, nativeTs ? "routes" : ".cache")
180-
.replaceAll("\\", "/");
179+
const compiledPathsDirectory = toForwardSlashPath(
180+
nodePath.join(modulesPath, nativeTs ? "routes" : ".cache"),
181+
);
181182

182183
if (!nativeTs) {
183184
await rm(compiledPathsDirectory, { force: true, recursive: true });
@@ -208,7 +209,7 @@ export async function counterfact(config: Config) {
208209
);
209210

210211
const transpiler = new Transpiler(
211-
nodePath.join(modulesPath, "routes").replaceAll("\\", "/"),
212+
toForwardSlashPath(nodePath.join(modulesPath, "routes")),
212213
compiledPathsDirectory,
213214
"commonjs",
214215
);
@@ -217,7 +218,7 @@ export async function counterfact(config: Config) {
217218
compiledPathsDirectory,
218219
registry,
219220
contextRegistry,
220-
nodePath.join(modulesPath, "scenarios").replaceAll("\\", "/"),
221+
toForwardSlashPath(nodePath.join(modulesPath, "scenarios")),
221222
scenarioRegistry,
222223
);
223224

src/migrate/update-route-types.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from "node:path";
33

44
import createDebug from "debug";
55

6+
import { toForwardSlashPath } from "../util/forward-slash-path.js";
67
import {
78
OperationTypeCoder,
89
type SecurityScheme,
@@ -271,9 +272,7 @@ async function processRouteDirectory(
271272
);
272273
} else if (entry.name.endsWith(".ts") && entry.name !== "_.context.ts") {
273274
// Process TypeScript route files (skip context files)
274-
const routePath = relativePath
275-
.replace(/\.ts$/, "")
276-
.replaceAll("\\", "/");
275+
const routePath = toForwardSlashPath(relativePath.replace(/\.ts$/, ""));
277276
const methodMap = mapping.get(routePath);
278277

279278
if (methodMap) {

src/server/file-discovery.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
22
import fs from "node:fs/promises";
33
import nodePath from "node:path";
44

5+
import { toForwardSlashPath } from "../util/forward-slash-path.js";
56
import { escapePathForWindows } from "../util/windows-escape.js";
67

78
const JS_EXTENSIONS = new Set(["cjs", "cts", "js", "mjs", "mts", "ts"]);
@@ -17,7 +18,7 @@ export class FileDiscovery {
1718
private readonly basePath: string;
1819

1920
public constructor(basePath: string) {
20-
this.basePath = basePath.replaceAll("\\", "/");
21+
this.basePath = toForwardSlashPath(basePath);
2122
}
2223

2324
/**
@@ -29,9 +30,7 @@ export class FileDiscovery {
2930
* @throws When `basePath/directory` does not exist.
3031
*/
3132
public async findFiles(directory = ""): Promise<string[]> {
32-
const fullDir = nodePath
33-
.join(this.basePath, directory)
34-
.replaceAll("\\", "/");
33+
const fullDir = toForwardSlashPath(nodePath.join(this.basePath, directory));
3534

3635
if (!existsSync(fullDir)) {
3736
throw new Error(`Directory does not exist ${fullDir}`);
@@ -43,7 +42,7 @@ export class FileDiscovery {
4342
entries.map(async (entry) => {
4443
if (entry.isDirectory()) {
4544
return this.findFiles(
46-
nodePath.join(directory, entry.name).replaceAll("\\", "/"),
45+
toForwardSlashPath(nodePath.join(directory, entry.name)),
4746
);
4847
}
4948

@@ -53,9 +52,9 @@ export class FileDiscovery {
5352
return [];
5453
}
5554

56-
const fullPath = nodePath
57-
.join(this.basePath, directory, entry.name)
58-
.replaceAll("\\", "/");
55+
const fullPath = toForwardSlashPath(
56+
nodePath.join(this.basePath, directory, entry.name),
57+
);
5958

6059
return [escapePathForWindows(fullPath)];
6160
}),

src/server/module-loader.ts

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ModuleDependencyGraph } from "./module-dependency-graph.js";
1818
import type { Module, Registry } from "./registry.js";
1919
import { ScenarioRegistry } from "./scenario-registry.js";
2020
import { uncachedImport } from "./uncached-import.js";
21+
import { toForwardSlashPath } from "../util/forward-slash-path.js";
2122
import { unescapePathForWindows } from "../util/windows-escape.js";
2223

2324
const { uncachedRequire } = await import("./uncached-require.cjs");
@@ -69,10 +70,13 @@ export class ModuleLoader extends EventTarget {
6970
scenarioRegistry?: ScenarioRegistry,
7071
) {
7172
super();
72-
this.basePath = basePath.replaceAll("\\", "/");
73+
this.basePath = toForwardSlashPath(basePath);
7374
this.registry = registry;
7475
this.contextRegistry = contextRegistry;
75-
this.scenariosPath = scenariosPath?.replaceAll("\\", "/");
76+
this.scenariosPath =
77+
scenariosPath === undefined
78+
? undefined
79+
: toForwardSlashPath(scenariosPath);
7680
this.scenarioRegistry = scenarioRegistry;
7781
this.fileDiscovery = new FileDiscovery(this.basePath);
7882
}
@@ -98,7 +102,7 @@ export class ModuleLoader extends EventTarget {
98102
)
99103
return;
100104

101-
const pathName = pathNameOriginal.replaceAll("\\", "/");
105+
const pathName = toForwardSlashPath(pathNameOriginal);
102106

103107
if (pathName.includes("$.context") && eventName === "add") {
104108
process.stdout.write(
@@ -114,17 +118,18 @@ export class ModuleLoader extends EventTarget {
114118

115119
const parts = nodePath.parse(pathName.replace(this.basePath, ""));
116120
const url = unescapePathForWindows(
117-
`/${parts.dir}/${parts.name}`
118-
.replaceAll("\\", "/")
119-
.replaceAll(/\/+/gu, "/"),
121+
toForwardSlashPath(`/${parts.dir}/${parts.name}`).replaceAll(
122+
/\/+/gu,
123+
"/",
124+
),
120125
);
121126

122127
if (eventName === "unlink") {
123128
this.registry.remove(url);
124129
this.dispatchEvent(new Event("remove"));
125130
if (this.isContextFile(pathName)) {
126131
this.contextRegistry.remove(
127-
unescapePathForWindows(parts.dir).replaceAll("\\", "/") || "/",
132+
unescapePathForWindows(toForwardSlashPath(parts.dir)) || "/",
128133
);
129134
}
130135
return;
@@ -155,7 +160,7 @@ export class ModuleLoader extends EventTarget {
155160

156161
if (!["add", "change", "unlink"].includes(eventName)) return;
157162

158-
const pathName = pathNameOriginal.replaceAll("\\", "/");
163+
const pathName = toForwardSlashPath(pathNameOriginal);
159164

160165
if (eventName === "unlink") {
161166
const fileKey = this.scenarioFileKey(pathName);
@@ -215,18 +220,18 @@ export class ModuleLoader extends EventTarget {
215220
}
216221

217222
private scenarioFileKey(pathName: string): string {
218-
const normalizedScenariosPath = (this.scenariosPath ?? "").replaceAll(
219-
"\\",
220-
"/",
223+
const normalizedScenariosPath = toForwardSlashPath(
224+
this.scenariosPath ?? "",
225+
);
226+
const directory = toForwardSlashPath(
227+
dirname(pathName.slice(normalizedScenariosPath.length)),
221228
);
222-
const directory = dirname(
223-
pathName.slice(normalizedScenariosPath.length),
224-
).replaceAll("\\", "/");
225229
const name = nodePath.parse(basename(pathName)).name;
226230
const url = unescapePathForWindows(
227-
`/${nodePath.join(directory, name)}`
228-
.replaceAll("\\", "/")
229-
.replaceAll(/\/+/gu, "/"),
231+
toForwardSlashPath(`/${nodePath.join(directory, name)}`).replaceAll(
232+
/\/+/gu,
233+
"/",
234+
),
230235
);
231236

232237
return url.slice(1); // strip leading "/"
@@ -258,15 +263,14 @@ export class ModuleLoader extends EventTarget {
258263
private async loadEndpoint(pathName: string) {
259264
debug("importing module: %s", pathName);
260265

261-
const directory = dirname(pathName.slice(this.basePath.length)).replaceAll(
262-
"\\",
263-
"/",
266+
const directory = toForwardSlashPath(
267+
dirname(pathName.slice(this.basePath.length)),
264268
);
265269

266270
const url = unescapePathForWindows(
267-
`/${nodePath.join(directory, nodePath.parse(basename(pathName)).name)}`
268-
.replaceAll("\\", "/")
269-
.replaceAll(/\/+/gu, "/"),
271+
toForwardSlashPath(
272+
`/${nodePath.join(directory, nodePath.parse(basename(pathName)).name)}`,
273+
).replaceAll(/\/+/gu, "/"),
270274
);
271275

272276
debug(`loading pathName from dependencyGraph: ${pathName}`);
@@ -290,9 +294,9 @@ export class ModuleLoader extends EventTarget {
290294
importError instanceof SyntaxError ||
291295
String(importError).startsWith("SyntaxError:");
292296

293-
const displayPath = nodePath
294-
.relative(process.cwd(), unescapePathForWindows(pathName))
295-
.replaceAll("\\", "/");
297+
const displayPath = toForwardSlashPath(
298+
nodePath.relative(process.cwd(), unescapePathForWindows(pathName)),
299+
);
296300

297301
const message = isSyntaxError
298302
? `There is a syntax error in the route file: ${displayPath}`

src/server/transpiler.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import createDebug from "debug";
99
import ts from "typescript";
1010

1111
import { ensureDirectoryExists } from "../util/ensure-directory-exists.js";
12+
import { toForwardSlashPath } from "../util/forward-slash-path.js";
1213
import { CHOKIDAR_OPTIONS } from "./constants.js";
1314
import { convertFileExtensionsToCjs } from "./convert-js-extensions-to-cjs.js";
1415

@@ -79,12 +80,13 @@ export class Transpiler extends EventTarget {
7980
)
8081
return;
8182

82-
const sourcePath = sourcePathOriginal.replaceAll("\\", "/");
83+
const sourcePath = toForwardSlashPath(sourcePathOriginal);
8384

84-
const destinationPath = sourcePath
85-
.replace(this.sourcePath, this.destinationPath)
86-
.replaceAll("\\", "/")
87-
.replace(".ts", this.extension);
85+
const destinationPath = toForwardSlashPath(
86+
sourcePath
87+
.replace(this.sourcePath, this.destinationPath)
88+
.replace(".ts", this.extension),
89+
);
8890

8991
if (["add", "change"].includes(eventName)) {
9092
transpiles.push(
@@ -152,13 +154,13 @@ export class Transpiler extends EventTarget {
152154

153155
const result: string = transpileOutput.outputText;
154156

155-
const fullDestination = nodePath
156-
.join(
157+
const fullDestination = toForwardSlashPath(
158+
nodePath.join(
157159
sourcePath
158160
.replace(this.sourcePath, this.destinationPath)
159161
.replace(".ts", this.extension),
160-
)
161-
.replaceAll("\\", "/");
162+
),
163+
);
162164

163165
const resultWithTransformedFileExtensions =
164166
convertFileExtensionsToCjs(result);

src/typescript-generator/generate.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import nodePath from "node:path";
55
import createDebug from "debug";
66

77
import { ensureDirectoryExists } from "../util/ensure-directory-exists.js";
8+
import { toForwardSlashPath } from "../util/forward-slash-path.js";
89
import { OperationCoder } from "./operation-coder.js";
910
import { type SecurityScheme } from "./operation-type-coder.js";
1011
import { pruneRoutes } from "./prune.js";
@@ -210,9 +211,9 @@ async function walkForContextFiles(
210211
results,
211212
);
212213
} else if (entry.name === "_.context.ts") {
213-
const relDir = nodePath
214-
.relative(routesDir, currentDir)
215-
.replaceAll("\\", "/");
214+
const relDir = toForwardSlashPath(
215+
nodePath.relative(routesDir, currentDir),
216+
);
216217
const routePath = relDir === "" ? "/" : `/${relDir}`;
217218
const depth = relDir === "" ? 0 : relDir.split("/").length;
218219
const importPath =

src/typescript-generator/operation-coder.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import nodePath from "node:path";
22

3+
import { toForwardSlashPath } from "../util/forward-slash-path.js";
34
import { Coder } from "./coder.js";
45
import {
56
OperationTypeCoder,
@@ -88,8 +89,6 @@ export class OperationCoder extends Coder {
8889
.at(-2)!
8990
.replaceAll("~1", "/");
9091

91-
return `${nodePath
92-
.join("routes", pathString)
93-
.replaceAll("\\", "/")}.types.ts`;
92+
return `${toForwardSlashPath(nodePath.join("routes", pathString))}.types.ts`;
9493
}
9594
}

src/typescript-generator/operation-type-coder.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import nodePath from "node:path";
22

3+
import { toForwardSlashPath } from "../util/forward-slash-path.js";
34
import { CONTEXT_FILE_TOKEN } from "./context-file-token.js";
45
import { buildJsDoc } from "./jsdoc.js";
56
import { ParameterExportTypeCoder } from "./parameter-export-type-coder.js";
@@ -187,9 +188,9 @@ export class OperationTypeCoder extends TypeCoder {
187188
.at(-2)!
188189
.replaceAll("~1", "/");
189190

190-
return `${nodePath
191-
.join("types/paths", pathString === "/" ? "/index" : pathString)
192-
.replaceAll("\\", "/")}.types.ts`;
191+
return `${toForwardSlashPath(
192+
nodePath.join("types/paths", pathString === "/" ? "/index" : pathString),
193+
)}.types.ts`;
193194
}
194195

195196
/**

0 commit comments

Comments
 (0)