# vinyl-fs is the file system adapter for Vinyl, and its options are the story

> vinyl-fs is the Vinyl adapter for the file system, and it is small enough to read in full. The interesting parts are the order in which globs are evaluated, the options that decide whether a file is buffered or streamed or not read at all, and the dest stream's habit of reconciling the mode and timestamps of a file it has just written.

**gulpjs/vinyl-fs** — Vinyl adapter for the file system.

- Repository: https://github.com/gulpjs/vinyl-fs
- Stars: 972 · Forks: 154
- Language: JavaScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/gulpjs-vinyl-fs

## A Vinyl object describes a file, wherever it lives

The README starts by explaining the object it adapts, because that is the part newcomers get wrong. A Vinyl is a very simple metadata object that describes a file, and when you think of a file, two attributes come to mind, `path` and `contents`. Those are the main attributes on a Vinyl object. The important idea is the second half of that sentence, a Vinyl file does not necessarily represent something on your computer's file system, because you have files on S3, on FTP, on Dropbox, on Box and on CloudThingly.io, and Vinyl can describe a file from any of them. So Vinyl is the common currency and an adapter is the thing that knows how to get it in and out of a particular medium. A Vinyl adapter simply exposes a `src(globs)` and a `dest(folder)` method, each returning a stream, with src producing Vinyl objects and dest consuming them. The file system is one such adapter, and the extra method that vinyl-fs adds for its medium is `symlink`. The whole shape of the library is visible in a short pipeline, where a transform sits between the source and the destination and never touches the disk itself:

```javascript
var map = require("map-stream");
var vfs = require("vinyl-fs");

var log = function (file, cb) {
  console.log(file.path);
  cb(null, file);
};

vfs
  .src(["./js/**/*.js", "!./js/vendor/*.js"])
  .pipe(map(log))
  .pipe(vfs.dest("./output"));
```

## Glob order is not a detail

The `src` method takes a glob string or an array of glob strings and returns a stream of Vinyl File objects. The one rule that surprises people is that globs are executed in order, so negations should follow positive globs. The documentation gives the pair of examples that make the point. An array of `["!b*", "*"]` would not exclude any files, because the negation runs first and the positive glob afterwards brings everything back. The array `["*", "!b*"]` excludes all the files starting with b. Nothing errors, nothing warns, the result is simply wrong, and it is the sort of bug that survives a long time in a build script because the output looks plausible. There is one more behaviour attached to reading, and it is not an option but a default: a UTF-8 BOM will be removed from all UTF-8 files read with `.src` unless it is disabled in the options, which matters if your source files legitimately begin with a byte order mark.

## The src options decide what actually gets read

The options are documented with types and defaults, and two of them change the shape of the stream rather than just its content. `buffer`, true by default, decides whether the file contents are buffered into memory, and setting it to false makes `file.contents` a paused stream instead, which is the difference between a build that holds every file in memory and one that does not. `read`, also true by default, decides whether the file is read at all, and setting it to false sets `file.contents` to null and disables writing the file to disk through `.dest()`, which is what you want for a task that only needs to know a file exists, such as removing files. The rest are more conventional: `since` streams only the files modified since a given date or timestamp, `removeBOM` is the switch for the BOM behaviour described above, and `encoding` transcodes with iconv-lite and can be set to false to pass raw binary data around. `sourcemaps` loads inline source maps and resolves source map links, `resolveSymlinks` decides whether symlinks are followed or preserved, and `dot` decides whether globs match dot files such as `.gitignore`.

## Options can be functions, with one documented exception

There is a pattern across the whole option set that is worth knowing before you read any single option. Values passed to the options must be of the expected type, otherwise they are ignored, and every option can be given a function instead of a value, in which case the function is called with the Vinyl File object as its only argument and must return a value of the expected type for that option. So `mode` can be computed per file, `since` can depend on the file, and `encoding` can be chosen from the file extension, and the same mechanism works for dest. The one documented exception is `dot`, which is not resolved from a function because it is passed verbatim to anymatch, and the glob related options in general are forwarded verbatim to glob-stream and anymatch, so their own documentation is the reference for them. The practical upshot is that vinyl-fs gives you per file control of almost everything except the glob engine itself.

## dest writes the file, then reconciles its metadata

The `dest` method takes a folder path string or a function, in which case it is called with each Vinyl File object and must return a folder path, and it returns a stream that accepts File objects, writes them to disk at the folder or working directory you specified, and passes them downstream so you can keep piping them around. The interesting part is what happens after the write. Once the file is on disk, an attempt is made to determine whether the `stat.mode`, `stat.mtime` and `stat.atime` of the Vinyl object differ from the file on the file system, and if they differ and the running process owns the file, the corresponding metadata is updated. If they do not differ, or the process does not own the file, the attempt is skipped silently. In other words the library tries to preserve the permissions and timestamps of a source file when it copies it, and it does so without failing when it cannot. The stream also rewrites the object it was given, with `cwd`, `base` and `path` overwritten to match the destination folder, `stat` updated to match the file on disk, and the position of `contents` reset to the beginning if it is a stream.

