Skip to content

Commit 11ceaf8

Browse files
authored
Merge pull request #38 from SimplyLiz/develop
Release: merge develop into main
2 parents 175b101 + 6af1c6e commit 11ceaf8

88 files changed

Lines changed: 3157 additions & 812 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,42 @@ jobs:
6969
size=$(stat -c%s packages/probe/dist/device-router-probe.min.js.gz 2>/dev/null || stat -f%z packages/probe/dist/device-router-probe.min.js.gz)
7070
echo "Probe gzipped size: ${size} bytes"
7171
[ "$size" -le 1024 ] || (echo "Probe exceeds 1 KB limit" && exit 1)
72+
73+
package-lint:
74+
runs-on: ubuntu-latest
75+
steps:
76+
- uses: actions/checkout@v6
77+
78+
- uses: pnpm/action-setup@v4
79+
80+
- uses: actions/setup-node@v6
81+
with:
82+
node-version: 22
83+
cache: pnpm
84+
85+
- run: pnpm install --frozen-lockfile
86+
87+
- run: pnpm build
88+
89+
- name: publint
90+
run: pnpm lint:publint
91+
92+
- name: attw
93+
run: pnpm lint:attw
94+
95+
e2e:
96+
runs-on: ubuntu-latest
97+
needs: [test]
98+
steps:
99+
- uses: actions/checkout@v6
100+
101+
- uses: pnpm/action-setup@v4
102+
103+
- uses: actions/setup-node@v6
104+
with:
105+
node-version: 22
106+
cache: pnpm
107+
108+
- run: pnpm install --frozen-lockfile
109+
110+
- run: pnpm test:e2e

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ coverage/
77
*.tsbuildinfo
88
.turbo/
99
.DS_Store
10+
.ckb/
1011
*.log

CHANGELOG.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,36 @@
11
# Changelog
22

3-
## Unreleased
3+
## 1.0.0 (2026-03-04)
4+
5+
### Breaking Changes
6+
7+
- **Normalize `ConnectionThresholds` field names** — Renamed `downlink2gUpperBound``lowUpperBound`, `downlink3gUpperBound``midUpperBound`, `downlink4gUpperBound``highUpperBound` to match the `CpuThresholds`/`MemoryThresholds` naming pattern. Update any custom `connection` threshold objects in your `thresholds` config
8+
- **Rename `ConnectionTier` value `'fast'``'high'`** — Aligns connection tier vocabulary with CPU, memory, and GPU tiers which all use `'low' | 'mid' | 'high'`. Update any code comparing `tiers.connection === 'fast'` to use `'high'` instead
9+
- **`classify()` and `deriveHints()` now accept `StoredSignals` instead of `RawSignals`** — These functions never used `userAgent` or `viewport` (which are stripped before storage). The narrower type makes the API honest. Existing call sites are unaffected — `RawSignals` is structurally assignable to `StoredSignals`
10+
- **`profile:store` event now carries `StoredSignals` instead of `RawSignals`** — The event previously emitted the raw probe payload (including `userAgent`/`viewport`), not what was actually stored. The `signals` field now matches the persisted data. `bot:reject` still carries `RawSignals` (it fires before stripping)
11+
- **Rename default cookie `dr_session``device-router-session`** — Self-documenting name before 1.0 locks the cookie in. If you hardcode `cookieName: 'dr_session'` in your options you are unaffected; if you rely on the default, existing sessions will reset once on deploy
12+
- **Remove `disableAutoplay` rendering hint**`disableAutoplay` triggered on identical conditions to `deferHeavyComponents` (`isLowEnd || isSlowConnection || isBatteryConstrained`). Use `deferHeavyComponents` instead
13+
- **Remove `has()` from `StorageAdapter`**`has()` was a redundant alias for `exists()`. Custom adapters must remove their `has()` implementation
14+
- **middleware-fastify: normalized return shape**`createDeviceRouter()` now returns raw Fastify hooks instead of a `fastify-plugin` wrapped plugin. Migrate `await app.register(middleware)``app.addHook('preHandler', middleware)`. When using `injectProbe: true`, register the injection hook separately: `app.addHook('onSend', injectionMiddleware)`. Removed `fastify-plugin` dependency
15+
16+
### Migration Guide
17+
18+
If you have a custom `StorageAdapter` implementation:
19+
20+
- Remove the `has()` method — use `exists()` instead
21+
- Replace any `adapter.has(token)` calls with `adapter.exists(token)`
22+
23+
### Features
24+
25+
- **Composable middleware**`createMiddleware()`, `createProbeEndpoint()`, and `createInjectionMiddleware()` are now first-class exports with full threshold validation and documentation. Use them independently for fine-grained control without the `createDeviceRouter()` factory
26+
- **`loadProbeScript()` utility** — New helper exported from all middleware packages that reads the minified probe bundle and optionally rewrites the endpoint URL. Pairs with `createInjectionMiddleware()` for standalone probe injection
27+
- **`clear()` on StorageAdapter**`clear()` is now part of the `StorageAdapter` interface. `MemoryStorageAdapter` and `RedisStorageAdapter` both implement it. Redis implementation uses SCAN (when available) or KEYS + `DEL` with graceful error handling
28+
- **StorageAdapter introspection** — New `count()` and `keys()` methods on `StorageAdapter` for inspecting stored profiles. `keys()` returns session tokens (not prefixed keys). All methods handle errors gracefully on Redis
29+
- **Redis SCAN support**`RedisStorageAdapter` uses the optional `scan()` method on the client for `clear()`, `count()`, and `keys()` operations, avoiding the blocking `KEYS` command. Falls back to `KEYS` when `scan` is not available
30+
- **`durationMs` on `bot:reject` event** — The `bot:reject` event now includes `durationMs` measuring validation and bot detection time, matching the pattern used by `profile:store`
31+
- **`errorMessage` on error events** — Error events now include a pre-extracted `errorMessage: string` field, avoiding the need to narrow the `error: unknown` field. New `extractErrorMessage()` helper exported from `@device-router/types`
32+
- **New rendering hints** — Three new hints: `limitVideoQuality` (slow connection or low battery), `useSystemFonts` (low-end device or slow connection), `disablePrefetch` (slow connection or low battery)
33+
- **Probe retry logic** — New `runProbeWithRetry()` function with exponential backoff and jitter. Exported from `@device-router/probe` as an ESM-only export (does not affect the IIFE bundle size)
434

