Skip to content

Commit

Permalink
introduce file system utilities (#211)
Browse files Browse the repository at this point in the history
  • Loading branch information
souporserious authored Nov 15, 2024
1 parent 4d5a9e0 commit 93da61f
Show file tree
Hide file tree
Showing 19 changed files with 1,758 additions and 40 deletions.
31 changes: 31 additions & 0 deletions .changeset/stale-maps-tell.md
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.
8 changes: 8 additions & 0 deletions packages/renoun/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
"types": "./dist/components/*.d.ts",
"import": "./dist/components/*.js"
},
"./file-system": {
"types": "./dist/file-system/index.d.ts",
"import": "./dist/file-system/index.js"
},
"./mdx": {
"types": "./dist/mdx/index.d.ts",
"import": "./dist/mdx/index.js"
Expand Down Expand Up @@ -75,6 +79,9 @@
"components/*": [
"./dist/components/*.d.ts"
],
"file-system": [
"./dist/file-system/index.d.ts"
],
"mdx": [
"./dist/mdx/index.d.ts"
],
Expand Down Expand Up @@ -111,6 +118,7 @@
"@renoun/mdx": "workspace:*",
"fast-glob": "^3.3.2",
"glob-parent": "^6.0.2",
"ignore": "^6.0.2",
"resolve": "^1.22.8",
"restyle": "catalog:",
"minimatch": "^10.0.1",
Expand Down
4 changes: 2 additions & 2 deletions packages/renoun/src/collections/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { getGitMetadata } from '../utils/get-git-metadata.js'
import { getJsDocMetadata } from '../utils/get-js-doc-metadata.js'
import { resolveType } from '../project/client.js'
import { resolveTsConfigPath } from '../utils/resolve-ts-config-path.js'
import type { SymbolFilter } from '../utils/resolve-type.js'
import type { ResolvedType, SymbolFilter } from '../utils/resolve-type.js'
import { getSourceFilesPathMap } from '../utils/get-source-files-path-map.js'
import { getSourceFilesOrderMap } from '../utils/get-source-files-order-map.js'

Expand Down Expand Up @@ -89,7 +89,7 @@ export interface ExportSource<Value> extends BaseSource {
getName(): string

/** The resolved type of the exported source based on the TypeScript type if it exists. */
getType(filter?: SymbolFilter): Promise<ReturnType<typeof resolveType>>
getType(filter?: SymbolFilter): Promise<ResolvedType | undefined>

/** The name of the exported source formatted as a title. */
getTitle(): string
Expand Down
60 changes: 31 additions & 29 deletions packages/renoun/src/components/APIReference.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,52 +97,54 @@ async function APIReferenceAsync({
workingDirectory
)

return exportedTypes.map((type) => (
<div
key={type.name}
css={{
display: 'flex',
flexDirection: 'column',
padding: '1.6rem 0',
borderBottom: '1px solid var(--color-separator-secondary)',
}}
>
return exportedTypes
.filter((type): type is ResolvedType => Boolean(type))
.map((type) => (
<div
key={type.name}
css={{
display: 'flex',
flexDirection: 'column',
gap: '0.8rem',
padding: '1.6rem 0',
borderBottom: '1px solid var(--color-separator-secondary)',
}}
>
<div
css={{
display: 'flex',
alignItems: 'center',
gap: '1rem',
flexDirection: 'column',
gap: '0.8rem',
}}
>
<h3
id={type.name ? createSlug(type.name) : undefined}
css={{ flexShrink: 0, margin: '0 !important' }}
<div
css={{
display: 'flex',
alignItems: 'center',
gap: '1rem',
}}
>
{type.name}
</h3>
<h3
id={type.name ? createSlug(type.name) : undefined}
css={{ flexShrink: 0, margin: '0 !important' }}
>
{type.name}
</h3>

<CodeInline value={type.text} language="typescript" />
<CodeInline value={type.text} language="typescript" />

{/* {type.path && <ViewSource href={type.path} />} */}
</div>
{/* {type.path && <ViewSource href={type.path} />} */}
</div>

{type.description ? (
<MDXContent value={type.description} components={mdxComponents} />
) : null}
</div>
{type.description ? (
<MDXContent value={type.description} components={mdxComponents} />
) : null}
</div>

<div css={{ display: 'flex' }}>
<TypeChildren type={type} css={{ marginTop: '2rem' }} />
<div css={{ display: 'flex' }}>
<TypeChildren type={type} css={{ marginTop: '2rem' }} />
</div>
</div>
</div>
))
))
}

const type = await source.getType(filter)
Expand Down
91 changes: 91 additions & 0 deletions packages/renoun/src/file-system/FileSystem.ts
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
}
}
70 changes: 70 additions & 0 deletions packages/renoun/src/file-system/NodeFileSystem.ts
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 []
}
}
Loading

0 comments on commit 93da61f

Please sign in to comment.