## The Windows exception, and the destination options

One whole feature is switched off on Windows, and the documentation explains why in detail. The metadata reconciliation is disabled on Windows and on any other operating system that does not support `process.getuid` or `process.geteuid` in node, because Windows produces very unexpected results through `fs.fchmod` and `fs.futimes`. The same section warns that `fs.futimes()` converts the modification and access times to seconds internally, and that this division by a thousand may cost some precision on 32 bit Node.js. The destination options are otherwise unremarkable and follow the same rules as the source options, with values of the expected type and functions allowed. `cwd` is the working directory the folder is relative to and defaults to `process.cwd()`. `mode` is the mode files are created with, resolved only when the File is not symbolic, and it defaults to the mode of the input file or the process mode. `dirMode` does the same for directories. `overwrite` decides whether existing files with the same path are replaced and defaults to true, and `append` controls whether new data is appended to an existing file.

## The dependency list shows what was modernised

The manifest is where a project like this tells you its history. The package is `vinyl-fs` at version 4.0.2, authored by the Gulp Team, MIT licensed, with an engines field demanding Node 26.1.0 or later, which is strict enough to rule out most existing CI images and is the single most likely reason an upgrade would fail on an older machine. The runtime dependencies are the interesting list. `vinyl` itself is at version 3 and `vinyl-sourcemap` handles the source map side, `glob-stream` at version 8 does the globbing, `iconv-lite` does the transcoding, `graceful-fs` patches the file system module, and `lead` is the helper that makes a stream wait for another stream to be ready, which is the trick that makes a source to transform to destination pipeline work. `streamx` at version 2.14 is the stream implementation, and `stream-composer`, `to-through`, `value-or-function` and `resolve-options` are the small pieces that turn options into behaviour. On the tooling side, the tests run under mocha with nyc for coverage and an async-only flag, the linting uses eslint 10 with the gulp configuration, and the formatting is prettier. The releases show v4.0.0 in June 2023 and two patch releases in June 2025, with the last push to the repository on 2026-10-13.

## Conclusion

vinyl-fs suits a build that is already stream shaped, because it hands you Vinyl objects rather than file paths and lets a transform sit between reading and writing without touching the disk twice. It is a poor fit if you want a modern file system API, since it is a stream library with a decade of conventions attached, and it is a poor fit if you are on an old Node, since the package now requires 26.1.0 or later. Before you adopt it, check three things: the Node version, since the engines field is strict and the recent releases have moved it, whether your build needs the metadata reconciliation that dest performs, which is skipped on Windows and is the most surprising behaviour in the library, and the glob order in your patterns, since a negation placed before a positive pattern silently excludes nothing.

## FAQ

### What is vinyl-fs and what is a Vinyl object?

vinyl-fs is a Vinyl adapter for the file system. A Vinyl is a simple metadata object describing a file, whose main attributes are path and contents, and it does not have to represent something on the local disk, since the same object can describe a file on S3, FTP or Dropbox. An adapter exposes src and dest streams for one such medium.

### Why does my vinyl-fs glob not exclude anything?

Globs are executed in order, so negations have to follow positive globs. The array `["!b*", "*"]` excludes nothing because the positive glob brings everything back, while `["*", "!b*"]` excludes the files starting with b. The library does not warn about the wrong order.

### How do I stop vinyl-fs from reading file contents?

Set the `read` option to false. The contents are then set to null and writing the file through dest is disabled, which is what you want for a task that only needs to know that a file exists. Setting `buffer` to false is the gentler option, and it leaves the contents as a paused stream rather than buffering them in memory.

### What does vinyl-fs do with file permissions and timestamps?

After writing a file, dest compares the mode, modification time and access time on the Vinyl object with those on disk, and updates the file system metadata if they differ and the process owns the file. The attempt is skipped silently otherwise, and the whole feature is disabled on Windows and on systems without process.getuid or process.geteuid.

### Which Node version does vinyl-fs need?

The package manifest requires Node 26.1.0 or later, which is a strict floor worth checking before an upgrade. The package is at version 4.0.2, MIT licensed, with the last push to the repository on 2026-10-13.

## Sources

- [gulpjs/vinyl-fs on GitHub](https://github.com/gulpjs/vinyl-fs)
- [Issues](https://github.com/gulpjs/vinyl-fs/issues)
- [License: MIT](https://github.com/gulpjs/vinyl-fs/blob/main/LICENSE)
- [README](https://github.com/gulpjs/vinyl-fs/blob/main/README.md)
- [Releases](https://github.com/gulpjs/vinyl-fs/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/gulpjs-vinyl-fs
