mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-03 05:53:56 +08:00
880 lines
45 KiB
Markdown
880 lines
45 KiB
Markdown
# Development Guide
|
||
|
||
A getting-started guide for contributors new to `luci-app-aurora-config`. It
|
||
covers the big picture, the parts that are easy to get wrong, and recipes for
|
||
common tasks.
|
||
|
||
> **What this is.** The configuration app for the Aurora theme
|
||
> (`luci-theme-aurora`). It adds a LuCI admin page where users tune theme
|
||
> colors, layout, branding, and the toolbar, plus a Configuration Marketplace page to
|
||
> browse, apply, and share complete configurations with other users. This
|
||
> repo only owns the *configuration UI* and *writes settings into UCI* — the
|
||
> actual rendering lives in the theme package.
|
||
|
||
---
|
||
|
||
## 1. One build step, and it is minification only
|
||
|
||
**Edit `.dev/src/resource/`. Never edit `htdocs/`** — it is generated, and the
|
||
next build overwrites it.
|
||
|
||
```bash
|
||
cd .dev && pnpm install # once
|
||
pnpm build # .dev/src/resource/ -> htdocs/luci-static/resources/
|
||
pnpm test # runs --check first, so a forgotten build fails here
|
||
```
|
||
|
||
There is no bundler and no transpiler. Each file is minified in place and stays
|
||
a separate resource, because LuCI's own loader is what resolves
|
||
`"require utils.hub-api as hubApi"` — it fetches
|
||
`/luci-static/resources/utils/hub-api.js` over HTTP. Merging files would break
|
||
that. The frontend uses LuCI's `E()` DOM API, not React/Vue; the backend is a
|
||
single rpcd shell script.
|
||
|
||
**Why the artifacts are committed.** uhttpd has no gzip — there is no
|
||
`Content-Encoding` anywhere in its source — so every byte is a flash read and a
|
||
socket write on the router. And the feed builds each package by cloning its
|
||
repo at a git ref and compiling from source
|
||
(`openwrt-cloud/feed/scripts/resolve-refs.sh`), so minifying only in our own
|
||
release workflow would compress the one channel nobody installs from. The
|
||
generated bytes have to be in git for anyone's build to get them.
|
||
|
||
`.gitattributes` marks `htdocs/luci-static/resources/**` as
|
||
`linguist-generated`, so diffs there fold away — read the source instead.
|
||
|
||
Everything in `.dev/scripts/` writes to `.dev/src/resource/`; `build-js.mjs` is
|
||
the only thing that writes to `htdocs/`. That way "are the artifacts fresh?" has
|
||
exactly one answer, and `--check` is the one place that answers it.
|
||
|
||
To test a change on a device, run `pnpm build`, then sync `htdocs/` and `root/`
|
||
to the matching paths and reload the page or restart rpcd.
|
||
|
||
---
|
||
|
||
## 2. Repository layout
|
||
|
||
```
|
||
.dev/ # Sources, tooling, tests. Not installed.
|
||
├── package.json # The only install root (pnpm). Pins luci-theme-tokens
|
||
├── scripts/build-js.mjs # terser -> htdocs. The ONLY writer of htdocs
|
||
├── tests/paths.mjs # repo() / srcPath() — tests never depend on cwd
|
||
└── src/resource/ # ★ EDIT HERE
|
||
├── view/aurora/
|
||
│ ├── studio.js # Theme Studio: color editor, layout, branding, toolbar (largest file)
|
||
│ └── marketplace.js # Configuration Marketplace: browse/apply, share, "my shares"
|
||
├── aurora/presets.json # Store card data: the 4 swatch colours + layout/typography per preset — GENERATED
|
||
└── utils/
|
||
├── color.global.js # Vendored colorjs.io (global `Color`)
|
||
├── color.global.js.license # Its three MIT notices; build-js re-attaches them as a banner
|
||
├── tokens.global.js # ★ Color derivation engine (global `AuroraTokens`) — GENERATED, do not edit
|
||
├── theme-preview.js # Shared light/dark split preview card
|
||
├── feed-check.js # Version comparison + package-page lookup (pure, unit-tested)
|
||
├── hub-api.js # rpc.declare wrappers for the hub_* RPC methods + list cache
|
||
└── asset-upload.js # Shared dropzone/upload widget (fonts, brand assets)
|
||
|
||
htdocs/luci-static/resources/ # GENERATED by `pnpm build` — committed, never hand-edited
|
||
|
||
root/
|
||
├── etc/uci-defaults/80_aurora # First-install setup + schema migration
|
||
├── etc/sysupgrade.conf.d/aurora-device # Preserves /etc/aurora/device.{key,hash} across sysupgrade
|
||
├── usr/libexec/rpcd/luci.aurora # Backend RPC (shell)
|
||
└── usr/share/aurora/
|
||
├── *.template # Five built-in presets (UCI fragments). Colour block GENERATED; the layout/typography block below it is hand-authored
|
||
├── color-tokens.conf # Ordered token key list for the backend — GENERATED
|
||
└── font-presets.conf # Font preset manifest (Fontsource packages + pinned versions) — GENERATED
|
||
|
||
.dev/scripts/sync-tokens.mjs # Vendors tokens.global.js + color-tokens.conf + aurora-presets.json (resolved preset hex) from @eamonxg/luci-theme-tokens, stamps TOKENS_ENGINE_VERSION into studio.js, then runs gen-presets
|
||
.dev/scripts/gen-presets.mjs # Injects the colour block into *.template, reads each template's layout/typography back out, and writes src/resource/aurora/presets.json (swatch colours only — §8.1)
|
||
.dev/scripts/aurora-presets.json # Resolved preset hex values, vendored by sync-tokens.mjs — GENERATED
|
||
.dev/scripts/gen-font-presets.mjs # Regenerates font-presets.conf (curated manifest lives in this file)
|
||
.dev/scripts/gen-pot.mjs # Scans src/resource for _() calls; its "#:" refs deliberately still name htdocs, which is where an installed reader finds the string
|
||
Makefile # OpenWrt package metadata (version lives here)
|
||
```
|
||
|
||
---
|
||
|
||
## 3. The color token system (core concepts)
|
||
|
||
This is the part most likely to trip you up. Read it before touching colors.
|
||
|
||
### 3.1 Inputs vs. derived tokens
|
||
|
||
The entire palette is driven by **10 editable inputs**; every other color is
|
||
**derived** from them.
|
||
|
||
| Kind | Count | Examples |
|
||
| --- | --- | --- |
|
||
| Inputs (user-editable) | 10 | `bg`, `surface`, `text`, `brand`, `on_brand`, `link`, `info`, `warning`, `success`, `danger` |
|
||
| Derived (computed) | 20 | `text_muted`, `surface_sunken`, `brand_hover`, `brand_subtle`, `focus_ring`, `*_surface`, `scrim`, `mega_menu_bg`, … |
|
||
|
||
The derivation rules (`mix` / `shade` / `set` / `alpha` / `const`) live in the
|
||
theme's token spec. **`utils/tokens.global.js` is the prebuilt browser bundle
|
||
from the `@eamonxg/luci-theme-tokens` npm package**, vendored by
|
||
`.dev/scripts/sync-tokens.mjs` — never edit it by hand (see §3.4).
|
||
|
||
### 3.2 Why the config app computes derived tokens itself
|
||
|
||
The theme authors derived tokens in `_tokens.css` **at build time** as flat
|
||
color literals — they no longer reference `var()` / `color-mix()`. The compiled
|
||
`main.css` is emitted as hex fallbacks plus `lab(...)` values for compatible
|
||
browsers. As a result, **overriding `--brand` alone does not cascade** into
|
||
`--brand-hover`, `--focus-ring`, and friends.
|
||
|
||
So the config app's strategy is: the user edits the 10 inputs → the frontend
|
||
expands them into all 30 values via `AuroraTokens.resolve()` → **all 30 are
|
||
written to UCI as hex/hex8 runtime colors** → the theme's template injects them,
|
||
overriding the baked defaults wholesale. Advanced users may also override
|
||
individual derived values; blank derived fields continue to follow the generated
|
||
values.
|
||
|
||
### 3.3 End-to-end data flow
|
||
|
||
```
|
||
User edits input colors in the UI
|
||
│
|
||
▼
|
||
studio.js: persistDerivedTokens()
|
||
runs AuroraTokens.resolve(mode, <10 inputs>) for light + dark
|
||
writes 30 light_<key> / dark_<key> hex entries into UCI (aurora.theme)
|
||
│ on save
|
||
▼
|
||
/etc/config/aurora ← UCI storage
|
||
│
|
||
▼
|
||
Theme template header.ut, at page render:
|
||
emits each light_* as :root { --<key> }
|
||
emits each dark_* as [data-darkmode=true] { --<key> }
|
||
│
|
||
▼
|
||
Overrides the baked defaults in _tokens.css → user's palette renders
|
||
```
|
||
|
||
> ⚠️ `header.ut` injects **every** `light_*` / `dark_*` key indiscriminately.
|
||
> Stale keys left in UCI become dangling overrides pointing at CSS variables
|
||
> that no longer exist. Pruning them is the job of schema migration (see §5).
|
||
|
||
### 3.4 Single source of truth
|
||
|
||
The token model lives in one npm package:
|
||
|
||
- **`@eamonxg/luci-theme-tokens`**
|
||
([repo](https://github.com/eamonxg/luci-theme-tokens)) — an independent repo
|
||
with its own semver, published from its own root. It owns the engine (the
|
||
five operators + spec-driven resolver, also exported standalone as
|
||
`@eamonxg/luci-theme-tokens/engine`), the aurora spec (`aurora/spec.js` /
|
||
`aurora/defaults.js` / `aurora/presets.js`), **and all derivation** — its
|
||
`build.mjs` resolves every derived token and every built-in preset at
|
||
*package* build time and ships the results as prebuilt `dist/aurora/`
|
||
artifacts. `luci-theme-aurora` and this repo are both just consumers of the
|
||
prebuilt output; neither derives anything locally anymore.
|
||
|
||
This repo **vendors** that package's `dist/aurora/` output by the exact
|
||
version pinned in `.dev/package.json` — `.dev/scripts/sync-tokens.mjs` copies three
|
||
files verbatim, no local derivation:
|
||
|
||
- `utils/tokens.global.js` ← `dist/aurora/tokens.global.js` — the browser
|
||
global, still used by `studio.js` for live preview / on-save recompute.
|
||
- `root/usr/share/aurora/color-tokens.conf` ← `dist/aurora/color-tokens.conf`
|
||
— the ordered key list the backend (`luci.aurora`) reads at runtime.
|
||
- `.dev/scripts/aurora-presets.json` ← `dist/aurora/presets.json` — every built-in
|
||
preset, fully resolved to runtime hex, for **all 5** presets × light/dark.
|
||
- `view/aurora/studio.js` — only the `TOKENS_ENGINE_VERSION` constant, stamped
|
||
from the vendored package's `package.json` version. `studio.js` appends it as
|
||
`?v=` when loading `tokens.global.js`, so a version bump busts the browser's
|
||
HTTP cache.
|
||
|
||
It then reruns `gen-presets.mjs`, which is a pure formatter: it reads
|
||
`.dev/scripts/aurora-presets.json` and injects `option light_<key>` /
|
||
`option dark_<key>` lines into `root/usr/share/aurora/*.template`, preserving
|
||
each template's non-color tail (`struct_*`, toolbar, etc.). It performs no
|
||
color math and does not load `tokens.global.js` — all values are already
|
||
resolved upstream.
|
||
|
||
`sync-tokens.mjs` resolves the package from three sources, in order:
|
||
|
||
1. `--local <path>` — a local `luci-theme-tokens` checkout, for iterating on
|
||
an unreleased spec (run `npm install` + `node build.mjs` there first).
|
||
2. **npm registry** — the version pinned in `.dev/package.json` devDependencies;
|
||
this is the normal path.
|
||
3. **sibling checkout** `../luci-theme-tokens` — fallback when the registry is
|
||
unreachable; logs a warning since the output may not match the pin exactly.
|
||
|
||
All vendored artifacts are committed, so nothing downstream (CI, SDK build,
|
||
device) needs npm or the luci-theme-tokens repo. The pinned version doubles as
|
||
the compatibility statement: it names the luci-theme-tokens release this app
|
||
targets.
|
||
|
||
Drift is caught in three layers:
|
||
|
||
1. `studio.js` joins its UI metadata with the registry at load time and throws
|
||
on missing/stale entries (`buildColorTokenTables`).
|
||
2. `.dev/tests/theme-token-sync.test.mjs` re-runs `sync-tokens.mjs --check` against
|
||
the registry (the check also covers the `TOKENS_ENGINE_VERSION` stamp in
|
||
`studio.js`, and a dedicated test asserts it matches the engine header).
|
||
3. `.github/workflows/token-sync-check.yml` runs the same check in CI (push/PR
|
||
and weekly); Renovate/Dependabot can bump the pin when luci-theme-tokens
|
||
releases.
|
||
|
||
Consumers of `tokens.global.js` at runtime:
|
||
|
||
1. **Frontend** — `studio.js` loads it for live preview, on-save computation,
|
||
and the ordered token tables (`COLOR_TOKENS` et al.).
|
||
|
||
(`.dev/scripts/gen-presets.mjs` no longer loads it — see above; the presets it
|
||
emits come straight from `.dev/scripts/aurora-presets.json`.)
|
||
|
||
---
|
||
|
||
## 4. Recipes
|
||
|
||
### A. Retune a preset / add a new one
|
||
|
||
A preset is a **whole look, not a palette**: its template carries the 62
|
||
colour options *and* a structural block — `nav_type`, `struct_spacing`,
|
||
`struct_radius_base`, `struct_content_width_centered`, `struct_font_sans`,
|
||
`struct_font_mono`, `toolbar_enabled` (the shell's `PRESET_STRUCT_KEYS`).
|
||
`apply_theme_preset` writes all of it. The two halves have different owners:
|
||
|
||
- **Colours** come from upstream. Preset ownership moved into
|
||
`@eamonxg/luci-theme-tokens` 2.0.0: the `PRESETS` map lives in that
|
||
package's `aurora/presets.js`, its `build.mjs` resolves every preset to hex
|
||
and ships `dist/aurora/presets.json`, and `gen-presets.mjs` only formats
|
||
whatever `.dev/scripts/aurora-presets.json` says.
|
||
- **Structure** is authored here, in the template itself. `sync-tokens.mjs`
|
||
overwrites `.dev/scripts/aurora-presets.json` on every sync, so nothing this repo
|
||
decides may live there.
|
||
|
||
To retune colours:
|
||
|
||
1. `luci-theme-tokens` repo: edit the `PRESETS` map in `aurora/presets.js`
|
||
(each preset specifies only the **10 inputs** × light/dark), `npm test`.
|
||
2. Tag `vX.Y.Z`, push — CI auto-publishes.
|
||
3. `config` (here): bump the pin in `.dev/package.json` → `node
|
||
.dev/scripts/sync-tokens.mjs` — refreshes `.dev/scripts/aurora-presets.json` and
|
||
reruns `gen-presets.mjs`, which rewrites `root/usr/share/aurora/*.template`
|
||
in place, preserving each template's non-color tail (`struct_*`, toolbar,
|
||
etc.) → commit.
|
||
To iterate on an unreleased preset: `node .dev/scripts/sync-tokens.mjs --local
|
||
../luci-theme-tokens`.
|
||
|
||
`sync-tokens` writes into `.dev/src/resource/`, so finish with `pnpm build`
|
||
before committing.
|
||
|
||
To retune the look, edit the structural block in
|
||
`root/usr/share/aurora/<name>.template` directly, then `pnpm gen-presets`
|
||
(it re-reads the block into the browser copy). Then `pnpm build`. Two rules the tests enforce:
|
||
|
||
- Numeric values must sit on the settings page's slider steps (spacing 0.05,
|
||
radius 0.125, width 1) and inside the bounds
|
||
`validate_and_apply_hub_payload` enforces on hub configurations.
|
||
- Font stacks must be **copied verbatim** from field 7 of the matching
|
||
`font|<slot>|<id>|…` row in `font-presets.conf`. The device reverse-maps the
|
||
stack back to a roster id to know which woff2 files to fetch; a retyped
|
||
stack matches nothing and the preset silently renders its fallback family.
|
||
|
||
A brand-new preset also needs:
|
||
|
||
- a new `root/usr/share/aurora/<name>.template` file in this repo (copy an
|
||
existing one — `gen-presets.mjs` only rewrites templates that already
|
||
exist, it never creates one)
|
||
- `marketplace.js` → `BUILTIN_PRESETS` (store card) and `BUILTIN_SEED_RE` (so the
|
||
hub's own seed of it is not listed twice)
|
||
- `luci.aurora` → `resolve_preset_path()` (name → template path)
|
||
- `80_aurora` → template fallback chain (optional)
|
||
- `.dev/tests/builtin-presets.test.mjs` → `PRESET_IDS`
|
||
|
||
### B. Add or change a color token
|
||
|
||
> The token spec's single source of truth is the standalone
|
||
> `luci-theme-tokens` repo's `aurora/spec.js`/`aurora/defaults.js`, released
|
||
> as `@eamonxg/luci-theme-tokens`; everything here is vendored from that
|
||
> package.
|
||
|
||
1. `luci-theme-tokens` repo: edit `aurora/spec.js`/`aurora/defaults.js`,
|
||
`npm test`.
|
||
2. Tag `vX.Y.Z`, push — CI auto-publishes.
|
||
3. `theme`: bump `.dev/package.json` → `npm install` → `npm run build`.
|
||
4. `config` (here): bump the pin in `.dev/package.json` → `pnpm sync-tokens` —
|
||
refreshes `tokens.global.js`, `color-tokens.conf`, and
|
||
`.dev/scripts/aurora-presets.json` (and, via `gen-presets.mjs`, the preset
|
||
templates) in one go (if there's a new token, follow the test prompts to
|
||
fill in `studio.js` copy).
|
||
To iterate on an unreleased spec: `node .dev/scripts/sync-tokens.mjs --local
|
||
../luci-theme-tokens`.
|
||
5. For a new token, add its UI copy in `studio.js` →
|
||
`COLOR_TOKEN_METADATA` / `DERIVED_COLOR_TOKEN_METADATA` (and a group entry if
|
||
needed). The tests and `buildColorTokenTables()` fail loudly until every
|
||
registry token has metadata.
|
||
6. **Bump `SCHEMA_VERSION` in `80_aurora`** (see §5).
|
||
7. `pnpm build` → `pnpm test` → commit. Steps 4 and 5 both wrote into
|
||
`.dev/src/resource/`, so the artifacts are stale until this runs; `pnpm test`
|
||
refuses to pass otherwise.
|
||
|
||
### C. Change frontend code
|
||
|
||
This is the everyday loop. The one thing to remember is that `htdocs/` is
|
||
generated, so an edit is not finished until it has been built.
|
||
|
||
```bash
|
||
cd .dev
|
||
$EDITOR src/resource/view/aurora/studio.js # edit the SOURCE
|
||
pnpm build # regenerate htdocs/
|
||
pnpm test # --check runs first
|
||
cd .. && git add -A && git commit # source and artifact travel together
|
||
```
|
||
|
||
Forgetting `pnpm build` is caught locally: `pnpm test` runs
|
||
`build-js.mjs --check` before the suite and fails naming the stale file. The
|
||
same check catches the reverse mistake — someone hand-editing `htdocs/`.
|
||
|
||
Two things that look like staleness but are not:
|
||
|
||
- **Adding or changing a comment leaves the artifact identical.** The
|
||
compressor strips comments, so `--check` correctly reports "up to date".
|
||
- **A terser upgrade moving the output by a few bytes** is expected; commit the
|
||
regeneration like any other.
|
||
|
||
To try it on a real device, copy `htdocs/` and `root/` to the matching paths
|
||
and reload the page (`/etc/init.d/rpcd restart` for backend changes).
|
||
|
||
### D. Verify without a device
|
||
|
||
```bash
|
||
cd .dev
|
||
|
||
# Everything: artifact freshness, then the full suite
|
||
pnpm test
|
||
|
||
# Just the artifacts vs. the sources
|
||
pnpm check
|
||
|
||
# Generated artifacts in sync with the pinned luci-theme-tokens package?
|
||
node scripts/sync-tokens.mjs --check
|
||
|
||
# Shell syntax (not covered by pnpm test)
|
||
sh -n ../root/usr/libexec/rpcd/luci.aurora
|
||
sh -n ../root/etc/uci-defaults/80_aurora
|
||
```
|
||
|
||
`pnpm test` asserts against **the shipped bytes**, not only the sources.
|
||
`.dev/tests/artifact-safety.test.mjs` checks that every artifact parses, keeps
|
||
the `require` directives and top-level `return` LuCI needs, still declares the
|
||
`*.global.js` globals, carries the vendored MIT notices, points at no
|
||
uninstalled sourcemap, ships no hidden file, and preserves every `_()` msgid
|
||
byte-for-byte. §11 explains why each of those can break.
|
||
|
||
---
|
||
|
||
## 5. Schema migration & upgrades
|
||
|
||
When the *meaning* of a color token changes incompatibly, snapshots in an
|
||
existing `/etc/config/aurora` go stale — they can inject invisible or wrong
|
||
colors, or leave behind obsolete keys. This is handled in
|
||
`root/etc/uci-defaults/80_aurora`:
|
||
|
||
- `SCHEMA_VERSION` at the top is the current version; the header comments record
|
||
what each version (v2 / v3 / v4 / …) changed.
|
||
- On upgrade, `migrate_color_schema()` compares the stored `config_version`; on a
|
||
mismatch it calls `reseed_colors_from_template()`, which:
|
||
- re-sets every `light_*` / `dark_*` key from the **current template**, and
|
||
- **deletes** any key absent from the template — this is what clears stale
|
||
state.
|
||
- On a fresh install it simply copies the template to `/etc/config/aurora`.
|
||
|
||
**Any change that alters the key set or token semantics must bump
|
||
`SCHEMA_VERSION` and add a comment** — otherwise existing users never run the
|
||
migration on upgrade.
|
||
|
||
---
|
||
|
||
## 6. Backend RPC reference (`luci.aurora`)
|
||
|
||
A shell rpcd script exposing the object `luci.aurora`. The frontend calls it via
|
||
`rpc.declare({ object: "luci.aurora", method: ... })`. The full method list is
|
||
the `case "$1" in "list")` block at the end of the script. Common methods:
|
||
|
||
| Method | Purpose |
|
||
| --- | --- |
|
||
| `get_init_data` | Read first-paint data in one RPC: installed versions, font presets, icons, and the active preset snapshot |
|
||
| `get_theme_preset` | Read a preset snapshot for UI placeholders and comparison |
|
||
| `apply_theme_preset` | Apply a built-in preset: colours **plus** navigation, spacing, radius, content width, both font stacks and the toolbar switch. Shortcut sections and brand images are left untouched |
|
||
| `export_config` / `import_config` | Configuration import/export |
|
||
| `list_icons` / `upload_icon` / `remove_icon` | Icon management |
|
||
| `prepare_font` / `get_font_presets` / `get_font_status` | Font handling |
|
||
| `upload_font` / `remove_font` | Custom (user-uploaded) font management |
|
||
| `hub_apply` / `get_hub_status` | Marketplace: kick off an async apply job / poll its progress |
|
||
| `hub_restore_backup` | Marketplace: one-tap rollback to the pre-apply snapshot |
|
||
| `hub_share` / `hub_update` / `hub_delete` | Marketplace: publish the current config / edit / unpublish a share |
|
||
| `hub_me` | Marketplace: this device's creator profile and everything it has published |
|
||
| `hub_set_nickname` / `hub_export_key` / `hub_import_key` | Marketplace: name the creator identity / back it up / restore it on another router |
|
||
|
||
Browsing the store is *not* in this list: the browser fetches the hub's public
|
||
list and detail endpoints directly (`utils/hub-api.js`), so no shell forks and
|
||
no rpcd round trip stand between the store page and its cards. Only calls that
|
||
need the device's write credential or the device's own filesystem go through
|
||
rpcd.
|
||
|
||
ACLs live in `root/usr/share/rpcd/acl.d/luci-app-aurora.json`; the menu entries
|
||
(`Theme Studio`, `Configuration Marketplace`) in
|
||
`root/usr/share/luci/menu.d/luci-app-aurora.json`. §8 below covers the Theme
|
||
Store in detail.
|
||
|
||
> `load_preset_snapshot()` validates that a template's `option (light|dark)_`
|
||
> line count equals `COLOR_TOKEN_KEYS count × 2`. The key list ships as
|
||
> `/usr/share/aurora/color-tokens.conf`, generated by `sync-tokens.mjs` from
|
||
> the same registry as the templates, so the two cannot drift apart. It then
|
||
> calls `load_preset_struct_snapshot()`, which requires **every**
|
||
> `PRESET_STRUCT_KEYS` option and checks each against the same bounds
|
||
> `validate_and_apply_hub_payload` applies to a hub configuration — a preset
|
||
> and a shared config are the same kind of thing, so one must not be able to
|
||
> write a value the other rejects. A template missing any of them fails the
|
||
> whole apply rather than landing half a look on top of the previous one.
|
||
|
||
---
|
||
|
||
## 7. Font system (v2)
|
||
|
||
Like the color templates, `root/usr/share/aurora/font-presets.conf` is a
|
||
**generated file — never hand-edit it**. Regenerate it with:
|
||
|
||
```bash
|
||
pnpm gen-font-presets
|
||
```
|
||
|
||
`.dev/scripts/gen-font-presets.mjs` holds the curated manifest (Fontsource npm
|
||
package id, pinned version, and weights per preset). Running it re-downloads
|
||
every woff2 from jsDelivr, recomputes its sha256, and rewrites the conf —
|
||
commit the result.
|
||
|
||
### 7.1 `font-presets.conf` format
|
||
|
||
```
|
||
v2|generated-by-gen-font-presets|do-not-edit
|
||
font|<slot>|<name>|<label>|<source>|<family>|<stack>
|
||
file|<slot>|<name>|<weight>|<sha256>|<url_jsdelivr>|<url_npmmirror>
|
||
```
|
||
|
||
- One `font` line per preset (`slot` is `sans` or `mono`); built-in presets
|
||
(`Lato`, `System UI`/`System Mono`) have no matching `file` lines.
|
||
- One `file` line per weight for Fontsource-backed presets, carrying the
|
||
sha256 and both a jsDelivr and an npmmirror URL for the same file.
|
||
- `.dev/tests/font-presets.test.mjs` guards this format (field counts, slot/weight
|
||
enums, sha256 shape, URL patterns) and runs as part of `npm test`.
|
||
|
||
### 7.2 Download pipeline (`luci.aurora`)
|
||
|
||
`prepare_font` downloads each required file **primary (jsDelivr) → fallback
|
||
(npmmirror)**, verifying the sha256 from `font-presets.conf` after each
|
||
attempt; a hash mismatch counts as a failure and triggers the fallback too.
|
||
The `@font-face` rule is always generated locally on the router — no remote
|
||
CSS is ever fetched or served to the browser. A slot's job status starts at
|
||
`ready` (queued/in progress) and settles on one of two final values:
|
||
`cached` (a verified file is in place — pure-stack presets with nothing to
|
||
download report this too) or `fallback` (download/verification failed and
|
||
the built-in face is used instead).
|
||
|
||
`fonts/preload.txt` is a single-line marker file consumed by the theme's
|
||
`header.ut` to emit `<link rel="preload">` for the active webfont:
|
||
|
||
- **Empty** — deliberately no preload (e.g. the built-in/system face is
|
||
active, nothing to preload).
|
||
- **Absent** — an older layout that predates this marker; the theme falls
|
||
back to preloading Lato.
|
||
|
||
### 7.3 Custom fonts
|
||
|
||
Users can upload their own woff2 via the `upload_font` RPC method (and remove
|
||
it via `remove_font`):
|
||
|
||
- Upload goes through cgi-io to `/tmp/aurora_font.tmp` (fonts) or
|
||
`/tmp/aurora_icon.tmp` (brand assets), then a `receive_upload` gate in
|
||
the rpcd script validates before anything touches flash:
|
||
- shared size cap: 8MB (`MAX_UPLOAD`);
|
||
- fonts: woff2 magic bytes (`wOF2`);
|
||
- images: extension allowlist `jpg jpeg png webp svg ico`, path
|
||
components rejected. The set is exactly what the themes hub will accept,
|
||
so nothing you can put on the router is something you cannot share --
|
||
avif and gif are absent because no asset slot can use them;
|
||
- the gate deletes the tmp file on every rejection (front-end callers
|
||
never clean up).
|
||
- Front-end plumbing lives in `utils/asset-upload.js` (dropzone, progress
|
||
row, delete confirm, cgi-upload XHR); `view/aurora/studio.js` composes it
|
||
for both the Custom Fonts and Brand Asset Library sections.
|
||
- Stored under `/www/luci-static/aurora/fonts/custom/<slot>-<slug>.{woff2,meta,face}`
|
||
(`.face` is the pre-rendered `@font-face` block, `.meta` carries the
|
||
display family + font stack).
|
||
- Custom faces are always included in the combined `aurora-font.css`, and
|
||
selection flows through the `struct_font_*` UCI stacks exactly like preset
|
||
fonts.
|
||
|
||
---
|
||
|
||
## 8. Configuration Marketplace
|
||
|
||
The Marketplace lets users browse configurations shared by other users, apply
|
||
one with a single click, and share their own — all through a public hub at
|
||
**`https://themes.eamonxg.fun`**. Nothing about it changes how colors are
|
||
derived or how UCI is structured; it is a distribution layer on top of the
|
||
existing config format.
|
||
|
||
### 8.1 Pieces
|
||
|
||
- **`view/aurora/marketplace.js`** — the `admin/system/aurora/marketplace` page: a
|
||
card grid (Hot/New sort, swatch preview, downloads count) that opens a
|
||
detail modal per config (palette, layout, typography, included assets) with
|
||
an Apply button; a "Share My Configuration" modal; and a "My Shares" table
|
||
(update-with-current-config / delete) shown only when the device has
|
||
published at least one share. All hub-sourced text (name, author, hex
|
||
values, toolbar URLs) is rendered via `E()`'s safe children or
|
||
`document.createTextNode` — never `innerHTML` — since it is untrusted
|
||
server content.
|
||
- **`utils/hub-api.js`** — `rpc.declare` wrappers for every `hub_*` method
|
||
against the `luci.aurora` object, plus a 5-minute `localStorage` cache
|
||
(`aurora.hub.list`) for the "hot" list so the page can paint instantly from
|
||
a stale copy while it revalidates.
|
||
- **`root/usr/libexec/rpcd/luci.aurora`** — the backend: talks to the hub over
|
||
HTTP, validates everything it gets back, and applies it to UCI.
|
||
- **`aurora/presets.json`** — the built-in preset cards, so they render with no
|
||
round trip and work offline. See below for why it is not the templates.
|
||
|
||
**Why the built-in presets exist twice.** `root/usr/share/aurora/*.template`
|
||
and `aurora/presets.json` hold the same five palettes in two shapes, on
|
||
opposite sides of the ubus boundary, and neither can replace the other:
|
||
|
||
- The **templates** never leave the router. `80_aurora` reads
|
||
`default.template` at *install* time, when no browser exists; then
|
||
`apply_theme_preset` and `reset_defaults` read them, and
|
||
`get_theme_preset` feeds the settings page its placeholder values. Applying a
|
||
built-in preset costs one ubus call carrying a name — the router does the
|
||
rest. Deleting the templates would mean the browser posting all 62 colours
|
||
plus layout and typography and the shell validating the lot, which is more
|
||
traffic, more work, and more attack surface than a name string.
|
||
- **`presets.json`** is downloaded by every visitor to the store, so it carries
|
||
only what the store reads: the four `SWATCH_KEYS` (`bg`, `surface`, `text`,
|
||
`brand`) × light/dark, plus layout, typography and the empty toolbar. It used
|
||
to carry all 62 colours; 7,968 of its 11,017 bytes were never looked at.
|
||
Replacing it with rpcd calls would put five shell forks on the store's first
|
||
paint — the thing that page was explicitly optimised away from — and break
|
||
offline rendering.
|
||
|
||
`gen-presets.mjs` writes both from one source, so they cannot drift, and
|
||
`builtin-presets.test.mjs` parses `SWATCH_KEYS` out of `marketplace.js` to make
|
||
sure the generator's copy of that list stays in step. Add a key there and the
|
||
test fails until the generator follows, rather than the card quietly rendering
|
||
a blank swatch.
|
||
|
||
### 8.2 rpcd methods (object `luci.aurora`)
|
||
|
||
| Method | Purpose |
|
||
| --- | --- |
|
||
| `get_hub_status(job_id)` | Poll a job's `state`/`step`/`error`; the frontend polls this every 1.5s (`marketplace.js: pollApplyStatus`) |
|
||
| `hub_apply(id)` | Forks an async apply job (`hub_apply_worker`) in the background, returns a `job_id` immediately |
|
||
| `hub_delete(id)` | Unpublish share `id` |
|
||
| `hub_export_key` | Read `device.key` back out so a creator identity can be moved to another router |
|
||
| `hub_import_key(key)` | Adopt an exported identity, taking over the shares it owns |
|
||
| `hub_local_state` | What the router is actually wearing: `active_source` (which card the look came from), `modified` (whether it still equals that card), and `backup` (the look it had before, projected into the same preview shape a presets.json row carries). Local only — no hub round trip |
|
||
| `hub_me` | One request for both halves of the answer: who this router publishes as, and what it has published |
|
||
| `hub_restore_backup` | Put back the single most recent pre-apply snapshot. Reached from the "My configuration" card, not from a button of its own |
|
||
| `hub_set_nickname(nickname)` | Set the creator name the hub shows on this device's shares |
|
||
| `hub_share_begin(name, description, target_id)` | Open a publish draft on the hub and return one upload ticket per asset, plus the local `/luci-static/` URL backing each. `target_id` empty publishes a new entry, non-empty replaces that share |
|
||
| `hub_share_commit(draft_id)` | Claim a draft whose assets the browser has finished uploading, turning it into a config (or an update) |
|
||
|
||
`hub_list`/`hub_get`/`hub_my_shares` are gone. The first two moved into the
|
||
browser (see §8.1); `hub_my_shares` walked a local id list and re-validated
|
||
each id against the hub on every page load, and `hub_me` replaced it with a
|
||
single request whose answer is the hub's own — there is no local record left to
|
||
lose or wrongly prune. `tests/rpcd-hub.test.mjs` pins this table to the ACL, so
|
||
adding a method without documenting it fails the suite.
|
||
|
||
The three helpers that talk to the hub — `hub_http_get`, `hub_http_put`,
|
||
`hub_http_delete` — return **0** on success, **2** when the hub answers 404,
|
||
and **1** when it cannot be reached at all. The distinction is not free:
|
||
uclient-fetch exits 8 for every error status alike and `-q` suppresses the one
|
||
line naming it, so the helpers keep stderr and read the status off it. Without
|
||
that, unpublishing a share that was already gone, or applying a theme the store
|
||
had taken down, both told the user their connection was broken.
|
||
|
||
### 8.3 Device identity
|
||
|
||
Every hub write (`hub_share`/`hub_update`/`hub_delete`) and the download-count
|
||
ping after an apply are authenticated by a per-device identity, created
|
||
lazily by `ensure_device_identity()` on first use:
|
||
|
||
- **`/etc/aurora/device.key`** (mode `0600`) — 32 random bytes, hex-encoded,
|
||
as `device_token`. This is a secret: it is the only thing that lets the hub
|
||
accept an update/delete against a device's own shares. It is never rendered
|
||
in any UI and never leaves the router except as the `device_token` field of
|
||
a `hub_share`/`hub_update`/`hub_delete` POST/PUT/DELETE body.
|
||
- **`/etc/aurora/device.hash`** (mode `0644`) — a second, independent random
|
||
hex value, `device_hash`, sent only with the download-count ping. It
|
||
carries no authority (there is nothing it can authenticate) and exists
|
||
purely for anonymous download deduplication on the hub side.
|
||
- Both files are listed in `/etc/sysupgrade.conf.d/aurora-device`, so a
|
||
`sysupgrade` that preserves configuration keeps the same device identity
|
||
(and thus the same ownership over previously published shares) across
|
||
firmware upgrades.
|
||
|
||
### 8.4 Apply flow (`hub_apply` → `hub_apply_worker`)
|
||
|
||
`hub_apply` validates the id, forks `hub_apply_worker` in the background, and
|
||
returns a `job_id` right away; the worker writes each step to a status file
|
||
under `/tmp/aurora_hub_jobs/<job_id>.status` for `get_hub_status` to read:
|
||
|
||
1. **backup** — copies the current `/etc/config/aurora` to
|
||
`/etc/aurora/pre-hub-backup.conf` (single slot: only the most recent apply
|
||
can be rolled back).
|
||
2. **fetch** — `GET`s the full config payload from the hub.
|
||
3. **validate** — `validate_and_apply_hub_payload()` treats the hub as
|
||
untrusted input and re-checks every field with the *same* rules the
|
||
frontend/local share path enforces (exact 62 `light_*`/`dark_*` hex keys,
|
||
enum-checked `nav_type`/font ids, bounded/regex-checked rem values and
|
||
font stacks, per-item toolbar validation, `has_control_char` guards
|
||
against embedded-newline UCI injection) using only `case`/`grep -E` — no
|
||
`eval`. Nothing is written to UCI until every field passes; a single `uci
|
||
batch` (colors deleted then reset, layout/typography set, `toolbar_item`
|
||
sections replaced, `hub_applied` + `icon_cache_version` bookkeeping) is
|
||
then committed atomically, and reverted if the commit fails.
|
||
4. **assets** — any image assets in the payload are fetched from the hub and
|
||
sha256-verified (`fetch_verified`) before being written under
|
||
`/www/luci-static/aurora/images/`; a login-background asset also updates
|
||
`struct_login_bg`. Best-effort: a failed asset fetch doesn't fail the job.
|
||
5. **finalize** — resyncs font CSS, bumps the icon cache timestamp, marks the
|
||
job `done`.
|
||
6. **download count** — fires a fire-and-forget `POST .../download` with
|
||
`device_hash` after the job completes, for the hub's download counter.
|
||
|
||
If validation or fetch fails, the job is marked `error` with a step and error
|
||
code; the frontend maps every code to a result-only message (never surfaces
|
||
words like `bad_payload` or `job`) and never leaves partial state — either the
|
||
whole batch commits or none of it does.
|
||
|
||
**Rollback**: `hub_restore_backup` copies `pre-hub-backup.conf` back over
|
||
`/etc/config/aurora`, resyncs font CSS and the icon cache timestamp, and
|
||
returns `no_backup` if nothing has been applied yet in this backup slot. The
|
||
gallery page shows a persistent banner ("Applied `<name>`" + "Restore
|
||
previous configuration") whenever `aurora.theme.hub_applied` is set, so
|
||
rollback is always one tap away, not just immediately after applying.
|
||
|
||
### 8.5 Share v1 scope
|
||
|
||
`build_share_payload()` assembles the current `/etc/config/aurora` into the
|
||
hub's schema (colors, layout, typography, toolbar, assets) and is shared by
|
||
both `hub_share` and `hub_update`. Deliberate v1 limits:
|
||
|
||
- **Images only, no custom fonts.** Only the six image asset kinds
|
||
(`logo_svg`, `favicon_png`, `favicon_ico`, `pwa_icon_192`, `pwa_icon_512`,
|
||
`login_bg`) are ever uploaded; `typography.font_sans`/`font_mono` are
|
||
shared as **preset ids** only (`resolve_font_preset_id`), never as uploaded
|
||
`.woff2` bytes — a custom font a user uploaded locally is never re-uploaded
|
||
to the public hub.
|
||
- **Skips factory-default images.** An image asset is only included if its
|
||
UCI value differs from the package's stock filename (`logo.svg`,
|
||
`favicon.ico`, `app-icon-192x192.png`, `app-icon-512x512.png`,
|
||
`apple-touch-icon.png`) — an unmodified slot means the user never
|
||
customized it, so there's nothing worth uploading and nothing
|
||
copyright-risky to check. Each included asset is also read back from disk,
|
||
sha256'd, and size-checked before upload, and any filename containing `/`
|
||
or `..` is rejected outright (defense against path traversal into
|
||
`$ICON_PATH`).
|
||
- Toolbar entries are re-validated with the hub's exact per-field limits
|
||
(title 1-30 chars, url ≤200 chars + scheme, icon charset+≤64 chars, ≤12
|
||
entries) at share time — an out-of-range item is silently dropped from the
|
||
share rather than rejecting the whole publish.
|
||
|
||
---
|
||
|
||
## 9. Releasing
|
||
|
||
OpenWrt package metadata lives in the `Makefile`:
|
||
|
||
```
|
||
PKG_VERSION # semantic version; bump at least a patch for breaking schema changes
|
||
PKG_RELEASE # YYYYMMDD-style date
|
||
```
|
||
|
||
CI builds `.ipk` / `.apk` artifacts through the `build-luci-package` Action using
|
||
the OpenWrt SDK.
|
||
|
||
**The SDK build needs no Node and runs no build step of ours** — it installs
|
||
`htdocs/` and `root/` verbatim. That is the whole reason the minified artifacts
|
||
are committed (§1): the feed rebuilds every package from a git ref, so whatever
|
||
is in the tree at that tag is what users install. Before tagging, confirm the
|
||
tree is self-consistent:
|
||
|
||
```bash
|
||
cd .dev && pnpm test # includes build-js --check
|
||
cd .. && git status --porcelain # must be empty
|
||
```
|
||
|
||
A tag pushed with stale artifacts ships the old frontend against the new
|
||
backend, and nothing downstream will notice.
|
||
|
||
---
|
||
|
||
## 10. Troubleshooting cheat sheet
|
||
|
||
- **Changed `brand` but hover/status colors don't follow?** Derived tokens
|
||
weren't written to UCI — check that `tokens.global.js` loaded and that
|
||
`persistDerivedTokens` ran on save.
|
||
- **Colors broken or invisible text after an upgrade?** Stale keys weren't
|
||
pruned — confirm `SCHEMA_VERSION` was bumped and the migration ran.
|
||
- **Applying a preset fails?** The backend `COLOR_TOKEN_KEYS` count no longer
|
||
matches the template line count.
|
||
- **Injection has no effect (variable name mismatch)?** Config keys (underscores)
|
||
become hyphenated CSS variables on injection; they must match the `--xxx`
|
||
names in the theme's `_tokens.css` exactly.
|
||
- **A page dies with `Invalid regular expression: missing /`, but the source
|
||
parses?** The packaged copy was mangled by jsmin — see §11. Confirm by
|
||
diffing the device's file size against `htdocs/`'s.
|
||
- **The device shows an older UI than the source says it should?** Someone
|
||
committed a source change without running `pnpm build`. `cd .dev && pnpm
|
||
check` names the stale files.
|
||
- **A page is blank with nothing in the console?** Check that
|
||
`htdocs/luci-static/resources/utils/color.global.js` still contains
|
||
`var Color=`. If it is an empty or near-empty file, `mangle.toplevel` or
|
||
`compress.toplevel` got switched on and deleted the global (§11).
|
||
- **A newly added string shows in English on a translated UI?** The msgid in
|
||
the artifact no longer matches the source `gen-pot` scanned. `pnpm test`
|
||
asserts this, so it should not survive a commit — but check there first.
|
||
|
||
---
|
||
|
||
## 11. Minification: why we do it ourselves
|
||
|
||
`luci.mk` minifies every `.js` with Crockford's **jsmin** whenever
|
||
`LUCI_MINIFY_JS` is 1 — and `luci.mk` defaults it to 1. jsmin is from the
|
||
1990s and predates every ES6 construct this package uses, so it does not
|
||
merely fail to shrink things: **it rewrites them into something else.**
|
||
|
||
This package therefore sets `LUCI_MINIFY_JS:=0` in its `Makefile`, before the
|
||
`luci.mk` include (the variable is declared with `?=`, so it must be set
|
||
first), and runs terser itself in `.dev/scripts/build-js.mjs` instead. The rest
|
||
of this section is why jsmin cannot be trusted, followed by what terser needs
|
||
to be told about LuCI.
|
||
|
||
### What terser needs to know about LuCI
|
||
|
||
Four settings in `build-js.mjs` are load-bearing. Three are present, one is
|
||
present by being absent:
|
||
|
||
| Setting | Why |
|
||
|---|---|
|
||
| `parse.bare_returns: true` | A LuCI view's last statement is a top-level `return view.extend(…)`. Without this, terser refuses to parse the file at all. |
|
||
| `compress.directives: false` | `"require uci"` and friends are string directives. A directive-dropping compressor deletes them and LuCI then loads the view with none of its dependencies. |
|
||
| `format.comments: false` | Maximum compression — but it also strips the three MIT notices inside the vendored colorjs build, which we redistribute. They are re-attached from `utils/color.global.js.license`; any future vendored library needs the same sidecar. |
|
||
| `mangle.toplevel` / `compress.toplevel` **left at their defaults (`false`)** | `color.global.js` and `tokens.global.js` exist to publish a top-level `var Color` / `var AuroraTokens`. Turning either on deletes the declaration outright — measured, the output becomes an empty string — and terser still reports success. The symptom is a white page with nothing in the console. **Never enable them.** |
|
||
|
||
Everything else LuCI-specific needs no special handling: `assetUpload`,
|
||
`feedCheck`, `hubApi`, `themePreview` and the injected globals (`L`, `E`, `_`,
|
||
`uci`, `rpc`, `ui`, `form`, `view`, `fs`) are all free variables, which terser
|
||
treats as globals and never renames.
|
||
|
||
`.dev/tests/artifact-safety.test.mjs` asserts all of this against the shipped
|
||
bytes: they parse, they keep every `require` directive their source declared,
|
||
they keep their top-level `return`, the two globals are still declared, the
|
||
licence banner is intact, no `sourceMappingURL` points at a map the package
|
||
does not install, every `_()` msgid survives byte-for-byte, and no hidden file
|
||
(`.DS_Store`) got installed.
|
||
|
||
### Cache busting: anything we fetch ourselves needs a `?v=`
|
||
|
||
uhttpd dates this package's installed files `Last-Modified: Thu, 01 Jan 1970`
|
||
and sends no `Cache-Control`. Browsers then compute heuristic freshness as
|
||
roughly 10% of the file's apparent age — from the epoch, that is years — and
|
||
serve their copy **without even sending `If-None-Match`**. (luci-base and the
|
||
theme carry real dates; only ours are at the epoch.)
|
||
|
||
That is not theoretical. The Marketplace spent months drawing its built-in
|
||
cards from a `presets.json` whose shape had changed underneath it: against the
|
||
old `{light,dark}` shape, `paletteOf` finds no colours and `navOf` no layout,
|
||
so every card rendered with the fallback palette and a top nav bar. Both fail
|
||
silently, so the page looked deliberate.
|
||
|
||
Two kinds of URL, and only one is ours:
|
||
|
||
- **LuCI `require`s it** (`view/aurora/*.js`, everything under `utils/`) —
|
||
LuCI appends `?v=${env.resource_version}`, parsed from the `luci.js` script
|
||
tag, i.e. **luci-base's** version. Bumping this package's `PKG_VERSION` does
|
||
not change it. Nothing here can fix that; a browser picks these up on a hard
|
||
reload or when luci-base is upgraded.
|
||
- **We build the URL** — then it must carry a version. Three do:
|
||
|
||
| Resource | Version | Stamped by |
|
||
|---|---|---|
|
||
| `utils/tokens.global.js` | engine version | `sync-tokens.mjs` → `TOKENS_ENGINE_VERSION` |
|
||
| `aurora/presets.json` | content hash | `gen-presets.mjs` → `PRESETS_VERSION` |
|
||
| `utils/color.global.js` | content hash | `build-js.mjs` → `__ASSET_HASH(...)__` |
|
||
|
||
`__ASSET_HASH(<path>)__` in any source is replaced at build time by the first
|
||
8 hex of that file's sha256. Use it for anything vendored that carries no
|
||
version of its own — the URL then changes if and only if the bytes do, so the
|
||
cache stays warm right up until it must not. An unknown path fails the build.
|
||
|
||
If you add another `fetch()` or script injection for a packaged file, give it a
|
||
version the same way. `artifact-safety.test.mjs` checks the three above and
|
||
that no sentinel survives into an artifact, but it cannot know about a fourth.
|
||
|
||
### What it gets wrong
|
||
|
||
**1. A regex literal after an arrow becomes a division.** jsmin decides a `/`
|
||
opens a regex only when the previous non-blank character is one of:
|
||
|
||
```
|
||
( , = : [ ! & | ? { } ; \n
|
||
```
|
||
|
||
An arrow's `>` is not in that set. So in `(url) => /^https?:\/\//.test(url)`
|
||
the `/` reads as division, and the two adjacent slashes inside the regex then
|
||
look like a line comment — **everything from there to the end of the line is
|
||
deleted**. The regex loses its terminator and the browser reports
|
||
`Invalid regular expression: missing /`. The whole view fails to load.
|
||
|
||
This actually shipped: the theme store was dead on-device for exactly this
|
||
reason, in `marketplace.js`'s `isExternalShortcut`.
|
||
|
||
**2. Template literals are not understood at all.** Backticks mean nothing to
|
||
jsmin, so:
|
||
|
||
- the `//` in `` `https://${FEED_HOST}/manifest.json` `` is eaten as a line
|
||
comment, same as above;
|
||
- whitespace *inside* the literal is stripped as if it were code. Multi-line
|
||
inline CSS is the dangerous case: a descendant selector `.a .b` is squeezed
|
||
to `.a.b`, which silently means "an element with both classes". **No syntax
|
||
error, no console message — the styling just quietly stops matching.**
|
||
|
||
### Reproducing it
|
||
|
||
```bash
|
||
npm install jsmin --no-save
|
||
for f in $(find .dev/src/resource -name '*.js'); do
|
||
./node_modules/.bin/jsmin "$f" > /tmp/m.js
|
||
node --check /tmp/m.js || echo "MANGLED: $f"
|
||
done
|
||
```
|
||
|
||
Note this only catches breakage #1 (a syntax error). Breakage #2 produces
|
||
*valid* JS with different behaviour, so it will pass this check — that
|
||
asymmetry is the main reason minification is off rather than worked around.
|
||
|
||
### The rule that used to be enforced, and is not any more
|
||
|
||
`.dev/tests/jsmin-safety.test.mjs` used to forbid a regex literal directly after an
|
||
arrow, because that is the shape jsmin destroys. It was retired when terser
|
||
took over: the rule existed only because jsmin is not a parser, and terser is.
|
||
`artifact-safety.test.mjs` replaced it with assertions on the artifacts
|
||
themselves, which is strictly stronger — it checks what actually shipped rather
|
||
than banning a syntax that might have shipped badly.
|
||
|
||
The style is still worth keeping if you are writing code that might get
|
||
vendored into a tree that minifies with jsmin:
|
||
|
||
```js
|
||
const EXTERNAL_URL_RE = /^https?:\/\//;
|
||
const isExternalShortcut = (url) => EXTERNAL_URL_RE.test(shortcutText(url));
|
||
```
|
||
|
||
**Sibling packages:** `luci-theme-aurora` and `luci-theme-shadcn` do not
|
||
override `LUCI_MINIFY_JS`. Their Vite output survives jsmin's syntax check,
|
||
but breakage #2 is invisible to that check — worth a look if styling ever
|
||
misbehaves only on-device.
|