npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

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

npm version License: MIT TypeScript

✨ 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

🚀 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>
@endif

Loops

Array/Collection Iteration:

@foreach(item in items)
    <li>{{ item.name }}</li>
@endforeach

@foreach(key, value in object)
    <li>{{ key }}: {{ value }}</li>
@endforeach

Empty State Handling:

@forelse(item in items)
    <li>{{ item.name }}</li>
@empty
    <p class="empty-state">No items found</p>
@endforelse

Traditional Loops:

@for(let i = 0; i < 3; i++)
    <span>Item {{ i + 1 }}</span>
@endfor

@while(user.isActive)
    <div class="ping">PING</div>
@endwhile

Switch 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>
@endswitch

Includes

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>
@endpush

Key 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>
@endif

Features:

  • Variables and functions declared in @code are 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 weights
  • Family:0 - Load with browser default weights
  • Family - 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 varName and 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.ts

Example 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/.bts files
  • 💾 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.vsix

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