535
## 0.4.0 (2026-02-24)
636

@@ -47,7 +77,7 @@
4777
### Features
4878

4979
- **GPU detection** — Classify GPU tier from WebGL renderer string: software renderers → `low`, RTX/RX 5000+/Apple M-series → `high`, everything else → `mid`
50-
- **Battery API signal** — Collect battery level and charging status via `navigator.getBattery()` (Chromium-only, silently skipped elsewhere). When unplugged and below 15%, `deferHeavyComponents`, `reduceAnimations`, and `disableAutoplay` are forced on
80+
- **Battery API signal** — Collect battery level and charging status via `navigator.getBattery()` (Chromium-only, silently skipped elsewhere). When unplugged and below 15%, `deferHeavyComponents` and `reduceAnimations` are forced on
5181
- **Signal validation** — New `isValidSignals()` type guard for validating incoming probe payloads
5282
- **Custom GPU thresholds**`softwarePattern` and `highEndPattern` are configurable via `GpuThresholds`
5383

README.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ A **~1 KB** client probe. One middleware call. Full device awareness on every re
1818
Responsive design adapts layout. DeviceRouter adapts **what you serve**.
1919

2020
- A budget phone on 2G? Skip the heavy animations, defer non-critical components, prefer server-side rendering.
21-
- A flagship on fiber? Go all out — autoplay, full interactivity, rich visuals.
21+
- A flagship on fiber? Go all out — full interactivity, rich visuals.
2222

2323
No user-agent sniffing. No guesswork. Real signals from real devices, classified into actionable tiers and rendering hints your server can act on immediately.
2424

@@ -120,7 +120,6 @@ Based on tiers and user preferences, DeviceRouter derives actionable booleans:
120120
| `serveMinimalCSS` | Low-end device |
121121
| `reduceAnimations` | Low-end device, prefers reduced motion, or low battery |
122122
| `useImagePlaceholders` | Slow connection (2G/3G) |
123-
| `disableAutoplay` | Low-end device, slow connection, or low battery |
124123
| `preferServerRendering` | Low-end device |
125124
| `disable3dEffects` | No GPU or software renderer |
126125

@@ -157,6 +156,27 @@ const { middleware, probeEndpoint, injectionMiddleware } = createDeviceRouter({
157156

158157
No need to manually add `<script>` tags — the probe is injected before `</head>` in every HTML response.
159158

159+
## Composable Middleware
160+
161+
Don't need the full factory? Use the individual pieces directly:
162+
163+
```typescript
164+
import {
165+
createMiddleware,
166+
createProbeEndpoint,
167+
createInjectionMiddleware,
168+
loadProbeScript,
169+
} from '@device-router/middleware-express';
170+
171+
const middleware = createMiddleware({ storage, thresholds });
172+
const endpoint = createProbeEndpoint({ storage, ttl: 3600 });
173+
const injection = createInjectionMiddleware({
174+
probeScript: loadProbeScript(),
175+
});
176+
```
177+
178+
Each piece validates its own options at creation time. See the [API docs](docs/api/middleware-express.md#standalone-functions) for full option tables.
179+
160180
## First-Request Handling
161181

162182
By default, `deviceProfile` is `null` on the first page load before the probe runs. Two opt-in strategies provide immediate classification:
@@ -224,8 +244,8 @@ new RedisStorageAdapter(redisClient, { prefix: 'dr:profile:' });
224244

225245
Each framework has an example app that renders a product landing page adapting in real time to device capabilities:
226246

227-
- **Full experience** (high-end device) — animated gradient hero, SVG icons, inline charts, pulsing CTA, hover transitions, autoplay visualizer
228-
- **Lite experience** (low-end device) — flat solid backgrounds, Unicode icons, placeholder boxes, no animations, autoplay disabled
247+
- **Full experience** (high-end device) — animated gradient hero, SVG icons, inline charts, pulsing CTA, hover transitions
248+
- **Lite experience** (low-end device) — flat solid backgrounds, Unicode icons, placeholder boxes, no animations
229249

230250
Run any example to see it in action:
231251

0 commit comments

Comments
 (0)