77 lines
3.3 KiB
Text
77 lines
3.3 KiB
Text
#!/usr/bin/env nu
|
|
# Resolve a layered override chain into ONE materialised tree.
|
|
#
|
|
# The chain has three levels and the same rule at every hop: a file present in a
|
|
# higher layer WINS; a file absent from it is INHERITED from the layer below.
|
|
#
|
|
# rustelo (framework) → website-htmx-rustelo (implementation) → outreach/site (consumer)
|
|
#
|
|
# The implementation already overrides framework pages this way; the consumer is the
|
|
# third slot, and until now it did not exist for assets at all — htmx-assets/ was
|
|
# refilled from the framework alone, so an override the implementation shipped never
|
|
# reached the site.
|
|
#
|
|
# The cascade is resolved HERE, at build time: the Minijinja loader and the static
|
|
# ServeDir each read one path and need no fallback logic.
|
|
#
|
|
# The output is destroyed and rebuilt on every run, which is the whole point — a file
|
|
# that survives only in the output is a file that exists nowhere. Anything the consumer
|
|
# owns belongs in its layer (site/_htmx/**), never in the target tree.
|
|
#
|
|
# Layers are positional, LOWEST FIRST. The base layer must exist; the rest are optional
|
|
# (a consumer with nothing to override is a normal state, not an error).
|
|
#
|
|
# Usage:
|
|
# nu assemble-layers.nu <base> [<mid> ...] --out <dir> [--exclude "*.lock.toml,*.md"]
|
|
|
|
def main [
|
|
...layers: string # override chain, lowest layer first; base must exist
|
|
--out: string # target tree (destroyed and rebuilt)
|
|
--exclude: string = "" # comma-separated basename globs pruned from the result
|
|
--label: string = "layers" # name used in the summary line
|
|
]: nothing -> nothing {
|
|
if ($layers | is-empty) {
|
|
error make { msg: "no layers given — at least the base layer is required" }
|
|
}
|
|
if ($out | is-empty) {
|
|
error make { msg: "--out is required" }
|
|
}
|
|
|
|
let base = ($layers | first)
|
|
if not ($base | path exists) {
|
|
error make { msg: $"base layer not found: ($base)" }
|
|
}
|
|
|
|
# Rebuild from scratch so a file deleted upstream never lingers downstream.
|
|
if ($out | path exists) { ^rm -rf $out }
|
|
mkdir $out
|
|
|
|
let applied = $layers | each {|layer|
|
|
if ($layer | path exists) {
|
|
# `src/.` copies CONTENTS; -L dereferences symlinks (layers may live behind
|
|
# links and the tree must be self-contained); cp merges dirs and overwrites
|
|
# colliding files — which IS the override semantics.
|
|
^cp -RL $"($layer)/." $out
|
|
{ layer: $layer, present: true }
|
|
} else {
|
|
{ layer: $layer, present: false }
|
|
}
|
|
}
|
|
|
|
let patterns = ($exclude | split row "," | each {|p| $p | str trim } | where {|p| $p | is-not-empty })
|
|
let pruned = $patterns | each {|pat|
|
|
let hits = (glob $"($out)/**/($pat)")
|
|
$hits | each {|f| ^rm -rf $f }
|
|
($hits | length)
|
|
} | append 0 | math sum
|
|
|
|
for l in $applied {
|
|
let mark = if $l.present { $"(ansi green)+(ansi reset)" } else { $"(ansi dark_gray)·(ansi reset)" }
|
|
let note = if $l.present { "" } else { " (absent — inherited from below)" }
|
|
print $" ($mark) ($l.layer)($note)"
|
|
}
|
|
|
|
let count = (^find -L $out -type f | lines | where {|l| $l | is-not-empty } | length)
|
|
let prune_note = if $pruned > 0 { $" — ($pruned) pruned by --exclude" } else { "" }
|
|
print $"(ansi green)✅(ansi reset) ($label) → ($out) — ($count) files($prune_note)"
|
|
}
|