diff --git a/eslint.config.ts b/eslint.config.ts index de1ae222a..01120c92e 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -91,6 +91,7 @@ export default defineConfig( enableAutofixRemoval: { imports: true, }, + ignoreUsingDeclarations: true, }, ], "@typescript-eslint/prefer-nullish-coalescing": [ diff --git a/packages/cli/src/findConfigFileName.ts b/packages/cli/src/findConfigFileName.ts index 1f5115247..eff7c7c72 100644 --- a/packages/cli/src/findConfigFileName.ts +++ b/packages/cli/src/findConfigFileName.ts @@ -1,4 +1,4 @@ -import fs from "node:fs/promises"; +import type { LinterHost } from "@flint.fyi/core"; const candidatesOrdered = [ "flint.config.ts", @@ -9,8 +9,11 @@ const candidatesOrdered = [ "flint.config.js", ]; -export async function findConfigFileName(directory: string) { - const children = new Set(await fs.readdir(directory)); +export async function findConfigFileName(host: LinterHost) { + const currentDirectoryContents = await host.readDirectory( + host.getCurrentDirectory(), + ); + const children = new Set(currentDirectoryContents.map((file) => file.name)); const fileName = candidatesOrdered.find((candidate) => children.has(candidate), diff --git a/packages/cli/src/renderers/singleRendererFactory.ts b/packages/cli/src/renderers/singleRendererFactory.ts index ca01ca00d..f173d33c0 100644 --- a/packages/cli/src/renderers/singleRendererFactory.ts +++ b/packages/cli/src/renderers/singleRendererFactory.ts @@ -19,6 +19,7 @@ export const singleRendererFactory: RendererFactory = { continue; } + // TODO: Can we re-use the sourcefile representation? const sourceFileText = await fs.readFile(filePath, "utf-8"); const body = presenter.renderFile({ diff --git a/packages/cli/src/runCli.ts b/packages/cli/src/runCli.ts index 7c70cd539..2cfa13a98 100644 --- a/packages/cli/src/runCli.ts +++ b/packages/cli/src/runCli.ts @@ -74,8 +74,9 @@ export async function runCli(args: string[]) { return 0; } - const cwd = process.cwd(); - const configFileName = await findConfigFileName(cwd); + const host = createDiskBackedLinterHost(process.cwd()); + const cwd = host.getCurrentDirectory(); + const configFileName = await findConfigFileName(host); if (!configFileName) { console.error(`No flint.config.* file found in ${cwd}.`); console.error( @@ -89,8 +90,6 @@ export async function runCli(args: string[]) { const getRenderer = createRendererFactory(configFileName, values); - const host = createDiskBackedLinterHost(cwd); - if (values.watch) { await runCliWatch(host, configFileName, getRenderer, values); console.log("👋 Thanks for using Flint!"); diff --git a/packages/cli/src/runCliWatch.ts b/packages/cli/src/runCliWatch.ts index 13a8e4733..3d236ebff 100644 --- a/packages/cli/src/runCliWatch.ts +++ b/packages/cli/src/runCliWatch.ts @@ -2,7 +2,6 @@ import type { LinterHost, LintResults } from "@flint.fyi/core"; import { normalizePath } from "@flint.fyi/core"; import debounce from "debounce"; import { debugForFile } from "debug-for-file"; -import * as fs from "node:fs"; import type { OptionsValues } from "./options.ts"; import type { Renderer } from "./renderers/types.ts"; @@ -16,7 +15,6 @@ export async function runCliWatch( getRenderer: () => Renderer, values: OptionsValues, ) { - const abortController = new AbortController(); const cwd = host.getCurrentDirectory(); log("Running single-run CLI once before watching"); @@ -46,7 +44,7 @@ export async function runCliWatch( ); renderer.onQuit?.(() => { - abortController.abort(); + watcher[Symbol.dispose](); resolve(); }); @@ -56,19 +54,6 @@ export async function runCliWatch( currentRenderer = startNewTask(true); const rerun = debounce((fileName: string) => { - if ( - fileName.startsWith("node_modules/.cache") || - fileName.startsWith(".git") || - fileName.startsWith(".jj") || - fileName.startsWith(".turbo") - ) { - log( - "Skipping re-running watch mode for ignored change to: %s", - fileName, - ); - return; - } - const normalizedPath = normalizePath(fileName, true); const shouldRerun = shouldRerunForFileChange( @@ -90,18 +75,9 @@ export async function runCliWatch( }, 100); log("Watching cwd:", cwd); - fs.watch( - cwd, - { - recursive: true, - signal: abortController.signal, - }, - (_, fileName) => { - if (fileName) { - rerun(fileName); - } - }, - ); + const watcher = host.watchDirectorySync(cwd, rerun, { + recursive: true, + }); }); } diff --git a/packages/core/src/cache/getFileTouchTime.ts b/packages/core/src/cache/getFileTouchTime.ts deleted file mode 100644 index af3a81db4..000000000 --- a/packages/core/src/cache/getFileTouchTime.ts +++ /dev/null @@ -1,7 +0,0 @@ -import * as fsSync from "node:fs"; - -export function getFileTouchTime(filePath: string) { - // TODO: Speed this up with an in-memory file system - // https://github.com/flint-fyi/flint/issues/73 - return fsSync.statSync(filePath).mtimeMs; -} diff --git a/packages/core/src/cache/readFromCache.ts b/packages/core/src/cache/readFromCache.ts index e91183d92..4a457422f 100644 --- a/packages/core/src/cache/readFromCache.ts +++ b/packages/core/src/cache/readFromCache.ts @@ -2,21 +2,21 @@ import { nullThrows } from "@flint.fyi/utils"; import { CachedFactory } from "cached-factory"; import { debugForFile } from "debug-for-file"; -import { readFileSafe } from "../running/readFileSafe.ts"; import type { FileCacheStorage } from "../types/cache.ts"; +import type { LinterHost } from "../types/host.ts"; import { cacheStorageSchema } from "./cacheSchema.ts"; import { getCacheFilePath } from "./getCacheFilePath.ts"; -import { getFileTouchTime } from "./getFileTouchTime.ts"; const log = debugForFile(import.meta.filename); export async function readFromCache( + host: LinterHost, allFilePaths: Set, configFilePath: string, cacheLocation: string | undefined, ): Promise | undefined> { const cacheFilePath = getCacheFilePath(cacheLocation); - const rawCacheString = await readFileSafe(cacheFilePath); + const rawCacheString = await host.readFile(cacheFilePath); if (!rawCacheString) { log("Linting all %d file path(s) due to lack of cache.", allFilePaths.size); @@ -50,7 +50,7 @@ export async function readFromCache( cache.configs[filePath], "Cache timestamp is expected to be present", ); - const timestampTouched = getFileTouchTime(filePath); + const timestampTouched = await host.getFileTouchTime(filePath); if (timestampTouched > timestampCached) { log( "Linting all %d file path(s) due to %s touch timestamp %d after cache timestamp %d", @@ -93,7 +93,7 @@ export async function readFromCache( } const timestampCached = fileCached.timestamp; - const timestampTouched = getFileTouchTime(filePath); + const timestampTouched = await host.getFileTouchTime(filePath); if (timestampTouched > timestampCached) { log( "Directly invalidating cache for: %s due to touch timestamp %d after cache timestamp %d", diff --git a/packages/core/src/cache/writeToCache.ts b/packages/core/src/cache/writeToCache.ts index cf6169e6c..236d736a9 100644 --- a/packages/core/src/cache/writeToCache.ts +++ b/packages/core/src/cache/writeToCache.ts @@ -5,14 +5,15 @@ import { dirname } from "node:path"; import omitEmpty from "omit-empty"; import type { CacheStorage } from "../types/cache.ts"; +import type { LinterHost } from "../types/host.ts"; import type { LintResults } from "../types/linting.ts"; import { cacheStorageSchema } from "./cacheSchema.ts"; import { getCacheFilePath } from "./getCacheFilePath.ts"; -import { getFileTouchTime } from "./getFileTouchTime.ts"; const log = debugForFile(import.meta.filename); export async function writeToCache( + host: LinterHost, configFileName: string, lintResults: LintResults, cacheLocation: string | undefined, @@ -28,8 +29,8 @@ export async function writeToCache( const storage: CacheStorage = { configs: { - [configFileName]: getFileTouchTime(configFileName), - "package.json": getFileTouchTime("package.json"), + [configFileName]: await host.getFileTouchTime(configFileName), + "package.json": await host.getFileTouchTime("package.json"), }, files: { ...Object.fromEntries( diff --git a/packages/core/src/changing/applyChangesToFile.ts b/packages/core/src/changing/applyChangesToFile.ts index 4e5430e9a..f99143ed2 100644 --- a/packages/core/src/changing/applyChangesToFile.ts +++ b/packages/core/src/changing/applyChangesToFile.ts @@ -1,12 +1,14 @@ +import { nullThrows } from "@flint.fyi/utils"; import { debugForFile } from "debug-for-file"; -import * as fs from "node:fs/promises"; import type { FileChange } from "../types/changes.ts"; +import type { LinterHost } from "../types/host.ts"; import { applyChangesToText } from "./applyChangesToText.ts"; const log = debugForFile(import.meta.filename); export async function applyChangesToFile( + host: LinterHost, absoluteFilePath: string, changes: FileChange[], ) { @@ -16,20 +18,15 @@ export async function applyChangesToFile( absoluteFilePath, ); + const fileContent = await host.readFile(absoluteFilePath); const updatedFileContent = applyChangesToText( changes, - // TODO: Eventually, the file system should be abstracted - // Direct fs read calls don't make sense in e.g. virtual file systems - // https://github.com/flint-fyi/flint/issues/73 - await fs.readFile(absoluteFilePath, "utf8"), + nullThrows(fileContent, "Expected linted file to exist."), ); log("Writing %d changes to file: %s", changes.length, absoluteFilePath); - // TODO: Eventually, the file system should be abstracted - // Direct fs write calls don't make sense in e.g. virtual file systems - // https://github.com/flint-fyi/flint/issues/73 - await fs.writeFile(absoluteFilePath, updatedFileContent); + await host.writeFile(absoluteFilePath, updatedFileContent); log("Wrote changes to file: %s", absoluteFilePath); } diff --git a/packages/core/src/changing/applyChangesToFiles.ts b/packages/core/src/changing/applyChangesToFiles.ts index 338917ff8..1f556f0b6 100644 --- a/packages/core/src/changing/applyChangesToFiles.ts +++ b/packages/core/src/changing/applyChangesToFiles.ts @@ -1,5 +1,6 @@ import { debugForFile } from "debug-for-file"; +import type { LinterHost } from "../index.ts"; import type { FileResults } from "../types/linting.ts"; import { applyChangesToFile } from "./applyChangesToFile.ts"; import { resolveChangesByFile } from "./resolveChangesByFile.ts"; @@ -7,21 +8,23 @@ import { resolveChangesByFile } from "./resolveChangesByFile.ts"; const log = debugForFile(import.meta.filename); export async function applyChangesToFiles( + host: LinterHost, filesResults: Map, requestedSuggestions: Set, ) { log("Resolving changes from results."); const changesByFile = await resolveChangesByFile( + host, filesResults, requestedSuggestions, ); - log("Resolved %d changes from results."); + log("Resolved %d changes from results.", changesByFile.length); await Promise.all( changesByFile.map(async ([absoluteFilePath, fileChanges]) => { - await applyChangesToFile(absoluteFilePath, fileChanges); + await applyChangesToFile(host, absoluteFilePath, fileChanges); }), ); diff --git a/packages/core/src/changing/resolveChange.ts b/packages/core/src/changing/resolveChange.ts index 9bbb07718..e37869963 100644 --- a/packages/core/src/changing/resolveChange.ts +++ b/packages/core/src/changing/resolveChange.ts @@ -1,9 +1,11 @@ -import * as fs from "node:fs/promises"; +import { nullThrows } from "@flint.fyi/utils"; import type { Change, ResolvedChange } from "../types/changes.ts"; +import type { LinterHost } from "../types/host.ts"; import { isSuggestionForFiles } from "../utils/predicates.ts"; export async function resolveChange( + host: LinterHost, change: Change, sourceFilePath: string, ): Promise { @@ -17,11 +19,12 @@ export async function resolveChange( return ( await Promise.all( Object.entries(change.files).flatMap(async ([filePath, generator]) => { - // TODO: Eventually, the file system should be abstracted - // Direct fs read calls don't make sense in e.g. virtual file systems - // https://github.com/flint-fyi/flint/issues/73 - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const fileChanges = generator!(await fs.readFile(filePath, "utf8")); + const gen = nullThrows( + generator, + "Expected suggestion generator to exist", + ); + const file = await host.readFile(filePath); + const fileChanges = gen(nullThrows(file, "Expected file to exist")); return fileChanges.map((fileChange) => ({ filePath, diff --git a/packages/core/src/changing/resolveChangesByFile.ts b/packages/core/src/changing/resolveChangesByFile.ts index 40637ec86..e53ec36b8 100644 --- a/packages/core/src/changing/resolveChangesByFile.ts +++ b/packages/core/src/changing/resolveChangesByFile.ts @@ -1,6 +1,7 @@ import { CachedFactory } from "cached-factory"; import type { FileChange } from "../types/changes.ts"; +import type { LinterHost } from "../types/host.ts"; import type { FileResults } from "../types/linting.ts"; import type { FileReport } from "../types/reports.ts"; import { flatten } from "../utils/arrays.ts"; @@ -8,6 +9,7 @@ import { createReportSuggestionKey } from "./createReportSuggestionKey.ts"; import { resolveChange } from "./resolveChange.ts"; export async function resolveChangesByFile( + host: LinterHost, filesResults: Map, requestedSuggestions: Set, ) { @@ -20,13 +22,18 @@ export async function resolveChangesByFile( } async function collectReportSuggestions( + host: LinterHost, absoluteFilePath: string, report: FileReport, ) { for (const suggestion of report.suggestions ?? []) { const key = createReportSuggestionKey(report, suggestion); if (requestedSuggestions.has(key)) { - const resolved = await resolveChange(suggestion, absoluteFilePath); + const resolved = await resolveChange( + host, + suggestion, + absoluteFilePath, + ); for (const change of flatten(resolved)) { changesByFile.get(change.filePath).push(change); @@ -40,7 +47,7 @@ export async function resolveChangesByFile( async ([absoluteFilePath, fileResults]) => { for (const report of fileResults.reports) { collectReportFix(absoluteFilePath, report); - await collectReportSuggestions(absoluteFilePath, report); + await collectReportSuggestions(host, absoluteFilePath, report); } }, ), diff --git a/packages/core/src/host/createDiskBackedLinterHost.test.ts b/packages/core/src/host/createDiskBackedLinterHost.test.ts index 351615ea2..e2f491b6d 100644 --- a/packages/core/src/host/createDiskBackedLinterHost.test.ts +++ b/packages/core/src/host/createDiskBackedLinterHost.test.ts @@ -23,8 +23,6 @@ function findUpNodeModules(startDir: string): string { } } -/* eslint @typescript-eslint/no-unused-vars: ["error", { "varsIgnorePattern": "^_$" }] */ - describe("createDiskBackedLinterHost", () => { const integrationRoot = path.join( findUpNodeModules(import.meta.dirname), @@ -57,9 +55,9 @@ describe("createDiskBackedLinterHost", () => { fs.writeFileSync(filePath, "hello"); fs.mkdirSync(dirPath, { recursive: true }); - expect(host.stat(filePath)).toEqual("file"); - expect(host.stat(dirPath)).toEqual("directory"); - expect(host.stat(missingPath)).toEqual(undefined); + expect(host.fileTypeSync(filePath)).toEqual("file"); + expect(host.fileTypeSync(dirPath)).toEqual("directory"); + expect(host.fileTypeSync(missingPath)).toEqual(undefined); }); it("reads file contents", () => { @@ -68,7 +66,7 @@ describe("createDiskBackedLinterHost", () => { fs.writeFileSync(filePath, "hello"); - expect(host.readFile(filePath)).toEqual("hello"); + expect(host.readFileSync(filePath)).toEqual("hello"); }); it("lists directory entries and resolves directory symlinks", () => { @@ -79,7 +77,7 @@ describe("createDiskBackedLinterHost", () => { fs.mkdirSync(dirPath, { recursive: true }); fs.symlinkSync(dirPath, dirLink, "junction"); - const entries = host.readDirectory(integrationRoot); + const entries = host.readDirectorySync(integrationRoot); const sortedEntries = entries .map((entry) => ({ name: entry.name, type: entry.type })) .toSorted((a, b) => a.name.localeCompare(b.name)); @@ -104,7 +102,7 @@ describe("createDiskBackedLinterHost", () => { fs.symlinkSync(filePath, fileLink); fs.symlinkSync(missingPath, brokenLink); - const entries = host.readDirectory(integrationRoot); + const entries = host.readDirectorySync(integrationRoot); const sortedEntries = entries .map((entry) => ({ name: entry.name, type: entry.type })) .toSorted((a, b) => a.name.localeCompare(b.name)); @@ -121,7 +119,9 @@ describe("createDiskBackedLinterHost", () => { const host = createDiskBackedLinterHost(integrationRoot); const filePath = path.join(integrationRoot, "watch.txt"); const onEvent = vi.fn(); - using _ = host.watchFile(filePath, onEvent, 10); + using _ = host.watchFileSync(filePath, onEvent, { + pollingInterval: 10, + }); await sleep(50); @@ -136,7 +136,7 @@ describe("createDiskBackedLinterHost", () => { const filePath = path.join(integrationRoot, "watch-change.txt"); fs.writeFileSync(filePath, "first"); const onEvent = vi.fn(); - using _ = host.watchFile(filePath, onEvent); + using _ = host.watchFileSync(filePath, onEvent); await sleep(50); @@ -151,7 +151,7 @@ describe("createDiskBackedLinterHost", () => { const filePath = path.join(integrationRoot, "watch-delete.txt"); fs.writeFileSync(filePath, "first"); const onEvent = vi.fn(); - using _ = host.watchFile(filePath, onEvent); + using _ = host.watchFileSync(filePath, onEvent); await sleep(50); @@ -165,7 +165,7 @@ describe("createDiskBackedLinterHost", () => { const host = createDiskBackedLinterHost(integrationRoot); const filePath = path.join(integrationRoot, "watch-recreate.txt"); const onEvent = vi.fn(); - using _ = host.watchFile(filePath, onEvent, 10); + using _ = host.watchFileSync(filePath, onEvent, { pollingInterval: 10 }); await sleep(50); @@ -193,7 +193,7 @@ describe("createDiskBackedLinterHost", () => { const secondDir = path.join(firstDir, "second"); const filePath = path.join(secondDir, "deep.txt"); const onEvent = vi.fn(); - using _ = host.watchFile(filePath, onEvent, 10); + using _ = host.watchFileSync(filePath, onEvent, { pollingInterval: 10 }); await sleep(50); @@ -241,7 +241,7 @@ describe("createDiskBackedLinterHost", () => { const filePath = path.join(integrationRoot, "disposed.txt"); const onEvent = vi.fn(); { - using _ = host.watchFile(filePath, onEvent); + using _ = host.watchFileSync(filePath, onEvent); } fs.writeFileSync(filePath, "content"); @@ -255,7 +255,9 @@ describe("createDiskBackedLinterHost", () => { const filePath = path.join(integrationRoot, "disposed.txt"); const onEvent = vi.fn(); { - using _ = host.watchFile(filePath, onEvent, 10); + using _ = host.watchFileSync(filePath, onEvent, { + pollingInterval: 10, + }); await sleep(50); fs.writeFileSync(filePath, "first"); await vi.waitFor(() => { @@ -275,7 +277,9 @@ describe("createDiskBackedLinterHost", () => { const onEvent = vi.fn(); fs.writeFileSync(filePath, "first"); { - using _ = host.watchFile(filePath, onEvent, 10); + using _ = host.watchFileSync(filePath, onEvent, { + pollingInterval: 10, + }); await sleep(50); fs.rmSync(filePath); await vi.waitFor(() => { @@ -294,7 +298,9 @@ describe("createDiskBackedLinterHost", () => { const targetPath = path.join(integrationRoot, "target.txt"); const otherPath = path.join(integrationRoot, "other.txt"); const onEvent = vi.fn(); - using _ = host.watchFile(targetPath, onEvent, 10); + using _ = host.watchFileSync(targetPath, onEvent, { + pollingInterval: 10, + }); await sleep(50); @@ -309,7 +315,7 @@ describe("createDiskBackedLinterHost", () => { const dirPath = path.join(integrationRoot, "directory"); const onEvent = vi.fn(); fs.mkdirSync(dirPath, { recursive: true }); - using _ = host.watchFile(dirPath, onEvent, 10); + using _ = host.watchFileSync(dirPath, onEvent, { pollingInterval: 10 }); await sleep(50); @@ -331,7 +337,7 @@ describe("createDiskBackedLinterHost", () => { const filePath = path.join(dirPath, "file.txt"); const onEvent = vi.fn(); fs.mkdirSync(dirPath, { recursive: true }); - using _ = host.watchFile(dirPath, onEvent, 10); + using _ = host.watchFileSync(dirPath, onEvent, { pollingInterval: 10 }); await sleep(50); @@ -349,7 +355,9 @@ describe("createDiskBackedLinterHost", () => { fs.mkdirSync(nestedPath, { recursive: true }); const onEvent = vi.fn(); - using _ = host.watchDirectory(directoryPath, false, onEvent); + using _ = host.watchDirectorySync(directoryPath, onEvent, { + recursive: false, + }); await sleep(50); @@ -375,7 +383,9 @@ describe("createDiskBackedLinterHost", () => { fs.mkdirSync(nestedPath, { recursive: true }); fs.writeFileSync(path.join(directoryPath, "existing.txt"), "content"); const onEvent = vi.fn(); - using _ = host.watchDirectory(directoryPath, true, onEvent); + using _ = host.watchDirectorySync(directoryPath, onEvent, { + recursive: true, + }); await sleep(50); @@ -397,7 +407,7 @@ describe("createDiskBackedLinterHost", () => { const baseDir = path.join(integrationRoot, "base-git"); fs.mkdirSync(baseDir, { recursive: true }); const onEvent = vi.fn(); - using _ = host.watchDirectory(baseDir, true, onEvent); + using _ = host.watchDirectorySync(baseDir, onEvent, { recursive: true }); await sleep(50); @@ -418,7 +428,7 @@ describe("createDiskBackedLinterHost", () => { const baseDir = path.join(integrationRoot, "base-node-modules"); fs.mkdirSync(baseDir, { recursive: true }); const onEvent = vi.fn(); - using _ = host.watchDirectory(baseDir, true, onEvent); + using _ = host.watchDirectorySync(baseDir, onEvent, { recursive: true }); await sleep(50); @@ -444,7 +454,7 @@ describe("createDiskBackedLinterHost", () => { const baseDir = path.join(integrationRoot, "lookalike"); fs.mkdirSync(baseDir, { recursive: true }); const onEvent = vi.fn(); - using _ = host.watchDirectory(baseDir, true, onEvent); + using _ = host.watchDirectorySync(baseDir, onEvent, { recursive: true }); await sleep(50); @@ -465,7 +475,10 @@ describe("createDiskBackedLinterHost", () => { host.isCaseSensitiveFS(), ); const onEvent = vi.fn(); - using _ = host.watchDirectory(directoryPath, false, onEvent, 10); + using _ = host.watchDirectorySync(directoryPath, onEvent, { + pollingInterval: 10, + recursive: false, + }); await sleep(50); @@ -486,7 +499,10 @@ describe("createDiskBackedLinterHost", () => { const onEvent = vi.fn(); fs.mkdirSync(directoryPath, { recursive: true }); - using _ = host.watchDirectory(directoryPath, false, onEvent, 10); + using _ = host.watchDirectorySync(directoryPath, onEvent, { + pollingInterval: 10, + recursive: false, + }); await sleep(50); @@ -516,7 +532,10 @@ describe("createDiskBackedLinterHost", () => { host.isCaseSensitiveFS(), ); - using _ = host.watchDirectory(directoryPath, false, onEvent, 10); + using _ = host.watchDirectorySync(directoryPath, onEvent, { + pollingInterval: 10, + recursive: false, + }); await sleep(50); @@ -538,7 +557,10 @@ describe("createDiskBackedLinterHost", () => { const directoryPath = path.join(integrationRoot, "recreate-dir"); const onEvent = vi.fn(); fs.mkdirSync(directoryPath, { recursive: true }); - using _ = host.watchDirectory(directoryPath, false, onEvent, 10); + using _ = host.watchDirectorySync(directoryPath, onEvent, { + pollingInterval: 10, + recursive: false, + }); await sleep(50); @@ -595,7 +617,10 @@ describe("createDiskBackedLinterHost", () => { host.isCaseSensitiveFS(), ); const onEvent = vi.fn(); - using _ = host.watchDirectory(directoryPath, false, onEvent, 10); + using _ = host.watchDirectorySync(directoryPath, onEvent, { + pollingInterval: 10, + recursive: false, + }); await sleep(50); diff --git a/packages/core/src/host/createDiskBackedLinterHost.ts b/packages/core/src/host/createDiskBackedLinterHost.ts index 4bd204602..a51b7ac9e 100644 --- a/packages/core/src/host/createDiskBackedLinterHost.ts +++ b/packages/core/src/host/createDiskBackedLinterHost.ts @@ -54,7 +54,7 @@ export function createDiskBackedLinterHost(cwd: string): LinterHost { .watch( normalizedWatchPath, { persistent: false, recursive }, - (event, filename) => { + (_event, filename) => { if (unwatched) { return; } @@ -158,20 +158,70 @@ export function createDiskBackedLinterHost(cwd: string): LinterHost { } return { + fileTypeSync(pathAbsolute) { + try { + const stat = fs.statSync(pathAbsolute); + if (stat.isDirectory()) { + return "directory"; + } + if (stat.isFile()) { + return "file"; + } + } catch { + // Fall through to undefined. + } + return undefined; + }, getCurrentDirectory() { return cwd; }, + async getFileTouchTime(filePath) { + const stat = await fs.promises.stat(filePath); + return stat.mtimeMs; + }, + getFileTouchTimeSync(filePath) { + return fs.statSync(filePath).mtimeMs; + }, isCaseSensitiveFS() { return caseSensitiveFS; }, - readDirectory(directoryPathAbsolute) { + async readDirectory(directoryPathAbsolute) { + const dirents = await fs.promises.readdir(directoryPathAbsolute, { + withFileTypes: true, + }); + + const result = await Promise.all( + dirents.map(async (entry): Promise<[] | LinterHostDirectoryEntry> => { + let stat: Pick = entry; + if (entry.isSymbolicLink()) { + try { + stat = await fs.promises.stat( + path.join(directoryPathAbsolute, entry.name), + ); + } catch { + return []; + } + } + if (stat.isDirectory()) { + return { name: entry.name, type: "directory" }; + } else if (stat.isFile()) { + return { name: entry.name, type: "file" }; + } + + return []; + }), + ); + + return result.flat(); + }, + readDirectorySync(directoryPathAbsolute) { const result: LinterHostDirectoryEntry[] = []; const dirents = fs.readdirSync(directoryPathAbsolute, { withFileTypes: true, }); for (const entry of dirents) { - let stat = entry as Pick; + let stat: Pick = entry; if (entry.isSymbolicLink()) { try { stat = fs.statSync(path.join(directoryPathAbsolute, entry.name)); @@ -181,35 +231,28 @@ export function createDiskBackedLinterHost(cwd: string): LinterHost { } if (stat.isDirectory()) { result.push({ name: entry.name, type: "directory" }); - } - if (stat.isFile()) { + } else if (stat.isFile()) { result.push({ name: entry.name, type: "file" }); } } return result; }, - readFile(filePathAbsolute) { - return fs.readFileSync(filePathAbsolute, "utf8"); + async readFile(filePathAbsolute) { + try { + return await fs.promises.readFile(filePathAbsolute, "utf8"); + } catch { + return undefined; + } }, - stat(pathAbsolute) { + readFileSync(filePathAbsolute) { try { - const stat = fs.statSync(pathAbsolute); - if (stat.isDirectory()) { - return "directory"; - } - if (stat.isFile()) { - return "file"; - } - } catch {} // eslint-disable-line no-empty - return undefined; + return fs.readFileSync(filePathAbsolute, "utf8"); + } catch { + return undefined; + } }, - watchDirectory( - directoryPathAbsolute, - recursive, - callback, - pollingInterval = 2_000, - ) { + watchDirectorySync(directoryPathAbsolute, callback, options) { directoryPathAbsolute = normalizePath( directoryPathAbsolute, caseSensitiveFS, @@ -217,8 +260,8 @@ export function createDiskBackedLinterHost(cwd: string): LinterHost { return createWatcher( directoryPathAbsolute, - recursive, - pollingInterval, + options.recursive, + options.pollingInterval ?? 2_000, (normalizedChangedFilePath) => { normalizedChangedFilePath ??= directoryPathAbsolute; if (normalizedChangedFilePath !== directoryPathAbsolute) { @@ -239,13 +282,13 @@ export function createDiskBackedLinterHost(cwd: string): LinterHost { }, ); }, - watchFile(filePathAbsolute, callback, pollingInterval = 2_000) { + watchFileSync(filePathAbsolute, callback, options) { filePathAbsolute = normalizePath(filePathAbsolute, caseSensitiveFS); return createWatcher( filePathAbsolute, false, - pollingInterval, + options?.pollingInterval ?? 2_000, (normalizedChangedFilePath, event) => { if (normalizedChangedFilePath === filePathAbsolute) { callback(event); @@ -253,5 +296,11 @@ export function createDiskBackedLinterHost(cwd: string): LinterHost { }, ); }, + async writeFile(filePathAbsolute, content) { + await fs.promises.writeFile(filePathAbsolute, content, "utf8"); + }, + writeFileSync(filePathAbsolute, content) { + fs.writeFileSync(filePathAbsolute, content, "utf8"); + }, }; } diff --git a/packages/core/src/host/createEphemeralLinterHost.ts b/packages/core/src/host/createEphemeralLinterHost.ts index 64cc7de46..ff7cd8a08 100644 --- a/packages/core/src/host/createEphemeralLinterHost.ts +++ b/packages/core/src/host/createEphemeralLinterHost.ts @@ -9,14 +9,14 @@ import type { LinterHost } from "../types/host.ts"; export function createEphemeralLinterHost(baseHost: LinterHost): LinterHost { return { ...baseHost, - watchDirectory() { + watchDirectorySync() { return { [Symbol.dispose]() { // Intentionally empty to satisfy the Disposable interface. }, }; }, - watchFile() { + watchFileSync() { return { [Symbol.dispose]() { // Intentionally empty to satisfy the Disposable interface. diff --git a/packages/core/src/host/createVFSLinterHost.test.ts b/packages/core/src/host/createVFSLinterHost.test.ts index 9bc4502ac..4f9f2809c 100644 --- a/packages/core/src/host/createVFSLinterHost.test.ts +++ b/packages/core/src/host/createVFSLinterHost.test.ts @@ -2,8 +2,6 @@ import { describe, expect, it, vi } from "vitest"; import { createVFSLinterHost } from "./createVFSLinterHost.ts"; -/* eslint @typescript-eslint/no-unused-vars: ["error", { "varsIgnorePattern": "^_$" }] */ - describe(createVFSLinterHost, () => { it("normalizes cwd", () => { const host = createVFSLinterHost({ @@ -43,8 +41,8 @@ describe(createVFSLinterHost, () => { host.vfsUpsertFile("/root/file.ts", "content"); host.vfsUpsertFile("/root/nested/file.ts", "content"); - expect(host.stat("/root/file.ts")).toEqual("file"); - expect(host.stat("/root/nested/file.ts")).toEqual("file"); + expect(host.fileTypeSync("/root/file.ts")).toEqual("file"); + expect(host.fileTypeSync("/root/nested/file.ts")).toEqual("file"); }); it("existing directory", () => { @@ -52,13 +50,13 @@ describe(createVFSLinterHost, () => { host.vfsUpsertFile("/root/nested/file.ts", "content"); - expect(host.stat("/root/nested")).toEqual("directory"); + expect(host.fileTypeSync("/root/nested")).toEqual("directory"); }); it("non-existent file", () => { const host = createVFSLinterHost({ caseSensitive: true, cwd: "/root" }); - expect(host.stat("/root/missing")).toBeUndefined(); + expect(host.fileTypeSync("/root/missing")).toBeUndefined(); }); it("propagates to base host", () => { @@ -70,7 +68,7 @@ describe(createVFSLinterHost, () => { baseHost.vfsUpsertFile("/root/file.ts", "content"); - expect(host.stat("/root/file.ts")).toEqual("file"); + expect(host.fileTypeSync("/root/file.ts")).toEqual("file"); }); it("prefers overlay file over base dir", () => { @@ -83,7 +81,7 @@ describe(createVFSLinterHost, () => { baseHost.vfsUpsertFile("/root/file.ts/file.ts", "content"); host.vfsUpsertFile("/root/file.ts", "content"); - expect(host.stat("/root/file.ts")).toEqual("file"); + expect(host.fileTypeSync("/root/file.ts")).toEqual("file"); }); it("prefers overlay dir over base file", () => { @@ -96,7 +94,7 @@ describe(createVFSLinterHost, () => { baseHost.vfsUpsertFile("/root/file.ts", "content"); host.vfsUpsertFile("/root/file.ts/file.ts", "content"); - expect(host.stat("/root/file.ts")).toEqual("directory"); + expect(host.fileTypeSync("/root/file.ts")).toEqual("directory"); }); }); @@ -104,14 +102,14 @@ describe(createVFSLinterHost, () => { it("returns undefined when reading a missing file", () => { const host = createVFSLinterHost({ caseSensitive: true, cwd: "/root" }); - expect(host.readFile("/root/missing.txt")).toBeUndefined(); + expect(host.readFileSync("/root/missing.txt")).toBeUndefined(); }); it("reads existing file", () => { const host = createVFSLinterHost({ caseSensitive: true, cwd: "/root" }); host.vfsUpsertFile("/root/file.ts", "content"); - expect(host.readFile("/root/file.ts")).toEqual("content"); + expect(host.readFileSync("/root/file.ts")).toEqual("content"); }); it("propagates to base host", () => { @@ -123,7 +121,7 @@ describe(createVFSLinterHost, () => { const host = createVFSLinterHost({ baseHost }); - expect(host.readFile("/root/base.txt")).toEqual("base"); + expect(host.readFileSync("/root/base.txt")).toEqual("base"); }); it("prefers overlay over base", () => { @@ -136,14 +134,14 @@ describe(createVFSLinterHost, () => { const host = createVFSLinterHost({ baseHost }); host.vfsUpsertFile("/root/file.txt", "vfs"); - expect(host.readFile("/root/file.txt")).toEqual("vfs"); + expect(host.readFileSync("/root/file.txt")).toEqual("vfs"); }); it("returns undefined when reading directory", () => { const host = createVFSLinterHost({ caseSensitive: true, cwd: "/root" }); host.vfsUpsertFile("/root/nested/file.txt", "vfs"); - expect(host.readFile("/root/nested")).toBeUndefined(); + expect(host.readFileSync("/root/nested")).toBeUndefined(); }); }); @@ -152,14 +150,14 @@ describe(createVFSLinterHost, () => { const host = createVFSLinterHost({ caseSensitive: true, cwd: "/root" }); host.vfsUpsertFile("/root/other/file.txt", "content"); - expect(host.readDirectory("/root/dir")).toEqual([]); + expect(host.readDirectorySync("/root/dir")).toEqual([]); }); it("returns nothing when reading file", () => { const host = createVFSLinterHost({ caseSensitive: true, cwd: "/root" }); host.vfsUpsertFile("/root/file.txt", "content"); - expect(host.readDirectory("/root/file.txt")).toEqual([]); + expect(host.readDirectorySync("/root/file.txt")).toEqual([]); }); it("lists files", () => { @@ -167,7 +165,7 @@ describe(createVFSLinterHost, () => { host.vfsUpsertFile("/root/file.txt", "content"); host.vfsUpsertFile("/root/sub/file.txt", "content"); - expect(host.readDirectory("/root")).toEqual([ + expect(host.readDirectorySync("/root")).toEqual([ { name: "file.txt", type: "file", @@ -191,7 +189,7 @@ describe(createVFSLinterHost, () => { host.vfsUpsertFile("/root/file.txt", "vfs"); host.vfsUpsertFile("/root/sub/file.txt", "vfs"); - const entries = host.readDirectory("/root"); + const entries = host.readDirectorySync("/root"); expect(entries).toEqual([ { @@ -217,7 +215,7 @@ describe(createVFSLinterHost, () => { host.vfsUpsertFile("/root/vfs.txt", "vfs"); host.vfsUpsertFile("/root/vfs-sub/file.txt", "vfs"); - const entries = host.readDirectory("/root"); + const entries = host.readDirectorySync("/root"); expect(entries).toEqual([ { @@ -249,7 +247,7 @@ describe(createVFSLinterHost, () => { const host = createVFSLinterHost({ baseHost }); host.vfsUpsertFile("/root/file.txt", "vfs"); - const entries = host.readDirectory("/root"); + const entries = host.readDirectorySync("/root"); expect(entries).toEqual([ { @@ -269,7 +267,7 @@ describe(createVFSLinterHost, () => { const host = createVFSLinterHost({ baseHost }); host.vfsUpsertFile("/root/file.txt/file.txt", "host"); - const entries = host.readDirectory("/root"); + const entries = host.readDirectorySync("/root"); expect(entries).toEqual([ { @@ -333,12 +331,12 @@ describe(createVFSLinterHost, () => { }); }); - describe("watchFile", () => { + describe("watchFileSync", () => { it("reports creation", () => { const host = createVFSLinterHost({ caseSensitive: true, cwd: "/root" }); const onEvent = vi.fn(); - using _ = host.watchFile("/root/file.txt", onEvent); + using _ = host.watchFileSync("/root/file.txt", onEvent); expect(onEvent).not.toHaveBeenCalled(); host.vfsUpsertFile("/root/file.txt", "content"); expect(onEvent).toHaveBeenCalledExactlyOnceWith("created"); @@ -349,7 +347,7 @@ describe(createVFSLinterHost, () => { const onEvent = vi.fn(); host.vfsUpsertFile("/root/file.txt", "content"); - using _ = host.watchFile("/root/file.txt", onEvent); + using _ = host.watchFileSync("/root/file.txt", onEvent); expect(onEvent).not.toHaveBeenCalled(); @@ -363,7 +361,7 @@ describe(createVFSLinterHost, () => { const onEvent = vi.fn(); host.vfsUpsertFile("/root/file.txt", "content"); - using _ = host.watchFile("/root/file.txt", onEvent); + using _ = host.watchFileSync("/root/file.txt", onEvent); expect(onEvent).not.toHaveBeenCalled(); @@ -377,7 +375,7 @@ describe(createVFSLinterHost, () => { const onEvent = vi.fn(); { - using _ = host.watchFile("/root/file.txt", onEvent); + using _ = host.watchFileSync("/root/file.txt", onEvent); } host.vfsUpsertFile("/root/file.txt", "content"); @@ -392,7 +390,7 @@ describe(createVFSLinterHost, () => { const host = createVFSLinterHost({ baseHost }); const onEvent = vi.fn(); - using _ = host.watchFile("/root/file.txt", onEvent); + using _ = host.watchFileSync("/root/file.txt", onEvent); expect(onEvent).not.toHaveBeenCalled(); baseHost.vfsUpsertFile("/root/file.txt", "content"); @@ -403,18 +401,22 @@ describe(createVFSLinterHost, () => { it("propagates correct params to base host watcher", () => { const baseHost = { ...createVFSLinterHost({ caseSensitive: true, cwd: "/root" }), - watchFile: vi.fn(() => ({ + watchFileSync: vi.fn(() => ({ [Symbol.dispose]: vi.fn(), })), }; const host = createVFSLinterHost({ baseHost }); - using _ = host.watchFile("/root/file.txt", vi.fn(), 555); + using _ = host.watchFileSync("/root/file.txt", vi.fn(), { + pollingInterval: 555, + }); - expect(baseHost.watchFile).toHaveBeenCalledExactlyOnceWith( + expect(baseHost.watchFileSync).toHaveBeenCalledExactlyOnceWith( "/root/file.txt", expect.any(Function), - 555, + { + pollingInterval: 555, + }, ); }); @@ -422,12 +424,12 @@ describe(createVFSLinterHost, () => { const dispose = vi.fn(); const baseHost = { ...createVFSLinterHost({ caseSensitive: true, cwd: "/root" }), - watchFile: () => ({ [Symbol.dispose]: dispose }), + watchFileSync: () => ({ [Symbol.dispose]: dispose }), }; const host = createVFSLinterHost({ baseHost }); { - using _ = host.watchFile("/root/file.txt", vi.fn()); + using _ = host.watchFileSync("/root/file.txt", vi.fn()); expect(dispose).not.toHaveBeenCalled(); } @@ -435,7 +437,7 @@ describe(createVFSLinterHost, () => { }); }); - describe("watchDirectory", () => { + describe("watchDirectorySync", () => { describe("non-recursive", () => { it("reports file creation", () => { const host = createVFSLinterHost({ @@ -444,7 +446,9 @@ describe(createVFSLinterHost, () => { }); const onEvent = vi.fn(); - using _ = host.watchDirectory("/root", false, onEvent); + using _ = host.watchDirectorySync("/root", onEvent, { + recursive: false, + }); host.vfsUpsertFile("/root/file.txt", "content"); expect(onEvent).toHaveBeenCalledExactlyOnceWith("/root/file.txt"); @@ -457,7 +461,9 @@ describe(createVFSLinterHost, () => { }); const onEvent = vi.fn(); - using _ = host.watchDirectory("/root", false, onEvent); + using _ = host.watchDirectorySync("/root", onEvent, { + recursive: false, + }); host.vfsUpsertFile("/root/dir/file.txt", "content"); expect(onEvent).toHaveBeenCalledExactlyOnceWith("/root/dir"); @@ -470,7 +476,7 @@ describe(createVFSLinterHost, () => { }); const onEvent = vi.fn(); - using _ = host.watchDirectory("/", false, onEvent); + using _ = host.watchDirectorySync("/", onEvent, { recursive: false }); host.vfsUpsertFile("/root/dir/file.txt", "content"); expect(onEvent).toHaveBeenCalledExactlyOnceWith("/root"); @@ -483,7 +489,9 @@ describe(createVFSLinterHost, () => { }); const onEvent = vi.fn(); - using _ = host.watchDirectory("C:\\", false, onEvent); + using _ = host.watchDirectorySync("C:\\", onEvent, { + recursive: false, + }); host.vfsUpsertFile("C:\\file.txt", "content"); expect(onEvent).toHaveBeenCalledExactlyOnceWith("c:/file.txt"); @@ -497,7 +505,9 @@ describe(createVFSLinterHost, () => { const onEvent = vi.fn(); host.vfsUpsertFile("/root/file.txt", "content"); - using _ = host.watchDirectory("/root", false, onEvent); + using _ = host.watchDirectorySync("/root", onEvent, { + recursive: false, + }); expect(onEvent).not.toHaveBeenCalled(); host.vfsUpsertFile("/root/file.txt", "new content"); @@ -512,7 +522,9 @@ describe(createVFSLinterHost, () => { const onEvent = vi.fn(); host.vfsUpsertFile("/root/file.txt", "content"); - using _ = host.watchDirectory("/root", false, onEvent); + using _ = host.watchDirectorySync("/root", onEvent, { + recursive: false, + }); expect(onEvent).not.toHaveBeenCalled(); host.vfsDeleteFile("/root/file.txt"); @@ -527,7 +539,9 @@ describe(createVFSLinterHost, () => { const onEvent = vi.fn(); host.vfsUpsertFile("/root/nested/file.txt", "content"); - using _ = host.watchDirectory("/root", false, onEvent); + using _ = host.watchDirectorySync("/root", onEvent, { + recursive: false, + }); expect(onEvent).not.toHaveBeenCalled(); host.vfsDeleteFile("/root/nested/file.txt"); @@ -543,7 +557,9 @@ describe(createVFSLinterHost, () => { }); const onEvent = vi.fn(); - using _ = host.watchDirectory("/root", true, onEvent); + using _ = host.watchDirectorySync("/root", onEvent, { + recursive: true, + }); host.vfsUpsertFile("/root/nested/file.txt", "content"); @@ -560,7 +576,9 @@ describe(createVFSLinterHost, () => { const onEvent = vi.fn(); host.vfsUpsertFile("/root/nested/file.txt", "content"); - using _ = host.watchDirectory("/root", true, onEvent); + using _ = host.watchDirectorySync("/root", onEvent, { + recursive: true, + }); expect(onEvent).not.toHaveBeenCalled(); host.vfsUpsertFile("/root/nested/file.txt", "new content"); @@ -578,7 +596,9 @@ describe(createVFSLinterHost, () => { const onEvent = vi.fn(); host.vfsUpsertFile("/root/nested/file.txt", "content"); - using _ = host.watchDirectory("/root", true, onEvent); + using _ = host.watchDirectorySync("/root", onEvent, { + recursive: true, + }); expect(onEvent).not.toHaveBeenCalled(); host.vfsDeleteFile("/root/nested/file.txt"); @@ -592,19 +612,24 @@ describe(createVFSLinterHost, () => { it("propagates correct params to base host watcher", () => { const baseHost = { ...createVFSLinterHost({ caseSensitive: true, cwd: "/root" }), - watchDirectory: vi.fn(() => ({ + watchDirectorySync: vi.fn(() => ({ [Symbol.dispose]: vi.fn(), })), }; const host = createVFSLinterHost({ baseHost }); - using _ = host.watchDirectory("/root/file.txt", false, vi.fn(), 555); + using _ = host.watchDirectorySync("/root/file.txt", vi.fn(), { + pollingInterval: 555, + recursive: false, + }); - expect(baseHost.watchDirectory).toHaveBeenCalledExactlyOnceWith( + expect(baseHost.watchDirectorySync).toHaveBeenCalledExactlyOnceWith( "/root/file.txt", - false, expect.any(Function), - 555, + { + pollingInterval: 555, + recursive: false, + }, ); }); @@ -612,12 +637,14 @@ describe(createVFSLinterHost, () => { const dispose = vi.fn(); const baseHost = { ...createVFSLinterHost({ caseSensitive: true, cwd: "/root" }), - watchDirectory: () => ({ [Symbol.dispose]: dispose }), + watchDirectorySync: () => ({ [Symbol.dispose]: dispose }), }; const host = createVFSLinterHost({ baseHost }); { - using _ = host.watchDirectory("/root/file.txt", false, vi.fn()); + using _ = host.watchDirectorySync("/root/file.txt", vi.fn(), { + recursive: false, + }); expect(dispose).not.toHaveBeenCalled(); } diff --git a/packages/core/src/host/createVFSLinterHost.ts b/packages/core/src/host/createVFSLinterHost.ts index 9ab60d963..93f418469 100644 --- a/packages/core/src/host/createVFSLinterHost.ts +++ b/packages/core/src/host/createVFSLinterHost.ts @@ -89,14 +89,38 @@ export function createVFSLinterHost( } } } - return { + const host: VFSLinterHost = { + fileTypeSync(pathAbsolute) { + pathAbsolute = normalizePath(pathAbsolute, caseSensitiveFS); + for (const filePath of fileMap.keys()) { + if (pathAbsolute === filePath) { + return "file"; + } + if (filePath.startsWith(pathAbsolute + "/")) { + return "directory"; + } + } + return baseHost?.fileTypeSync(pathAbsolute); + }, getCurrentDirectory() { return cwd; }, + // eslint-disable-next-line @typescript-eslint/require-await + async getFileTouchTime(filePath) { + return host.getFileTouchTimeSync(filePath); + }, + getFileTouchTimeSync() { + // TODO: uhh... this probably doesn't work amazingly + return Date.now(); + }, isCaseSensitiveFS() { return caseSensitiveFS; }, - readDirectory(directoryPathAbsolute) { + // eslint-disable-next-line @typescript-eslint/require-await + async readDirectory(directoryPathAbsolute) { + return host.readDirectorySync(directoryPathAbsolute); + }, + readDirectorySync(directoryPathAbsolute) { directoryPathAbsolute = normalizePath(directoryPathAbsolute, caseSensitiveFS) + "/"; const result = new Map(); @@ -124,36 +148,28 @@ export function createVFSLinterHost( return [ ...result.values(), - ...(baseHost?.stat(directoryPathAbsolute) === "directory" + ...(baseHost?.fileTypeSync(directoryPathAbsolute) === "directory" ? baseHost - .readDirectory(directoryPathAbsolute) + .readDirectorySync(directoryPathAbsolute) .filter(({ name }) => !result.has(name)) : []), ]; }, - readFile(filePathAbsolute) { + // eslint-disable-next-line @typescript-eslint/require-await + async readFile(filePathAbsolute) { + return host.readFileSync(filePathAbsolute); + }, + readFileSync(filePathAbsolute) { filePathAbsolute = normalizePath(filePathAbsolute, caseSensitiveFS); const file = fileMap.get(filePathAbsolute); if (file != null) { return file; } - if (baseHost?.stat(filePathAbsolute) === "file") { - return baseHost.readFile(filePathAbsolute); + if (baseHost?.fileTypeSync(filePathAbsolute) === "file") { + return baseHost.readFileSync(filePathAbsolute); } return undefined; }, - stat(pathAbsolute) { - pathAbsolute = normalizePath(pathAbsolute, caseSensitiveFS); - for (const filePath of fileMap.keys()) { - if (pathAbsolute === filePath) { - return "file"; - } - if (filePath.startsWith(pathAbsolute + "/")) { - return "directory"; - } - } - return baseHost?.stat(pathAbsolute); - }, vfsDeleteFile(filePathAbsolute) { filePathAbsolute = normalizePath(filePathAbsolute, caseSensitiveFS); if (!fileMap.delete(filePathAbsolute)) { @@ -170,17 +186,12 @@ export function createVFSLinterHost( fileMap.set(filePathAbsolute, content); watchEvent(filePathAbsolute, fileEvent); }, - watchDirectory( - directoryPathAbsolute, - recursive, - callback, - pollingInterval, - ) { + watchDirectorySync(directoryPathAbsolute, callback, options) { directoryPathAbsolute = normalizePath( directoryPathAbsolute, caseSensitiveFS, ); - const collection = recursive + const collection = options.recursive ? recursiveDirectoryWatchers : directoryWatchers; let watchers = collection.get(directoryPathAbsolute); @@ -189,11 +200,10 @@ export function createVFSLinterHost( collection.set(directoryPathAbsolute, watchers); } watchers.add(callback); - const baseWatcher = baseHost?.watchDirectory( + const baseWatcher = baseHost?.watchDirectorySync( directoryPathAbsolute, - recursive, callback, - pollingInterval, + options, ); return { [Symbol.dispose]() { @@ -205,7 +215,7 @@ export function createVFSLinterHost( }, }; }, - watchFile(filePathAbsolute, callback, pollingInterval) { + watchFileSync(filePathAbsolute, callback, options) { filePathAbsolute = normalizePath(filePathAbsolute, caseSensitiveFS); let watchers = fileWatchers.get(filePathAbsolute); if (watchers == null) { @@ -213,10 +223,10 @@ export function createVFSLinterHost( fileWatchers.set(filePathAbsolute, watchers); } watchers.add(callback); - const baseWatcher = baseHost?.watchFile( + const baseWatcher = baseHost?.watchFileSync( filePathAbsolute, callback, - pollingInterval, + options, ); return { [Symbol.dispose]() { @@ -228,5 +238,14 @@ export function createVFSLinterHost( }, }; }, + // eslint-disable-next-line @typescript-eslint/require-await + async writeFile(filePathAbsolute, content) { + host.vfsUpsertFile(filePathAbsolute, content); + }, + writeFileSync(filePathAbsolute, content) { + host.vfsUpsertFile(filePathAbsolute, content); + }, }; + + return host; } diff --git a/packages/core/src/running/collectFilesAndOptions.ts b/packages/core/src/running/collectFilesAndOptions.ts index a9376954d..819dee706 100644 --- a/packages/core/src/running/collectFilesAndOptions.ts +++ b/packages/core/src/running/collectFilesAndOptions.ts @@ -48,14 +48,15 @@ export async function collectFilesAndOptions( ): Promise { // 1. Collect all file paths to lint and the 'use' rule configuration groups const { allFilePaths, useDefinitions } = await computeUseDefinitions( - configDefinition, host, + configDefinition, ); // 2. Retrieve any past cached results from those files const cached = ignoreCache ? undefined : await readFromCache( + host, allFilePaths, configDefinition.filePath, cacheLocationOverride, diff --git a/packages/core/src/running/collectLanguageFilesByFilePath.ts b/packages/core/src/running/collectLanguageFilesByFilePath.ts index e75012554..b0f272098 100644 --- a/packages/core/src/running/collectLanguageFilesByFilePath.ts +++ b/packages/core/src/running/collectLanguageFilesByFilePath.ts @@ -24,7 +24,8 @@ export function collectLanguageFilesByFilePath( filePath, filePathAbsolute: makeAbsolute(filePath), sourceText: nullThrows( - host.readFile(filePath), + // TODO: switch to read this async + host.readFileSync(filePath), `Expected ${filePath} to exist`, ), }), diff --git a/packages/core/src/running/computeUseDefinitions.ts b/packages/core/src/running/computeUseDefinitions.ts index ef1031342..655936265 100644 --- a/packages/core/src/running/computeUseDefinitions.ts +++ b/packages/core/src/running/computeUseDefinitions.ts @@ -26,8 +26,8 @@ export interface ConfigUseDefinitionWithFiles extends ConfigUseDefinition { } export async function computeUseDefinitions( - configDefinition: ProcessedConfigDefinition, host: LinterHost, + configDefinition: ProcessedConfigDefinition, ): Promise { log("Collecting files from %d use pattern(s)", configDefinition.use.length); diff --git a/packages/core/src/running/createGitignoreFilter.ts b/packages/core/src/running/createGitignoreFilter.ts index d0670b6d3..b4d0aeccb 100644 --- a/packages/core/src/running/createGitignoreFilter.ts +++ b/packages/core/src/running/createGitignoreFilter.ts @@ -5,7 +5,7 @@ import type { LinterHost } from "../types/host.ts"; export function createGitignoreFilter(cwd: string, host: LinterHost) { const ig = ignore(); - const visited = new Set(); + const visited = new Set(); function loadDir(dir: string): void { if (visited.has(dir) || !dir.startsWith(cwd)) { @@ -19,11 +19,11 @@ export function createGitignoreFilter(cwd: string, host: LinterHost) { visited.add(dir); const gitignorePath = path.posix.join(dir, ".gitignore"); - if (host.stat(gitignorePath) !== "file") { + if (host.fileTypeSync(gitignorePath) !== "file") { return; } - const content = host.readFile(gitignorePath); + const content = host.readFileSync(gitignorePath); if (content === undefined) { return; } diff --git a/packages/core/src/running/readFileSafe.ts b/packages/core/src/running/readFileSafe.ts deleted file mode 100644 index 12f9508fa..000000000 --- a/packages/core/src/running/readFileSafe.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as fs from "node:fs/promises"; - -export async function readFileSafe(filePath: string) { - try { - return await fs.readFile(filePath, "utf-8"); - } catch { - return undefined; - } -} diff --git a/packages/core/src/running/runConfig.ts b/packages/core/src/running/runConfig.ts index 33df1390e..3dcff65e2 100644 --- a/packages/core/src/running/runConfig.ts +++ b/packages/core/src/running/runConfig.ts @@ -77,6 +77,7 @@ export async function runConfig( const lintResults = { allFilePaths, cached, filesResults }; await writeToCache( + host, configDefinition.filePath, lintResults, cacheLocationOverride, diff --git a/packages/core/src/running/runConfigFixing.ts b/packages/core/src/running/runConfigFixing.ts index b19c215a6..2c6a0c011 100644 --- a/packages/core/src/running/runConfigFixing.ts +++ b/packages/core/src/running/runConfigFixing.ts @@ -39,7 +39,7 @@ export async function runConfigFixing( ); // TODO: Investigate reusing file contents from previous iterations. - // Why read file many time when few do trick? + // Why read file many times when only a few will do the trick? // Or, at least it should all be virtual... // https://github.com/flint-fyi/flint/issues/73 const lintResults = await runConfig(configDefinition, host, { @@ -51,6 +51,7 @@ export async function runConfigFixing( log("Applying fixes from file results."); const fixedFilePaths = await applyChangesToFiles( + host, lintResults.filesResults, requestedSuggestions, ); diff --git a/packages/core/src/types/host.ts b/packages/core/src/types/host.ts index ed21e460b..cbbc2d81a 100644 --- a/packages/core/src/types/host.ts +++ b/packages/core/src/types/host.ts @@ -1,20 +1,27 @@ export interface LinterHost { + fileTypeSync(pathAbsolute: string): "directory" | "file" | undefined; getCurrentDirectory(): string; + getFileTouchTime(filePath: string): Promise; + getFileTouchTimeSync(filePath: string): number; isCaseSensitiveFS(): boolean; - readDirectory(directoryPathAbsolute: string): LinterHostDirectoryEntry[]; - readFile(filePathAbsolute: string): string | undefined; - stat(pathAbsolute: string): "directory" | "file" | undefined; - watchDirectory( + readDirectory( + directoryPathAbsolute: string, + ): Promise; + readDirectorySync(directoryPathAbsolute: string): LinterHostDirectoryEntry[]; + readFile(filePathAbsolute: string): Promise; + readFileSync(filePathAbsolute: string): string | undefined; + watchDirectorySync( directoryPathAbsolute: string, - recursive: boolean, callback: LinterHostDirectoryWatcher, - pollingInterval?: number, + options: WatchDirectoryOptions, ): Disposable; - watchFile( + watchFileSync( filePathAbsolute: string, callback: LinterHostFileWatcher, - pollingInterval?: number, + options?: WatchOptions, ): Disposable; + writeFile(filePathAbsolute: string, content: string): Promise; + writeFileSync(filePathAbsolute: string, content: string): void; } export interface LinterHostDirectoryEntry { @@ -32,3 +39,11 @@ export interface VFSLinterHost extends LinterHost { vfsListFiles(): ReadonlyMap; vfsUpsertFile(filePathAbsolute: string, content: string): void; } + +export interface WatchDirectoryOptions extends WatchOptions { + recursive: boolean; +} + +export interface WatchOptions { + pollingInterval?: number; +} diff --git a/packages/typescript-language/src/createTypeScriptServerHost.ts b/packages/typescript-language/src/createTypeScriptServerHost.ts index 500b8e7ac..853e274a6 100644 --- a/packages/typescript-language/src/createTypeScriptServerHost.ts +++ b/packages/typescript-language/src/createTypeScriptServerHost.ts @@ -37,8 +37,9 @@ export function createTypeScriptServerHost( }, directoryExists(directoryPath) { return ( - host.stat(path.resolve(host.getCurrentDirectory(), directoryPath)) === - "directory" + host.fileTypeSync( + path.resolve(host.getCurrentDirectory(), directoryPath), + ) === "directory" ); }, exit() { @@ -46,7 +47,9 @@ export function createTypeScriptServerHost( }, fileExists(filePath) { return ( - host.stat(path.resolve(host.getCurrentDirectory(), filePath)) === "file" + host.fileTypeSync( + path.resolve(host.getCurrentDirectory(), filePath), + ) === "file" ); }, readDirectory(directoryPath, extensions, exclude, include, depth) { @@ -69,7 +72,9 @@ export function createTypeScriptServerHost( try { fs.readdirSync = originalReadDirSync; return host - .readDirectory(path.resolve(host.getCurrentDirectory(), readPath)) + .readDirectorySync( + path.resolve(host.getCurrentDirectory(), readPath), + ) .map( (dirent) => new DirentCtor( @@ -99,17 +104,19 @@ export function createTypeScriptServerHost( } }, readFile(filePath) { - return host.readFile(path.resolve(host.getCurrentDirectory(), filePath)); + return host.readFileSync( + path.resolve(host.getCurrentDirectory(), filePath), + ); }, setImmediate: timers.setImmediate, setTimeout: timers.setTimeout, watchDirectory(directoryPath, callback, recursive = false) { - const watcher = host.watchDirectory( + const watcher = host.watchDirectorySync( path.resolve(host.getCurrentDirectory(), directoryPath), - recursive, (filePathAbsolute) => { callback(filePathAbsolute); }, + { recursive }, ); return { close() { @@ -118,7 +125,7 @@ export function createTypeScriptServerHost( }; }, watchFile(filePath, callback) { - const watcher = host.watchFile( + const watcher = host.watchFileSync( path.resolve(host.getCurrentDirectory(), filePath), (event) => { let eventKind: ts.FileWatcherEventKind;