Skip to content

Portable Recipe Examples

This page shows practical examples of portable recipes - JavaScript recipes executed in Fabrik's embedded QuickJS runtime with built-in caching support.

What Are Portable Recipes?

Portable recipes are JavaScript files that:

  • ✅ Run in Fabrik's embedded runtime (no Node.js/Deno installation needed)
  • ✅ Have access to Fabrik cache APIs (fabrik:cache, fabrik:kv, fabrik:fs)
  • ✅ Can be distributed via Git repositories using @ syntax
  • ✅ Support content-addressed caching with FABRIK annotations
  • ✅ Start instantly (~1ms vs ~50ms for Node.js)

All examples demonstrate how to leverage Fabrik's content-addressed caching to speed up repetitive build tasks.

TypeScript Compilation Cache

Cache TypeScript compilation to avoid rebuilding unchanged code.

Repository: @tuist/recipes/typescript-build.js@v1.0.0

javascript
// typescript-build.js
// FABRIK input "src/**/*.ts"
// FABRIK input "tsconfig.json"
// FABRIK input "package.json"
// FABRIK output "dist/"
// FABRIK env "NODE_ENV"

import { spawn } from 'child_process';
import { glob } from 'fabrik:fs';

console.log("Compiling TypeScript...");

const result = await spawn("tsc", ["--build"]);
if (result.exitCode !== 0) {
    throw new Error("TypeScript compilation failed");
}

const distFiles = await glob("dist/**/*");
console.log(`Generated ${distFiles.length} output files`);

Cache Behavior:

  • ✅ Cache hits when source files unchanged
  • ✅ Invalidates when tsconfig.json changes
  • ✅ Separate caches for NODE_ENV=production vs development

Usage:

bash
# First run: builds and caches (2.5s)
fabrik run @tuist/recipes/typescript-build.js@v1.0.0

# Second run: cache hit, restores dist/ instantly (0.1s)
fabrik run @tuist/recipes/typescript-build.js@v1.0.0

Test Suite Cache

Cache test runs to skip running tests when code hasn't changed.

Repository: @company/ci/test.js@v1.0.0

javascript
// test.js
// FABRIK input "src/**/*.{ts,tsx}"
// FABRIK input "tests/**/*.test.{ts,tsx}"
// FABRIK input "package.json"
// FABRIK input "jest.config.js"
// FABRIK output "coverage/"
// FABRIK env "CI"

import { spawn } from 'child_process';

console.log("Running test suite...");

const result = await spawn("npm", ["test", "--", "--coverage"]);
if (result.exitCode !== 0) {
    throw new Error("Tests failed");
}

console.log("All tests passed! Coverage report saved to coverage/");

Cache Behavior:

  • ✅ Skips test runs when source and test files unchanged
  • ✅ Restores previous coverage reports instantly
  • ✅ Separate cache for CI vs local (via CI env var)

Usage:

bash
# First run: executes full test suite (45s)
fabrik run @company/ci/test.js@v1.0.0

# Subsequent runs with no changes: instant (0.1s)
fabrik run @company/ci/test.js@v1.0.0
# Cache key: script-a8f3c9e2 | HIT ✓ | 0.10s (exit: 0)

# After changing a test file: cache miss, re-runs tests
echo "// new test" >> tests/app.test.ts
fabrik run @company/ci/test.js@v1.0.0
# Cache key: script-b9f4d0e3 | MISS ✗ | 45.2s (exit: 0)

Asset Processing Cache

Cache expensive asset transformations like image optimization.

Repository: @tuist/recipes/optimize-images.js@v1.0.0

javascript
// optimize-images.js
// FABRIK input "assets/images/**/*.{png,jpg,jpeg}"
// FABRIK output "public/images/"
// FABRIK cache ttl="30d"

import { spawn } from 'child_process';
import { glob } from 'fabrik:fs';

console.log("Optimizing images...");

const images = await glob("assets/images/**/*.{png,jpg,jpeg}");
console.log(`Found ${images.length} images to optimize`);

// Use imagemagick to optimize and resize
for (const img of images) {
    const outputPath = img.replace('assets/images', 'public/images');
    await spawn("convert", [
        img,
        "-resize", "1920x1920>",
        "-quality", "85",
        outputPath
    ]);
}

