1.4.1

Configuration

Preview ecosystem guide: Asset Pipeline is not part of the Amber 2.0.0-beta.2 core web-app release gate. Its package version, API, and platform support may change independently. Do not add a personal fork as a default dependency.

This guide covers Asset Pipeline configuration options, environment setup, and deployment best practices.

FrontLoader Options

Basic Configuration

front_loader = AssetPipeline::FrontLoader.new(
  js_source_path: Path["src/javascript"],
  js_output_path: Path["public/javascript"]
) do |import_maps|
  # Import map configuration
end

Configuration Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | js_source_path | Path | - | Source directory for JavaScript files | | js_output_path | Path | - | Output directory for processed files | | clear_cache_upon_change | Bool | true | Auto-clear cache when files change |

Cache Clearing

# Automatic cache clearing (default)
front_loader = AssetPipeline::FrontLoader.new(
  js_source_path: Path["src/javascript"],
  js_output_path: Path["public/javascript"]
)

# Disable for debugging
front_loader = AssetPipeline::FrontLoader.new(
  js_source_path: Path["src/javascript"],
  js_output_path: Path["public/javascript"],
  clear_cache_upon_change: false
)

Directory Structure

Recommended Layout

my_app/
├── src/
│   └── javascript/
│       ├── controllers/
│       │   ├── application_controller.js
│       │   ├── dropdown_controller.js
│       │   └── modal_controller.js
│       ├── services/
│       │   └── api_service.js
│       └── utils/
│           └── helpers.js
├── public/
│   └── javascript/
│       └── (generated files)
└── config/
    └── initializers/
        └── assets.cr

Initializer Setup

# config/initializers/assets.cr
require "asset_pipeline"

JS_SOURCE_PATH = Path["src/javascript"]
JS_OUTPUT_PATH = Path["public/javascript"]

FRONT_LOADER = AssetPipeline::FrontLoader.new(
  js_source_path: JS_SOURCE_PATH,
  js_output_path: JS_OUTPUT_PATH
) do |import_maps|
  import_map = AssetPipeline::ImportMap.new("application", Path["/javascript"])

  # Stimulus
  import_map.add_import(
    "@hotwired/stimulus",
    "https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/+esm",
    preload: true
  )

  # Controllers
  Dir.glob("#{JS_SOURCE_PATH}/controllers/*_controller.js").each do |file|
    name = File.basename(file, ".js").split("_").map(&.capitalize).join
    import_map.add_import(name, "controllers/#{File.basename(file)}")
  end

  import_maps << import_map
end

Environment Configuration

Development vs Production

ENVIRONMENT = ENV["AMBER_ENV"]? || "development"

FRONT_LOADER = AssetPipeline::FrontLoader.new(
  js_source_path: Path["src/javascript"],
  js_output_path: Path["public/javascript"],
  clear_cache_upon_change: ENVIRONMENT == "development"
) do |import_maps|
  import_map = AssetPipeline::ImportMap.new("application", Path["/javascript"])

  # Always include Stimulus
  import_map.add_import(
    "@hotwired/stimulus",
    "https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/+esm",
    preload: true
  )

  # Environment-specific configuration
  case ENVIRONMENT
  when "development"
    # Development tools
    import_map.add_import("DebugController", "controllers/debug_controller.js")
  when "production"
    # Production analytics
    import_map.add_import("AnalyticsController", "controllers/analytics_controller.js")
  end

  import_maps << import_map
end

Feature Flags

FEATURES = {
  "new_checkout"       => ENV["FEATURE_NEW_CHECKOUT"]? == "true",
  "advanced_analytics" => ENV["FEATURE_ANALYTICS"]? == "true"
}

FRONT_LOADER = AssetPipeline::FrontLoader.new(...) do |import_maps|
  import_map = AssetPipeline::ImportMap.new("application", Path["/javascript"])

  # Base imports
  import_map.add_import("@hotwired/stimulus", "...", preload: true)

  # Feature-flagged imports
  if FEATURES["new_checkout"]
    import_map.add_import("NewCheckoutController", "controllers/new_checkout_controller.js")
    import_map.add_import("stripe", "https://js.stripe.com/v3/")
  else
    import_map.add_import("LegacyCheckoutController", "controllers/legacy_checkout_controller.js")
  end

  if FEATURES["advanced_analytics"]
    import_map.add_import("chart.js", "https://cdn.jsdelivr.net/npm/[email protected]/+esm", preload: true)
    import_map.add_import("AnalyticsController", "controllers/analytics_controller.js")
  end

  import_maps << import_map
end

CDN Configuration

Using Subresource Integrity (SRI)

For production, consider using SRI hashes:

# Add integrity checks for CDN resources
import_map.add_import(
  "@hotwired/stimulus",
  "https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/+esm"
)

# In your layout, add integrity attribute manually if needed
# <script src="..." integrity="sha384-..." crossorigin="anonymous">

Fallback Strategy

production_js = <<-JS
  // CDN fallback strategy
  window.loadWithFallback = async (primary, fallback) => {
    try {
      return await import(primary);
    } catch (e) {
      console.warn(`Failed to load ${primary}, trying fallback`);
      return await import(fallback);
    }
  };
JS

FRONT_LOADER.render_initialization_script(production_js)

Layout Integration

ECR Template

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title><%= page_title %></title>
    <%= FRONT_LOADER.render_import_map_tag %>
    <link rel="stylesheet" href="/css/application.css">
  </head>
  <body>
    <header><%= render_partial "layouts/_navigation" %></header>
    <main><%= content %></main>
    <footer><%= render_partial "layouts/_footer" %></footer>
  </body>
</html>

    / Stimulus initialization at end of body
    == FRONT_LOADER.render_stimulus_initialization_script

ECR Template

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title><%= page_title %></title>

  <%= FRONT_LOADER.render_import_map_tag %>
  <link rel="stylesheet" href="/css/application.css">
</head>
<body>
  <main>
    <%= content %>
  </main>

  <%= FRONT_LOADER.render_stimulus_initialization_script %>
</body>
</html>

Deployment

Docker Configuration

FROM crystallang/crystal:latest

WORKDIR /app

# Copy source
COPY . .

# Install dependencies
RUN shards install --production

# Build application
RUN crystal build src/app.cr -o bin/app --release

# JavaScript files are served directly (no build step needed)
# public/javascript/ contains your source files

EXPOSE 3000
CMD ["./bin/app"]

Nginx Configuration

server {
    listen 80;
    server_name myapp.com;

    # Serve static JavaScript with caching
    location /javascript/ {
        alias /var/www/myapp/public/javascript/;
        expires 1y;
        add_header Cache-Control "public, immutable";

        # Enable CORS for CDN resources
        add_header Access-Control-Allow-Origin *;
    }

    # Proxy to Crystal app
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

CI/CD Pipeline

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Install Crystal
        uses: crystal-lang/install-crystal@v1

      - name: Install dependencies
        run: shards install --production

      - name: Build
        run: crystal build src/app.cr -o bin/app --release

      - name: Deploy
        run: |
          # JavaScript files are ready to serve
          # No npm/webpack build step needed
          rsync -avz public/ $DEPLOY_HOST:/var/www/myapp/public/
          rsync -avz bin/app $DEPLOY_HOST:/var/www/myapp/bin/

Performance Tips

1. Preload Critical Resources

# Preload essential libraries
import_map.add_import("@hotwired/stimulus", "...", preload: true)
import_map.add_import("ApplicationController", "...", preload: true)

# Don't preload optional features
import_map.add_import("ChartController", "...")  # No preload

2. Use CDN for Libraries

# Good: CDN with global caching
import_map.add_import("lodash", "https://cdn.jsdelivr.net/npm/[email protected]/+esm")

# Less optimal: Self-hosted (unless you have specific requirements)
import_map.add_import("lodash", "/vendor/lodash.js")

3. Lazy Load Non-Critical Features

// Load chart library only when needed
export default class extends Controller {
  async showChart() {
    const { Chart } = await import("chart.js/auto")
    new Chart(this.element, this.config)
  }
}

4. Monitor Bundle Size

Keep track of what you're importing. ESM modules from CDNs are tree-shakeable, but be mindful of large dependencies.