mrkeyoor.com_
Wed 23 Sept 00:36 UTC
npmUtilsupdated 22 Sept 2026

vinyl-fs review

vinyl-fs is the Node filesystem adapter used by Gulp-style pipelines. src() expands ordered globs and emits object-mode Vinyl files containing path, base, stat, contents, and optional source-map data. dest() writes those objects and passes their updated form downstream, while symlink() creates links. Version 4.0.2 fixes eager globbing so matches are not resolved before the read stream opens. The abstraction pays off when several transforms already speak Vinyl; it is a lot of machinery for ordinary file copies. Our install loaded through require() and ESM interop, but shipped no declarations, and esbuild could not make a browser bundle from the Node filesystem code.

Verdict

vinyl-fs 4.0.2 installed 48 packages in 5.1 seconds and its browser build failed in our sandbox, which is acceptable only when a Node build pipeline already depends on Vinyl. New standalone file scripts are usually clearer with fs/promises and a glob package.

We installed it

Lab card: what happened when we installed vinyl-fsScreenshot of vinyl-fs documentation
Install✓ · 5.1s48 packages on disk · 3 MB
ImportESM import works · require() works · CommonJS package
Browsern/acould not be bundled for the browser (Node-only code, most likely)
Typesno TypeScript types found
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does vinyl-fs install cleanly?

Yes. In a fresh container with an empty cache, npm install vinyl-fs finished in 5 seconds, leaving 48 packages and 3 MB on disk. npm audit reported no known vulnerabilities.

Can vinyl-fs run in a browser?

Not directly: esbuild could not bundle it for the browser in our run, which normally means it depends on Node built-ins. Use it on the server, or find a browser-targeted alternative.

Does vinyl-fs work with both ESM and CommonJS?

Yes. Both import 'vinyl-fs' and require('vinyl-fs') worked in Node 22 in our run. The package is published as CommonJS.

Does vinyl-fs include TypeScript types?

No type declarations were found in our install, so TypeScript users need their own declarations.

vinyl-fs or fast-glob: which should you use?

fast-glob: Use it to collect paths quickly while your code performs direct reads and writes. vinyl-fs 4.0.2 installed 48 packages in 5.1 seconds and its browser build failed in our sandbox, which is acceptable only when a Node build pipeline already depends on Vinyl.

When should you not use vinyl-fs?

A script only reads, copies, or writes a few paths; fs/promises plus a glob library avoids 14 direct dependencies and the Vinyl object model

API stability4/5The public surface remains only src(), dest(), and symlink(), and their Vinyl stream roles have stayed recognizable across releases. Version 4 still changed behavior that can alter outputs: UTF-8 decoding and encoding became defaults, stream internals moved to streamx, Node below 10.13 was dropped, since compares ctime and mtime, and link targeting changed. Major upgrades need byte, metadata, and symlink fixtures even though the method names barely move.
Docs4/5The README documents more than 20 source, destination, and symlink options with types and defaults. It explains ordered negations, BOM removal, encoding, buffering, null contents, metadata mutation, source maps, overwrite behavior, uid and gid, Windows metadata omissions, junction defaults, and dangling-link guesses. The weak spots are practical: the main example uses old CommonJS piping, no promise pipeline is shown, and TypeScript users must find third-party declarations elsewhere.
Maintenance4/5npm published 4.0.2 on June 1, 2025, the date of the repository's latest push. That patch prevents globbing before the read stream is opened. Version 4.0.1 on the prior day removed a Node fs.Stats deprecation warning and fixed streaming transcoding under default options. GitHub reports 8 open issues and pull requests and the project is unarchived. Work is periodic, but the last releases addressed observable Node and stream behavior.
Ecosystem4/5npm counted 3,546,418 downloads for the week ending August 24, 2026, and GitHub shows 973 stars. Vinyl remains the shared path, contents, stat, and source-map object used across Gulp plugins, which makes vinyl-fs a natural boundary in that ecosystem. Our checks confirmed CommonJS and ESM interop on Node. Outside those pipelines, 48 installed packages, missing bundled types, and a failed browser build make the abstraction a poor general default.

Use it if

  • A Gulp plugin or build pipeline already exchanges Vinyl file objects
  • Ordered positive and negative globs must feed backpressure-aware transforms
  • Source maps, stat metadata, symlinks, and file contents need one object contract
  • Each incoming file may choose its own destination directory before continuing downstream
Skip it if

Setup reality

Our fresh vinyl-fs 4.0.2 installation took 5.1 seconds on Node 22. npm left 48 packages occupying 3 MB, and audit reported 0 known vulnerabilities. vinyl-fs itself is 172 KB unpacked, declares 14 direct dependencies and 0 peers, and supports Node >=10.13.0. It is CommonJS with no exports map; require() and ESM import both worked through interop. No TypeScript declarations ship. esbuild failed to create a browser bundle, matching a package built entirely around Node filesystem streams.