console.log(`Optimized ${images.length} images → public/images/`);

Cache Behavior:

  • ✅ Only reprocesses images when source files change
  • ✅ 30-day TTL keeps cache fresh
  • ✅ Saves minutes on large image sets

Performance:

bash
# First run: processes 150 images (2m 15s)
fabrik run @tuist/recipes/optimize-images.js@v1.0.0

# Subsequent runs: restores optimized images (0.2s)
fabrik run @tuist/recipes/optimize-images.js@v1.0.0

# After adding 5 new images: only processes new ones
# (In practice, recipe runs on all images, but cache invalidates the whole set)

Docker Build Cache

Cache Docker builds to avoid rebuilding identical images.

Repository: @tuist/recipes/docker-build.js@v1.0.0

javascript
// docker-build.js
// FABRIK input "Dockerfile"
// FABRIK input "src/**/*"
// FABRIK input "package.json"
// FABRIK input "package-lock.json"
// FABRIK output ".docker-cache/image.tar"
// FABRIK env "DOCKER_TAG"

import { spawn } from 'child_process';
import { existsSync } from 'fs';

console.log("Building Docker image...");

const tag = process.env.DOCKER_TAG || "myapp:latest";

// Check if cached image exists
if (existsSync(".docker-cache/image.tar")) {
    console.log("Loading cached Docker image...");
    await spawn("docker", ["load", "-i", ".docker-cache/image.tar"]);
    await spawn("docker", ["tag", "myapp:cached", tag]);
    console.log(`Loaded cached image as ${tag}`);
} else {
    console.log(`Building fresh image: ${tag}`);
    const result = await spawn("docker", [
        "build",
        "-t", tag,
        "."
    ]);

    if (result.exitCode !== 0) {
        throw new Error("Docker build failed");
    }

    // Save image to cache
    await spawn("docker", ["save", "-o", ".docker-cache/image.tar", tag]);
    console.log(`Built and cached image: ${tag}`);
}

Cache Behavior:

  • ✅ Skips Docker build when Dockerfile and source unchanged
  • ✅ Different cache for each DOCKER_TAG value
  • ✅ Saves 5-10 minutes on large images

Usage:

bash
# First build: runs docker build (8m 30s)
DOCKER_TAG=myapp:v1.2.3 fabrik run @tuist/recipes/docker-build.js@v1.0.0

# Subsequent builds: loads from cache (15s)
DOCKER_TAG=myapp:v1.2.3 fabrik run @tuist/recipes/docker-build.js@v1.0.0
# Cache key: script-f8a3d9c2 | HIT ✓ | 15.2s (exit: 0)

Dependency Installation Cache

Cache node_modules to avoid reinstalling dependencies.

Repository: @tuist/recipes/npm-install.js@v1.0.0

javascript
// npm-install.js
// FABRIK input "package.json"
// FABRIK input "package-lock.json"
// FABRIK output "node_modules/"
// FABRIK cache ttl="7d"

import { spawn } from 'child_process';
import { existsSync } from 'fs';

console.log("Installing dependencies...");

// Fabrik will restore node_modules/ if cache hits
if (!existsSync("node_modules")) {
    const result = await spawn("npm", ["ci"]);
    if (result.exitCode !== 0) {
        throw new Error("npm install failed");
    }
    console.log("Dependencies installed successfully");
} else {
    console.log("Dependencies restored from cache");
}

Cache Behavior:

  • ✅ Skips npm ci when package-lock.json unchanged
  • ✅ 7-day TTL ensures fresh dependencies
  • ✅ Massive time savings in CI

Performance:

bash
# First run: runs npm ci (45s)
fabrik run @tuist/recipes/npm-install.js@v1.0.0

# Subsequent runs: restores node_modules/ (2s)
fabrik run @tuist/recipes/npm-install.js@v1.0.0
# Cache key: script-c8f9a3e2 | HIT ✓ | 2.1s (exit: 0)

# After updating a dependency
npm install lodash@latest
fabrik run @tuist/recipes/npm-install.js@v1.0.0
# Cache key: script-d9f0b4e3 | MISS ✗ | 48.3s (exit: 0)

Code Generation Cache

