A high-performance, stream-based runtime library for executing transpiled Twig templates in JavaScript/TypeScript. This is not a template engine - it's a runtime execution environment for pre-compiled Twig template code.
This library provides the execution environment for Twig templates that have been transpiled to JavaScript. Templates are converted from Twig syntax to executable JavaScript code stubs by a separate transpiler (such as @tugrul/twig-transpiler), and this runtime handles their execution with proper streaming, inheritance, and context management.
Traditional template engines parse and compile templates at runtime, which introduces overhead. This library takes a different approach:
- Transpilation Phase (separate tool): Twig templates → JavaScript code stubs
- Runtime Phase (this library): Execute pre-compiled JavaScript with streaming support
This separation provides:
- Zero runtime parsing - templates are already JavaScript
- Type safety - generated TypeScript code with full type checking
- Better performance - no template compilation overhead
- Tree-shaking - unused templates can be eliminated by bundlers
- Stream-based - efficient memory usage with Web Streams API
npm install @tugrul/twig-runtimeimport { getRuntime } from '@tugrul/twig-runtime';
// These template stubs are generated by the transpiler
import { BaseTemplate, IndexTemplate } from './compiled-templates';
const runtime = getRuntime({
blobOptions: { basePath: './cache' }
});
runtime.register([BaseTemplate, IndexTemplate]);
const stream = runtime.render('index.html.twig', {
title: 'Hello World',
user: { name: 'Alice' }
});
// Use with Node.js streams
import { Readable } from 'stream';
Readable.fromWeb(stream).pipe(response);The transpiler converts Twig templates into JavaScript objects like this:
Input (Twig):
{# base.html.twig #}
<!DOCTYPE html>
<html>
<head>
{% block head %}
<title>{% block title %}{% endblock %} - My Site</title>
{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>Output (Generated JavaScript):
export const BaseTemplate: TwigTemplate = {
name: 'base.html.twig',
async main() {
await this.blob('header_abc123'); // Cached: "<!DOCTYPE html>\n<html>\n <head>\n "
await this.block('head');
await this.blob('body_def456'); // Cached: "\n </head>\n <body>\n "
await this.block('content');
await this.blob('footer_ghi789'); // Cached: "\n </body>\n</html>"
},
blocks: {
async head() {
await this.blob('title_start_jkl012');
await this.block('title');
await this.blob('title_end_mno345');
},
async title() {
// Empty block
},
async content() {
// Empty block
}
}
};Child Template:
export const IndexTemplate: TwigTemplate = {
name: 'index.html.twig',
extends: 'base.html.twig',
blocks: {
async title() {
this.write('Home');
},
async content() {
this.write('<h1>Welcome, ');
this.write(this.vars.user.name);
this.write('!</h1>');
}
}
};Templates are organized in an inheritance tree, resolved at registration time:
base.html.twig (root)
├── layout.html.twig
│ ├── page.html.twig
│ └── post.html.twig
└── admin.html.twig
When you register templates, the runtime:
- Creates
TwigTemplateNodefor each template - Links child nodes to parent nodes
- Builds the complete inheritance tree
Block resolution traverses this tree: child blocks override parent blocks, and parent() calls navigate up the tree.
The runtime uses Web Streams API (ReadableStream<Uint8Array>) for efficient, non-blocking output:
// Each template render returns a stream
const stream = runtime.render('template.twig', vars);
// Stream chunks are emitted as they're generated
// No buffering of entire output in memoryExecution flow:
- Create a
ReadableStreamwith a controller - Create
TwigTemplateContextwith the controller - Execute template's
main()function - Template calls
this.write(),this.blob(),this.block()- all enqueue to stream - Stream chunks are consumed as they're produced
Each block execution creates an isolated scope:
async block(name: string): Promise<void> {
// Creates new ReadableStream for this block
const stream = this.node.block(name, this.vars);
// Consumes block's stream into parent stream
await this.consumeStream(stream);
}This ensures:
- Variables are properly scoped
- Blocks can't interfere with each other
- Circular references are detected
- Async operations maintain execution order
Large static content is extracted and cached:
// Instead of embedding large HTML strings in JavaScript:
this.write('<!DOCTYPE html><html><head>...[1000 lines]...</html>');
// Content is hashed and stored separately:
await this.blob('content_hash_abc123');Benefits:
- Smaller JavaScript bundles
- Efficient caching (content-addressed)
- Parallel loading of cached chunks
- Reduced memory pressure
Default loader (filesystem):
const runtime = getRuntime({
blobOptions: { basePath: './cache' }
});
// Loads from: ./cache/{hash}.txtCustom loader (e.g., Redis):
const redisLoader: BlobLoader = function(hash: string) {
const redis = this.options.redis as RedisClient;
return new ReadableStream({
async start(controller) {
const content = await redis.get(`twig:blob:${hash}`);
controller.enqueue(new TextEncoder().encode(content));
controller.close();
}
});
};
const runtime = getRuntime({
loadBlob: redisLoader,
blobOptions: { redis: redisClient }
});getRuntime(options?: TwigRuntimeOptions): TwigRuntimeOptions:
loadBlob?: BlobLoader- Custom blob loading functionblobOptions?: Record<string, unknown>- Options passed to blob loader
register(templates: TwigTemplate[]): this
Register template stubs and build inheritance tree.
render(name: string, vars?: Record<string, unknown>): ReadableStream<Uint8Array>
Render a template to a stream.
registerFilters(filters: Record<string, TwigFilter>): this
Register custom filters.
registerFunctions(functions: Record<string, TwigFunction>): this
Register custom functions.
Available to template stubs via this:
write(chunk: string | Uint8Array | ArrayBufferLike): void
Write content to output stream.
async blob(hash: string): Promise<void>
Load and write cached content.
async block(name: string): Promise<void>
Render a block.
async parent(): Promise<void>
Render parent block implementation.
async getBlob(hash: string): Promise<string>
Load cached content as string (for manipulation).
async getBlock(name: string): Promise<string>
Render block and capture as string.
async getParent(): Promise<string>
Render parent block and capture as string.
async filter(subject: unknown, pipeline: TwigFilterPipelineItem[]): Promise<unknown>
Apply filter pipeline.
async execute(name: string, ...args: unknown[]): Promise<unknown>
Execute a registered function.
vars: Record<string, unknown>
Access template variables.
name: string | null
Current block name (or null if in main template).
Built-in filter:
escape- HTML entity escaping
Register custom filters:
runtime.registerFilters({
async upper(value: unknown): Promise<unknown> {
return String(value).toUpperCase();
},
async truncate(value: unknown, length: number): Promise<unknown> {
const str = String(value);
return str.length > length ? str.slice(0, length) + '...' : str;
}
});Use in template stubs:
const filtered = await this.filter(this.vars.title, [
['upper'],
['truncate', 50]
]);
this.write(filtered as string);Register custom functions:
runtime.registerFunctions({
async now(): Promise<unknown> {
return Date.now();
},
async formatDate(timestamp: number, format: string): Promise<unknown> {
// Custom date formatting logic
return new Date(timestamp).toLocaleDateString();
}
});Use in template stubs:
const timestamp = await this.execute('now') as number;
const formatted = await this.execute('formatDate', timestamp, 'YYYY-MM-DD');
this.write(formatted as string);import express from 'express';
import { Readable } from 'stream';
import { getRuntime } from '@tugrul/twig-runtime';
const app = express();
const runtime = getRuntime({ blobOptions: { basePath: './cache' } });
runtime.register([/* your compiled templates */]);
app.get('/', (req, res) => {
const stream = runtime.render('index.html.twig', {
user: req.user,
title: 'Home'
});
res.type('html');
Readable.fromWeb(stream).pipe(res);
});import { createGunzip } from 'zlib';
const compressedLoader: BlobLoader = function(hash: string) {
const basePath = this.options.basePath as string;
return new ReadableStream({
async start(controller) {
const fs = await import('fs');
const path = await import('path');
const filePath = path.join(basePath, `${hash}.txt.gz`);
const fileStream = fs.createReadStream(filePath);
const gunzip = createGunzip();
fileStream
.pipe(gunzip)
.on('data', (chunk) => controller.enqueue(chunk))
.on('end', () => controller.close())
.on('error', (err) => controller.error(err));
}
});
};const tieredLoader: BlobLoader = function(hash: string) {
const { memoryCache, redis, filesystem } = this.options;
return new ReadableStream({
async start(controller) {
// Try memory cache
let content = memoryCache.get(hash);
// Try Redis
if (!content) {
content = await redis.get(`blob:${hash}`);
if (content) memoryCache.set(hash, content);
}
// Fallback to filesystem
if (!content) {
content = await fs.readFile(`./cache/${hash}.txt`, 'utf-8');
await redis.set(`blob:${hash}`, content);
memoryCache.set(hash, content);
}
controller.enqueue(new TextEncoder().encode(content));
controller.close();
}
});
};import { createGzip } from 'zlib';
import { pipeline } from 'stream/promises';
app.get('/page', async (req, res) => {
const stream = runtime.render('page.html.twig', { data: req.query });
res.setHeader('Content-Type', 'text/html');
res.setHeader('Content-Encoding', 'gzip');
await pipeline(
Readable.fromWeb(stream),
createGzip(),
res
);
});- Zero template parsing - Templates are pre-compiled JavaScript
- Streaming output - Memory usage independent of output size
- Lazy loading - Blobs loaded on-demand, not upfront
- Parallel blob loading - Multiple cache reads can happen concurrently
- Tree shaking - Unused templates eliminated by bundlers
- No RegEx - No runtime pattern matching or parsing
This runtime is designed to work with a Twig-to-JavaScript transpiler. The transpiler should:
- Parse Twig templates - Build AST from
.twigfiles - Extract static content - Hash and save large text chunks
- Generate TypeScript stubs - Convert AST to
TwigTemplateobjects - Resolve dependencies - Track
extendsandincluderelationships
Example transpiler output structure:
dist/
templates/
base.ts
index.ts
page.ts
cache/
abc123.txt
def456.txt
ghi789.txt
See @tugrul/twig-transpiler for a reference implementation.
Full TypeScript support with strict typing:
import { TwigTemplate } from '@tugrul/twig-runtime';
export const MyTemplate: TwigTemplate = {
name: 'my-template.twig',
async main() {
// Full type checking and autocomplete
this.write(this.vars.title as string);
await this.block('content');
}
};npm testRun with coverage:
npm test -- --coverageContributions are welcome! This library focuses on the runtime execution environment. For transpiler-related features, see the transpiler project.
MIT
- @tugrul/twig-transpiler - Transpiles Twig templates to JavaScript
- Twig - The original PHP template engine
Note: This is a runtime library only. You need a separate transpiler to convert .twig files to JavaScript template stubs that this runtime can execute.