A lightweight template engine for code generation and scaffolding. Embed conditional blocks, loops, and expressions directly in real source files — templates stay syntactically valid and readable. Supports both source code (text mode with comment-based directives) and HTML/XML (Vue-like v-if, v-for, :attr bindings).
- Templates compile to cached JS functions — repeated renders are near-instant
- Under 27 KB bundled, zero runtime dependencies beyond
@prostojs/parser - Auto-detects text vs HTML mode by file extension; switch modes mid-file with directives
- Point at a directory to scaffold entire projects with a single call
npm install @prostojs/rewrite
# or
pnpm add @prostojs/rewriteimport { ProstoRewrite } from '@prostojs/rewrite'
const rw = new ProstoRewrite()
// Text mode — rewrite a source code template
const result = rw.textRewriter.rewrite(
`Hello {{ name }}! You have {{ count }} items.`,
{ name: 'World', count: 3 }
)
// => "Hello World! You have 3 items."
// HTML mode — rewrite markup
const html = rw.htmlRewriter.rewrite(
`<ul><li v-for="item of items">{{ item }}</li></ul>`,
{ items: ['apple', 'banana', 'cherry'] }
)
// => "<ul><li>apple</li>\n<li>banana</li>\n<li>cherry</li></ul>"Text mode is designed for source code, config files, Dockerfiles, and any non-HTML content. Directives are embedded in line comments, keeping your templates syntactically valid.
Use {{ }} delimiters (customizable) to interpolate any JavaScript expression:
const greeting = '{{ salutation }} {{ name }}!'
const sum = {{ a + b }}
Context: { salutation: 'Hello', name: 'World', a: 2, b: 3 }
Output:
const greeting = 'Hello World!'
const sum = 5
//=IF (env === 'production')
const API = 'https://api.prod.com'
//=ELSEIF (env === 'staging')
const API = 'https://api.staging.com'
//=ELSE
const API = 'http://localhost:3000'
//=ENDIFContext: { env: 'staging' } produces:
const API = 'https://api.staging.com'The comment prefix (//, #, --, etc.) is automatically detected from the line. Use whatever comment style fits your language:
#=IF (includeRedis)
redis:
image: redis:alpine
#=ENDIFOperation blocks reference:
| Key | Example | Description |
|---|---|---|
IF |
//=IF(condition) |
Includes lines below only if condition is truthy. |
ELSEIF |
//=ELSEIF(condition) |
Alternative branch. Must follow IF or ELSEIF. |
ELSE |
//=ELSE |
Fallback branch. Must follow IF or ELSEIF. |
ENDIF |
//=ENDIF |
Closes the IF block chain. |
FOR |
//=FOR(a of b) |
Iterates with any valid JS loop expression. |
ENDFOR |
//=ENDFOR |
Closes the FOR block. |
Operation blocks can have spaces between words:
ELSE IF,END FOR, etc. All blocks work with#and//comment prefixes.
//=FOR (const route of routes)
app.get('{{ route.path }}', {{ route.handler }})
//=ENDFORContext: { routes: [{ path: '/api', handler: 'apiHandler' }, { path: '/', handler: 'indexHandler' }] }
Output:
app.get('/api', apiHandler)
app.get('/', indexHandler)Blocks nest freely:
//=IF (features.auth)
//=FOR (const provider of authProviders)
//=IF (provider !== 'local')
import {{ provider }}Strategy from './strategies/{{ provider }}'
//=ENDIF
//=ENDFOR
//=ENDIFLines prefixed with //: (comment + reveal marker) are hidden in the template but appear in the output. This is the inverse of normal lines — useful for generating code that shouldn't be visible in the template source:
//=IF (useTypescript)
//: import type { Config } from './types'
//=ENDIF
const config = {}Context: { useTypescript: true }
Output:
import type { Config } from './types'
const config = {}Reveal lines can contain expressions too:
//: const {{ varName }} = {{ JSON.stringify(defaultValue) }}| Directive | Example | Description |
|---|---|---|
ignore-next-line |
//!@ignore-next-line |
The next line passes through as-is, without interpolation. |
html-mode-on |
//!@html-mode-on |
Switch to HTML mode (mixed rewriter only). |
html-mode-off |
//!@html-mode-off |
Switch back to text mode. |
//!@ ignore-next-line
const template = '{{ this is not interpolated }}'
const value = '{{ this IS interpolated }}'Source:
let myVar = 1
//=IF (a === b)
//=FOR (const i of items)
//: const item{{ i }} = '{{ i }}' // reveal comment
//=IF (c === d)
myVar += 2
//=ELSE
myVar -= 4
//=ENDIF
//=END FOR
//=END IF
const myVar2 = 2Context: { a: 1, b: 1, c: 2, d: 2, items: [1, 2] }
Output:
let myVar = 1
const item1 = '1' // reveal comment
myVar += 2
const item2 = '2' // reveal comment
myVar += 2
const myVar2 = 2HTML mode parses markup structure and supports Vue-inspired directives for conditionals, loops, and dynamic attributes.
Interpolation works the same as text mode:
<h1>{{ title }}</h1>
<p>Welcome, {{ user.name }}!</p><div v-if="user.isAdmin">
<h2>Admin Panel</h2>
</div>
<div v-else-if="user.isEditor">
<h2>Editor Dashboard</h2>
</div>
<div v-else>
<h2>Welcome, {{ user.name }}</h2>
</div>Only the matching block is rendered. The v-if / v-else-if / v-else chain must be on sibling elements.
<ul>
<li v-for="item of items">{{ item }}</li>
</ul>Any valid JavaScript loop expression works:
<tr v-for="let i = 0; i < rows.length; i++">
<td>{{ rows[i].name }}</td>
</tr>v-for and v-if can be combined on the same element:
<li v-for="item of items" v-if="item.visible">{{ item.name }}</li>Prefix any attribute with : to evaluate it as a JavaScript expression:
<img :src="baseUrl + '/images/' + image.file" :alt="image.title">
<a :href="link.url" :target="link.external ? '_blank' : undefined">{{ link.text }}</a>Smart boolean handling: if the expression evaluates to true, the attribute is rendered without a value (<input disabled>). If false, the attribute is omitted entirely:
<input type="text" :disabled="isLocked" :required="isRequired">Context: { isLocked: true, isRequired: false }
Output:
<input type="text" disabled>Inside an HTML file, switch to text-mode parsing with HTML comment directives:
<script>
<!--!@ text-mode-on -->
//=FOR (const key of Object.keys(config))
window.{{ key }} = {{ JSON.stringify(config[key]) }}
//=ENDFOR
<!--!@ text-mode-off -->
</script>And the reverse — switch to HTML inside a text file:
const html = `
//!@ html-mode-on
<div v-for='item of items'>
<span>{{ item }}</span>
</div>
//!@ html-mode-off
`The main use case: scaffold entire project templates.
import { ProstoRewrite } from '@prostojs/rewrite'
const rw = new ProstoRewrite()
// Returns rendered content; optionally writes to output path
const content = await rw.rewriteFile(
{
input: 'path/to/template.js',
output: 'path/to/output.js', // optional
mode: 'auto', // optional: 'text' | 'html' | 'auto'
},
{ name: 'my-app', version: '1.0.0' }
)await rw.rewriteDir(
{
baseDir: './template',
output: './my-new-project',
include: ['**/*'], // optional glob patterns
exclude: ['node_modules/**'], // optional glob patterns
renameFile: (name) => name.replace('__name__', 'my-app'), // optional
onFile: (path, output) => console.log(`Wrote: ${path}`), // optional callback
mode: 'auto', // optional
},
{
name: 'my-app',
description: 'My awesome app',
useTypescript: true,
features: ['auth', 'api', 'database'],
}
)Mode auto-detection picks the right parser based on file extension:
- HTML mode:
*.html,*.xhtml,*.xml,*.svg - Text mode:
*.js,*.ts,*.jsx,*.tsx,*.json,*.yml,*.yaml,*.md,*.txt,*.ini,Dockerfile,*config,.gitignore
Files that don't match any pattern are copied as-is.
ProstoRewrite provides three rewriter flavors:
const rw = new ProstoRewrite()
const trw = rw.textRewriter // text only
const hrw = rw.htmlRewriter // html only
const mrw = rw.mixedRewriter // both (supports mode-switching directives)
// mrw.text — text rewriter with html-mode-on support
// mrw.html — html rewriter with text-mode-on supportEach rewriter has the same interface:
// Generate the compiled JavaScript source (for inspection/debugging)
trw.genRewriteCode(source)
// Compile once, execute many times — ideal for repeated renders
const render = trw.genRewriteFunction(source)
const out1 = render({ name: 'Alice' })
const out2 = render({ name: 'Bob' })
// One-shot: parse + compile + execute
trw.rewrite(source, context)
// Debug: print the parse tree to console
trw.printAsTree(source)For simple interpolation without block operations (config values, file paths):
import { getStringExpressionRewriter } from '@prostojs/rewrite'
const srw = getStringExpressionRewriter()
// Mixed string: always returns string
srw.rewrite('Hello {{ name }}, age {{ age }}', { name: 'World', age: 25 })
// => "Hello World, age 25"
// Single expression: preserves the original type
srw.rewrite('{{ count }}', { count: 42 })
// => 42 (number, not string)This is useful for configuration files where property values can contain expressions:
const config = {
path: "some/path/{{ key.toLowerCase() }}.{{ type === 'javascript' ? 'js' : 'json' }}",
}
const pathFunc = srw.genRewriteFunction(config.path)
pathFunc({ key: 'TEST', type: 'javascript' })
// => "some/path/test.js"All options are optional. The example below shows the default values:
const rw = new ProstoRewrite({
defaultMode: 'auto', // 'text' | 'html' | 'auto'
debug: false,
htmlPattern: ['*.{html,xhtml,xml,svg}'],
textPattern: [
'*.{js,jsx,ts,tsx,txt,json,yml,yaml,md,ini}',
'Dockerfile',
'*config',
'.gitignore',
],
text: {
exprDelimiters: ['{{', '}}'],
blockOperation: '=',
revealLine: ':',
directive: '!@',
},
html: {
exprDelimiters: ['{{', '}}'],
blockOperation: 'v-',
attrExpression: ':',
directive: '!@',
voidTags: ['area', 'base', 'br', 'col', 'command', 'embed', 'hr',
'img', 'input', 'keygen', 'link', 'meta', 'param',
'source', 'track', 'wbr'],
textTags: ['script', 'style'],
},
})| Option | Type | Description |
|---|---|---|
defaultMode |
'text' | 'html' | 'auto' |
Template processor selection. auto uses file patterns to decide. |
debug |
boolean |
Print debug parse trees to console. |
htmlPattern |
string[] |
Glob patterns for files processed by HTML parser. |
textPattern |
string[] |
Glob patterns for files processed by text parser. |
text.exprDelimiters |
[string, string] |
Expression delimiters. Default: ['{{', '}}'] |
text.blockOperation |
string |
Prefix for block operations (IF, FOR, ...). Default: '=' |
text.revealLine |
string |
Prefix for reveal lines. Default: ':' |
text.directive |
string |
Prefix for directives (ignore-next-line, ...). Default: '!@' |
html.exprDelimiters |
[string, string] |
Expression delimiters. Default: ['{{', '}}'] |
html.blockOperation |
string |
Prefix for block attributes (if, for, ...). Default: 'v-' |
html.attrExpression |
string |
Prefix for expression attributes. Default: ':' |
html.directive |
string |
Prefix for directives (text-mode-on, ...). Default: '!@' |
html.voidTags |
string[] |
Self-closing HTML tags. |
html.textTags |
string[] |
Tags with text-only content (no child tag parsing). |
MIT