There are no credentials, native builds, or config files. src() buffers contents by default, strips UTF-8 BOMs, and uses UTF-8 decoding. Set buffer: false for paused content streams, removeBOM: false when the marker is significant, and encoding: false for byte-preserving binary work. A downstream transform must check file.isBuffer(), file.isStream(), or file.isNull() before touching contents. Glob order is semantic: a negative expression only excludes matches from positive globs that appear earlier in the array.

Use node:stream/promises pipeline to await completion and propagate errors from every stage. dest() overwrites by default and changes each Vinyl object's cwd, base, path, stat, and stream position before emitting it again. Save original paths first if a later step needs them. The since option compares against the greater of ctime and mtime in version 4. Source-map loading is opt-in on src(), and dest() needs its own sourcemaps option to emit inline or separate maps.

Symlink behavior needs OS fixtures. src() follows links unless resolveSymlinks is false. On Windows, directory links default to junctions; disabling useJunctions can require developer mode or extra privileges. A dangling directory link passed through dest() may be guessed as a file link because its target cannot be inspected. Version 4.0.2 delays globbing until the read stream opens, fixing work that happened before consumption. For a standalone copy task, fs/promises plus fast-glob is easier to reason about and type.

Patterns

Copy an ordered glob set copy-globs

const vfs = require('vinyl-fs')

vfs.src(['src/**/*', '!src/**/*.test.js'])
  .pipe(vfs.dest('dist'))

The negative glob must follow its positive source. Putting it first excludes nothing from the later match set.

Await copy completion and errors await-pipeline

const { pipeline } = require('node:stream/promises')
const vfs = require('vinyl-fs')

await pipeline(
  vfs.src('assets/**/*'),
  vfs.dest('public/assets'),
)

pipeline rejects for errors from either stream and closes connected stages. A lone finish listener does not cover source failures.

Edit buffered Vinyl contents transform-buffers

const { Transform } = require('node:stream')
const vfs = require('vinyl-fs')

const replaceVersion = new Transform({
  objectMode: true,
  transform(file, _encoding, callback) {
    if (file.isBuffer()) {
      file.contents = Buffer.from(
        file.contents.toString().replaceAll('__VERSION__', '4.2.0'),
      )
    }
    callback(null, file)
  },
})

vfs.src('templates/**/*').pipe(replaceVersion).pipe(vfs.dest('dist'))

src() buffers by default, but Vinyl also represents streams, directories, and files with null contents. Check the content form first.

Keep large contents as streams stream-large-files

const vfs = require('vinyl-fs')

vfs.src('videos/**/*.mp4', { buffer: false, encoding: false })
  .pipe(vfs.dest('archive', { encoding: false }))

buffer false makes file.contents a paused stream. Every transform in the pipeline must support streaming Vinyl files.

Match paths without reading bytes list-paths

const files = vfs.src('src/**/*', { read: false })
files.on('data', (file) => console.log(file.relative))
files.on('error', console.error)

read false sets contents to null. dest() cannot write those entries unless another transform supplies contents.

Select files newer than a build time copy-changed-files

const lastBuild = new Date('2026-08-01T00:00:00Z')
vfs.src('src/**/*', { since: lastBuild })
  .pipe(vfs.dest('dist'))

Version 4 compares since with the greater of ctime and mtime. Store the build marker outside the output tree to avoid feedback.

Disable text conversion and BOM removal preserve-binary

vfs.src('fixtures/**/*', {
  encoding: false,
  removeBOM: false,
}).pipe(vfs.dest('copied', { encoding: false }))

Version 4 defaults both sides to UTF-8, and src removes UTF-8 BOMs. All 3 settings matter for byte-for-byte copying.

Load and emit external source maps write-source-maps

vfs.src('src/**/*.js', { sourcemaps: true })
  .pipe(vfs.dest('dist', { sourcemaps: '.' }))

The source option loads inline and linked maps. The destination string writes .map files relative to that location.

Choose a directory per Vinyl file route-files

vfs.src('src/**/*').pipe(vfs.dest((file) => {
  return file.extname === '.css' ? 'dist/styles' : 'dist/assets'
}))

dest() rewrites cwd, base, path, and stat before downstream emission. Preserve the incoming path if later logic needs it.

Keep an existing destination file avoid-overwrite

vfs.src('defaults/**/*')
  .pipe(vfs.dest('project', { overwrite: false }))

overwrite defaults to true. false protects user files in a scaffold, but it also leaves an outdated destination silently.

Copy links instead of targets preserve-symlinks

vfs.src('tree/**/*', { resolveSymlinks: false })
  .pipe(vfs.dest('copy'))

Windows directory links default to junctions. A dangling directory link sent to dest() can be misclassified as a file link.

Create relative directory links create-relative-links

vfs.src('packages/*', { read: false })
  .pipe(vfs.symlink('workspace-links', {
    relativeSymlinks: true,
    useJunctions: false,
  }))

Turning off junctions changes Windows directory-link behavior and may require developer mode or elevated OS permission.

Alternatives

PackageRegistryPick it when
fast-globnpmUse it to collect paths quickly while your code performs direct reads and writes
globbynpmUse it for a promise-based ESM glob interface with ignore-file support
fs-extranpmUse it for direct copy, move, directory, and JSON helpers without Vinyl streams

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.