-
Notifications
You must be signed in to change notification settings - Fork 137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Distinguish between URL paths and file paths at the type level #881
Changes from 10 commits
7221d92
8ad8455
9025a89
58705a3
ffd4cd2
f4699e5
dcb4866
b3065f2
254355d
12118dc
034bb47
a3c73cf
6b7c8c3
1b5aea5
ca96b12
845e50a
12f91f9
bcd8185
aa162a0
420c1f4
1b6fdbc
b7064b0
196c962
06801cb
2ecad19
dd7dc15
a6a0a4a
9ddefa4
518f5bf
6786e32
464ca6c
80f4b90
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import fs, {type MakeDirectoryOptions, type Stats, type WatchListener, type WriteFileOptions} from "node:fs"; | ||
import fsp, {type FileHandle} from "node:fs/promises"; | ||
import {FilePath, unFilePath} from "./brandedPath.js"; | ||
|
||
export const constants = fsp.constants; | ||
|
||
export function access(path: FilePath, mode?: number): Promise<void> { | ||
return fsp.access(unFilePath(path), mode); | ||
} | ||
|
||
export function accessSync(path: FilePath, mode?: number): void { | ||
return fs.accessSync(unFilePath(path), mode); | ||
} | ||
|
||
export function copyFile(source: FilePath, destination: FilePath, flags?: number): Promise<void> { | ||
return fsp.copyFile(unFilePath(source), unFilePath(destination), flags); | ||
} | ||
|
||
export function readFile(path: FilePath, encoding?: undefined): Promise<Buffer>; | ||
export function readFile(path: FilePath, encoding: BufferEncoding): Promise<string>; | ||
export function readFile(path: FilePath, encoding?: BufferEncoding | undefined): Promise<string | Buffer> { | ||
return fsp.readFile(unFilePath(path), encoding); | ||
} | ||
|
||
export function readFileSync(path: FilePath, encoding: BufferEncoding): string { | ||
return fs.readFileSync(unFilePath(path), encoding); | ||
} | ||
|
||
export function writeFile(path: FilePath, data: string | Buffer, options?: WriteFileOptions): Promise<void> { | ||
return fsp.writeFile(unFilePath(path), data, options); | ||
} | ||
|
||
export function writeFileSync(path: FilePath, data: string | Buffer, options?: WriteFileOptions): void { | ||
return fs.writeFileSync(unFilePath(path), data, options); | ||
} | ||
|
||
export async function mkdir(path: FilePath, options?: MakeDirectoryOptions): Promise<FilePath | undefined> { | ||
const rv = await fsp.mkdir(unFilePath(path), options); | ||
return rv ? FilePath(rv) : undefined; | ||
} | ||
|
||
export function readdir(path: FilePath): Promise<string[]> { | ||
return fsp.readdir(unFilePath(path)); | ||
} | ||
|
||
export function stat(path: FilePath): Promise<Stats> { | ||
return fsp.stat(unFilePath(path)); | ||
} | ||
|
||
export function open(path: FilePath, flags: string | number, mode?: number): Promise<FileHandle> { | ||
return fsp.open(unFilePath(path), flags, mode); | ||
} | ||
|
||
export function rename(oldPath: FilePath, newPath: FilePath): Promise<void> { | ||
return fsp.rename(unFilePath(oldPath), unFilePath(newPath)); | ||
} | ||
|
||
export function renameSync(oldPath: FilePath, newPath: FilePath): void { | ||
return fs.renameSync(unFilePath(oldPath), unFilePath(newPath)); | ||
} | ||
|
||
export function unlink(path: FilePath): Promise<void> { | ||
return fsp.unlink(unFilePath(path)); | ||
} | ||
|
||
export function unlinkSync(path: FilePath): void { | ||
return fs.unlinkSync(unFilePath(path)); | ||
} | ||
|
||
export function existsSync(path: FilePath): boolean { | ||
return fs.existsSync(unFilePath(path)); | ||
} | ||
|
||
export function readdirSync(path: FilePath): FilePath[] { | ||
return fs.readdirSync(unFilePath(path)) as unknown as FilePath[]; | ||
} | ||
|
||
export function statSync(path: FilePath): Stats { | ||
return fs.statSync(unFilePath(path)); | ||
} | ||
|
||
export function watch(path: FilePath, listener?: WatchListener<string>): fs.FSWatcher { | ||
return fs.watch(unFilePath(path), listener); | ||
} | ||
|
||
export function utimes(path: FilePath, atime: Date, mtime: Date): Promise<void> { | ||
return fsp.utimes(unFilePath(path), atime, mtime); | ||
} | ||
|
||
export function utimesSync(path: FilePath, atime: Date, mtime: Date): void { | ||
return fs.utimesSync(unFilePath(path), atime, mtime); | ||
} | ||
|
||
export function createReadStream(path: FilePath): fs.ReadStream { | ||
return fs.createReadStream(unFilePath(path)); | ||
} | ||
|
||
export function rm(path: FilePath, options?: {force?: boolean; recursive?: boolean}): Promise<void> { | ||
return fsp.rm(unFilePath(path), options); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import osPath from "node:path"; | ||
import posixPath from "node:path/posix"; | ||
|
||
// including "unknown &" here improves type error message | ||
type BrandedString<T> = unknown & | ||
Omit<string, "replace" | "slice"> & {__type: T} & { | ||
replace: (search: string | RegExp, replace: string) => BrandedString<T>; | ||
slice: (start?: number, end?: number) => BrandedString<T>; | ||
}; | ||
|
||
export type FilePath = BrandedString<"FilePath">; | ||
export type UrlPath = BrandedString<"UrlPath">; | ||
|
||
export function FilePath(path: string | FilePath): FilePath { | ||
if (osPath.sep === "\\") { | ||
path = path.replaceAll("/", osPath.sep); | ||
} else if (osPath.sep === "/") { | ||
path = path.replaceAll("\\", osPath.sep); | ||
} | ||
return path as unknown as FilePath; | ||
} | ||
|
||
export function UrlPath(path: string | UrlPath): UrlPath { | ||
path = path.replaceAll("\\", posixPath.sep); | ||
return path as unknown as UrlPath; | ||
} | ||
|
||
export function unFilePath(path: FilePath | string): string { | ||
return path as unknown as string; | ||
} | ||
|
||
export function unUrlPath(path: UrlPath | string): string { | ||
return path as unknown as string; | ||
} | ||
|
||
Comment on lines
+28
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these no-op functions are here only for the sake of typing (or, untyping), there should be a better way to do this with typescript only? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One thing that I was disappointed about was that I couldn't figure out how to safely tell TypeScript that I think if we wanted to eliminate these functions, we'd have to inline the I agree though that I would prefer some better way to do this, like maybe a "type level function". I don't know of one though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All of the wrapping worries me, but I can't express why. It feels excessive or invasive maybe? I'm definitely wishing we didn't have to do it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This pattern, of having a new type that is just an existing type with a new name is something that I've used in other languages. In those languages there are often nice features built into the language that help make this easier, like implicit conversion, operator overloading, and monad-style-map. TypeScript doesn't really give us any of those things, so it is all a bit clunky. I could imagine taking out most of these wrappers now (except for the places where we need slash conversion). I don't know how we would prevent new changes to the code base from getting it wrong in the future. Maybe just test coverage and Windows in CI? That feels much less robust than TypeScript to me. |
||
export function filePathToUrlPath(path: FilePath): UrlPath { | ||
if (osPath.sep === "/") return path as unknown as UrlPath; | ||
return urlJoin(...(path.split(osPath.sep) as string[])); | ||
} | ||
|
||
export function urlPathToFilePath(path: UrlPath): FilePath { | ||
if (osPath.sep === "/") return path as unknown as FilePath; | ||
return fileJoin(...(path.split(posixPath.sep) as string[])); | ||
} | ||
|
||
// Implemenations of node:path functions: | ||
|
||
export function urlJoin(...paths: (string | UrlPath)[]): UrlPath { | ||
return posixPath.join(...(paths as string[])) as unknown as UrlPath; | ||
} | ||
|
||
export function fileJoin(...paths: (string | FilePath)[]): FilePath { | ||
return osPath.join(...(paths as string[])) as unknown as FilePath; | ||
} | ||
|
||
export function fileRelative(from: string | FilePath, to: string | FilePath): FilePath { | ||
return FilePath(osPath.relative(unFilePath(from), unFilePath(to))); | ||
} | ||
|
||
export function fileDirname(path: string | FilePath): FilePath { | ||
return FilePath(osPath.dirname(unFilePath(path))); | ||
} | ||
|
||
export function urlDirname(path: string | UrlPath): UrlPath { | ||
return UrlPath(posixPath.dirname(unUrlPath(path))); | ||
} | ||
|
||
export function fileNormalize(path: string | FilePath): FilePath { | ||
return FilePath(osPath.normalize(unFilePath(path))); | ||
} | ||
|
||
export function urlNormalize(path: string | UrlPath): UrlPath { | ||
return UrlPath(osPath.normalize(unUrlPath(path))); | ||
} | ||
|
||
export function fileBasename(path: string | FilePath, suffix?: string): string { | ||
return osPath.basename(unFilePath(path), suffix); | ||
} | ||
|
||
export function urlBasename(path: string | UrlPath, suffix?: string): string { | ||
return osPath.basename(unUrlPath(path), suffix); | ||
} | ||
|
||
export function fileExtname(path: string | FilePath | string): string { | ||
return osPath.extname(unFilePath(path)); | ||
} | ||
|
||
export function urlExtname(path: string | UrlPath | string): string { | ||
return osPath.extname(unUrlPath(path)); | ||
} | ||
|
||
export function fileResolve(...paths: (string | FilePath)[]): FilePath { | ||
return FilePath(osPath.resolve(...(paths as string[]))); | ||
} | ||
|
||
export function urlResolve(...paths: (string | UrlPath)[]): UrlPath { | ||
return UrlPath(osPath.resolve(...(paths as string[]))); | ||
} | ||
|
||
export const fileSep = osPath.sep as unknown as FilePath; | ||
export const urlSep = posixPath.sep as unknown as UrlPath; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is in
dependencies
already, we shouldn’t also need it indevDependencies
.