Skip to content

feat: add this.fs interface across bundlers#596

Open
guyutongxue wants to merge 1 commit into
unjs:mainfrom
guyutongxue:feat-fs
Open

feat: add this.fs interface across bundlers#596
guyutongxue wants to merge 1 commit into
unjs:mainfrom
guyutongxue:feat-fs

Conversation

@guyutongxue
Copy link
Copy Markdown

@guyutongxue guyutongxue commented May 9, 2026

resolves #593 .

Summary by CodeRabbit

  • New Features

    • Added filesystem API (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

    • Updated supported context section to reflect this.fs availability across bundlers.

Review Change Stack

Copilot AI review requested due to automatic review settings May 9, 2026 07:29
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 9, 2026

📝 Walkthrough

Walkthrough

This pull request adds a standardized this.fs context property to the unplugin build context across webpack, rspack, bun, esbuild, and farm bundlers. The implementation provides a shared filesystem abstraction with readFile, stat, and lstat methods, supporting both native bundler filesystem APIs and Node.js fallback.

Changes

File System Context Abstraction

Layer / File(s) Summary
Type Contracts
src/types.ts
Defines UnpluginContextFs interface with async readFile, stat, lstat methods and extends UnpluginBuildContext to include fs property.
Filesystem Utility
src/utils/fs.ts
Implements createBuildContextFs(inputFs?) factory that promisifies webpack InputFileSystem via pify or dynamically imports Node.js fs/promises.
Dependency Management
pnpm-workspace.yaml, package.json
Adds pify ^6.1.0 to prod catalog and dependencies for promisifying webpack filesystem APIs.
Webpack & Rspack Integration
src/webpack/context.ts, src/rspack/context.ts
Extract InputFileSystem from loader context or compiler and initialize fs via createBuildContextFs(inputFs).
Esbuild, Bun & Farm Integration
src/esbuild/utils.ts, src/bun/utils.ts, src/farm/context.ts
Add fs field to returned build context via createBuildContextFs() with Node.js fallback.
Test Coverage
test/unit-tests/*/, test/unit-tests/resolve-id/resolve-id.test.ts
Verify fs object presence and that readFile, stat, lstat are functions across all bundler contexts.
Documentation
docs/guide/index.md
Documents this.fs support in Supported Context table and notes fallback behavior for bundlers without native filesystem APIs.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A rabbit's ode to abstract file flows,
From webpack to bun, the fs now grows,
Read files and stats, wherever you roam,
No Node.js needed—make any place home! 🌳✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add this.fs interface across bundlers' accurately reflects the main change: adding a standardized filesystem interface to the plugin context for multiple bundlers.
Linked Issues check ✅ Passed The PR successfully implements the standardized this.fs interface across bundlers (Webpack, Rspack, Farm, Esbuild, Bun) with fallback to Node.js fs when needed, directly addressing issue #593's objectives.
Out of Scope Changes check ✅ Passed All changes are focused on implementing the this.fs interface feature; documentation updates, dependency additions (pify), and test expansions are all directly related to the stated objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/utils/fs.ts (2)

6-22: ⚡ Quick win

Verify the dynamic import pattern efficiency.

The fsPromiseModule promise 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 win

Type assertions suppress important type checking.

The type assertions (as UnpluginContextFs['readFile']) bypass TypeScript's type checking and could hide incompatibilities between pify's output and the expected UnpluginContextFs interface.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1623b46 and 953ab32.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (16)
  • docs/guide/index.md
  • package.json
  • pnpm-workspace.yaml
  • src/bun/utils.ts
  • src/esbuild/utils.ts
  • src/farm/context.ts
  • src/rspack/context.ts
  • src/types.ts
  • src/utils/fs.ts
  • src/webpack/context.ts
  • test/unit-tests/bun/utils.test.ts
  • test/unit-tests/esbuild/utils.test.ts
  • test/unit-tests/farm/context.test.ts
  • test/unit-tests/resolve-id/resolve-id.test.ts
  • test/unit-tests/rspack/context.test.ts
  • test/unit-tests/webpack/context.test.ts

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) in src/types.ts.
  • Implements createBuildContextFs with 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.fs support 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.

Comment thread src/types.ts
Comment on lines +69 to +73
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>
}
Comment thread src/utils/fs.ts
Comment on lines +1 to +3
import type { InputFileSystem } from 'webpack'
import type { UnpluginContextFs } from '../types'
import pify from 'pify'
Comment thread src/utils/fs.ts
Comment on lines +8 to +18
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
Comment thread docs/guide/index.md

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request]: Standardized Abstract File System Interface (this.fs)

3 participants