favicon-env 0.3.0 → 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
@@ -16,6 +16,8 @@ Tint your favicon per environment so you can tell instances apart at a glance
16
16
  pnpm add favicon-env
17
17
  ```
18
18
 
19
+ > **`dependencies` or `devDependencies`?** Either works — every mode below resolves the import during your build, so nothing looks `favicon-env` up at runtime on the deployed host, staging included. `dependencies` is the safer default: npm reads `NODE_ENV=production` as `--omit=dev`, so a CI or PaaS builder with that set skips a devDependency and fails the build on `Cannot find module 'favicon-env'` (pnpm doesn't couple the two). `-D` is defensible if you only use [build-time mode](#build-time--ssr-mode) and want a clean production audit. Neither costs you bundle size — [it tree-shakes to zero bytes in prod](#zero-bytes-in-prod).
20
+
19
21
  > **Using an AI coding agent?** Install the bundled [Agent Skill](https://tanstack.com/intent) with `pnpm dlx @tanstack/intent@latest install`.
20
22
 
21
23
  ## Runtime mode
@@ -543,6 +545,37 @@ For a zero-byte prod page, have the HTML build/template omit these scripts when
543
545
 
544
546
  </details>
545
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
+
546
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:
547
580
 
548
581
  ```js
@@ -677,6 +710,8 @@ const pr = process.env.VERCEL_GIT_PULL_REQUEST_ID;
677
710
  faviconDataUri(favicon, pr ? { badge: { text: `#${pr}` } } : { hue: 45 });
678
711
  ```
679
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
+
680
715
  ### Vite
681
716
 
682
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.0",
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",
@@ -49,16 +49,16 @@
49
49
  "access": "public"
50
50
  },
51
51
  "devDependencies": {
52
- "@changesets/cli": "^2.31.0",
53
- "@playwright/test": "^1.61.1",
54
- "@swc/core": "^1.15.43",
55
- "@tanstack/intent": "^0.3.5",
56
- "dependency-cruiser": "^18.0.0",
57
- "knip": "^6.26.0",
58
- "oxfmt": "^0.58.0",
59
- "oxlint": "^1.73.0",
60
- "publint": "^0.3.21",
61
- "simple-git-hooks": "^2.13.1",
52
+ "@changesets/cli": "^3.0.1",
53
+ "@playwright/test": "^1.62.1",
54
+ "@swc/core": "^1.16.1",
55
+ "@tanstack/intent": "^0.3.8",
56
+ "dependency-cruiser": "^18.2.0",
57
+ "knip": "^6.33.0",
58
+ "oxfmt": "^0.65.0",
59
+ "oxlint": "^1.80.0",
60
+ "publint": "^0.3.24",
61
+ "simple-git-hooks": "^2.14.0",
62
62
  "tsup": "^8.5.1",
63
63
  "typescript": "^7.0.2"
64
64
  },
@@ -74,10 +74,13 @@
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",
81
+ "skill:sync": "node scripts/skill-version.mjs",
82
+ "skill:check": "node scripts/skill-version.mjs --check",
83
+ "skill:stale": "node scripts/skill-stale.mjs",
81
84
  "pretest": "pnpm build",
82
85
  "test": "node --test test/pure.test.mjs test/skill.test.mjs",
83
86
  "test:skill": "node --test test/skill.test.mjs",
@@ -89,7 +92,7 @@
89
92
  "test:all": "pnpm test && pnpm test:frameworks",
90
93
  "ncu": "pnpm dlx npm-check-updates",
91
94
  "changeset": "changeset",
92
- "version": "changeset version",
95
+ "version": "changeset version && pnpm run skill:sync",
93
96
  "release": "changeset publish"
94
97
  }
95
98
  }
@@ -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.2.0'
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