feat: add this.fs interface across bundlers#596
Conversation
📝 WalkthroughWalkthroughThis pull request adds a standardized ChangesFile System Context Abstraction
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/utils/fs.ts (2)
6-22: ⚡ Quick winVerify the dynamic import pattern efficiency.
The
fsPromiseModulepromise is created once and awaited on every method call. While this works correctly (promises can be awaited multiple times), it adds overhead. Consider storing the resolved module instead.♻️ Suggested optimization
function createNodeFs(): UnpluginContextFs { - const fsPromiseModule = import('node:fs/promises') + let fsPromise: Promise<typeof import('node:fs/promises')> | undefined return { readFile: async (path, options) => { - const fs = await fsPromiseModule + fsPromise ??= import('node:fs/promises') + const fs = await fsPromise return fs.readFile(path, options) }, stat: async (path, options) => { - const fs = await fsPromiseModule + fsPromise ??= import('node:fs/promises') + const fs = await fsPromise return fs.stat(path, options) }, lstat: async (path, options) => { - const fs = await fsPromiseModule + fsPromise ??= import('node:fs/promises') + const fs = await fsPromise return fs.lstat(path, options) }, } }Or better, cache the resolved module:
function createNodeFs(): UnpluginContextFs { - const fsPromiseModule = import('node:fs/promises') + let fsModule: typeof import('node:fs/promises') | undefined + async function getFs() { + if (!fsModule) { + fsModule = await import('node:fs/promises') + } + return fsModule + } return { readFile: async (path, options) => { - const fs = await fsPromiseModule + const fs = await getFs() return fs.readFile(path, options) }, stat: async (path, options) => { - const fs = await fsPromiseModule + const fs = await getFs() return fs.stat(path, options) }, lstat: async (path, options) => { - const fs = await fsPromiseModule + const fs = await getFs() return fs.lstat(path, options) }, } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/utils/fs.ts` around lines 6 - 22, The dynamic import promise fsPromiseModule in createNodeFs is awaited on every call to readFile/stat/lstat which adds unnecessary overhead; change the implementation to cache the resolved module (e.g., keep a local variable like cachedFs or resolvedFs) and on the first call await fsPromiseModule and store the module, then use the cached module for subsequent calls in readFile, stat, and lstat to avoid repeated awaits.
28-30: ⚡ Quick winType assertions suppress important type checking.
The type assertions (
as UnpluginContextFs['readFile']) bypass TypeScript's type checking and could hide incompatibilities betweenpify's output and the expectedUnpluginContextFsinterface.Consider adding runtime validation or using more specific typing from
pify.💡 Alternative approach
Instead of type assertions, explicitly type the pified object:
export function createBuildContextFs(inputFs?: InputFileSystem | null): UnpluginContextFs { - const fs = inputFs ? pify(inputFs) : createNodeFs() + const fs = inputFs ? pify(inputFs) as { + readFile: (path: PathLike, options?: any) => Promise<string | Uint8Array> + stat: (path: PathLike, options?: any) => Promise<Stats> + lstat: (path: PathLike, options?: any) => Promise<Stats> + } : createNodeFs() return { - readFile: fs.readFile as UnpluginContextFs['readFile'], - stat: fs.stat as UnpluginContextFs['stat'], - lstat: fs.lstat as UnpluginContextFs['lstat'], + readFile: fs.readFile, + stat: fs.stat, + lstat: fs.lstat, } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/utils/fs.ts` around lines 28 - 30, The current lines use type assertions (readFile: fs.readFile as UnpluginContextFs['readFile'], stat, lstat) which silence mismatches between pify(fs) output and the UnpluginContextFs shape; replace the assertions by explicitly typing the pify call (e.g. pify<UnpluginContextFs>(fs)) or by creating a typed wrapper that maps and validates each function (readFile, stat, lstat) to the expected signatures, and add minimal runtime checks (typeof fn === 'function' and argument/return shape checks or try/catch) to surface incompatible implementations rather than suppressing errors with "as". Ensure you update the assignment site where pify(fs) is produced so the resulting object already conforms to UnpluginContextFs instead of asserting per-property.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/utils/fs.ts`:
- Around line 6-22: The dynamic import promise fsPromiseModule in createNodeFs
is awaited on every call to readFile/stat/lstat which adds unnecessary overhead;
change the implementation to cache the resolved module (e.g., keep a local
variable like cachedFs or resolvedFs) and on the first call await
fsPromiseModule and store the module, then use the cached module for subsequent
calls in readFile, stat, and lstat to avoid repeated awaits.
- Around line 28-30: The current lines use type assertions (readFile:
fs.readFile as UnpluginContextFs['readFile'], stat, lstat) which silence
mismatches between pify(fs) output and the UnpluginContextFs shape; replace the
assertions by explicitly typing the pify call (e.g. pify<UnpluginContextFs>(fs))
or by creating a typed wrapper that maps and validates each function (readFile,
stat, lstat) to the expected signatures, and add minimal runtime checks (typeof
fn === 'function' and argument/return shape checks or try/catch) to surface
incompatible implementations rather than suppressing errors with "as". Ensure
you update the assignment site where pify(fs) is produced so the resulting
object already conforms to UnpluginContextFs instead of asserting per-property.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6b657dd7-39c0-455e-939a-11fe181bfc0c
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (16)
docs/guide/index.mdpackage.jsonpnpm-workspace.yamlsrc/bun/utils.tssrc/esbuild/utils.tssrc/farm/context.tssrc/rspack/context.tssrc/types.tssrc/utils/fs.tssrc/webpack/context.tstest/unit-tests/bun/utils.test.tstest/unit-tests/esbuild/utils.test.tstest/unit-tests/farm/context.test.tstest/unit-tests/resolve-id/resolve-id.test.tstest/unit-tests/rspack/context.test.tstest/unit-tests/webpack/context.test.ts
There was a problem hiding this comment.
Pull request overview
Adds a standardized this.fs interface to Unplugin’s unified build context so plugins can perform basic file operations across bundlers (and use bundler-provided virtual/input FS where available), addressing the portability gap described in #593.
Changes:
- Introduces
UnpluginBuildContext.fs(readFile,stat,lstat) insrc/types.ts. - Implements
createBuildContextFswith Webpack/Rspack input FS support (promisified) and a Node fallback, and wires it into webpack/rspack/farm/esbuild/bun contexts. - Updates unit tests and docs to assert/describe
this.fssupport across bundlers.
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/unit-tests/webpack/context.test.ts | Asserts buildContext.fs exists and exposes readFile/stat/lstat. |
| test/unit-tests/rspack/context.test.ts | Asserts buildContext.fs exists and exposes readFile/stat/lstat. |
| test/unit-tests/resolve-id/resolve-id.test.ts | Adds fs to cross-bundler context property checks and updates assertion counting. |
| test/unit-tests/farm/context.test.ts | Asserts Farm context exposes fs methods. |
| test/unit-tests/esbuild/utils.test.ts | Asserts esbuild build context exposes fs methods. |
| test/unit-tests/bun/utils.test.ts | Asserts bun build context exposes fs methods. |
| src/webpack/context.ts | Adds fs to webpack build context using loader/compiler input FS. |
| src/rspack/context.ts | Adds fs to rspack build context using loader/compiler input FS. |
| src/farm/context.ts | Adds fs to Farm build context (Node fallback). |
| src/esbuild/utils.ts | Adds fs to esbuild build context (Node fallback). |
| src/bun/utils.ts | Adds fs to bun build context (Node fallback). |
| src/utils/fs.ts | New utility to create a unified FS wrapper (promisify input FS or Node fallback). |
| src/types.ts | Adds UnpluginContextFs and includes fs in UnpluginBuildContext. |
| docs/guide/index.md | Documents this.fs support and adds a note about fallback behavior. |
| pnpm-workspace.yaml | Adds pify to prod catalog. |
| package.json | Adds pify dependency. |
| pnpm-lock.yaml | Locks pify@6.1.0. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export interface UnpluginContextFs { | ||
| readFile: (path: PathLike, options?: any) => Promise<string | Uint8Array> | ||
| stat: (path: PathLike, options?: any) => Promise<Stats> | ||
| lstat: (path: PathLike, options?: any) => Promise<Stats> | ||
| } |
| import type { InputFileSystem } from 'webpack' | ||
| import type { UnpluginContextFs } from '../types' | ||
| import pify from 'pify' |
| return { | ||
| readFile: async (path, options) => { | ||
| const fs = await fsPromiseModule | ||
| return fs.readFile(path, options) | ||
| }, | ||
| stat: async (path, options) => { | ||
| const fs = await fsPromiseModule | ||
| return fs.stat(path, options) | ||
| }, | ||
| lstat: async (path, options) => { | ||
| const fs = await fsPromiseModule |
|
|
||
| 1. For bundlers other than Rollup, Rolldown, or Vite, `setParseImpl` must be called to manually provide a parser implementation. Parsers such as [Acorn](https://github.com/acornjs/acorn), [Babel](https://babeljs.io/), or [Oxc](https://oxc.rs/) can be used. | ||
| 2. Currently, [`this.emitFile`](https://rollupjs.org/plugin-development/#thisemitfile) only supports the `EmittedAsset` variant. | ||
| 3. For bundlers without native plugin context file-system APIs, `this.fs` falls back to a Node-compatible implementation. |
resolves #593 .
Summary by CodeRabbit
New Features
fs) to the build context across all supported bundlers, providing async methods for reading files and retrieving file statistics. Includes automatic fallback to Node.js implementation for bundlers without native filesystem support.Documentation
this.fsavailability across bundlers.