filesystem.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import { chmod, mkdir, readFile, writeFile } from "fs/promises"
  2. import { createWriteStream, existsSync, statSync } from "fs"
  3. import { lookup } from "mime-types"
  4. import { realpathSync } from "fs"
  5. import { dirname, join, relative } from "path"
  6. import { Readable } from "stream"
  7. import { pipeline } from "stream/promises"
  8. import { Glob } from "./glob"
  9. export namespace Filesystem {
  10. // Fast sync version for metadata checks
  11. export async function exists(p: string): Promise<boolean> {
  12. return existsSync(p)
  13. }
  14. export async function isDir(p: string): Promise<boolean> {
  15. try {
  16. return statSync(p).isDirectory()
  17. } catch {
  18. return false
  19. }
  20. }
  21. export function stat(p: string): ReturnType<typeof statSync> | undefined {
  22. return statSync(p, { throwIfNoEntry: false }) ?? undefined
  23. }
  24. export async function size(p: string): Promise<number> {
  25. const s = stat(p)?.size ?? 0
  26. return typeof s === "bigint" ? Number(s) : s
  27. }
  28. export async function readText(p: string): Promise<string> {
  29. return readFile(p, "utf-8")
  30. }
  31. export async function readJson<T = any>(p: string): Promise<T> {
  32. return JSON.parse(await readFile(p, "utf-8"))
  33. }
  34. export async function readBytes(p: string): Promise<Buffer> {
  35. return readFile(p)
  36. }
  37. export async function readArrayBuffer(p: string): Promise<ArrayBuffer> {
  38. const buf = await readFile(p)
  39. return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer
  40. }
  41. function isEnoent(e: unknown): e is { code: "ENOENT" } {
  42. return typeof e === "object" && e !== null && "code" in e && (e as { code: string }).code === "ENOENT"
  43. }
  44. export async function write(p: string, content: string | Buffer | Uint8Array, mode?: number): Promise<void> {
  45. try {
  46. if (mode) {
  47. await writeFile(p, content, { mode })
  48. } else {
  49. await writeFile(p, content)
  50. }
  51. } catch (e) {
  52. if (isEnoent(e)) {
  53. await mkdir(dirname(p), { recursive: true })
  54. if (mode) {
  55. await writeFile(p, content, { mode })
  56. } else {
  57. await writeFile(p, content)
  58. }
  59. return
  60. }
  61. throw e
  62. }
  63. }
  64. export async function writeJson(p: string, data: unknown, mode?: number): Promise<void> {
  65. return write(p, JSON.stringify(data, null, 2), mode)
  66. }
  67. export async function writeStream(
  68. p: string,
  69. stream: ReadableStream<Uint8Array> | Readable,
  70. mode?: number,
  71. ): Promise<void> {
  72. const dir = dirname(p)
  73. if (!existsSync(dir)) {
  74. await mkdir(dir, { recursive: true })
  75. }
  76. const nodeStream = stream instanceof ReadableStream ? Readable.fromWeb(stream as any) : stream
  77. const writeStream = createWriteStream(p)
  78. await pipeline(nodeStream, writeStream)
  79. if (mode) {
  80. await chmod(p, mode)
  81. }
  82. }
  83. export function mimeType(p: string): string {
  84. return lookup(p) || "application/octet-stream"
  85. }
  86. /**
  87. * On Windows, normalize a path to its canonical casing using the filesystem.
  88. * This is needed because Windows paths are case-insensitive but LSP servers
  89. * may return paths with different casing than what we send them.
  90. */
  91. export function normalizePath(p: string): string {
  92. if (process.platform !== "win32") return p
  93. try {
  94. return realpathSync.native(p)
  95. } catch {
  96. return p
  97. }
  98. }
  99. export function overlaps(a: string, b: string) {
  100. const relA = relative(a, b)
  101. const relB = relative(b, a)
  102. return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..")
  103. }
  104. export function contains(parent: string, child: string) {
  105. return !relative(parent, child).startsWith("..")
  106. }
  107. export async function findUp(target: string, start: string, stop?: string) {
  108. let current = start
  109. const result = []
  110. while (true) {
  111. const search = join(current, target)
  112. if (await exists(search)) result.push(search)
  113. if (stop === current) break
  114. const parent = dirname(current)
  115. if (parent === current) break
  116. current = parent
  117. }
  118. return result
  119. }
  120. export async function* up(options: { targets: string[]; start: string; stop?: string }) {
  121. const { targets, start, stop } = options
  122. let current = start
  123. while (true) {
  124. for (const target of targets) {
  125. const search = join(current, target)
  126. if (await exists(search)) yield search
  127. }
  128. if (stop === current) break
  129. const parent = dirname(current)
  130. if (parent === current) break
  131. current = parent
  132. }
  133. }
  134. export async function globUp(pattern: string, start: string, stop?: string) {
  135. let current = start
  136. const result = []
  137. while (true) {
  138. try {
  139. const matches = await Glob.scan(pattern, {
  140. cwd: current,
  141. absolute: true,
  142. include: "file",
  143. dot: true,
  144. })
  145. result.push(...matches)
  146. } catch {
  147. // Skip invalid glob patterns
  148. }
  149. if (stop === current) break
  150. const parent = dirname(current)
  151. if (parent === current) break
  152. current = parent
  153. }
  154. return result
  155. }
  156. }