favicon-env 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -545,6 +545,37 @@ For a zero-byte prod page, have the HTML build/template omit these scripts when
545
545
 
546
546
  </details>
547
547
 
548
+ <details>
549
+ <summary><b>Non-JS backends</b> — Rails, Django, Laravel, Phoenix, Go, Rust</summary>
550
+
551
+ Runtime mode asks nothing of the server: it reads the page's `<link rel="icon">` (or falls back to `/favicon.ico`), redraws it on a canvas, and swaps the `href`. No bundler and no JavaScript framework are involved, so serve the global build from your static assets and let the template supply the environment:
552
+
553
+ ```html
554
+ <link rel="icon" href="/favicon.svg" />
555
+
556
+ <!-- render these two tags only when the app environment is not `prod` -->
557
+ <script src="/static/favicon-env.global.js"></script>
558
+ <script>
559
+ void faviconEnv.envFavicon({
560
+ environments: {
561
+ dev: { tint: '#22c55e' },
562
+ staging: { badge: { text: 'S', color: '#f59e0b', shape: 'cover' } },
563
+ },
564
+ detect: () => 'staging', // interpolate the server's env value here
565
+ });
566
+ </script>
567
+ ```
568
+
569
+ Pass whatever the stack already knows — `Rails.env`, `settings.DEBUG`, `config('app.env')`, `Application.get_env(:my_app, :env)`, `os.Getenv("APP_ENV")` — through your template's normal escaping. Without it the hostname heuristic still applies, which is enough when your hosts follow the usual conventions.
570
+
571
+ Having the template omit those two tags in `prod` is the whole [zero-bytes-in-prod](#zero-bytes-in-prod) story here: nothing is served, so there is no bundler dead-code elimination to arrange. The `Plain HTML (global)` entry in the framework matrix exercises this exact shape — a server that renders the script conditionally and injects `detect` — so only the template language differs.
572
+
573
+ Add the head-observer pattern from the examples above only if something re-renders `<head>` after load: Turbo Drive, Inertia's head manager, `leptos_meta`. Plain server templates, htmx body swaps, and LiveView leave `<head>` alone, so a single call is enough. For a Rust/Wasm SPA (Leptos, Dioxus, Yew), the script tag in the `index.html` shell is all you need — no `wasm-bindgen` interop.
574
+
575
+ [Build-time mode](#build-time--ssr-mode) is the one piece that needs a JavaScript runtime; see the note there.
576
+
577
+ </details>
578
+
548
579
  By default the environment is guessed from the hostname (`localhost` / `*.local` / raw IPs → `dev`; a `staging`/`preview`/`qa`/… segment → `staging`; everything else → `prod`). Override it:
549
580
 
550
581
  ```js
@@ -679,6 +710,8 @@ const pr = process.env.VERCEL_GIT_PULL_REQUEST_ID;
679
710
  faviconDataUri(favicon, pr ? { badge: { text: `#${pr}` } } : { hue: 45 });
680
711
  ```
681
712
 
713
+ `favicon-env/ssr` is JavaScript, so a non-JS server can't call it in-process. Run it as a Node build step that writes one SVG per environment (`favicon.dev.svg`, `favicon.staging.svg`) and have the template pick one, or stay on runtime mode and accept its brief flash. `tintSvg` itself is plain string manipulation — a wrapping filtered `<g>` plus an optional badge `<g>` — if you would rather port it to the host language.
714
+
682
715
  ### Vite
683
716
 
684
717
  This Vite plugin rewrites an SVG favicon using the current mode:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "favicon-env",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Tint your favicon per environment (dev/staging/prod) so you can tell instances apart at a glance. Runtime canvas mode + build-time SVG mode. Zero dependencies.",
5
5
  "keywords": [
6
6
  "canvas",
@@ -52,13 +52,13 @@
52
52
  "@changesets/cli": "^3.0.1",
53
53
  "@playwright/test": "^1.62.1",
54
54
  "@swc/core": "^1.16.1",
55
- "@tanstack/intent": "^0.3.7",
55
+ "@tanstack/intent": "^0.3.8",
56
56
  "dependency-cruiser": "^18.2.0",
57
- "knip": "^6.32.3",
57
+ "knip": "^6.33.0",
58
58
  "oxfmt": "^0.65.0",
59
59
  "oxlint": "^1.80.0",
60
60
  "publint": "^0.3.24",
61
- "simple-git-hooks": "^2.13.1",
61
+ "simple-git-hooks": "^2.14.0",
62
62
  "tsup": "^8.5.1",
63
63
  "typescript": "^7.0.2"
64
64
  },
@@ -74,7 +74,7 @@
74
74
  "format": "oxfmt src",
75
75
  "knip": "knip",
76
76
  "depcruise": "depcruise src",
77
- "check": "oxfmt --check src && oxlint src && knip && depcruise src",
77
+ "check": "oxfmt --check src && oxlint src && knip && depcruise src && pnpm run skill:stale && pnpm run skill:check",
78
78
  "publint": "publint --strict",
79
79
  "intent:validate": "intent validate skills",
80
80
  "intent:stale": "intent stale skills",
@@ -5,12 +5,13 @@ description: >
5
5
  identical tabs are distinguishable. Load when calling envFavicon, choosing
6
6
  runtime (canvas) vs build-time (SSR) mode, adding hue/invert/filter tints or
7
7
  badges/PR numbers, wiring it into Next.js/TanStack/Astro/SvelteKit/SolidStart/
8
- Angular/Nuxt/Vite/plain HTML, or configuring detect, environments, rules, or
9
- auto mode.
8
+ Angular/Nuxt/Vite/plain HTML or a server-rendered non-JS backend
9
+ (Rails/Django/Laravel/Phoenix/Go/Rust), or configuring detect, environments,
10
+ rules, or auto mode.
10
11
  metadata:
11
12
  type: core
12
13
  library: favicon-env
13
- library_version: '0.3.1'
14
+ library_version: '0.3.2'
14
15
  sources:
15
16
  - 'Amir-Abushanab/favicon-env:README.md'
16
17
  - 'Amir-Abushanab/favicon-env:src/tint.ts'
@@ -66,6 +67,7 @@ The environment name defaults to a `location.hostname` heuristic (`defaultDetect
66
67
  | Nuxt | `app/plugins/favicon-env.client.ts`, using an explicit Vite build constant |
67
68
  | Vite SPA | Client entry module (`src/main.ts`, etc.) |
68
69
  | Plain HTML | Native module or the global build (`window.faviconEnv`) |
70
+ | Non-JS backend | Global build `<script>` in the server template; `detect` from the injected env |
69
71
 
70
72
  For any SSR router/head manager that declares the favicon itself, wrap the call in
71
73
  the head-observer pattern below. Hydration or navigation can otherwise restore its
@@ -81,6 +83,9 @@ are folded to literals. SvelteKit and Nuxt may preserve their normal public runt
81
83
  configuration, so define a dedicated constant through Vite's `define` option and use
82
84
  that in the guard.
83
85
 
86
+ In a server-rendered template there is no bundler step to arrange: render the
87
+ `<script>` tags only when the environment is not `prod` and prod ships nothing.
88
+
84
89
  Angular's application builder can emit a lazy chunk even behind a false `define`
85
90
  guard. Put the dynamic import in a local loader module and replace that module with a
86
91
  typed no-op in the prod build using Angular `fileReplacements`. For plain HTML, omit
@@ -130,6 +135,32 @@ void envFavicon({ auto: true });
130
135
  Derives a deterministic hue from `location.host`, so every origin and port gets its
131
136
  own colour — handy for telling several dev servers apart.
132
137
 
138
+ ### Server-rendered templates — the global build, no bundler
139
+
140
+ ```html
141
+ <link rel="icon" href="/favicon.svg" />
142
+
143
+ <!-- render these two tags only when the app environment is not prod -->
144
+ <script src="/static/favicon-env.global.js"></script>
145
+ <script>
146
+ void faviconEnv.envFavicon({
147
+ environments: { dev: { tint: '#22c55e' }, staging: { badge: '#f59e0b' } },
148
+ detect: () => 'staging', // interpolate Rails.env / config('app.env') / os.Getenv("APP_ENV")
149
+ });
150
+ </script>
151
+ ```
152
+
153
+ Runtime mode needs only a browser — it reads `link[rel~="icon"]` (falling back to
154
+ `/favicon.ico`), redraws on canvas, and swaps the `href` — so Rails, Django, Laravel,
155
+ Phoenix, Go, and Rust/Wasm apps serve `dist/favicon-env.global.js` from their static
156
+ assets and let the template inject `detect`. Omitting the two tags in prod is the
157
+ whole zero-bytes story there. Add the head observer only where something re-renders
158
+ `<head>` (Turbo Drive, Inertia, `leptos_meta`); plain templates, htmx body swaps, and
159
+ LiveView leave it alone. For a Rust/Wasm SPA the tag in the `index.html` shell is
160
+ enough — no `wasm-bindgen` interop. `favicon-env/ssr` is the one JS-only entry point:
161
+ run it as a Node build step emitting one SVG per environment if the no-flash path
162
+ matters.
163
+
133
164
  ### Build-time SSR — no first-paint flash
134
165
 
135
166
  ```js
@@ -355,3 +386,32 @@ const href = document.querySelector('link[rel~="icon"]').href;
355
386
  redrawing; the `<link>` is not replaced until it resolves.
356
387
 
357
388
  Source: src/tint.ts (returns `Promise`, `img` load listener)
389
+
390
+ ### MEDIUM — Expecting the global build's `data-*` attributes to carry the environment
391
+
392
+ Wrong:
393
+
394
+ ```html
395
+ <!-- beta.acme.internal — no `staging`/`qa`/… segment, so the heuristic says `prod` -->
396
+ <script src="/static/favicon-env.global.js" data-staging="45"></script>
397
+ ```
398
+
399
+ Correct:
400
+
401
+ ```html
402
+ <script src="/static/favicon-env.global.js"></script>
403
+ <script>
404
+ void faviconEnv.envFavicon({
405
+ environments: { staging: { hue: 45 } },
406
+ detect: () => 'staging', // filled in by the server template
407
+ });
408
+ </script>
409
+ ```
410
+
411
+ `data-<env>="<degrees>"` only declares hues (non-numeric values are dropped, and any
412
+ other numeric `data-*` becomes an environment); the current env is still resolved by
413
+ `defaultDetect`. When the host doesn't match that heuristic, call `envFavicon`
414
+ explicitly with a `detect` the server fills in. `data-auto` is the exception: it
415
+ ignores `environments` and hashes `location.host`.
416
+
417
+ Source: src/global.ts (boot), src/detect.ts