filesystem.ts 4.8 KB

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