Cache generated code from protobuf, GraphQL schemas, or OpenAPI specs.

Repository: @tuist/recipes/codegen.js@v1.0.0

javascript
// codegen.js
// FABRIK input "schema/**/*.proto"
// FABRIK input "codegen.config.js"
// FABRIK output "generated/"
// FABRIK cache ttl="30d"

import { spawn } from 'child_process';
import { glob } from 'fabrik:fs';

console.log("Running code generation...");

const protoFiles = await glob("schema/**/*.proto");
console.log(`Found ${protoFiles.length} proto files`);

// Generate TypeScript code from protobuf
const result = await spawn("protoc", [
    "--plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts",
    "--ts_out=generated/",
    ...protoFiles
]);

if (result.exitCode !== 0) {
    throw new Error("Code generation failed");
}

const generatedFiles = await glob("generated/**/*.ts");
console.log(`Generated ${generatedFiles.length} TypeScript files`);

Cache Behavior:

  • ✅ Skips codegen when schemas unchanged
  • ✅ Long TTL (30 days) for stable schemas
  • ✅ Instant builds when schema stable

Performance:

bash
# First run: runs protoc (12s)
fabrik run @tuist/recipes/codegen.js@v1.0.0
# Cache key: script-e8a9f3c2 | MISS ✗ | 12.4s (exit: 0)

# Subsequent runs: restores generated/ (0.3s)
fabrik run @tuist/recipes/codegen.js@v1.0.0
# Cache key: script-e8a9f3c2 | HIT ✓ | 0.30s (exit: 0)

# After updating schema
echo "message NewMessage {}" >> schema/api.proto
fabrik run @tuist/recipes/codegen.js@v1.0.0
# Cache key: script-f9b0a4d3 | MISS ✗ | 12.8s (exit: 0)

Multi-Step Build Pipeline with Dependencies

Chain multiple cached recipes together for complex builds.

Repository: @company/ci/full-build.js@v1.0.0

javascript
// full-build.js
// FABRIK depends "@company/ci/install-deps.js@v1.0.0" use-outputs=true
// FABRIK depends "@company/ci/lint.js@v1.0.0"
// FABRIK depends "@company/ci/test.js@v1.0.0" use-outputs=true
// FABRIK depends "@company/ci/build.js@v1.0.0" use-outputs=true
// FABRIK output "build/"
// FABRIK output "coverage/"

console.log("Running full CI pipeline...");
console.log("All dependent steps completed successfully");
console.log("Build artifacts ready in build/");
console.log("Coverage report available in coverage/");

Dependency recipes:

javascript
// install-deps.js
// FABRIK input "package.json"
// FABRIK input "package-lock.json"
// FABRIK output "node_modules/"
import { spawn } from 'child_process';
await spawn("npm", ["ci"]);
javascript
// lint.js
// FABRIK input "src/**/*.ts"
// FABRIK input ".eslintrc.js"
import { spawn } from 'child_process';
const result = await spawn("npm", ["run", "lint"]);
if (result.exitCode !== 0) throw new Error("Linting failed");
javascript
// test.js
// FABRIK input "src/**/*.ts"
// FABRIK input "tests/**/*.test.ts"
// FABRIK output "coverage/"
import { spawn } from 'child_process';
await spawn("npm", ["test", "--", "--coverage"]);
javascript
// build.js
// FABRIK input "src/**/*.ts"
// FABRIK input "tsconfig.json"
// FABRIK output "build/"
import { spawn } from 'child_process';
await spawn("npm", ["run", "build"]);

Cache Behavior:

  • ✅ Each step caches independently
  • ✅ Unchanged steps skip execution instantly
  • ✅ Only changed steps re-run
  • use-outputs=true shares artifacts between steps

Example Workflow:

bash
# First run: all steps execute
fabrik run @company/ci/full-build.js@v1.0.0
# [fabrik] Running: install-deps.js (45s)
# [fabrik] Running: lint.js (8s)
# [fabrik] Running: test.js (32s)
# [fabrik] Running: build.js (28s)
# Total: 113s

