diff --git a/src/dev/builder.ts b/src/dev/builder.ts index a91c2d46cfd..cd157d0bdf6 100644 --- a/src/dev/builder.ts +++ b/src/dev/builder.ts @@ -5,7 +5,6 @@ import { type ListenOptions, setBuildCache, } from "../app.ts"; -import { fsAdapter } from "../fs.ts"; import * as path from "@std/path"; import * as colors from "@std/fmt/colors"; import { bundleJs } from "./esbuild.ts"; @@ -45,7 +44,7 @@ export interface FreshBuilder { } export class Builder implements FreshBuilder { - #transformer = new FreshFileTransformer(fsAdapter); + #transformer = new FreshFileTransformer(); #addedInternalTransforms = false; #options: { target: string | string[] }; diff --git a/src/dev/dev_build_cache.ts b/src/dev/dev_build_cache.ts index 358addc1bc3..4e8f0dc9527 100644 --- a/src/dev/dev_build_cache.ts +++ b/src/dev/dev_build_cache.ts @@ -5,9 +5,10 @@ import { getSnapshotPath, type ResolvedFreshConfig } from "../config.ts"; import type { BuildSnapshot } from "../build_cache.ts"; import { encodeHex } from "@std/encoding/hex"; import { crypto } from "@std/crypto"; -import { fsAdapter } from "../fs.ts"; import type { FreshFileTransformer } from "./file_transformer.ts"; import { assertInDir } from "../utils.ts"; +import { ensureDir } from "@std/fs/ensure-dir"; +import { walk } from "@std/fs/walk"; export interface MemoryFile { hash: string | null; @@ -193,7 +194,7 @@ export class DiskBuildCache implements DevBuildCache { const filePath = path.join(outDir, pathname); assertInDir(filePath, outDir); - await fsAdapter.mkdirp(path.dirname(filePath)); + await ensureDir(path.dirname(filePath)); await Deno.writeFile(filePath, content); } @@ -206,8 +207,8 @@ export class DiskBuildCache implements DevBuildCache { const staticDir = this.config.staticDir; const outDir = this.config.build.outDir; - if (await fsAdapter.isDirectory(staticDir)) { - const entries = fsAdapter.walk(staticDir, { + try { + const entries = walk(staticDir, { includeDirs: false, includeFiles: true, followSymlinks: false, @@ -240,6 +241,10 @@ export class DiskBuildCache implements DevBuildCache { this.addUnprocessedFile(pathname); } } + } catch (error) { + if (!(error instanceof Deno.errors.NotFound)) { + throw error; + } } const snapshot: BuildSnapshot = { diff --git a/src/dev/dev_build_cache_test.ts b/src/dev/dev_build_cache_test.ts index 513865ed18a..937a27fa4fa 100644 --- a/src/dev/dev_build_cache_test.ts +++ b/src/dev/dev_build_cache_test.ts @@ -2,7 +2,7 @@ import { expect } from "@std/expect"; import * as path from "@std/path"; import { MemoryBuildCache } from "./dev_build_cache.ts"; import { FreshFileTransformer } from "./file_transformer.ts"; -import { createFakeFs, withTmpDir } from "../test_utils.ts"; +import { withTmpDir } from "../test_utils.ts"; import type { ResolvedFreshConfig } from "../mod.ts"; Deno.test({ @@ -19,7 +19,7 @@ Deno.test({ outDir: path.join(tmp, "dist"), }, }; - const fileTransformer = new FreshFileTransformer(createFakeFs({})); + const fileTransformer = new FreshFileTransformer(); const buildCache = new MemoryBuildCache( config, "testing", diff --git a/src/dev/file_transformer.ts b/src/dev/file_transformer.ts index a9e7bf379b6..1dd75646669 100644 --- a/src/dev/file_transformer.ts +++ b/src/dev/file_transformer.ts @@ -1,5 +1,4 @@ import { globToRegExp, isGlob } from "@std/path"; -import type { FsAdapter } from "../fs.ts"; import { BUILD_ID } from "../runtime/build_id.ts"; import { assetInternal } from "../runtime/shared_internal.tsx"; @@ -58,11 +57,6 @@ interface TransformReq { export class FreshFileTransformer { #transformers: Transformer[] = []; - #fs: FsAdapter; - - constructor(fs: FsAdapter) { - this.#fs = fs; - } onTransform(options: OnTransformOptions, callback: TransformFn): void { this.#transformers.push({ options, fn: callback }); @@ -88,7 +82,7 @@ export class FreshFileTransformer { let content: Uint8Array; try { - content = await this.#fs.readFile(filePath); + content = await Deno.readFile(filePath); } catch (err) { if (err instanceof Deno.errors.NotFound) { return null; diff --git a/src/dev/file_transformer_test.ts b/src/dev/file_transformer_test.ts index e8537fa2f41..eff4077fc88 100644 --- a/src/dev/file_transformer_test.ts +++ b/src/dev/file_transformer_test.ts @@ -1,27 +1,17 @@ import { expect } from "@std/expect"; -import type { FsAdapter } from "../fs.ts"; import { FreshFileTransformer, type ProcessedFile, } from "./file_transformer.ts"; import { delay } from "../test_utils.ts"; +import { stub } from "@std/testing/mock"; -function testTransformer(files: Record) { - const mockFs: FsAdapter = { - cwd: () => "/", - isDirectory: () => Promise.resolve(false), - mkdirp: () => Promise.resolve(), - walk: async function* foo() { - }, - readFile: (file) => { - if (file instanceof URL) throw new Error("Not supported"); - // deno-lint-ignore no-explicit-any - const content = (files as any)[file]; - const buf = new TextEncoder().encode(content); - return Promise.resolve(buf); - }, - }; - return new FreshFileTransformer(mockFs); +function stubDenoReadFile(content: string) { + return stub(Deno, "readFile", () => { + return Promise.resolve( + new TextEncoder().encode(content) as Uint8Array, + ); + }); } function consumeResult(result: ProcessedFile[]) { @@ -52,9 +42,8 @@ function consumeResult(result: ProcessedFile[]) { } Deno.test("FileTransformer - transform sync", async () => { - const transformer = testTransformer({ - "foo.txt": "foo", - }); + const transformer = new FreshFileTransformer(); + using _denoReadFileStub = stubDenoReadFile("foo"); transformer.onTransform({ pluginName: "foo", filter: /.*/ }, (args) => { return { @@ -70,9 +59,8 @@ Deno.test("FileTransformer - transform sync", async () => { }); Deno.test("FileTransformer - transform async", async () => { - const transformer = testTransformer({ - "foo.txt": "foo", - }); + const transformer = new FreshFileTransformer(); + using _denoReadFileStub = stubDenoReadFile("foo"); transformer.onTransform({ pluginName: "foo", filter: /.*/ }, async (args) => { await delay(1); @@ -89,9 +77,8 @@ Deno.test("FileTransformer - transform async", async () => { }); Deno.test("FileTransformer - transform return Uint8Array", async () => { - const transformer = testTransformer({ - "foo.txt": "foo", - }); + const transformer = new FreshFileTransformer(); + using _denoReadFileStub = stubDenoReadFile("foo"); transformer.onTransform({ pluginName: "foo", filter: /.*/ }, () => { return { @@ -107,9 +94,8 @@ Deno.test("FileTransformer - transform return Uint8Array", async () => { }); Deno.test("FileTransformer - pass transformed content", async () => { - const transformer = testTransformer({ - "input.txt": "input", - }); + const transformer = new FreshFileTransformer(); + using _denoReadFileStub = stubDenoReadFile("input"); transformer.onTransform({ pluginName: "A", filter: /.*/ }, (args) => { return { @@ -137,9 +123,8 @@ Deno.test("FileTransformer - pass transformed content", async () => { Deno.test( "FileTransformer - pass transformed content with multiple", async () => { - const transformer = testTransformer({ - "input.txt": "input", - }); + const transformer = new FreshFileTransformer(); + using _denoReadFileStub = stubDenoReadFile("input"); transformer.onTransform({ pluginName: "A", filter: /.*/ }, (args) => { return [{ @@ -167,9 +152,8 @@ Deno.test( ); Deno.test("FileTransformer - return multiple results", async () => { - const transformer = testTransformer({ - "foo.txt": "foo", - }); + const transformer = new FreshFileTransformer(); + using _denoReadFileStub = stubDenoReadFile("foo"); const received: string[] = []; transformer.onTransform({ pluginName: "A", filter: /foo\.txt$/ }, () => { @@ -197,9 +181,8 @@ Deno.test("FileTransformer - return multiple results", async () => { Deno.test( "FileTransformer - track input files through temporary results", async () => { - const transformer = testTransformer({ - "foo.txt": "foo", - }); + const transformer = new FreshFileTransformer(); + using _denoReadFileStub = stubDenoReadFile("foo"); transformer.onTransform({ pluginName: "A", filter: /foo\.txt$/ }, () => { return [{ diff --git a/src/fs.ts b/src/fs.ts deleted file mode 100644 index 51a48ea455f..00000000000 --- a/src/fs.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { walk, type WalkEntry, type WalkOptions } from "@std/fs/walk"; - -export interface FsAdapter { - cwd(): string; - walk( - root: string | URL, - options?: WalkOptions, - ): AsyncIterableIterator; - isDirectory(path: string | URL): Promise; - mkdirp(dir: string): Promise; - readFile(path: string | URL): Promise; -} - -export const fsAdapter: FsAdapter = { - walk, - cwd: Deno.cwd, - async isDirectory(path) { - try { - const stat = await Deno.stat(path); - return stat.isDirectory; - } catch (err) { - if (err instanceof Deno.errors.NotFound) return false; - throw err; - } - }, - async mkdirp(dir: string) { - try { - await Deno.mkdir(dir, { recursive: true }); - } catch (err) { - if (!(err instanceof Deno.errors.AlreadyExists)) { - throw err; - } - } - }, - readFile: Deno.readFile, -}; diff --git a/src/plugins/fs_routes/mod.ts b/src/plugins/fs_routes/mod.ts index 5ead0050cc7..436f404f777 100644 --- a/src/plugins/fs_routes/mod.ts +++ b/src/plugins/fs_routes/mod.ts @@ -1,6 +1,6 @@ import type { AnyComponent } from "preact"; import type { App } from "../../app.ts"; -import type { WalkEntry } from "@std/fs/walk"; +import { walk } from "@std/fs/walk"; import * as path from "@std/path"; import type { RouteConfig } from "../../types.ts"; import type { RouteHandler } from "../../handlers.ts"; @@ -11,7 +11,6 @@ import { } from "./render_middleware.ts"; import { type Method, pathToPattern } from "../../router.ts"; import { type HandlerFn, isHandlerByMethod } from "../../handlers.ts"; -import { type FsAdapter, fsAdapter } from "../../fs.ts"; import { HttpError } from "../../error.ts"; import { parseRootPath } from "../../config.ts"; import type { FreshReqContext, PageProps } from "../../context.ts"; @@ -40,6 +39,9 @@ export interface FreshFsItem { | AsyncAnyComponent>; } +// For stubbing +export const internals = { walk }; + // deno-lint-ignore no-explicit-any function isFreshFile(mod: any): mod is FreshFsItem { return mod !== null && typeof mod === "object" && @@ -62,20 +64,14 @@ export interface FsRoutesOptions { loadIsland: (path: string) => Promise; } -export interface TESTING_ONLY__FsRoutesOptions { - _fs?: FsAdapter; -} - export async function fsRoutes( app: App, - options_: FsRoutesOptions, + options: FsRoutesOptions, ) { - const options = options_ as FsRoutesOptions & TESTING_ONLY__FsRoutesOptions; - const ignore = options.ignoreFilePattern ?? [TEST_FILE_PATTERN]; - const fs = options._fs ?? fsAdapter; + const skip = options.ignoreFilePattern ?? [TEST_FILE_PATTERN]; const dir = options.dir - ? parseRootPath(options.dir, fs.cwd()) + ? parseRootPath(options.dir, Deno.cwd()) : app.config.root; const islandDir = path.join(dir, "islands"); const routesDir = path.join(dir, "routes"); @@ -83,39 +79,52 @@ export async function fsRoutes( const islandPaths: string[] = []; const relRoutePaths: string[] = []; - // Walk routes folder - await Promise.all([ - walkDir( - islandDir, - (entry) => { - islandPaths.push(entry.path); - }, - ignore, - fs, - ), - walkDir( - routesDir, - (entry) => { - const relative = path.relative(routesDir, entry.path); - - // A `(_islands)` path segment is a local island folder. - // Any route path segment wrapped in `(_...)` is ignored - // during route collection. - const match = relative.match(GROUP_REG); - if (match && match[2][0] === "_") { - if (match[2] === "_islands") { - islandPaths.push(entry.path); - } - return; + try { + for await ( + const entry of walk(islandDir, { + includeDirs: false, + exts: ["tsx", "jsx", "ts", "js"], + skip, + }) + ) { + islandPaths.push(entry.path); + } + } catch (error) { + if (!(error instanceof Deno.errors.NotFound)) { + throw error; + } + } + + try { + for await ( + const entry of internals.walk(routesDir, { + includeDirs: false, + exts: ["tsx", "jsx", "ts", "js"], + skip, + }) + ) { + const relative = path.relative(routesDir, entry.path); + + // A `(_islands)` path segment is a local island folder. + // Any route path segment wrapped in `(_...)` is ignored + // during route collection. + const match = relative.match(GROUP_REG); + if (match && match[2][0] === "_") { + if (match[2] === "_islands") { + islandPaths.push(entry.path); } + continue; + } - const url = new URL(relative, "http://localhost/"); - relRoutePaths.push(url.pathname.slice(1)); - }, - ignore, - fs, - ), - ]); + const url = new URL(relative, "http://localhost/"); + relRoutePaths.push(url.pathname.slice(1)); + } + } catch (error) { + // `islandDir` or `routesDir` does not exist, so we can skip it + if (!(error instanceof Deno.errors.NotFound)) { + throw error; + } + } await Promise.all(islandPaths.map(async (islandPath) => { const relative = path.relative(islandDir, islandPath); @@ -400,26 +409,6 @@ function warnInvalidRoute(message: string) { ); } -async function walkDir( - dir: string, - callback: (entry: WalkEntry) => void, - ignore: RegExp[], - fs: FsAdapter, -) { - if (!await fs.isDirectory(dir)) return; - - const entries = fs.walk(dir, { - includeDirs: false, - includeFiles: true, - exts: ["tsx", "jsx", "ts", "js"], - skip: ignore, - }); - - for await (const entry of entries) { - callback(entry); - } -} - const APP_REG = /_app(?!\.[tj]sx?)?$/; /** diff --git a/src/plugins/fs_routes/mod_test.tsx b/src/plugins/fs_routes/mod_test.tsx index 291356e0a6b..15dd7cde7a8 100644 --- a/src/plugins/fs_routes/mod_test.tsx +++ b/src/plugins/fs_routes/mod_test.tsx @@ -2,12 +2,10 @@ import { App } from "../../app.ts"; import { type FreshFsItem, fsRoutes, - type FsRoutesOptions, + internals, sortRoutePaths, - type TESTING_ONLY__FsRoutesOptions, } from "./mod.ts"; import { delay, FakeServer } from "../../test_utils.ts"; -import { createFakeFs } from "../../test_utils.ts"; import { expect, fn } from "@std/expect"; import { stub } from "@std/testing/mock"; import { type HandlerByMethod, type HandlerFn, page } from "../../handlers.ts"; @@ -19,6 +17,18 @@ async function createServer( files: Record>, ): Promise { const app = new App(); + using _denoCwdStub = stub(Deno, "cwd", () => "."); + using _walkStub = stub(internals, "walk", async function* () { + for (const file of Object.keys(files)) { + yield { + isDirectory: false, + isFile: true, + isSymlink: false, + name: file, + path: file, + }; + } + }); await fsRoutes( app, @@ -33,8 +43,7 @@ async function createServer( } throw new Error(`Mock FS: file ${full} not found`); }, - _fs: createFakeFs(files), - } as FsRoutesOptions & TESTING_ONLY__FsRoutesOptions, + }, ); return new FakeServer(app.handler()); } diff --git a/src/test_utils.ts b/src/test_utils.ts index 74217d0ba67..2fce571f063 100644 --- a/src/test_utils.ts +++ b/src/test_utils.ts @@ -1,8 +1,6 @@ import { FreshReqContext } from "./context.ts"; -import type { FsAdapter } from "./fs.ts"; import { type BuildCache, ProdBuildCache } from "./build_cache.ts"; import type { ResolvedFreshConfig } from "./config.ts"; -import type { WalkEntry } from "@std/fs/walk"; import { DEFAULT_CONN_INFO } from "./app.ts"; const STUB = {} as unknown as Deno.ServeHandlerInfo; @@ -90,32 +88,6 @@ export function serveMiddleware( }); } -export function createFakeFs(files: Record): FsAdapter { - return { - cwd: () => ".", - async *walk(_root) { - // FIXME: ignore - for (const file of Object.keys(files)) { - const entry: WalkEntry = { - isDirectory: false, - isFile: true, - isSymlink: false, - name: file, // FIXME? - path: file, - }; - yield entry; - } - }, - // deno-lint-ignore require-await - async isDirectory(dir) { - return Object.keys(files).some((file) => file.startsWith(dir + "/")); - }, - async mkdirp(_dir: string) { - }, - readFile: Deno.readFile, - }; -} - export const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); export async function withTmpDir(