Skip to content

tugrul/twig-runtime

Repository files navigation

Twig Runtime Library

🚧 Work in Progress 🚧


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.

Overview

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.

Why a Runtime Library?

Traditional template engines parse and compile templates at runtime, which introduces overhead. This library takes a different approach:

  1. Transpilation Phase (separate tool): Twig templates → JavaScript code stubs
  2. 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

Installation

npm install @tugrul/twig-runtime

Quick Start

import { 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);

Template Stub Format

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>');
    }
  }
};

Architecture

Template Tree Resolution

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:

  1. Creates TwigTemplateNode for each template
  2. Links child nodes to parent nodes
  3. Builds the complete inheritance tree

Block resolution traverses this tree: child blocks override parent blocks, and parent() calls navigate up the tree.

Stream-Based Execution

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 memory

Execution flow:

  1. Create a ReadableStream with a controller
  2. Create TwigTemplateContext with the controller
  3. Execute template's main() function
  4. Template calls this.write(), this.blob(), this.block() - all enqueue to stream
  5. Stream chunks are consumed as they're produced

Context Scoping

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

Blob Loading Strategy

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}.txt

Custom 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 }
});

API Reference

Runtime Creation

getRuntime(options?: TwigRuntimeOptions): TwigRuntime

Options:

  • loadBlob?: BlobLoader - Custom blob loading function
  • blobOptions?: Record<string, unknown> - Options passed to blob loader

TwigRuntime Methods

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.

Template Context API

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).

Filters

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);

Functions

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);

Express Integration

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);
});

Advanced Usage

Custom Blob Loader with Compression

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));
    }
  });
};

Multi-Tier Caching

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();
    }
  });
};

Progressive Streaming with Compression

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
  );
});

Performance Characteristics

  • 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

Transpiler Integration

This runtime is designed to work with a Twig-to-JavaScript transpiler. The transpiler should:

  1. Parse Twig templates - Build AST from .twig files
  2. Extract static content - Hash and save large text chunks
  3. Generate TypeScript stubs - Convert AST to TwigTemplate objects
  4. Resolve dependencies - Track extends and include relationships

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.

TypeScript Support

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');
  }
};

Testing

npm test

Run with coverage:

npm test -- --coverage

Contributing

Contributions are welcome! This library focuses on the runtime execution environment. For transpiler-related features, see the transpiler project.

License

MIT

Related Projects

  • @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.

About

A high-performance, stream-based runtime library for executing transpiled Twig templates in JavaScript/TypeScript.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors