# Vendor Frontend Assets — Implementation Guide

**Purpose:** Establish the vendored-assets pattern in a project that serves static
frontend files through the ontoref daemon (or any Axum-based server with a static
file route). Covers the full setup: directory layout, just recipes, and template
references.

**Substitutions required before use:**
- `{project_dir}` — absolute path to project root
- `{daemon_crate}` — path to the Axum daemon crate (e.g. `crates/myproject-daemon`)
- `{assets_route}` — URL prefix the daemon serves assets from (e.g. `/assets`)

---

## Why vendor instead of CDN

CDN references in server-rendered templates create three failure modes that vendoring
eliminates:

1. **Version drift** — a CDN package can be yanked or silently updated at the minor
   level depending on the version specifier used.
2. **Offline development** — daemon templates break without network access.
3. **Registration race** — some Cytoscape extensions auto-register against
   `window.cytoscape` at load time; if the CDN is slow or returns a 404 for a
   sub-resource (e.g. a CSS that does not exist in that package version), the
   extension silently fails to register and `cy.pluginName(...)` throws at runtime
   with no network error visible in the console.

The canonical signal that a CDN asset has a sub-resource problem: `curl -sI
<url>` returns 200 but the body starts with "Couldn't find the requested file".
Always `curl -fsSL` (fail on HTTP errors) when downloading to catch this.

---

## Directory layout

```text
assets/
  vendor/          ← all vendored JS; served at {assets_route}/vendor/<file>
    <lib>@<ver>.js ← optional: keep version in filename for cache-busting
justfiles/
  assets.just      ← recipes for downloading/updating vendor assets
justfile           ← root just file; imports assets.just
```

Vendor files are committed to the repository. They are not gitignored — the goal
is reproducible builds without network access.

---

## assets.just structure

```just
# Frontend asset management
#
# Vendored JS libs live in assets/vendor/ and are served by the daemon
# at {assets_route}/vendor/<file>. Pin versions explicitly; bump manually.

LIB_VERSION := "x.y.z"

# Download/update all vendored frontend JS dependencies
[doc("Vendor all frontend JS dependencies")]
vendor-js: vendor-<lib>

[doc("Vendor <lib>")]
vendor-<lib>:
    mkdir -p assets/vendor
    curl -fsSL \
        "https://cdn.jsdelivr.net/npm/<lib>@{{LIB_VERSION}}/<lib>.js" \
        -o assets/vendor/<lib>.js
    @echo "vendored <lib>@{{LIB_VERSION}}"
```

Rules:
- One version variable per library, at the top of the file.
- `vendor-js` is the aggregate recipe — depends on all individual `vendor-*` recipes.
- Use `curl -fsSL` (not `-sL`): `-f` makes curl exit non-zero on HTTP errors, catching
  404s before they write a garbage file to disk.
- Adding a new library: add a version variable, add a `vendor-<lib>` recipe, add it
  as a dependency of `vendor-js`.

---

## Importing in the root justfile

```just
import 'justfiles/ci.just'
import 'justfiles/assets.just'
```

---

## Referencing vendor assets in Tera templates

In `{% block head %}`, load vendored scripts **after** their host library:

```html
<!-- Host library (CDN is acceptable for well-established, stable libs) -->
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.30.2/dist/cytoscape.min.js"></script>

<!-- Extension: vendored locally — loads after host, auto-registers on window.cytoscape -->
<script src="{assets_route}/vendor/cytoscape-navigator.js"></script>
```

**Do not use** `<link rel="stylesheet">` for a vendor asset unless you have verified
the CSS file actually exists in the npm package (`curl -fsSI <url>` and check Content-Type).
Many Cytoscape extensions ship JS-only and use inline styles; fetching a non-existent
CSS produces a silent 404 that can confuse debugging.

---

## Verifying a new CDN asset before vendoring

```sh
# 1. Check the package contents on npm
curl -s "https://registry.npmjs.org/<lib>/<version>" | jq '.dist.unpackedSize, .dist.fileCount'

# 2. Confirm the specific file exists (fail fast)
curl -fsSI "https://cdn.jsdelivr.net/npm/<lib>@<version>/<file>.js" | head -3

# 3. Check auto-registration pattern (for Cytoscape extensions)
curl -sL "https://cdn.jsdelivr.net/npm/<lib>@<version>/<file>.js" | tail -10
# Look for: if (typeof cytoscape !== 'undefined') { register(cytoscape); }
# If absent, the extension requires manual registration: cytoscape.use(require('<lib>'))
```

---

## Agent steps to apply this pattern

1. Read `justfiles/assets.just` if it exists; otherwise create it from the structure
   above.
2. Add the library version variable and `vendor-<lib>` recipe.
3. Add the new recipe as a dependency of `vendor-js`.
4. Run `just vendor-<lib>` to download the file.
5. Verify the downloaded file is valid JS: `head -3 assets/vendor/<lib>.js` — if the
   first line starts with "Couldn't" or "<!DOCTYPE", the download silently failed; re-run
   with `curl -fsSL` (the `-f` flag was likely missing).
6. Update the Tera template: replace the CDN `<script src="...">` with
   `<script src="{assets_route}/vendor/<lib>.js"></script>`.
7. Remove any `<link rel="stylesheet">` that referenced the same CDN package unless
   you have verified the CSS exists (step 2 of the verification section above).
8. Commit `assets/vendor/<lib>.js` and `justfiles/assets.just` together.
