-
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
introduce file system utilities (#211)
- Loading branch information
1 parent
4d5a9e0
commit 93da61f
Showing
19 changed files
with
1,758 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
--- | ||
'renoun': major | ||
--- | ||
|
||
Introduces more performant, type-safe file system from utilities exported from `renoun/file-system` to replace the `renoun/collections` API, which will be removed in a future major release. | ||
|
||
- **New Classes:** | ||
- `NodeFileSystem`, `VirtualFileSystem`, `Directory`, `File`, `JavaScriptFile`, and `JavaScriptFileExport`. | ||
- **Improvements:** | ||
- Optimized performance, stronger TypeScript support, and in-memory support with `VirtualFileSystem`. | ||
|
||
### Migration Example | ||
|
||
**Before:** | ||
|
||
```typescript | ||
const collection = new Collection({ | ||
filePattern: 'src/**/*.{ts,tsx}', | ||
baseDirectory: 'src', | ||
}) | ||
const sources = await collection.getSources() | ||
``` | ||
|
||
**After:** | ||
|
||
```typescript | ||
const directory = new Directory({ path: 'src' }) | ||
const entries = await directory.getEntries() | ||
``` | ||
|
||
The new file system utilities offer clearer APIs, better performance, and improved developer experience. This is still experimental and API parity with the old collections API is still in progress. Please report any issues you encounter. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import { minimatch } from 'minimatch' | ||
|
||
import { relative } from './path.js' | ||
import type { DirectoryEntry } from './types.js' | ||
|
||
interface FileSystemOptions { | ||
/** Root path to use when reading files. */ | ||
rootPath?: string | ||
|
||
/** Base path to prepend to all paths. */ | ||
basePath?: string | ||
|
||
/** | ||
* Path to the tsconfig.json file to use when analyzing types and determining if a file is excluded. */ | ||
tsConfigPath?: string | ||
} | ||
|
||
export abstract class FileSystem { | ||
#rootPath: string | ||
#basePath?: string | ||
#tsConfigPath: string | ||
#tsConfig?: any | ||
|
||
constructor(options: FileSystemOptions = {}) { | ||
this.#rootPath = options.rootPath || '.' | ||
this.#basePath = options.basePath | ||
this.#tsConfigPath = options.tsConfigPath || 'tsconfig.json' | ||
} | ||
|
||
abstract readFileSync(path: string): string | ||
abstract readFile(path: string): Promise<string> | ||
abstract readDirectory(path?: string): Promise<DirectoryEntry[]> | ||
abstract isFilePathGitIgnored(filePath: string): boolean | ||
|
||
getRootPath() { | ||
return this.#rootPath | ||
} | ||
|
||
getBasePath() { | ||
return this.#basePath | ||
} | ||
|
||
getUrlPathRelativeTo(path: string, includeBasePath = true) { | ||
const parsedPath = relative(this.getRootPath(), path) | ||
// remove leading dot | ||
.replace(/^\.\//, '') | ||
// remove trailing slash | ||
.replace(/\/$/, '') | ||
const basePath = this.getBasePath() | ||
|
||
if (includeBasePath && basePath) { | ||
return `/${basePath}/${parsedPath}` | ||
} | ||
|
||
return `/${parsedPath}` | ||
} | ||
|
||
#getTsConfig() { | ||
try { | ||
const tsConfigContents = this.readFileSync(this.#tsConfigPath) | ||
try { | ||
const parsedTsConfig = JSON.parse(tsConfigContents) | ||
return parsedTsConfig | ||
} catch (error) { | ||
throw new Error('Failed to parse tsconfig.json', { cause: error }) | ||
} | ||
} catch (error) { | ||
return null | ||
} | ||
} | ||
|
||
isFilePathExcludedFromTsConfig(filePath: string) { | ||
if (this.#tsConfig === undefined) { | ||
this.#tsConfig = this.#getTsConfig() | ||
} | ||
|
||
if (this.#tsConfig === null) { | ||
return false | ||
} | ||
|
||
if (this.#tsConfig.exclude?.length) { | ||
for (const exclude of this.#tsConfig.exclude) { | ||
if (minimatch(filePath, exclude)) { | ||
return true | ||
} | ||
} | ||
} | ||
|
||
return false | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { readFileSync } from 'node:fs' | ||
import { readdir, readFile } from 'node:fs/promises' | ||
import { join, resolve, relative } from 'node:path' | ||
import ignore from 'ignore' | ||
|
||
import { getRootDirectory } from '../utils/get-root-directory.js' | ||
import { FileSystem } from './FileSystem.js' | ||
import type { DirectoryEntry } from './types.js' | ||
|
||
let ignoreManager: ReturnType<typeof ignore> | ||
|
||
export class NodeFileSystem extends FileSystem { | ||
async readDirectory(path: string = '.'): Promise<DirectoryEntry[]> { | ||
const entries = await readdir(path, { withFileTypes: true }) | ||
|
||
return entries.map((entry) => { | ||
let entryPath = join(path, entry.name) | ||
|
||
if (!entryPath.startsWith('.')) { | ||
entryPath = `./${entryPath}` | ||
} | ||
|
||
return { | ||
name: entry.name, | ||
path: entryPath, | ||
absolutePath: resolve(entryPath), | ||
isDirectory: entry.isDirectory(), | ||
isFile: entry.isFile(), | ||
} satisfies DirectoryEntry | ||
}) | ||
} | ||
|
||
readFileSync(path: string): string { | ||
return readFileSync(path, 'utf-8') | ||
} | ||
|
||
async readFile(path: string): Promise<string> { | ||
return readFile(path, 'utf-8') | ||
} | ||
|
||
isFilePathGitIgnored(filePath: string): boolean { | ||
const relativePath = relative(getRootDirectory(), filePath) | ||
|
||
if (!ignoreManager) { | ||
const gitignorePatterns = getGitIgnorePatterns() | ||
ignoreManager = ignore().add(gitignorePatterns) | ||
} | ||
|
||
return ignoreManager.ignores(relativePath) | ||
} | ||
} | ||
|
||
function getGitIgnorePatterns(): string[] { | ||
const gitignorePath = join(getRootDirectory(), '.gitignore') | ||
|
||
try { | ||
const gitignoreContent = readFileSync(gitignorePath, 'utf-8') | ||
|
||
return ( | ||
gitignoreContent | ||
.split('\n') | ||
.map((line) => line.trim()) | ||
// Filter out comments and empty lines | ||
.filter((line) => line && !line.startsWith('#')) | ||
) | ||
} catch (error) { | ||
// If .gitignore is not found, return an empty array | ||
return [] | ||
} | ||
} |
Oops, something went wrong.