mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
feat(dev): port SPA asset tooling
This commit is contained in:
parent
a03c689a44
commit
f52ef5aa07
12 changed files with 501 additions and 81 deletions
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
|
|
@ -26,7 +26,8 @@ jobs:
|
|||
with:
|
||||
no-cache: true
|
||||
- run: bun install
|
||||
- run: scripts/refresh-fabro-spa.sh
|
||||
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
- run: cargo dev refresh-spa
|
||||
- run: git diff --exit-code -- lib/crates/fabro-spa/assets
|
||||
|
||||
compile:
|
||||
|
|
|
|||
6
.github/workflows/typescript.yml
vendored
6
.github/workflows/typescript.yml
vendored
|
|
@ -71,9 +71,9 @@ jobs:
|
|||
persist-credentials: false
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: scripts/refresh-fabro-spa.sh
|
||||
- run: git diff --exit-code -- lib/crates/fabro-spa/assets
|
||||
- run: scripts/check-fabro-spa-budgets.sh
|
||||
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
- run: cargo dev refresh-spa
|
||||
- run: git diff --exit-code -- lib/crates/fabro-spa/assets
|
||||
- run: cargo dev check-spa-budgets
|
||||
- run: cargo build -p fabro-cli --release
|
||||
- run: wc -c < target/release/fabro
|
||||
|
|
|
|||
|
|
@ -22,11 +22,11 @@ macOS note: if `cargo nextest run` fails with `Too many open files (os error 24)
|
|||
- `cd apps/fabro-web && bun test` — run tests
|
||||
- `cd apps/fabro-web && bun run typecheck` — type check
|
||||
- `cd apps/fabro-web && bun run build` — production build (writes to `apps/fabro-web/dist/` only; does NOT update the bundled SPA that ships in the Rust binary)
|
||||
- `scripts/refresh-fabro-spa.sh` — **run this before committing any TypeScript change in `apps/fabro-web/` or `lib/packages/fabro-api-client/`**. It runs the production build and then copies `dist/` into `lib/crates/fabro-spa/assets/` (which is tracked in git). CI's TypeScript `Build` job reruns this script and then `git diff --exit-code -- lib/crates/fabro-spa/assets` — if the committed bundle drifts from source (e.g. content-hashed filenames like `entry-<hash>.js` change), the check fails. `bun run build` on its own is not enough.
|
||||
- `cargo dev refresh-spa` — **run this before committing any TypeScript change in `apps/fabro-web/` or `lib/packages/fabro-api-client/`**. It runs the production build and then copies `dist/` into `lib/crates/fabro-spa/assets/` (which is tracked in git). CI's TypeScript `Build` job reruns this command and then `git diff --exit-code -- lib/crates/fabro-spa/assets` — if the committed bundle drifts from source (e.g. content-hashed filenames like `entry-<hash>.js` change), the check fails. `bun run build` on its own is not enough.
|
||||
|
||||
### Docker image
|
||||
- `cargo dev docker-build` — builds the local Docker image from the current tree using the release pipeline's cargo-zigbuild approach. Honors `--arch amd64|arm64`, `--tag <name>` (default `fabro`), `--compile-only` (stages `docker-context/<arch>/fabro` without `docker build`), and `--dry-run` (prints the Docker commands without running them). Prefer this over writing a throwaway Dockerfile; the release pipeline, `Dockerfile`, and this command share the same binary layout.
|
||||
- Refresh the embedded SPA before rebuilding the image after any `apps/fabro-web` change: `scripts/refresh-fabro-spa.sh` runs the bun build and copies `dist/` into `lib/crates/fabro-spa/assets/`. Skipping this step produces a Docker image whose Rust binary embeds a stale SPA bundle.
|
||||
- Refresh the embedded SPA before rebuilding the image after any `apps/fabro-web` change: `cargo dev refresh-spa` runs the bun build and copies `dist/` into `lib/crates/fabro-spa/assets/`. Skipping this step produces a Docker image whose Rust binary embeds a stale SPA bundle.
|
||||
|
||||
### Release automation
|
||||
- `cargo dev release [nightly]` — creates the next stable release or nightly prerelease tag. Use `--dry-run` to print planned commands without mutating git or running Cargo, `--skip-tests` only after running the release-mode smoke yourself, and `--release-date YYYY-MM-DD` or `FABRO_RELEASE_DATE` for deterministic version computation.
|
||||
|
|
|
|||
|
|
@ -702,7 +702,7 @@ Solid arrows are real dependencies. Dashed lines show phases that are independen
|
|||
|
||||
---
|
||||
|
||||
- [ ] **Unit 5.5: Port `refresh-fabro-spa.sh` and `check-fabro-spa-budgets.sh` to `fabro-dev`**
|
||||
- [x] **Unit 5.5: Port `refresh-fabro-spa.sh` and `check-fabro-spa-budgets.sh` to `fabro-dev`**
|
||||
|
||||
**Goal:** Replace the two SPA-related scripts with `cargo dev refresh-spa` and `cargo dev check-spa-budgets`.
|
||||
|
||||
|
|
|
|||
137
lib/crates/fabro-dev/src/commands/check_spa_budgets.rs
Normal file
137
lib/crates/fabro-dev/src/commands/check_spa_budgets.rs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use clap::Args;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
const DEFAULT_ASSET_BUDGET_BYTES: u64 = 15 * 1024 * 1024;
|
||||
const DEFAULT_PAYLOAD_BUDGET_BYTES: u64 = 5 * 1024 * 1024;
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub(crate) struct CheckSpaBudgetsArgs {
|
||||
/// Repository root containing lib/crates/fabro-spa/assets.
|
||||
#[arg(long, hide = true)]
|
||||
root: Option<PathBuf>,
|
||||
/// Override the raw asset budget.
|
||||
#[arg(long, hide = true, default_value_t = DEFAULT_ASSET_BUDGET_BYTES)]
|
||||
asset_budget_bytes: u64,
|
||||
/// Override the estimated gzip payload budget.
|
||||
#[arg(long, hide = true, default_value_t = DEFAULT_PAYLOAD_BUDGET_BYTES)]
|
||||
payload_budget_bytes: u64,
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::print_stdout,
|
||||
reason = "dev check-spa-budgets command reports measured budgets directly"
|
||||
)]
|
||||
pub(crate) fn check_spa_budgets(args: CheckSpaBudgetsArgs) -> Result<()> {
|
||||
let root = args.root.unwrap_or_else(workspace_root);
|
||||
let asset_dir = root.join("lib/crates/fabro-spa/assets");
|
||||
let report = budget_report(&asset_dir)?;
|
||||
|
||||
println!("fabro-spa asset bytes: {}", report.asset_bytes);
|
||||
println!(
|
||||
"fabro-spa estimated compressed payload bytes: {}",
|
||||
report.compressed_payload_bytes
|
||||
);
|
||||
|
||||
if report.asset_bytes > args.asset_budget_bytes {
|
||||
bail!(
|
||||
"fabro-spa committed assets exceed budget: {} > {}",
|
||||
report.asset_bytes,
|
||||
args.asset_budget_bytes
|
||||
);
|
||||
}
|
||||
|
||||
if report.compressed_payload_bytes > args.payload_budget_bytes {
|
||||
bail!(
|
||||
"fabro-spa compressed payload exceeds budget: {} > {}",
|
||||
report.compressed_payload_bytes,
|
||||
args.payload_budget_bytes
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct BudgetReport {
|
||||
asset_bytes: u64,
|
||||
compressed_payload_bytes: u64,
|
||||
}
|
||||
|
||||
fn budget_report(asset_dir: &Path) -> Result<BudgetReport> {
|
||||
if !asset_dir.is_dir() {
|
||||
bail!(
|
||||
"fabro-spa assets directory is missing: {}",
|
||||
asset_dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
let mut files = Vec::new();
|
||||
for entry in WalkDir::new(asset_dir) {
|
||||
let entry = entry.context("walking fabro-spa assets")?;
|
||||
if !entry.file_type().is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = entry.path().to_path_buf();
|
||||
if path.extension().and_then(|ext| ext.to_str()) == Some("map") {
|
||||
bail!(
|
||||
"source map files are not allowed in fabro-spa assets: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
files.push(path);
|
||||
}
|
||||
files.sort();
|
||||
|
||||
let mut asset_bytes = 0;
|
||||
let mut compressed_payload_bytes = 0;
|
||||
for file in files {
|
||||
asset_bytes += file
|
||||
.metadata()
|
||||
.with_context(|| format!("reading metadata for {}", file.display()))?
|
||||
.len();
|
||||
compressed_payload_bytes += gzip_size(&file)?;
|
||||
}
|
||||
|
||||
Ok(BudgetReport {
|
||||
asset_bytes,
|
||||
compressed_payload_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "dev check-spa-budgets intentionally shells out to gzip to match the legacy script"
|
||||
)]
|
||||
fn gzip_size(file: &Path) -> Result<u64> {
|
||||
let output = Command::new("gzip")
|
||||
.args(["-9", "-n", "-c"])
|
||||
.arg(file)
|
||||
.output()
|
||||
.with_context(|| format!("compressing {}", file.display()))?;
|
||||
ensure_gzip_success(file, &output)
|
||||
}
|
||||
|
||||
fn ensure_gzip_success(file: &Path, output: &Output) -> Result<u64> {
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"gzip failed for {} with {}: {}",
|
||||
file.display(),
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(output.stdout.len() as u64)
|
||||
}
|
||||
|
||||
fn workspace_root() -> PathBuf {
|
||||
let mut root = Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf();
|
||||
root.pop();
|
||||
root.pop();
|
||||
root.pop();
|
||||
root
|
||||
}
|
||||
|
|
@ -1,28 +1,11 @@
|
|||
use anyhow::{Result, bail};
|
||||
use clap::Args;
|
||||
|
||||
mod check_boundary;
|
||||
mod check_spa_budgets;
|
||||
mod docker_build;
|
||||
mod refresh_spa;
|
||||
mod release;
|
||||
|
||||
pub(crate) use check_boundary::{CheckBoundaryArgs, check_boundary};
|
||||
pub(crate) use check_spa_budgets::{CheckSpaBudgetsArgs, check_spa_budgets};
|
||||
pub(crate) use docker_build::{DockerBuildArgs, docker_build};
|
||||
pub(crate) use refresh_spa::{RefreshSpaArgs, refresh_spa};
|
||||
pub(crate) use release::{ReleaseArgs, release};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub(crate) struct RefreshSpaArgs;
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub(crate) struct CheckSpaBudgetsArgs;
|
||||
|
||||
pub(crate) fn refresh_spa(_args: RefreshSpaArgs) -> Result<()> {
|
||||
not_yet_implemented("refresh-spa")
|
||||
}
|
||||
|
||||
pub(crate) fn check_spa_budgets(_args: CheckSpaBudgetsArgs) -> Result<()> {
|
||||
not_yet_implemented("check-spa-budgets")
|
||||
}
|
||||
|
||||
fn not_yet_implemented(command: &str) -> Result<()> {
|
||||
bail!("{command} is not yet implemented")
|
||||
}
|
||||
|
|
|
|||
111
lib/crates/fabro-dev/src/commands/refresh_spa.rs
Normal file
111
lib/crates/fabro-dev/src/commands/refresh_spa.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use clap::Args;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub(crate) struct RefreshSpaArgs {
|
||||
/// Repository root containing apps/fabro-web and lib/crates/fabro-spa.
|
||||
#[arg(long, hide = true)]
|
||||
root: Option<PathBuf>,
|
||||
/// Skip bun run build and only mirror an existing dist directory.
|
||||
#[arg(long, hide = true)]
|
||||
skip_build: bool,
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::print_stdout,
|
||||
reason = "dev refresh-spa command reports progress directly"
|
||||
)]
|
||||
pub(crate) fn refresh_spa(args: RefreshSpaArgs) -> Result<()> {
|
||||
let root = args.root.unwrap_or_else(workspace_root);
|
||||
let web_dir = root.join("apps/fabro-web");
|
||||
let dist_dir = web_dir.join("dist");
|
||||
let asset_dir = root.join("lib/crates/fabro-spa/assets");
|
||||
|
||||
if !args.skip_build {
|
||||
println!("Running bun run build in apps/fabro-web...");
|
||||
run_bun_build(&web_dir)?;
|
||||
}
|
||||
|
||||
mirror_dist(&dist_dir, &asset_dir)?;
|
||||
println!("Refreshed lib/crates/fabro-spa/assets");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "dev refresh-spa intentionally runs a synchronous Bun subprocess"
|
||||
)]
|
||||
fn run_bun_build(web_dir: &Path) -> Result<()> {
|
||||
let status = Command::new("bun")
|
||||
.args(["run", "build"])
|
||||
.current_dir(web_dir)
|
||||
.status()
|
||||
.with_context(|| format!("running bun run build in {}", web_dir.display()))?;
|
||||
if !status.success() {
|
||||
bail!("bun run build failed with {status}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "dev refresh-spa mirrors build output with synchronous filesystem operations"
|
||||
)]
|
||||
fn mirror_dist(dist_dir: &Path, asset_dir: &Path) -> Result<()> {
|
||||
if !dist_dir.is_dir() {
|
||||
bail!("apps/fabro-web/dist is missing; run `bun run build` before mirroring SPA assets");
|
||||
}
|
||||
|
||||
if asset_dir.exists() {
|
||||
std::fs::remove_dir_all(asset_dir)
|
||||
.with_context(|| format!("removing {}", asset_dir.display()))?;
|
||||
}
|
||||
std::fs::create_dir_all(asset_dir)
|
||||
.with_context(|| format!("creating {}", asset_dir.display()))?;
|
||||
|
||||
for entry in WalkDir::new(dist_dir) {
|
||||
let entry = entry.context("walking apps/fabro-web/dist")?;
|
||||
let source = entry.path();
|
||||
let relative = source
|
||||
.strip_prefix(dist_dir)
|
||||
.with_context(|| format!("{} is not under {}", source.display(), dist_dir.display()))?;
|
||||
if relative.as_os_str().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let destination = asset_dir.join(relative);
|
||||
if entry.file_type().is_dir() {
|
||||
std::fs::create_dir_all(&destination)
|
||||
.with_context(|| format!("creating {}", destination.display()))?;
|
||||
continue;
|
||||
}
|
||||
|
||||
if source.extension().and_then(|ext| ext.to_str()) == Some("map") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(parent) = destination.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("creating {}", parent.display()))?;
|
||||
}
|
||||
std::fs::copy(source, &destination).with_context(|| {
|
||||
format!("copying {} to {}", source.display(), destination.display())
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn workspace_root() -> PathBuf {
|
||||
let mut root = Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf();
|
||||
root.pop();
|
||||
root.pop();
|
||||
root.pop();
|
||||
root
|
||||
}
|
||||
|
|
@ -254,7 +254,7 @@ impl ReleasePlan {
|
|||
}
|
||||
|
||||
fn refresh_spa_command() -> PlannedCommand {
|
||||
PlannedCommand::new("scripts/refresh-fabro-spa.sh")
|
||||
PlannedCommand::new("cargo").arg("dev").arg("refresh-spa")
|
||||
}
|
||||
|
||||
fn spa_assets_diff_command() -> PlannedCommand {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use std::process::{Command, Output};
|
|||
mod check_boundary;
|
||||
mod docker_build;
|
||||
mod release;
|
||||
mod spa;
|
||||
|
||||
fn fabro_dev() -> assert_cmd::Command {
|
||||
assert_cmd::cargo::cargo_bin_cmd!("fabro-dev")
|
||||
|
|
|
|||
239
lib/crates/fabro-dev/tests/it/spa.rs
Normal file
239
lib/crates/fabro-dev/tests/it/spa.rs
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
use std::path::Path;
|
||||
|
||||
fn fabro_dev() -> assert_cmd::Command {
|
||||
assert_cmd::cargo::cargo_bin_cmd!("fabro-dev")
|
||||
}
|
||||
|
||||
fn output_text(bytes: &[u8]) -> String {
|
||||
String::from_utf8(bytes.to_vec()).expect("command output should be valid utf-8")
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "integration tests stage temporary SPA fixture files with sync std::fs::write"
|
||||
)]
|
||||
fn write_file(root: &Path, path: &str, contents: &[u8]) {
|
||||
let path = root.join(path);
|
||||
std::fs::create_dir_all(path.parent().expect("fixture path should have parent"))
|
||||
.expect("creating fixture parent directory");
|
||||
std::fs::write(path, contents).expect("writing fixture file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refresh_spa_mirrors_dist_and_removes_source_maps() {
|
||||
let fixture = tempfile::tempdir().expect("creating fixture");
|
||||
write_file(fixture.path(), "apps/fabro-web/dist/index.html", b"index");
|
||||
write_file(fixture.path(), "apps/fabro-web/dist/assets/app.js", b"app");
|
||||
write_file(
|
||||
fixture.path(),
|
||||
"apps/fabro-web/dist/assets/app.js.map",
|
||||
b"map",
|
||||
);
|
||||
write_file(
|
||||
fixture.path(),
|
||||
"lib/crates/fabro-spa/assets/stale.txt",
|
||||
b"stale",
|
||||
);
|
||||
|
||||
let output = fabro_dev()
|
||||
.args([
|
||||
"refresh-spa",
|
||||
"--root",
|
||||
fixture
|
||||
.path()
|
||||
.to_str()
|
||||
.expect("fixture path should be utf-8"),
|
||||
"--skip-build",
|
||||
])
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.clone();
|
||||
let stdout = output_text(&output.stdout);
|
||||
|
||||
assert!(
|
||||
stdout.contains("Refreshed lib/crates/fabro-spa/assets"),
|
||||
"refresh-spa should report refreshed assets:\n{stdout}"
|
||||
);
|
||||
assert!(
|
||||
fixture
|
||||
.path()
|
||||
.join("lib/crates/fabro-spa/assets/index.html")
|
||||
.is_file()
|
||||
);
|
||||
assert!(
|
||||
fixture
|
||||
.path()
|
||||
.join("lib/crates/fabro-spa/assets/assets/app.js")
|
||||
.is_file()
|
||||
);
|
||||
assert!(
|
||||
!fixture
|
||||
.path()
|
||||
.join("lib/crates/fabro-spa/assets/assets/app.js.map")
|
||||
.exists()
|
||||
);
|
||||
assert!(
|
||||
!fixture
|
||||
.path()
|
||||
.join("lib/crates/fabro-spa/assets/stale.txt")
|
||||
.exists()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refresh_spa_missing_dist_errors_cleanly() {
|
||||
let fixture = tempfile::tempdir().expect("creating fixture");
|
||||
|
||||
let output = fabro_dev()
|
||||
.args([
|
||||
"refresh-spa",
|
||||
"--root",
|
||||
fixture
|
||||
.path()
|
||||
.to_str()
|
||||
.expect("fixture path should be utf-8"),
|
||||
"--skip-build",
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.code(1)
|
||||
.get_output()
|
||||
.clone();
|
||||
let stderr = output_text(&output.stderr);
|
||||
|
||||
assert!(
|
||||
stderr.contains("apps/fabro-web/dist is missing; run `bun run build`"),
|
||||
"missing dist should explain how to recover:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_spa_budgets_passes_fixture_assets() {
|
||||
let fixture = tempfile::tempdir().expect("creating fixture");
|
||||
write_file(
|
||||
fixture.path(),
|
||||
"lib/crates/fabro-spa/assets/index.html",
|
||||
b"hello",
|
||||
);
|
||||
|
||||
let output = fabro_dev()
|
||||
.args([
|
||||
"check-spa-budgets",
|
||||
"--root",
|
||||
fixture
|
||||
.path()
|
||||
.to_str()
|
||||
.expect("fixture path should be utf-8"),
|
||||
"--asset-budget-bytes",
|
||||
"100",
|
||||
"--payload-budget-bytes",
|
||||
"100",
|
||||
])
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.clone();
|
||||
let stdout = output_text(&output.stdout);
|
||||
|
||||
assert!(
|
||||
stdout.contains("fabro-spa asset bytes: 5"),
|
||||
"budget check should print raw bytes:\n{stdout}"
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("fabro-spa estimated compressed payload bytes:"),
|
||||
"budget check should print compressed payload bytes:\n{stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_spa_budgets_fails_when_assets_exceed_budget() {
|
||||
let fixture = tempfile::tempdir().expect("creating fixture");
|
||||
write_file(
|
||||
fixture.path(),
|
||||
"lib/crates/fabro-spa/assets/index.html",
|
||||
b"hello",
|
||||
);
|
||||
|
||||
let output = fabro_dev()
|
||||
.args([
|
||||
"check-spa-budgets",
|
||||
"--root",
|
||||
fixture
|
||||
.path()
|
||||
.to_str()
|
||||
.expect("fixture path should be utf-8"),
|
||||
"--asset-budget-bytes",
|
||||
"4",
|
||||
"--payload-budget-bytes",
|
||||
"100",
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.code(1)
|
||||
.get_output()
|
||||
.clone();
|
||||
let stderr = output_text(&output.stderr);
|
||||
|
||||
assert!(
|
||||
stderr.contains("fabro-spa committed assets exceed budget: 5 > 4"),
|
||||
"budget failure should report raw byte overage:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_spa_budgets_fails_when_source_map_is_present() {
|
||||
let fixture = tempfile::tempdir().expect("creating fixture");
|
||||
write_file(
|
||||
fixture.path(),
|
||||
"lib/crates/fabro-spa/assets/assets/app.js.map",
|
||||
b"map",
|
||||
);
|
||||
|
||||
let output = fabro_dev()
|
||||
.args([
|
||||
"check-spa-budgets",
|
||||
"--root",
|
||||
fixture
|
||||
.path()
|
||||
.to_str()
|
||||
.expect("fixture path should be utf-8"),
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.code(1)
|
||||
.get_output()
|
||||
.clone();
|
||||
let stderr = output_text(&output.stderr);
|
||||
|
||||
assert!(
|
||||
stderr.contains("source map files are not allowed in fabro-spa assets"),
|
||||
"source maps should fail the budget check:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_spa_budgets_missing_assets_errors_cleanly() {
|
||||
let fixture = tempfile::tempdir().expect("creating fixture");
|
||||
|
||||
let output = fabro_dev()
|
||||
.args([
|
||||
"check-spa-budgets",
|
||||
"--root",
|
||||
fixture
|
||||
.path()
|
||||
.to_str()
|
||||
.expect("fixture path should be utf-8"),
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.code(1)
|
||||
.get_output()
|
||||
.clone();
|
||||
let stderr = output_text(&output.stderr);
|
||||
|
||||
assert!(
|
||||
stderr.contains("fabro-spa assets directory is missing"),
|
||||
"missing assets should be reported clearly:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
asset_dir="$repo_root/lib/crates/fabro-spa/assets"
|
||||
asset_budget_bytes=$((15 * 1024 * 1024))
|
||||
payload_budget_bytes=$((5 * 1024 * 1024))
|
||||
|
||||
if [[ ! -d "$asset_dir" ]]; then
|
||||
echo "fabro-spa assets directory is missing: $asset_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
asset_bytes=0
|
||||
compressed_payload_bytes=0
|
||||
|
||||
while IFS= read -r -d '' file; do
|
||||
file_bytes="$(wc -c < "$file" | tr -d '[:space:]')"
|
||||
compressed_bytes="$(gzip -9 -n -c "$file" | wc -c | tr -d '[:space:]')"
|
||||
asset_bytes=$((asset_bytes + file_bytes))
|
||||
compressed_payload_bytes=$((compressed_payload_bytes + compressed_bytes))
|
||||
done < <(find "$asset_dir" -type f -print0)
|
||||
|
||||
echo "fabro-spa asset bytes: $asset_bytes"
|
||||
echo "fabro-spa estimated compressed payload bytes: $compressed_payload_bytes"
|
||||
|
||||
if (( asset_bytes > asset_budget_bytes )); then
|
||||
echo "fabro-spa committed assets exceed budget: $asset_bytes > $asset_budget_bytes" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if (( compressed_payload_bytes > payload_budget_bytes )); then
|
||||
echo "fabro-spa compressed payload exceeds budget: $compressed_payload_bytes > $payload_budget_bytes" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
web_dir="$repo_root/apps/fabro-web"
|
||||
dist_dir="$web_dir/dist"
|
||||
asset_dir="$repo_root/lib/crates/fabro-spa/assets"
|
||||
|
||||
(
|
||||
cd "$web_dir"
|
||||
bun run build
|
||||
)
|
||||
|
||||
rm -rf "$asset_dir"
|
||||
mkdir -p "$asset_dir"
|
||||
cp -R "$dist_dir"/. "$asset_dir"/
|
||||
find "$asset_dir" -type f -name '*.map' -delete
|
||||
Loading…
Add table
Reference in a new issue