mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-02 03:33:56 +08:00
update 2026-10-22 17:05:51
This commit is contained in:
1 parent
06b74cb314
commit
3f38c69f75
303 files changed
+7299
-130543
No files matched your search
@@ -1,879 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,195 +0,0 @@
|
||||
# 让分享的配置带上自定义字体
|
||||
|
||||
## 背景
|
||||
|
||||
主题商店分享的配置,若使用了本机上传的自定义字体,接收方应用后字体不生效。
|
||||
|
||||
真机复现(JDCloud RE-CS-02,2026-08-06):
|
||||
|
||||
```
|
||||
uci struct_font_sans = "OPPO Sans Regular", "Lato", ui-sans-serif, system-ui, sans-serif
|
||||
本机 custom 字体只有 = Inter (sans-inter.meta)
|
||||
aurora-font.css = 只有 Lato + Inter 两个 @font-face,无 OPPO Sans Regular
|
||||
```
|
||||
|
||||
根因:分享端把引用了自定义字体的 CSS font-stack 原样发出,但**字体文件本身从不上传**
|
||||
(`luci.aurora:818` 注释 "v1 never ships font assets")。接收端写入这个 stack,
|
||||
字体族在本机不存在,浏览器静默回退到 stack 末尾的 Lato。
|
||||
|
||||
云端其实早已为字体建好整条通道,缺口只在路由器两端:
|
||||
|
||||
| 环节 | 状态 | 位置 |
|
||||
|---|---|---|
|
||||
| schema 白名单 + 8 MiB 限额 | 已就位 | `validate.js:19,22-30` |
|
||||
| magic byte `isWoff2` | 已就位 | `assets.js:72-73` |
|
||||
| Content-Type `font/woff2` | 已就位 | `assets.js:82-83` |
|
||||
| 上传票据通道(kind-agnostic) | 已就位 | `drafts.js:153-204` |
|
||||
| 审核 body 25 MB(为 8 MB 字体预留) | 已就位 | `admin.js:12-16` |
|
||||
| 审核台 woff2 passthrough | 已就位 | `admin/index.html:907-909` |
|
||||
| **分享端列出字体资产** | **缺** | `luci.aurora:935` 循环只有 6 种图片 |
|
||||
| **接收端落地字体资产** | **缺** | `luci.aurora:663` 只认 5 种图片 kind |
|
||||
| 网页详情页展示字体 | 缺(纯展示) | `site/config.html:377` |
|
||||
|
||||
## 决策(已确认)
|
||||
|
||||
1. 字体随配置分享,走已有的 asset 通道
|
||||
2. 尺寸上限**维持 8 MiB 不变**。曾短暂改到 16 MiB 又改回:R2 免费额度 10 GB,
|
||||
8 MiB 一份约能放 1280 份配置,翻倍不值得。连带把当时拆出来的
|
||||
`MAX_FONT_UPLOAD` 也收掉了——和 `MAX_UPLOAD` 值相同就只是个空壳抽象。
|
||||
云端因此零功能改动,只留一句注释记下 `ADMIN_APPROVE_BODY_BYTES`(25 MB)
|
||||
才是字体上限的真实天花板
|
||||
3. 加版权说明(分享时**和**详情抽屉,两处方向相反)与存储说明(详情抽屉)
|
||||
|
||||
## 关键杠杆点
|
||||
|
||||
接收端只要把下载到的 woff2 按 `upload_font` 的形状摆好——
|
||||
`$FONT_CUSTOM_PATH/` 下 `.woff2` + `.meta`(`family|stack`) + `.face` 三件套
|
||||
(`luci.aurora:2382-2399`)——下游渲染链路**一行都不用改**:
|
||||
|
||||
- `write_combined_font_css:1438` 的 custom 扫描自动收录
|
||||
- `write_font_preload:1488` 的 stack 匹配自动命中
|
||||
- `migrate_font_cache:1778` 的 custom 保留判断自动放行(不会被误删)
|
||||
|
||||
## 任务清单
|
||||
|
||||
### A. 尺寸上限 —— 最终不改
|
||||
- [x] A1–A4 全部回退到 8 MiB(见「决策」第 2 条)。唯一留下的是 `admin.js`
|
||||
的一句注释。附带收获:过程中确认了路由器端 `MAX_UPLOAD` 是图片和字体
|
||||
共用的,将来真要给字体单独放宽,必须拆常量而不是直接改它 —— 否则图片
|
||||
也会跟着放宽,然后在 hub 的 2 MiB 那关被拒
|
||||
|
||||
### B. 分享端(luci.aurora)
|
||||
- [x] B1 assets 循环加 font_sans/font_mono
|
||||
- [x] B2 `.files` 第二列改为相对 `/luci-static/aurora/` 的路径,
|
||||
`hub_share_begin` 去掉写死的 `images/` 前缀
|
||||
- [x] B3 只分享 custom 字体,名册预设不上传
|
||||
- [x] B4(计划外)抽出 `shared_custom_font_base()`:发布面板的清单和真正的
|
||||
打包本来会各判一次「哪份字体会被发出去」,两处迟早漂移成「清单里写着、
|
||||
实际没发」。现在两者问同一个函数
|
||||
|
||||
### C. 接收端(luci.aurora)
|
||||
- [x] C1/C2/C3 新增 `apply_hub_font_asset()`,在 assets 循环里对 font kind
|
||||
单独分流。字体落成与 `upload_font` 完全同形的三件套 —— 杠杆点成立,
|
||||
`write_combined_font_css` / `write_font_preload` / `migrate_font_cache`
|
||||
一行未改就自动认它(真机 preload.txt 已验证)
|
||||
- [x] C4 覆盖前备份,冲突即覆盖(family/stack 相同,视觉无差)
|
||||
- [x] C5 新增 `restore_pre_hub_fonts()`:覆盖的复制回来,**新引入的直接删掉**
|
||||
(`.new` 标记)。少了后者,回滚会留下几 MB 别人的字体永远占着 overlay
|
||||
- [x] C6(计划外)本地复验 woff2 magic:字节来自网络,不复验等于把校验外包给
|
||||
服务器
|
||||
|
||||
### D. 说明文案(含 14 语言 po)
|
||||
- [x] D1 分享面板版权声明(仅在真的要发字体时出现)
|
||||
- [x] D2 详情抽屉存储说明:体积 + 存放位置 + 升级不保留
|
||||
- [x] D3 详情抽屉此前**根本没有**字体下载提示(那句只在内置预设抽屉里)。
|
||||
现在两句各判各的,已带文件的槽位不再算进「待下载」
|
||||
- [x] D4 8 条字符串 × 14 语言全部翻译到位。另有 14 条**既有**未翻译条目
|
||||
属上一批 store 功能,不在本次范围
|
||||
- [ ] D5 po2lmo 真机看翻译(未做)
|
||||
|
||||
### E. 展示层
|
||||
- [x] E1 **无需改动**——`site/config.html:377` 的 `IMAGE_ASSET_KINDS` 是
|
||||
「能画预览图的 kind」而不是白名单,字体走 else 分支已按「只列名不预览」
|
||||
处理好了。原计划把它当缺口是看错了
|
||||
|
||||
## 已考证的事实(供文案写实)
|
||||
|
||||
- 字体落 `/www/luci-static/aurora/fonts/custom/`,属根文件系统 →
|
||||
写入实际落 **overlay 分区**,与 opkg 软件包、`/etc/config` 共用可写 flash
|
||||
- 上传中转 `/tmp`(tmpfs,吃 RAM),传完即删,非持久占用
|
||||
- 拉丁字体子集无压力(实测 Lato 23 KB、Inter 111 KB);
|
||||
中文全字集 woff2 通常几 MB 到十几 MB,是真正的风险
|
||||
- 8 MB flash 设备 overlay 常仅剩 1–3 MB,16 MB 的多在 5–10 MB(量级估计)
|
||||
- overlay 写满 → 装不了包、uci 保存失败、sysupgrade 可能出问题
|
||||
- `sysupgrade.conf.d/aurora-device` 只保留 device.key/hash,
|
||||
**字体升级后丢失**;但 `/etc/config/aurora` 默认保留,
|
||||
`migrate_font_cache:1774-1786` 会检测到并重置为默认,不留坏状态
|
||||
|
||||
## 验证
|
||||
|
||||
- [x] V1 真机(192.168.8.1)单机闭环,证据如下
|
||||
- [x] V2 hub 单测 383 passed;本仓库 321 passed
|
||||
- [x] V3 尺寸上限维持 8 MiB,原有边界单测未动
|
||||
- [x] V4 回归:mono 槽位用的是名册预设,全程正确地**不**出现在
|
||||
`shared_fonts` 和 assets 里
|
||||
|
||||
### V1 真机证据
|
||||
|
||||
分享端 —— 把 sans 指向本机上传的 Inter:
|
||||
|
||||
```
|
||||
hub_local_state → "shared_fonts": [{ "slot":"sans","family":"Inter","size":111268 }]
|
||||
build_share_payload → assets 含 { "kind":"font_sans","sha256":"e06f6b1b…","size":111268 }
|
||||
.files → login_bg images/hub-login-bg-08c6284def5e.jpg
|
||||
font_sans fonts/custom/sans-inter.woff2
|
||||
```
|
||||
|
||||
接收端 —— 删掉本机 Inter 三件套、uci 仍指着它(**bug 现场复现**:
|
||||
`grep -c Inter aurora-font.css` = 0,页面回退 Lato),再跑新代码:
|
||||
|
||||
```
|
||||
apply_hub_font_asset sans <sha> http://127.0.0.1/hubtest.woff2 → APPLY_OK
|
||||
custom/ → sans-inter.woff2 (111268) + .meta + .face
|
||||
.meta → Inter|"Inter", "Lato", ui-sans-serif, system-ui, sans-serif
|
||||
CSS → @font-face font-family:"Inter" src:url(custom/sans-inter.woff2?v=…)
|
||||
preload → /luci-static/aurora/fonts/custom/sans-inter.woff2?v=…
|
||||
```
|
||||
|
||||
负路径 —— sha256 不匹配:`REJECTED_AS_EXPECTED`,目录全空(.hubtmp 也没留)。
|
||||
|
||||
测试后真机已恢复原状(`struct_font_sans` 还是那份坏掉的
|
||||
"OPPO Sans Regular",即用户报告的现场,刻意没动)。
|
||||
|
||||
## F. 审核台不再往返字体字节(计划外,但必须做)
|
||||
|
||||
`ADMIN_APPROVE_BODY_BYTES` 是 25 MB,而一份资产拉满的配置是
|
||||
6×2 MiB(图片)+ 2×8 MiB(字体)= 28 MiB,base64 后约 **37 MB**。
|
||||
也就是说「能分享但永远批不了」这个缺陷**当前就存在**,不需要谁去抬字体上限。
|
||||
|
||||
- [x] F1 `assets.js` 新增 `APPROVE_FROM_R2_KINDS = {font_sans, font_mono}`:
|
||||
sanitize 不改变字节的 kind 没理由被下载、base64、再原样传回来
|
||||
- [x] F2 approve 对这些 kind 从 `pending/` 直接读,并照样重跑 magic + size
|
||||
检查(字节在存储里躺过,不复验就是把校验外包给桶)
|
||||
- [x] F3 线格式改成各自声明:passthrough 的 kind **必须**用
|
||||
`{passthrough:true}` 且不带 `data_b64`,其余 kind **必须**带
|
||||
`data_b64`。任一方漂移就 400 —— 防的是「某个 kind 将来加了真正的
|
||||
sanitizer 却还留在集合里」,那会静默地批准没人清洗过的字节
|
||||
- [x] F4 `favicon_ico` 刻意不加入:2 MiB 不构成体积问题,且它的 passthrough
|
||||
是被迫的(canvas 产不出 ICO)而非设计如此,是最可能长出真 sanitizer 的
|
||||
- [x] F5 补 6 个测试。**这条路径此前零覆盖** —— 所以我改了线格式,383 个测试
|
||||
依然全绿。并做了变异验证:把服务端自己的判断换成「前端说了算」,必须有
|
||||
测试变红
|
||||
|
||||
## 验证补充
|
||||
|
||||
- [x] D5 po2lmo:从 luci 源码编出 po2lmo → 编 zh_Hans → 推真机 →
|
||||
用 LuCI 自己的 `i18n.translate` 逐条查,9 条新字符串全部解析出中文
|
||||
(注意语言代码是 `zh-cn`,不是 po 目录名 `zh_Hans`)
|
||||
- [x] E2E:**本地起完整 hub**(`wrangler dev` + 本地 D1/R2),路由器
|
||||
`HUB_BASE` 临时指过去,跑完 分享→直传→审核→应用→回滚 全程,
|
||||
**完全没碰生产商店**
|
||||
|
||||
### E2E 全程记录
|
||||
|
||||
```
|
||||
hub_share_begin → assets 含 font_sans,src=/luci-static/aurora/fonts/custom/sans-inter.woff2
|
||||
凭 ticket 直传 → login_bg 866522B / font_sans 111268B,均 HTTP 200
|
||||
hub_share_commit → id=m399c8az
|
||||
admin approve → body 仅 103 字节(字体的 111KB 不在其中),HTTP 200
|
||||
{"kind":"font_sans","passthrough":true}
|
||||
GET /assets/… → HTTP 200, 111268B, content-type: font/woff2
|
||||
应用前 → 删光本机 custom/,CSS 中 Inter 出现 0 次
|
||||
hub_apply → custom/ 三件套重建,uci struct_font_sans 指向 Inter,
|
||||
CSS 有 @font-face,preload.txt 命中,active_source=hub:m399c8az
|
||||
回滚 → 备份目录里是 sans-inter.new(本次新引入)→ 三件套被删净,
|
||||
CSS 中 Inter 归 0,别人的字体没有留下来占 overlay
|
||||
```
|
||||
|
||||
## 尚未做
|
||||
|
||||
- 部署到生产:hub 的 approve 线格式变了,`src/` 和 `site/` 同属一次
|
||||
`wrangler deploy`,所以前后端会一起更新,不存在中间不一致的窗口
|
||||
- 真正在公开商店发布一条(本地 hub 已覆盖同样的链路,价值主要在验证生产配置)
|
||||
|
||||
注意:测试机 overlay 挂在 tmpfs(RAM overlay),重启全丢,
|
||||
且其 df 数字不代表普通设备——见 memory `router-ram-overlay`。
|
||||
Reference in new issue
Block a user