# Second run: all cached
fabrik run @company/ci/full-build.js@v1.0.0
# [fabrik] Cache HIT: install-deps.js (0.2s)
# [fabrik] Cache HIT: lint.js (0.1s)
# [fabrik] Cache HIT: test.js (0.3s)
# [fabrik] Cache HIT: build.js (0.2s)
# Total: 0.8s

# After changing a test file
echo "new test" >> tests/app.test.ts
fabrik run @company/ci/full-build.js@v1.0.0
# [fabrik] Cache HIT: install-deps.js (0.2s)
# [fabrik] Cache HIT: lint.js (0.1s)
# [fabrik] Running: test.js (34s)  ← Only this re-runs
# [fabrik] Cache HIT: build.js (0.2s)
# Total: 34.5s

Best Practices for Cache-Optimized Recipes

1. Declare All Inputs Explicitly

javascript
// ✅ Good - explicit inputs ensure proper cache invalidation
// FABRIK input "src/**/*.ts"
// FABRIK input "tsconfig.json"
// FABRIK input "package.json"

// ❌ Bad - missing inputs cause stale cache hits
// (no input declarations)

2. Use Appropriate TTLs

javascript
// Fast-changing data: short TTL
// FABRIK cache ttl="1h"

// Stable artifacts: longer TTL
// FABRIK cache ttl="30d"

// Build outputs: no TTL (invalidate by input hash)
// (no TTL declaration)

3. Track Environment Variables

javascript
// ✅ Good - separate caches for different environments
// FABRIK env "NODE_ENV"
// FABRIK env "BUILD_TARGET"

// Cache keys will differ for:
// NODE_ENV=development vs NODE_ENV=production
// BUILD_TARGET=web vs BUILD_TARGET=mobile

4. Pin Versions in Production

bash
# ✅ Production - pin to specific version
fabrik run @tuist/recipes/build.js@v1.2.3

# ⚠️ Development - can use branch
fabrik run @tuist/recipes/build.js@main

# ❌ Production - never use mutable refs
fabrik run @tuist/recipes/build.js@latest

5. Output Only What's Needed

javascript
// ✅ Good - only cache essential outputs
// FABRIK output "dist/"
// FABRIK output "coverage/summary.json"

// ❌ Bad - caching unnecessary files wastes storage
// FABRIK output "dist/"
// FABRIK output "node_modules/"  // Too large, change frequently
// FABRIK output "*.log"          // Not needed

6. Use Dependencies for Build Steps

javascript
// ✅ Good - chain steps with dependencies
// FABRIK depends "@company/ci/install.js@v1.0.0" use-outputs=true
// FABRIK depends "@company/ci/lint.js@v1.0.0"
// FABRIK depends "@company/ci/build.js@v1.0.0" use-outputs=true

// Each step caches independently
// Only changed steps re-run

Cache Performance Comparison

ScenarioWithout FabrikWith Fabrik (Cache Hit)Speedup
TypeScript build2.5s0.1s25x
Test suite (300 tests)45s0.1s450x
Image optimization (150 images)2m 15s0.2s675x
Docker build8m 30s15s (load)34x
npm install45s2s (restore)22x
Protobuf codegen12s0.3s40x
Full CI pipeline113s0.8s141x

Note: Cache hit times include downloading from remote cache (if using Layer 2/3). Local cache hits are even faster (<0.1s).


Publishing Your Own Recipes

Step 1: Create Repository

bash
mkdir my-recipes
cd my-recipes
git init

Step 2: Add Recipe Files with Cache Annotations

bash
# Create recipe with proper caching
cat > build.js << 'EOF'
// FABRIK input "src/**/*.ts"
// FABRIK input "tsconfig.json"
// FABRIK output "dist/"
// FABRIK env "NODE_ENV"

import { spawn } from 'child_process';

console.log("Building...");
await spawn("npm", ["run", "build"]);
console.log("Build complete!");
EOF

git add build.js
git commit -m "Add build recipe with caching"

Step 3: Push to Git

bash
# Push to GitHub
git remote add origin https://github.com/myorg/my-recipes.git
git push -u origin main

# Tag a version
git tag v1.0.0
git push --tags

Step 4: Use Your Recipe

bash
# Use from anywhere
fabrik run @myorg/my-recipes/build.js@v1.0.0

# First run: builds and caches
# Second run: instant cache hit

Next Steps