CLI tool
adamreisnz/replace-in-file avatar
adamreisnz/replace-in-file

replace-in-file: swapping text across files and globs from Node

A simple utility to quickly replace contents in one or more files

617 stars67 forksTypeScriptMIT

At a glance

What is it?
replace-in-file is a small TypeScript package that reads files, applies one or many string or regex replacements, and writes them back, with promise, callback and synchronous entry points plus a CLI. It suits codemods and config sweeps, and it punishes anyone who expects a backup or a rollback.
Who is it for?
Adopt replace-in-file when a build script or codemod needs a handful of literal or regex substitutions across a known set of paths, and your runtime is Node 22.12 or newer. Skip it for anything touching binary data, for jobs that need a per file backup, or where a global regex would rewrite text you have not reviewed.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 13 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A thin wrapper over one read, one replace, one write

replace-in-file exists to do one job: take a set of files, apply replacements to the contents, and write the results back. The README calls it "a simple utility to quickly replace text in one or more files or globs" and adds that it works synchronously or asynchronously with either promises or callbacks.

That is the whole architecture. There is no AST, no parser and no language detection. The `from` value is handed straight to the native String replace method, which the README states plainly, so the matching behaviour is whatever JavaScript regular expressions already do. Everything above that single call is plumbing: expanding the `files` option into concrete paths, reading each file, running the replacement, writing the file back, and returning a result record.

The audience is people writing build steps, codemods, rename scripts and configuration sweeps. The repository layout backs this up. The top level holds `index.ts`, a `src/` directory, a `bin/` directory and two tsconfig files, with `index.ts` as the library entry point and `bin/` supplying the command line. The test script runs mocha over `src/**/*.spec.ts`, and coverage is measured with c8 through a `.c8rc` file. This is a utility library with a CLI bolted on, not an application with a configuration system.

Installing the package and running the first replacement

The README gives three package manager options, all under the same package name:

shell
# Using npm
npm i replace-in-file

# Using yarn
yarn add replace-in-file

# Using pnpm
pnpm add replace-in-file

The async entry point is the one most scripts will use. You describe the work in an options object and await the call:

js
import {replaceInFile} from 'replace-in-file'

const options = {
  files: 'path/to/file',
  from: /foo/g,
  to: 'bar',
}

try {
  const results = await replaceInFile(options)
  console.log('Replacement results:', results)
}
catch (error) {
  console.error('Error occurred:', error)
}

The synchronous variant swaps the import and drops the await, which is handy inside a short script that cannot be async:

js
import {replaceInFileSync} from 'replace-in-file'

const options = {
  files: 'path/to/file',
  from: /foo/g,
  to: 'bar',
}

try {
  const results = replaceInFileSync(options)
  console.log('Replacement results:', results)
}
catch (error) {
  console.error('Error occurred:', error)
}

One detail matters before you paste either snippet: the package is an ES module, since `package.json` sets `"type": "module"` and exposes only an `import` condition for the main entry. A CommonJS script cannot `require` this package. The declared engine floor is Node 22.12.0, so an older runtime is outside the supported range.

A string in `from` replaces only the first hit

The most common way to misuse this package is to pass a plain string and expect every occurrence to change. The README is explicit that the `from` value is passed straight to the native String replace method, so a string argument only replaces the first occurrence.

js
const options = {
  from: 'foo',
  to: 'bar',
}

To sweep a file you need a regular expression with the global flag:

js
const options = {
  from: /foo/g,
  to: 'bar',
}

The same applies to patterns you might think are literal but are not. A regex like `/foo/g` will treat any regex metacharacter in the pattern as syntax, so a search for a string containing a dot or a bracket needs escaping. Because the library adds no safety layer on top of the native replace, the correctness of the whole operation depends on how carefully the pattern was written.

Multiple patterns are supported. They are applied sequentially, so the order matters, and the replacement value can be a single string shared by all patterns:

js
const options = {
  from: [/foo/g, /baz/g],
  to: 'bar',
}

Or you can pair each pattern with its own replacement, which keeps the two arrays aligned by position:

js
const options = {
  from: [/foo/g, /baz/g],
  to: ['bar', 'bax'],
}

The `files` option behaves the same way. A single path is a string, and several paths and globs go into one array:

js
const options = {
  files: [
    'path/to/file',
    'path/to/other/file',
    'path/to/files/*.html',
    'another/**/*.path',
  ],
}

The `glob` package sits in the dependency list, which is what makes patterns such as `another/**/*.path` resolve to real files before any content is read.

The result array reports every file, changed or not

The return value is an array with one entry per file that was processed, and it includes files where nothing was replaced. Each entry carries a `file` path and a `hasChanged` flag:

js
const results = replaceInFileSync({
  files: 'path/to/files/*.html',
  from: /foo/g,
  to: 'bar',
})

console.log(results)

Because unchanged files are still reported, a script that only wants the touched files has to filter:

js
const changedFiles = results
  .filter(result => result.hasChanged)
  .map(result => result.file)

That distinction matters for reporting. A glob that matches forty files and rewrites three of them yields forty results, and a naive `results.length` check would claim forty edits happened.

Counts are available behind the `countMatches` flag. With it enabled, each entry gains `numMatches` and `numReplacements`:

js
const results = replaceInFileSync({
  files: 'path/to/files/*.html',
  from: /foo/g,
  to: 'bar',
  countMatches: true,
})

The README notes an asymmetry worth remembering: the number of matches can exceed the number of replacements when the match and the replacement are the same string. That makes `numMatches` the number to watch when a pattern is accidentally similar to its own replacement.

One constraint is documented as an absence rather than a feature. There is no direct API for making multiple replacements across different files with different options. The README suggests looping over an array of replacement objects yourself. That is a small amount of extra code in a script, but it means each call re-enters the library rather than batching.

A CLI binary and a Node 22 floor in the same package

Two packaging facts tell you what kind of adopter this project expects. First, `package.json` declares a binary at `./dist/bin/cli.js`, and the README carries a CLI usage section along with a note about using globs with the CLI. Second, the dependency list contains yargs for argument parsing, chalk for terminal output and glob for pattern expansion, with the `bin/` directory at the top level of the repository.

The CLI is therefore a real entry point, not a placeholder, and it is the fastest route for a one-off sweep across a checkout. The caveat the README flags for the CLI is globs: shell expansion and library expansion are different things, so a quoted pattern and an unquoted pattern will not behave the same way.

The runtime floor is equally specific. `engines` requires Node 22.12.0 or newer, `packageManager` pins pnpm 11.18.0, and the lockfile is `pnpm-lock.yaml` next to a `pnpm-workspace.yaml`. The package is published as pure ESM with `types`, `import` and `default` export conditions pointing at the `dist/` build, and `files` limits what ships to `dist` alone.

That is a modern, narrow target. A team pinned to Node 16 or 18, or one that still runs CommonJS tooling without a transpiler, cannot adopt this package as is. The build itself is a single `tsc -p tsconfig.build.json` step with a separate `typecheck` script and an eslint configuration at the root.

Where replace-in-file stops being the right tool

The library has no notion of safety around an edit. It reads a file, applies a pattern and writes it back. The README documents no backup file, no undo log and no rollback, and a global regex across a glob will happily rewrite every file it reaches. If a pattern is wrong, the damage is only recoverable through version control or an external copy.

It is also text only. There is no binary detection and no encoding negotiation beyond what the file APIs do, so running it over a directory that holds images or archives is pointless work at best. The README describes a way to specify character encoding, which confirms encoding is treated as a configurable parameter rather than something the tool detects.

The counting feature is a second partial answer rather than a full preview. `countMatches` tells you how many times a pattern matched in each file, but it reports after the write, so it is a record of what happened rather than a plan of what would happen. The README's index also lists a dry run section and options for allowing empty or invalid paths, disabling globs and supplying glob configuration, which tells you the author thought about previewing and about defensive path handling.

Finally, for anything structural, a text replacement is the wrong abstraction. Renaming an exported function means editing import sites, call sites and type declarations, and a regex sweep across a tree will also hit comments, strings and unrelated identifiers that share the name.

Licence, versioning and the cost of staying current

The project is published under the MIT licence, and a LICENSE file sits at the top level of the repository next to the README, CHANGELOG and package metadata. MIT is a permissive licence that allows reuse, modification and distribution provided the copyright notice and licence text are retained, though the licence text itself governs what any specific use requires.

Version numbering is where a buyer should slow down. The `package.json` in the repository declares version 9.0.0, while the most recent tagged release listed for the project is v4.0.0 dated 2019-04-21, with v3.1.0 and v3.0.0 both from December 2017. The repository is not archived, and the last push was on 2026-10-10. So the code has moved on from the published tags, and the tag list is a poor guide to what npm will hand you.

The upgrade cost is therefore a version check rather than a migration plan. Because the current package targets Node 22.12.0 and ships as ESM only, moving from an older major version means moving your runtime as well. The dependency majors have moved too, with chalk, glob and yargs all on recent majors, so a stale lockfile will not resolve cleanly.

The honest summary for an engineer deciding this week: the utility does one small thing, it does it with the platform's own regex engine, and it asks for a modern Node runtime and a working version control workflow before it touches your files.

Editorial conclusion

Adopt replace-in-file when a build script or codemod needs a handful of literal or regex substitutions across a known set of paths, and your runtime is Node 22.12 or newer. Skip it for anything touching binary data, for jobs that need a per file backup, or where a global regex would rewrite text you have not reviewed. Before the first run, check the `from` pattern against a working copy, confirm the glob expansion by reading the `file` value on every entry of the returned array, and run the script once with `countMatches` enabled to see how many sites each pattern hits.

Frequently asked questions

How do I install replace-in-file and replace all occurrences of a string?

Install it with `npm i replace-in-file`, `yarn add replace-in-file` or `pnpm add replace-in-file`. To replace every occurrence, pass a regular expression with the global flag as `from`, for example `/foo/g`, because a plain string is handed to the native String replace method and only replaces the first occurrence.

How do I know which files replace-in-file actually changed?

The call returns an array of results for every file that was processed, including unchanged ones, and each entry has a `file` path and a `hasChanged` flag. Filter on `hasChanged` to get only the files that were rewritten.

Does replace-in-file need a modern Node version?

Yes. The package metadata declares an engine requirement of Node 22.12.0 or newer, and the package is published as an ES module with an `import` export condition rather than CommonJS.

Can replace-in-file count how many matches it made in each file?

Set the `countMatches` option to `true` and each result entry gains `numMatches` and `numReplacements` fields. The README notes that the match count can be higher than the replacement count when the match and the replacement are the same string.

Does replace-in-file support replacing multiple patterns with different results?

Yes. Pass an array of patterns in `from` and an array of replacements in `to`, which are applied sequentially, so the two arrays are paired by position. All patterns can also share one replacement string.

Official sources

  1. adamreisnz/replace-in-file on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/adamreisnz-replace-in-file.svg)](https://hysenlabs.com/projects/adamreisnz-replace-in-file)

Was this useful?