blade-ts
v0.3.11
Published
A minimal Blade-like template engine in TypeScript
Downloads
72
Readme
blade-ts (mini-blade)
A minimal, well-typed Blade-like template engine for JavaScript/TypeScript. Supports familiar Blade syntax ({{ }}, @if, @foreach, layouts/slots, includes) with a safe runtime and a small, modular core.
NB. currently typesafety is not implemented
✨ Features
- 🚀 Lightweight: Minimal footprint with zero dependencies
- 🔒 Type Safe: Full TypeScript support with proper type inference
- 🎨 Familiar Syntax: Laravel Blade-inspired syntax that's easy to learn
- 🛡️ Secure: Built-in HTML escaping and sandboxed execution
- ⚡ Fast: Compiled templates with optional caching
- 🔧 Extensible: Custom directives and flexible API
- 📱 Modern: ES6+ support with async/await capabilities
📋 Table of Contents
- blade-ts (mini-blade)
🚀 Installation
# npm
npm install blade-ts
# yarn
yarn add blade-ts
# pnpm
pnpm add blade-ts⚡ Quick Start
import Engine from "blade-ts";
// Initialize the engine with your templates directory
const engine = new Engine("./templates");
// Render a template with data
const html = await engine.render("home.blade", {
user: { name: "John Doe", isActive: true },
items: [{ name: "banana" }, { name: "apple" }],
title: "Welcome to My App",
});
console.log(html);Basic Template Example
templates/home.blade:
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<h1>Hello, {{ user.name }}!</h1>
@if(user.isActive)
<p>Welcome back!</p>
@endif
<ul>
@foreach(item in items)
<li>{{ item.name }}</li>
@endforeach
</ul>
</body>
</html>📝 Template Syntax
Echoing Values
Display data in your templates with these syntax options:
- Escaped Output:
{{ expr }}- HTML-escapes output for security - Raw Output:
{!! expr !!}- outputs as-is (use with caution) - Nullish Coalescing:
{{ title ?? "Default Title" }}- fallback values
<!-- Escaped (safe) -->
<p>{{ user.name }}</p>
<!-- Raw (use carefully) -->
<div>{!! user.htmlContent !!}</div>
<!-- With fallback -->
<h1>{{ pageTitle ?? "Welcome" }}</h1>Control Structures
Conditional Statements
@if(user.isActive)
<span class="status-active">Active User</span>
@elseif(user.role === "admin")
<span class="status-admin">Administrator</span>
@else
<span class="status-guest">Guest User</span>
@endifLoops
Array/Collection Iteration:
@foreach(item in items)
<li>{{ item.name }}</li>
@endforeach
@foreach(key, value in object)
<li>{{ key }}: {{ value }}</li>
@endforeachEmpty State Handling:
@forelse(item in items)
<li>{{ item.name }}</li>
@empty
<p class="empty-state">No items found</p>
@endforelseTraditional Loops:
@for(let i = 0; i < 3; i++)
<span>Item {{ i + 1 }}</span>
@endfor
@while(user.isActive)
<div class="ping">PING</div>
@endwhileSwitch Statements:
@switch(status)
@case("success")
<div class="alert-success">Operation successful!</div>
@break
@case("error")
<div class="alert-error">Something went wrong!</div>
@break
@default
<div class="alert-info">Unknown status</div>
@endswitchIncludes
Include other templates to keep your code DRY:
<!-- Include a partial template -->
@include('partials/_header.blade')
<!-- Include with data -->
@include('partials/_user-card.blade', { user: currentUser })
<!-- Include with additional context -->
@include('partials/_item.blade', {
item: product,
showPrice: true
})Layouts and Sections
Create reusable layouts with sections and slots:
Parent Layout (layouts/app.blade):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title ?? "My App" }}</title>
<!-- CSS slot for page-specific styles -->
@slot("css")
</head>
<body>
<header>
@include('partials/_navigation.blade')
</header>
<main>
<!-- Main content area -->
@yield("body")
</main>
<footer>
@include('partials/_footer.blade')
</footer>
<!-- JavaScript slot for page-specific scripts -->
@slot("js")
</body>
</html>Child View (pages/home.blade):
@extends("layouts/app.blade")
@block("body")
<div class="hero">
<h1>Welcome to {{ appName }}</h1>
<p>Hello, {{ user.name }}!</p>
</div>
<div class="content">
@foreach(item in featuredItems)
@include('partials/_item-card.blade', { item: item })
@endforeach
</div>
@endblock
@push("css")
<style>
.hero {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 4rem 2rem;
text-align: center;
}
</style>
@endpush
@push("js")
<script>
console.log("Home page loaded");
// Page-specific JavaScript here
</script>
@endpushKey Concepts:
@yield("name")- Placeholder replaced by matching@block("name")from child@slot("name")- Self-closing injection point for@push("name")content@push("name")...@endpush- Content concatenated into parent's@slot("name")
Code Blocks
Execute inline TypeScript/JavaScript during template rendering:
@code
// Declare variables and functions
let userName = user.firstName + " " + user.lastName;
let isVip = user.membershipLevel === "premium";
function formatPrice(price) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(price);
}
// Calculate derived values
let totalItems = items.length;
let hasItems = totalItems > 0;
@endcode
<div class="user-info">
<h2>{{ userName }}</h2>
@if(isVip)
<span class="vip-badge">VIP Member</span>
@endif
</div>
@if(hasItems)
<div class="items-summary">
<p>You have {{ totalItems }} items in your cart</p>
@foreach(item in items)
<div class="item">
<span>{{ item.name }}</span>
<span class="price">{{ formatPrice(item.price) }}</span>
</div>
@endforeach
</div>
@endifFeatures:
- Variables and functions declared in
@codeare available throughout the template - Supports TypeScript syntax:
@code ts - Output remains HTML-escaped unless using
{!! ... !!} - Perfect for data transformation and complex logic
Built-in Directives
Powerful built-in directives for common tasks:
JSON Serialization
<!-- Pretty-printed JSON (2 spaces indentation) -->
<pre>@json(user)</pre>
<!-- Compact JSON for JavaScript -->
<script>
const userData = @json(user, 0);
const appConfig = @json(config);
console.log('User:', userData);
</script>
<!-- Custom indentation -->
<pre>@json(complexObject, 4)</pre>Environment Variables
<!-- Read environment variables with fallbacks -->
<title>@env("APP_NAME", "My App")</title>
<meta name="description" content="@env("APP_DESCRIPTION", "Default description")">Google Fonts Integration
<!-- Load specific font weights -->
@googlefonts('Inter:100;400;700, Roboto:300;500;700')
<!-- Load font family with default weights -->
@googlefonts('Open Sans:0, Lato:0')
<!-- Mix of specific and default weights -->
@googlefonts('Inter:100;400;700, Roboto:0, JetBrains Mono:400;600')Font Syntax:
Family:weight1;weight2- Load specific weightsFamily:0- Load with browser default weightsFamily- Load all common weights (100-900)
Safe Value Display
Display values with fallbacks and null safety:
<!-- Show value with fallback -->
@show('user.name', 'Anonymous User')
<!-- Show nested object properties -->
@show('user.profile.avatar', '/default-avatar.png')
<!-- Show array elements -->
@show('items.0.name', 'No items')
<!-- Show with conditional fallback -->
@show('user.email', 'No email provided')Debug Logging
Log values to console during template rendering (useful for debugging):
<!-- Log a value to console -->
@log('user.name')
<!-- Log with fallback -->
@log('user.email', 'No email set')
<!-- Log nested properties -->
@log('user.profile.settings')
<!-- Log arrays -->
@log('items.length', '0')Note: @log outputs to the server console and returns empty string (no HTML output).
HTTP Context (Express) — Request, Response, and Locals
Access Express req, res, and res.locals from templates. These directives support:
- No args: returns a JSON summary
- One arg (path): returns the value at a dot-path
- Two args (path, varName): assigns value to
varNameand returns empty string - Three args (path, varName, fallback): uses fallback when missing
Pass the objects to the template:
const html = await blade.render("home", { req, res });Request examples:
@req() <!-- Summary JSON of the request -->
@req("method") <!-- e.g., GET -->
@req("query.userId") <!-- from req.query -->
@req("headers.authorization") <!-- from req.headers -->
@req("cookies.sessionId", sid) <!-- assign to variable 'sid' -->
@req("params.id", id, "0") <!-- assign or use fallback "0" -->Response examples:
@res() <!-- Summary JSON of the response -->
@res(statusCode) <!-- 200, 404, ... -->
@res("headers.content-type") <!-- a specific header -->
@res(locals.user, currentUser) <!-- assign res.locals.user to variable -->Locals examples (res.locals only):
@locals() <!-- JSON of res.locals -->
@locals("user.name") <!-- John Doe -->
@locals(user, currentUser) <!-- assign to variable -->
@locals("settings.theme", theme, "light")Variable assignment behavior (applies to @req, @res, @locals):
- If a second argument is provided, the directive assigns the computed value to that variable name in the template context and current scope, and returns an empty string.
- If only the first argument is provided, the directive returns the computed value.
Practical Example
Here's how these directives work together in a real template:
<!-- User profile template -->
<div class="user-profile">
<!-- Safe display with fallback -->
<h1>@show('user.name', 'Anonymous User')</h1>
<!-- Conditional display -->
<p class="bio">@showIfSet('user.bio', 'No bio available')</p>
<!-- Debug logging (removed in production) -->
@log('user', 'User data not found')
<!-- Nested property access -->
<img src="@show('user.avatar.url', '/default-avatar.png')"
alt="@show('user.avatar.alt', 'User avatar')">
<!-- Array access with fallback -->
<div class="stats">
<span>Posts: @show('user.posts.length', '0')</span>
<span>Followers: @show('user.followers.count', '0')</span>
</div>
<!-- Environment-based configuration -->
<div class="debug-info">
@if(env("DEBUG") === "true")
<p>Debug mode: @log('user.id', 'No user ID')</p>
@endif
</div>
</div>Key Benefits:
- Null Safety: No more undefined/null errors
- Clean Fallbacks: Graceful degradation with default values
- Debugging: Easy server-side logging for development
- Nested Access: Safe navigation through object properties and arrays
Embedding Data
Safely embed server data in client-side JavaScript:
<script>
// Safely embed server data
const user = @json(user);
const appConfig = @json(config);
const csrfToken = @json(csrfToken);
// Initialize client-side application
window.App = {
user: user,
config: appConfig,
csrfToken: csrfToken
};
console.$$log('App initialized:', window.App);
</script>🛡️ Security
blade-ts prioritizes security with multiple layers of protection:
HTML Escaping
{{ ... }}- Automatically escapes HTML entities (&,<,>,",',`){!! ... !!}- Raw output (use only with trusted content)
Sandboxed Execution
- Templates run in a sandboxed
with(ctx)scope using JavaScript proxies - Unknown identifiers safely evaluate to
undefined - Enables safe use of nullish coalescing (
??) operators
Best Practices
<!-- ✅ Safe - automatically escaped -->
<p>{{ userInput }}</p>
<!-- ⚠️ Dangerous - only use with trusted content -->
<div>{!! trustedHtmlContent !!}</div>
<!-- ✅ Safe - fallback for undefined values -->
<h1>{{ pageTitle ?? "Default Title" }}</h1>🔧 API Reference
Engine Class
The main entry point for blade-ts:
import { Engine } from "blade-ts";
// Basic usage
const engine = new Engine("./templates");
const html = await engine.render("home.blade", { user: { name: "John" } });
// Advanced configuration
const engine = new Engine("./views", {
ext: "blade", // Default file extension
cache: true, // Enable template caching
debug: false, // Include debug comments
globals: {
// Global variables available in all templates
appName: "My App",
formatDate: (date: Date) => date.toLocaleDateString(),
version: "1.0.0",
},
minify: true, // Minify output
envPath: ".env", // Load environment variables
});CompileOptions
| Option | Type | Description |
| ----------- | --------------------------- | -------------------------------------------------------- |
| cache | boolean | Cache compiled template functions for better performance |
| debug | boolean | Include compiled source comments in output |
| sourceMap | boolean | Generate source maps (reserved for future use) |
| loader | (path, parent?) => string | Custom file loader function |
| filename | string | Current file path for relative includes |
| ext | string | Default template extension (e.g., blade, bjs, bts) |
| globals | Record<string, any> | Global variables available in all templates |
| envPath | string | Path to .env file to load |
| minify | boolean \| MinifyOptions | Output minification settings |
MinifyOptions
interface MinifyOptions {
trimTrailingWhitespace?: boolean; // Remove trailing whitespace
collapseBlankLines?: boolean; // Collapse multiple blank lines
maxConsecutiveNewlines?: number; // Maximum consecutive newlines
collapseWhitespaceBetweenTags?: boolean; // Remove whitespace between tags
}Usage Examples
// 1. Default extension setup
const engine = new Engine("./templates", { ext: "blade" });
await engine.render("home"); // Automatically resolves to home.blade
// 2. Global helpers and constants
const engine = new Engine("./views", {
globals: {
appName: "My Awesome App",
formatCurrency: (amount: number) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount),
isProduction: process.env.NODE_ENV === "production",
},
});
// 3. Custom file loader (e.g., for webpack or bundlers)
const engine = new Engine("./templates", {
loader: (path: string) => {
// Custom loading logic
return fs.readFileSync(path, "utf8");
},
});
// 4. Minification configuration
const engine = new Engine("./templates", {
minify: {
trimTrailingWhitespace: true,
collapseBlankLines: true,
maxConsecutiveNewlines: 1,
collapseWhitespaceBetweenTags: true,
},
});Custom Directives
Extend blade-ts with your own directives:
import { registerDirective } from "blade-ts";
// Simple text transformation
registerDirective("uppercase", (args, ctx) => {
const val = new Function("__c", `with(__c){ return (${args}); }`)(ctx);
return String(val).toUpperCase();
});
// Date formatting directive
registerDirective("date", (args, ctx) => {
const [date, format = "YYYY-MM-DD"] = args.split(",").map((s) => s.trim());
const val = new Function("__c", `with(__c){ return (${date}); }`)(ctx);
return new Date(val).toLocaleDateString();
});Usage in templates:
<!-- Custom directives -->
<h1>@uppercase('hello world')</h1>
<p>Published: @date('article.publishDate', 'MM/DD/YYYY')</p>
<span>@if('user.isVip', 'VIP Member', 'Regular Member')</span>Built-in Directives:
@json(value, space?)- JSON serialization@env("KEY", fallback?)- Environment variables@googlefonts('Inter:400;700, Roboto:0')- Google Fonts integration@show('path', fallback?)- Safe value display with fallbacks@log('path', fallback?)- Debug logging to console@req(path?, varName?, fallback?)- Express request access@res(path?, varName?, fallback?)- Express response access@locals(path?, varName?, fallback?)- Express res.locals access
📚 Examples
Explore comprehensive examples in the examples/ directory:
# Build the project
npm run build
# Run examples
node examples/index.js
# or with TypeScript
tsx examples/index.tsExample templates include:
- Layout inheritance and sections
- Complex loops and conditionals
- Includes and partials
- Code blocks and data transformation
- Custom directives usage
🎨 Editor Support
Enhanced development experience with VS Code extension:
Features
- 🎨 Syntax Highlighting - Blade directives and embedded JS/TS
- 🔧 Code Formatting - Automatic indentation and whitespace management
- 🎯 IntelliSense - Smart completions for Blade syntax
- 📝 Emmet Support - HTML snippets work in
.blade/.bjs/.btsfiles - 💾 Format on Save - Automatic formatting for Blade files
Installation
# Build and package the extension
cd tools/blade-ts-x
npm run build && npx vsce package --allow-missing-repository
# Install in VS Code/Cursor
cursor --install-extension blade-ts-x-0.0.1.vsixSupported File Types
.blade- Standard Blade templates.bjs- Blade JavaScript templates.bts- Blade TypeScript templates
🚀 Advanced Topics
Nested Layouts
Create complex layout hierarchies with unlimited nesting depth:
<!-- home.blade -->
@extends("layouts/app.blade")
<!-- layouts/app.blade -->
@extends("layouts/base.blade")
<!-- layouts/base.blade -->
@extends("layouts/root.blade")Performance Optimization
- Enable template caching for production
- Use minification for smaller output
- Leverage global variables for shared data
TypeScript Integration (Not Implemented Yet!)
// Type-safe template data
interface TemplateData {
user: { name: string; email: string };
items: Array<{ id: number; name: string }>;
}
const html = await engine.render<TemplateData>("home.blade", {
user: { name: "John", email: "[email protected]" },
items: [{ id: 1, name: "Item 1" }],
});📄 License
MIT License - see LICENSE file for details.
