Skip to content

Repository files navigation

Taglify — keep generated content in sync, everywhere

npm version license TypeScript CI

Preview

Taglify replaces the content between marker comments in any text file, and leaves everything else byte-for-byte alone. Point it at a README, a config file, or a source file, hand it a string, and the block is up to date.

Zero runtime dependencies. ESM, CJS, and types. Node 18+.

Installation

bun add -D taglify
npm install -D taglify

Example

Add this to your readme:

<!-- STATS:START -->
<!-- STATS:END -->

In your build flow:

import { taglWrite } from 'taglify';

const coverageSummary = '...';
taglWrite('./README.md', { STATS: coverageSummary });

after:

<!-- STATS:START -->

| Browser | Passed | Failed |
| ------- | ------ | ------ |
| Brave   | 127    | 0      |
| Firefox | 127    | 0      |
| Edge    | 0      | 127    |

<!-- STATS:END -->

Same idea works for any file type, in a CI step, a git pre-commit hook, or a cron job.

API

taglText(text, tags, options?)

Param Type Description
text string Source text
tags Record<string, string> Tag name → replacement content
options TaglifyOptions See below

Returns { text: string, changed: boolean, diffs: BlockDiff[], write: (filePath: string) => void }. write writes text to filePath, but only if changed — so you can call taglText(...).write('./README.md') without importing fs yourself. diffs lists { tag, before, after } for each block that changed.

taglWrite(filePath, tags, options?)

Reads filePath, applies taglText, and writes back only if the content changed. Internally it's just taglText(...).write(filePath).

  • Returns boolean — whether the file was modified.
  • On error (missing file, unreadable file, write failure), logs a clear message (with the underlying error as cause) via console.error and returns false. Pass { throwOnError: true } to throw instead.

taglWriteAsync(filePath, tags, options?)

Async counterpart to taglWrite, using fs/promises for non-blocking I/O. Behaves identically otherwise.

taglRead(text, tagName, options?)

Reads the current content between an existing tag's markers, de-indented the same way taglText indents replacements.

import { taglRead } from 'taglify';

taglRead('<!-- STATS:START -->\nold\n<!-- STATS:END -->', 'STATS'); // -> 'old'

Returns string | nullnull if the tag isn't found.

formatList(items) / formatTable(headers, rows)

Small helpers for building the replacement string for common shapes — plain string in, plain string out, nothing else to learn.

import { formatList, formatTable } from 'taglify';

formatList(['a', 'b']); // -> '- a\n- b'
formatTable(['Browser', 'Passed'], [['Brave', '127']]);
// -> '| Browser | Passed |\n| ------- | ------ |\n| Brave   | 127    |'

TaglifyOptions

Option Type Default Description
commentStyle Record<open, close> { '<!-- ': ' -->' } Marker open/close pairs to recognize
startSuffix string 'START' Suffix marking the start of a block, e.g. TAG:START
endSuffix string 'END' Suffix marking the end of a block, e.g. TAG:END
throwOnError boolean false taglWrite throws on error instead of logging + false
createMissing boolean false Append a new block at EOF for a tag with no match, instead of skipping

commentStyle replaces the default entirely — pass every style you want matched. Multiple entries are all tried, so different blocks in the same file can use different styles:

taglText(text, tags, {
  commentStyle: { '// ': '', '# ': '', '/* ': ' */' },
});

Only HTML comments are matched by default because # and // are common in ordinary content (markdown headings, URLs) and risk false-positive matches — opt in explicitly for those.


Marker format

<!-- TAG:START -->
content
<!-- TAG:END -->
Rule Behavior
Tag name casing Case-insensitive, normalized to uppercase
Content between markers Fully replaced
Markers themselves Always preserved
Multiple blocks, same tag All replaced
Tag with no matching block Skipped (or appended at EOF with createMissing)
Replacement value type string only

Stability

The API above is complete and stable. Taglify does one thing, and that thing is done — new versions add options, they don't change what existing calls do.

Runtime dependencies none
Versioning semver; breaking changes only in a major
Formats ESM, CJS, and .d.ts types
Runtimes Node 18+ and Bun, both exercised in CI
Tests full suite + typecheck + build on every push (CI)
Failure mode never partially writes; returns false (or throws with throwOnError)

Deliberately out of scope: templating, markdown generation, non-string tag values, config files. Keeping those out is what keeps the failure surface this small.

Possible future work

Not scheduled, and not needed for the current API to be complete. Full list in BACKLOG.md.

  • CLI for running taglify against files/globs.
  • Glob support for matching multiple files.
  • Directory/recursive processing.
  • Ignore rules.
  • Support non-string values.

License

MIT © jayF0x

About

Synchronize generated content inside marker blocks in any text file.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages