Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 66 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,75 @@ end
```

**Options:**
- `bake_folder(path, dir = __DIR__, allow_empty: false, include_dotfiles: false, max_size: nil)` - Bake all files in a directory
- `bake_folder(path, dir = __DIR__, allow_empty: false, include_dotfiles: false, include_patterns: nil, exclude_patterns: nil, max_size: nil)` - Bake all files in a directory
- `include_dotfiles: true` - Include files/folders starting with `.` (e.g., `.gitignore`)
- `allow_empty: false` - Raise error if folder is empty
- `include_patterns: Array(String)` - Include only files matching glob patterns
- `exclude_patterns: Array(String)` - Exclude files matching glob patterns
- `max_size: Int64` - Maximum total compressed size in bytes (compilation fails if exceeded)

### File Filtering

Use glob patterns to selectively include or exclude files when baking directories.

**Pattern Syntax:**
- `*` - Matches any characters except path separator (e.g., `*.cr` matches `file.cr`)
- `**` - Matches zero or more directory levels (e.g., `**/*.cr` matches `src/file.cr`, `src/models/user.cr`)
- `?` - Matches single character except path separator (e.g., `file?.txt` matches `file1.txt`)

**Include Patterns** (whitelist approach):

```crystal
class Assets
extend BakedFileSystem

# Only include Crystal source files
bake_folder "./src", include_patterns: ["**/*.cr"]

# Include multiple file types
bake_folder "./docs", include_patterns: ["**/*.md", "**/*.txt"]
end
```

**Exclude Patterns** (blacklist approach):

```crystal
class Assets
extend BakedFileSystem

# Exclude test and spec files
bake_folder "./project", exclude_patterns: ["**/test/*", "**/spec/*"]

# Exclude build artifacts and temporary files
bake_folder "./app", exclude_patterns: ["**/build/*", "**/*.tmp", "**/*.log"]
end
```

**Combined Filtering** (include first, then exclude):

```crystal
class Assets
extend BakedFileSystem

# Include only source files, but exclude test files
bake_folder "./src",
include_patterns: ["**/*.cr", "**/*.md"],
exclude_patterns: ["**/test/*", "**/*_spec.cr"]
end
```

**Important Notes:**
- Patterns are relative to the baked directory (not absolute paths)
- Include patterns are applied first (OR logic - match any pattern)
- Exclude patterns are then applied (OR logic - exclude if matches any pattern)
- Empty results raise error unless `allow_empty: true`

**Use Cases:**
- Embed only production assets (exclude dev/test files)
- Include specific file types (source code, documentation)
- Exclude large or generated files (builds, logs, cache)
- Filter by directory structure (exclude vendor, node_modules)

### Loading Files

```crystal
Expand Down Expand Up @@ -126,7 +190,7 @@ crystal build your_app.cr
- **Keep total embedded size under 50 MB** for reasonable binary sizes
- **Use runtime loading for very large assets** (> 10 MB per file)
- **Review compilation statistics** to catch accidentally embedded files
- **Use `.gitignore`-style patterns** (future feature) to exclude build artifacts
- **Use file filtering patterns** to exclude build artifacts, tests, and development files
- **Monitor benchmark results** to understand compile time and binary size impact

### Advanced
Expand Down
4 changes: 2 additions & 2 deletions benchmarks/binary_size.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ require "json"

module BinarySizeBenchmark
BASELINE_DIR = File.expand_path("baseline", __DIR__)
BAKED_DIR = File.expand_path("baked", __DIR__)
PUBLIC_DIR = File.expand_path("public", __DIR__)
BAKED_DIR = File.expand_path("baked", __DIR__)
PUBLIC_DIR = File.expand_path("public", __DIR__)
RESULTS_FILE = File.expand_path("results/binary_size.json", __DIR__)

struct BinaryInfo
Expand Down
4 changes: 2 additions & 2 deletions benchmarks/compile_time.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ require "file_utils"
# Tests compilation time overhead of embedding assets

module CompileTimeBenchmark
ITERATIONS = 5
ITERATIONS = 5
BASELINE_DIR = File.expand_path("baseline", __DIR__)
BAKED_DIR = File.expand_path("baked", __DIR__)
BAKED_DIR = File.expand_path("baked", __DIR__)
RESULTS_FILE = File.expand_path("results/compile_time.json", __DIR__)

struct CompileResult
Expand Down
10 changes: 5 additions & 5 deletions benchmarks/memory.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ require "http/client"
# Measures RSS (Resident Set Size) at various stages

module MemoryBenchmark
BASELINE_DIR = File.expand_path("baseline", __DIR__)
BAKED_DIR = File.expand_path("baked", __DIR__)
RESULTS_FILE = File.expand_path("results/memory.json", __DIR__)
BASELINE_DIR = File.expand_path("baseline", __DIR__)
BAKED_DIR = File.expand_path("baked", __DIR__)
RESULTS_FILE = File.expand_path("results/memory.json", __DIR__)
BASELINE_PORT = 3000
BAKED_PORT = 3001
WARMUP_DELAY = 2 # seconds to wait for server startup
BAKED_PORT = 3001
WARMUP_DELAY = 2 # seconds to wait for server startup

struct MemoryProfile
include JSON::Serializable
Expand Down
14 changes: 7 additions & 7 deletions benchmarks/performance.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ require "http/client"
# Measures request latency and throughput

module PerformanceBenchmark
BASELINE_DIR = File.expand_path("baseline", __DIR__)
BAKED_DIR = File.expand_path("baked", __DIR__)
RESULTS_FILE = File.expand_path("results/performance.json", __DIR__)
BASELINE_PORT = 3000
BAKED_PORT = 3001
WARMUP_REQUESTS = 100
BASELINE_DIR = File.expand_path("baseline", __DIR__)
BAKED_DIR = File.expand_path("baked", __DIR__)
RESULTS_FILE = File.expand_path("results/performance.json", __DIR__)
BASELINE_PORT = 3000
BAKED_PORT = 3001
WARMUP_REQUESTS = 100
BENCHMARK_REQUESTS = 1000
CONCURRENT_CLIENTS = 10
CONCURRENT_CLIENTS = 10

struct LatencyStats
include JSON::Serializable
Expand Down
2 changes: 1 addition & 1 deletion examples/threshold_example.cr
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module Assets
# - medium.bin: 50KB → ~50KB compressed
# - large.bin: 100KB → ~100KB compressed
# Total: ~150KB compressed
bake_folder path: "./assets", max_size: 51_200 # 50 KB limit - will fail!
bake_folder path: "./assets", max_size: 51_200 # 50 KB limit - will fail!
end

# This code will never execute because compilation fails above
Expand Down
94 changes: 92 additions & 2 deletions spec/baked_file_system_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,37 @@ class EdgeCaseStorage
bake_folder "./storage_edge_cases"
end

# Test storage with include patterns - only .cr files
class FilteredStorageInclude
extend BakedFileSystem
bake_folder "./storage/filters", include_patterns: ["**/*.cr"]
end

# Test storage with exclude patterns - exclude test directory
class FilteredStorageExclude
extend BakedFileSystem
bake_folder "./storage/filters", exclude_patterns: ["**/test/*"]
end

# Test storage with both include and exclude patterns
class FilteredStorageCombined
extend BakedFileSystem
bake_folder "./storage/filters", include_patterns: ["**/*.cr", "**/*.md"], exclude_patterns: ["**/test/*"]
end

# Test storage with patterns that result in empty set (unless allow_empty)
class FilteredStorageEmpty
extend BakedFileSystem
bake_folder "./storage/filters", include_patterns: ["**/*.txt"], allow_empty: true
end

# This should raise an error at compile time - patterns match nothing and allow_empty is false
# Commented out because it would prevent compilation
# class FilteredStorageEmptyError
# extend BakedFileSystem
# bake_folder "./storage/filters", include_patterns: ["**/*.txt"]
# end

def read_slice(path)
File.open(path, "rb") do |io|
Slice(UInt8).new(io.size).tap do |buf|
Expand All @@ -53,7 +84,7 @@ end

describe BakedFileSystem do
it "load only files without hidden one" do
Storage.files.size.should eq(4)
Storage.files.size.should eq(10) # lorem.txt, images/sidekiq.png, string_encoding/*, filters/*
Storage.get?(".hidden/hidden_file.txt").should be_nil
end

Expand Down Expand Up @@ -115,7 +146,7 @@ describe BakedFileSystem do
end

it "handles interpolation in content" do
String.new(Storage.get("string_encoding/interpolation.gz").to_slice).should eq "\#{foo} \{% macro %}\n"
String.new(Storage.get("string_encoding/interpolation.gz").to_slice).should eq "\#{foo} {% macro %}\n"
end

describe "rewind functionality" do
Expand Down Expand Up @@ -580,4 +611,63 @@ describe BakedFileSystem do
file.not_nil!.closed?.should be_true
end
end

describe "file filtering" do
it "includes only files matching include patterns" do
# Should only have .cr files
FilteredStorageInclude.files.size.should eq(4) # src/main.cr, src/lib.cr, test/spec.cr, test/helper.cr
FilteredStorageInclude.get?("src/main.cr").should_not be_nil
FilteredStorageInclude.get?("src/lib.cr").should_not be_nil
FilteredStorageInclude.get?("test/spec.cr").should_not be_nil
FilteredStorageInclude.get?("test/helper.cr").should_not be_nil
FilteredStorageInclude.get?("docs/README.md").should be_nil
FilteredStorageInclude.get?("config.yml").should be_nil
end

it "excludes files matching exclude patterns" do
# Should have everything except test/* files
FilteredStorageExclude.files.size.should eq(4) # src/*, docs/*, config.yml
FilteredStorageExclude.get?("src/main.cr").should_not be_nil
FilteredStorageExclude.get?("src/lib.cr").should_not be_nil
FilteredStorageExclude.get?("docs/README.md").should_not be_nil
FilteredStorageExclude.get?("config.yml").should_not be_nil
FilteredStorageExclude.get?("test/spec.cr").should be_nil
FilteredStorageExclude.get?("test/helper.cr").should be_nil
end

it "applies both include and exclude patterns" do
# Include *.cr and *.md, exclude test/*
# Should have: src/*.cr and docs/*.md (not test/*.cr)
FilteredStorageCombined.files.size.should eq(3) # src/main.cr, src/lib.cr, docs/README.md
FilteredStorageCombined.get?("src/main.cr").should_not be_nil
FilteredStorageCombined.get?("src/lib.cr").should_not be_nil
FilteredStorageCombined.get?("docs/README.md").should_not be_nil
FilteredStorageCombined.get?("test/spec.cr").should be_nil
FilteredStorageCombined.get?("test/helper.cr").should be_nil
FilteredStorageCombined.get?("config.yml").should be_nil
end

it "handles empty result with allow_empty" do
# No .txt files in filters directory, but allow_empty is true
FilteredStorageEmpty.files.size.should eq(0)
end

it "filters are relative to baked directory" do
# Patterns should match from the baked folder root, not absolute paths
FilteredStorageInclude.get?("src/main.cr").should_not be_nil
# Not "/storage/filters/src/main.cr"
end

it "can read content from filtered files" do
file = FilteredStorageInclude.get("src/main.cr")
content = file.gets_to_end
content.should contain("Main file")
end

it "empty result behavior respects allow_empty flag" do
# FilteredStorageEmpty has allow_empty: true, so it should work with 0 files
FilteredStorageEmpty.files.size.should eq(0)
# Without allow_empty: true, it would raise at compile time (tested manually)
end
end
end
Loading