diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml deleted file mode 100644 index 1e4d6fa..0000000 --- a/.github/workflows/deno.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Deno CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: Setup repo - uses: actions/checkout@v4 - - - name: Setup Deno - uses: denoland/setup-deno@v1 - with: - deno-version: v1.x - - - name: Verify formatting - run: deno fmt --check - - - name: Run linter - run: deno lint - -# - name: Check types -# run: deno check mod.ts \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4b5f7ac --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,20 @@ +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + deno_ci: + uses: cross-org/workflows/.github/workflows/deno-ci.yml@main + with: + entrypoint: mod.ts + bun_ci: + uses: cross-org/workflows/.github/workflows/bun-ci.yml@main + with: + jsr_dependencies: "@cross/test @std/assert @cross/runtime" + node_ci: + uses: cross-org/workflows/.github/workflows/node-ci.yml@main + with: + jsr_dependencies: "@cross/test @std/assert @cross/runtime" + test_target: "src/*.test.ts" \ No newline at end of file diff --git a/README.md b/README.md index db1e7ba..aef128e 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,14 @@ will cover most scenarios, this library focuses on the file system operations. ## Coverage -| Method | Deno | Node | Bun | Browser (LocalStorage) | -| ------ | ---- | ---- | --- | ---------------------- | -| stat | X | X | X | | -| ... | | | | | +| Method | Deno | Node | Bun | Browser (LocalStorage) | +| --------- | ---- | ---- | --- | ---------------------- | +| stat | X | X | X | | +| exists | X | X | X | | +| isDir | X | X | X | | +| isFile | X | X | X | | +| isSymlink | X | X | X | | +| ... | | | | | ## Contribution guide diff --git a/deno.json b/deno.json index 97f65be..17ce561 100644 --- a/deno.json +++ b/deno.json @@ -2,5 +2,17 @@ "name": "@cross/fs", "version": "0.0.3", "exports": "./mod.ts", - "imports": { "@cross/runtime": "jsr:@cross/runtime@^1.0.0" } + "imports": { + "@cross/runtime": "jsr:@cross/runtime@^1.0.0", + "@cross/test": "jsr:@cross/test@^0.0.9", + "@std/assert": "jsr:@std/assert@^0.220.1" + }, + "publish": { + "exclude": [".github", "*.test.ts"] + }, + "tasks": { + "check": "deno fmt --check && deno lint && deno check mod.ts && deno task check-deps", + "test": "deno test -A", + "check-deps": "deno run -rA jsr:@check/deps" + } } diff --git a/mod.ts b/mod.ts index dc9c56f..6fd1e00 100644 --- a/mod.ts +++ b/mod.ts @@ -1 +1,2 @@ -export { stat } from "./stat/mod.ts"; +export { stat } from "./src/stat.ts"; +export { exists } from "./src/exists.ts"; diff --git a/src/exists.test.ts b/src/exists.test.ts new file mode 100644 index 0000000..9f464e5 --- /dev/null +++ b/src/exists.test.ts @@ -0,0 +1,14 @@ +import { assertEquals } from "@std/assert"; +import { test } from "@cross/test"; + +import { exists } from "./exists.ts"; + +test("exists returns true on existing file", async () => { + const result = await exists("./mod.ts"); + assertEquals(result, true); +}); + +test("exists returns false on non-existant file", async () => { + const result = await exists("./mod-nonexistant.ts"); + assertEquals(result, false); +}); diff --git a/src/exists.ts b/src/exists.ts new file mode 100644 index 0000000..a63711e --- /dev/null +++ b/src/exists.ts @@ -0,0 +1,13 @@ +import { NotFoundError, stat } from "./stat.ts"; +export async function exists(path: string): Promise { + try { + await stat(path); + return true; + } catch (error) { + if (error instanceof NotFoundError) { + return false; + } else { + throw error; + } + } +} diff --git a/src/is.test.ts b/src/is.test.ts new file mode 100644 index 0000000..1751b6c --- /dev/null +++ b/src/is.test.ts @@ -0,0 +1,29 @@ +import { assertEquals } from "@std/assert"; +import { test } from "@cross/test"; + +import { isDir, isFile } from "./is.ts"; + +test("isFile returns true on existing file", async () => { + const result = await isFile("./mod.ts"); + assertEquals(result, true); +}); + +test("isFile returns false on non-existiantfile", async () => { + const result = await isFile("./mod-nonexistant.ts"); + assertEquals(result, false); +}); + +test("isDir returns false on existing file", async () => { + const result = await isDir("./mod.ts"); + assertEquals(result, false); +}); + +test("isFile returns false on existing dir", async () => { + const result = await isFile("./src"); + assertEquals(result, false); +}); + +test("isDir returns true on existing dir", async () => { + const result = await isDir("./src"); + assertEquals(result, true); +}); diff --git a/src/is.ts b/src/is.ts new file mode 100644 index 0000000..5e80e9c --- /dev/null +++ b/src/is.ts @@ -0,0 +1,37 @@ +import { NotFoundError, stat } from "./stat.ts"; +export async function isDir(path: string) { + try { + const result = await stat(path); + return result.isDirectory; + } catch (error) { + if (error instanceof NotFoundError) { + return false; + } else { + throw error; + } + } +} +export async function isFile(path: string) { + try { + const result = await stat(path); + return result.isFile; + } catch (error) { + if (error instanceof NotFoundError) { + return false; + } else { + throw error; + } + } +} +export async function isSymlink(path: string) { + try { + const result = await stat(path); + return result.isSymlink; + } catch (error) { + if (error instanceof NotFoundError) { + return false; + } else { + throw error; + } + } +} diff --git a/stat/mod.ts b/src/stat.ts similarity index 78% rename from stat/mod.ts rename to src/stat.ts index 8c75a6f..e95545c 100644 --- a/stat/mod.ts +++ b/src/stat.ts @@ -1,11 +1,11 @@ import { CurrentRuntime, Runtime } from "@cross/runtime"; -interface StatOptions { +export interface StatOptions { /* Request bigInts, Only used with node */ bigInt: false | undefined; } -interface StatResult { +export interface StatResult { /** Common Properties */ isFile: boolean; isDirectory: boolean; @@ -61,6 +61,13 @@ interface StatResult { isSocket: boolean | null; } +export class NotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = "NotFoundError"; + } +} + async function statWrap( path: string, options?: StatOptions, @@ -68,17 +75,35 @@ async function statWrap( switch (CurrentRuntime) { case Runtime.Node: /* Falls through */ - // deno-lint-ignore no-case-declarations case Runtime.Bun: - const { stat } = await import("node:fs/promises"); - return mapNodeStats( - await stat(path, options), - options?.bigInt === undefined ? true : false, - ); - + try { + //@ts-ignore Cross + const { stat } = await import("node:fs/promises"); + return mapNodeStats( + //@ts-ignore Cross + await stat(path, options), + options?.bigInt === undefined ? true : false, + ); + } catch (err) { + //@ts-ignore Cross + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + throw new NotFoundError(`File not found: ${path}`); + } else { + throw err; + } + } case Runtime.Deno: - return mapDenoStats(await Deno.stat(path)); - + try { + //@ts-ignore Cross + return mapDenoStats(await Deno.stat(path)); + } catch (err) { + //@ts-ignore Cross + if (err instanceof Deno.errors.NotFound) { + throw new NotFoundError(`File not found: ${path}`); + } else { + throw err; + } + } case Runtime.Browser: // Add browser case for clarity throw new Error("File system access not supported in the browser");