Skip to content

Commit f29313e

Browse files
Release v0.2.1 (#11)
* Add CI/CD quality gates and Dependabot config Enforce coverage thresholds in CI and publish, add security audit, version-tag validation, GitHub Release creation, and Dependabot for automated dependency updates targeting develop. * Split CI into separate lint, audit, and test jobs * Remove redundant push trigger for develop in CI * Add Node.js version label to test job names * Bump actions/setup-node from 4 to 6 (#3) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](actions/setup-node@v4...v6) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump actions/checkout from 4 to 6 (#4) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump esbuild from 0.25.12 to 0.27.3 in the dev-deps group (#5) Bumps the dev-deps group with 1 update: [esbuild](https://github.com/evanw/esbuild). Updates `esbuild` from 0.25.12 to 0.27.3 - [Release notes](https://github.com/evanw/esbuild/releases) - [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md) - [Commits](evanw/esbuild@v0.25.12...v0.27.3) --- updated-dependencies: - dependency-name: esbuild dependency-version: 0.27.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-deps ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump koa and @types/koa (#6) Bumps [koa](https://github.com/koajs/koa) and [@types/koa](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/koa). These dependencies needed to be updated together. Updates `koa` from 2.16.3 to 3.1.1 - [Release notes](https://github.com/koajs/koa/releases) - [Changelog](https://github.com/koajs/koa/blob/master/History.md) - [Commits](koajs/koa@v2.16.3...v3.1.1) Updates `@types/koa` from 2.15.0 to 3.0.1 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/koa) --- updated-dependencies: - dependency-name: koa dependency-version: 3.1.1 dependency-type: direct:production update-type: version-update:semver-major - dependency-name: "@types/koa" dependency-version: 3.0.1 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Add GPU detection via WebGL renderer string (#7) * Add GPU detection via WebGL renderer string Detect GPU capabilities using the UNMASKED_RENDERER_WEBGL string from WebGL debug info. Classifies GPUs into none/low/mid/high tiers and derives a disable3dEffects rendering hint for apps serving 3D or GPU-intensive content. * Harden GPU detection, add thresholds, extract shared demo template - Fix probe bundle size references (762 B -> 988 B) across README, docs, examples - Add WebGL context cleanup via loseContext() to free GPU resources - Use failIfMajorPerformanceCaveat to detect software renderers even when WEBGL_debug_renderer_info is blocked by privacy settings - Add GpuThresholds (softwarePattern, highEndPattern) to TierThresholds for API consistency with CPU/memory/connection thresholds - Extract duplicated HTML template from 4 example servers into examples/shared/demo-template.ts (~1000 lines removed) - Fix misaligned ASCII diagram in README - Update API docs with GPU threshold documentation - Add tests for loseContext, failIfMajorPerformanceCaveat fallback, and custom GPU thresholds * Fix ASCII diagram box alignment in README * Fix prettier formatting * Add Battery API signal to constrain rendering hints (#8) * Add Battery API signal to constrain hints on low power Battery is transient state (not a device capability), so it bypasses tier classification and feeds directly into deriveHints(). When the device is unplugged and below 15% charge, deferHeavyComponents, reduceAnimations, and disableAutoplay are forced on. Probe collects battery via navigator.getBattery() (Chromium-only), silently skipped on Firefox/Safari. Bundle stays at 1024 bytes gzipped by reclaiming bytes from cookie handling code. * Fix prettier formatting * Fix stale docs, add package READMEs, patch minimatch CVE (#10) * Replace stale probe byte counts with ~1 KB Exact byte counts (988 B, 901 B) went stale after battery signal landed. Use ~1 KB in all display references — the actual limit is enforced at build time in bundle.js and ci.yml. * Fix minimatch ReDoS vulnerability (high) Override minimatch <10.2.1 → ^10.2.1 via pnpm overrides. Vulnerable 9.0.6 was pulled in transitively by typescript-eslint. * Add README to each package for npm npm shows the README from the package directory at publish time. Each package now has a detailed README covering installation, usage, API surface, options, and compatibility. * Link npm badge to scope search page * Update README and CHANGELOG for v0.2.0 - Add battery row to signals table - Update 3 hint descriptions to mention low battery - Fix classify step to mention GPU tier - Add 0.2.0 changelog entry (GPU, battery, validation, deps, CI) * Fix prettier formatting * Bump version to 0.2.1 --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent 01b8670 commit f29313e

20 files changed

Lines changed: 829 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# Changelog
22

3+
## 0.2.0 (2026-02-22)
4+
5+
### Features
6+
7+
- **GPU detection** — Classify GPU tier from WebGL renderer string: software renderers → `low`, RTX/RX 5000+/Apple M-series → `high`, everything else → `mid`
8+
- **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
9+
- **Signal validation** — New `isValidSignals()` type guard for validating incoming probe payloads
10+
- **Custom GPU thresholds**`softwarePattern` and `highEndPattern` are configurable via `GpuThresholds`
11+
12+
### Dependencies
13+
14+
- Bump `esbuild` from 0.25.12 to 0.27.3
15+
- Bump `koa` and `@types/koa`
16+
- Bump `actions/checkout` from v4 to v6
17+
- Bump `actions/setup-node` from v4 to v6
18+
19+
### Infrastructure
20+
21+
- Split CI into separate lint, audit, and test jobs
22+
- Add Dependabot config for automated dependency updates
23+
324
## 0.1.0 (2026-02-22)
425

526
Initial release.

README.md

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# DeviceRouter
22

3-
[![npm](https://img.shields.io/npm/v/@device-router/types?label=npm&color=cb3837)](https://www.npmjs.com/package/@device-router/types)
3+
[![npm](https://img.shields.io/npm/v/@device-router/types?label=npm&color=cb3837)](https://www.npmjs.com/search?q=%40device-router)
44
[![CI](https://img.shields.io/github/actions/workflow/status/SimplyLiz/DeviceRouter/ci.yml?branch=main&label=CI)](https://github.com/SimplyLiz/DeviceRouter/actions/workflows/ci.yml)
5-
[![bundle size](https://img.shields.io/badge/probe-988%20B%20gzipped-blue)](https://github.com/SimplyLiz/DeviceRouter/tree/main/packages/probe)
5+
[![bundle size](https://img.shields.io/badge/probe-~1%20KB%20gzipped-blue)](https://github.com/SimplyLiz/DeviceRouter/tree/main/packages/probe)
66
[![license](https://img.shields.io/github/license/SimplyLiz/DeviceRouter)](https://github.com/SimplyLiz/DeviceRouter/blob/main/LICENSE)
77
[![node](https://img.shields.io/badge/node-%E2%89%A520-417e38)](https://nodejs.org)
88
[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178c6)](https://www.typescriptlang.org/)
@@ -11,7 +11,7 @@
1111

1212
Stop guessing what your users' devices can handle. DeviceRouter detects real device capabilities — CPU cores, memory, network speed, and more — and gives your server the intelligence to adapt responses instantly.
1313

14-
A **988-byte** client probe. One middleware call. Full device awareness on every request.
14+
A **~1 KB** client probe. One middleware call. Full device awareness on every request.
1515

1616
## Why DeviceRouter?
1717

@@ -27,7 +27,7 @@ No user-agent sniffing. No guesswork. Real signals from real devices, classified
2727
```
2828
┌──────────┐ ┌────────────┐ ┌─────────┐
2929
│ Browser │ POST /probe │ Express │ │ Storage │
30-
│ (988 B) │ ──────────────> │ Middleware │ ──> │ │
30+
│ (~1 KB) │ ──────────────> │ Middleware │ ──> │ │
3131
│ │ device signals │ │ │ │
3232
└──────────┘ └────────────┘ └─────────┘
3333
@@ -39,7 +39,7 @@ No user-agent sniffing. No guesswork. Real signals from real devices, classified
3939
```
4040

4141
1. **Probe** — A tiny script runs once per session, collecting device signals via browser APIs
42-
2. **Classify** — The middleware classifies the device into CPU, memory, and connection tiers
42+
2. **Classify** — The middleware classifies the device into CPU, memory, connection, and GPU tiers
4343
3. **Hint** — Rendering hints like `deferHeavyComponents` and `reduceAnimations` are derived automatically
4444
4. **Serve** — Your route handlers read `req.deviceProfile` and respond accordingly
4545

@@ -93,6 +93,7 @@ app.get('/', (req, res) => {
9393
| Prefers reduced motion | `matchMedia` | All modern browsers |
9494
| Prefers color scheme | `matchMedia` | All modern browsers |
9595
| GPU renderer | WebGL debug info | Chrome, Firefox, Edge |
96+
| Battery status | `navigator.getBattery()` | Chrome, Edge |
9697

9798
All signals are optional — the probe gracefully degrades based on what the browser supports.
9899

@@ -111,15 +112,15 @@ Devices are classified across three dimensions:
111112

112113
Based on tiers and user preferences, DeviceRouter derives actionable booleans:
113114

114-
| Hint | When it activates |
115-
| ----------------------- | --------------------------------------------- |
116-
| `deferHeavyComponents` | Low-end device or slow connection |
117-
| `serveMinimalCSS` | Low-end device |
118-
| `reduceAnimations` | Low-end device or user prefers reduced motion |
119-
| `useImagePlaceholders` | Slow connection (2G/3G) |
120-
| `disableAutoplay` | Low-end device or slow connection |
121-
| `preferServerRendering` | Low-end device |
122-
| `disable3dEffects` | No GPU or software renderer |
115+
| Hint | When it activates |
116+
| ----------------------- | ------------------------------------------------------ |
117+
| `deferHeavyComponents` | Low-end device, slow connection, or low battery |
118+
| `serveMinimalCSS` | Low-end device |
119+
| `reduceAnimations` | Low-end device, prefers reduced motion, or low battery |
120+
| `useImagePlaceholders` | Slow connection (2G/3G) |
121+
| `disableAutoplay` | Low-end device, slow connection, or low battery |
122+
| `preferServerRendering` | Low-end device |
123+
| `disable3dEffects` | No GPU or software renderer |
123124

124125
## Custom Thresholds
125126

@@ -156,7 +157,7 @@ No need to manually add `<script>` tags — the probe is injected before `</head
156157

157158
| Package | Description | Size |
158159
| --------------------------------------------------------------------- | ----------------------------------------------------- | ----------------- |
159-
| [`@device-router/probe`](docs/api/probe.md) | Client-side capability probe | **988 B** gzipped |
160+
| [`@device-router/probe`](docs/api/probe.md) | Client-side capability probe | **~1 KB** gzipped |
160161
| [`@device-router/types`](docs/api/types.md) | Type definitions, classification, and hint derivation ||
161162
| [`@device-router/storage`](docs/api/storage.md) | Storage adapters (in-memory + Redis) ||
162163
| [`@device-router/middleware-express`](docs/api/middleware-express.md) | Express middleware ||

docs/getting-started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ app.use(async (ctx) => {
129129
<script src="/path/to/device-router-probe.min.js"></script>
130130
```
131131

132-
The probe script (~988 bytes gzipped) runs once per session. It collects device signals and POSTs them to the probe endpoint.
132+
The probe script (~1 KB gzipped) runs once per session. It collects device signals and POSTs them to the probe endpoint.
133133

134134
### Auto-Injection
135135

examples/shared/demo-template.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ ${full ? ' <svg style="display:block;width:100%;position:absolute;bottom:-1px
170170
: '\u26A1'
171171
}</span>
172172
<h3>Real-Time Detection</h3>
173-
<p>A 901-byte probe collects CPU, memory, GPU, and network signals via browser APIs \u2014 no user-agent sniffing.</p>
173+
<p>A ~1 KB probe collects CPU, memory, GPU, network, and battery signals via browser APIs \u2014 no user-agent sniffing.</p>
174174
</div>
175175
<div class="card">
176176
<span class="card-icon">${

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "device-router",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"private": true,
55
"type": "module",
66
"license": "MIT",
@@ -22,7 +22,10 @@
2222
"pnpm": {
2323
"onlyBuiltDependencies": [
2424
"esbuild"
25-
]
25+
],
26+
"overrides": {
27+
"minimatch@<10.2.1": "^10.2.1"
28+
}
2629
},
2730
"devDependencies": {
2831
"@eslint/js": "^10.0.1",
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# @device-router/middleware-express
2+
3+
Express middleware for [DeviceRouter](https://github.com/SimplyLiz/DeviceRouter). Adds device classification and rendering hints to every request.
4+
5+
## Installation
6+
7+
```bash
8+
pnpm add @device-router/middleware-express @device-router/storage
9+
```
10+
11+
For automatic probe injection:
12+
13+
```bash
14+
pnpm add @device-router/probe
15+
```
16+
17+
## Quick start
18+
19+
```typescript
20+
import express from 'express';
21+
import { createDeviceRouter } from '@device-router/middleware-express';
22+
import { MemoryStorageAdapter } from '@device-router/storage';
23+
24+
const app = express();
25+
const { middleware, probeEndpoint } = createDeviceRouter({
26+
storage: new MemoryStorageAdapter(),
27+
});
28+
29+
app.use(express.json());
30+
app.post('/device-router/probe', probeEndpoint);
31+
app.use(middleware);
32+
33+
app.get('/', (req, res) => {
34+
const profile = req.deviceProfile;
35+
36+
if (profile?.hints.preferServerRendering) {
37+
return res.send(renderSSR());
38+
}
39+
if (profile?.hints.deferHeavyComponents) {
40+
return res.send(renderLite());
41+
}
42+
res.send(renderFull());
43+
});
44+
45+
app.listen(3000);
46+
```
47+
48+
## How it works
49+
50+
1. **Probe endpoint** receives device signals from the browser and stores a classified profile
51+
2. **Middleware** reads the session cookie, loads the profile from storage, and attaches it to `req.deviceProfile`
52+
3. Your route handlers use `req.deviceProfile.hints` and `req.deviceProfile.tiers` to adapt responses
53+
54+
## Probe auto-injection
55+
56+
Automatically inject the probe `<script>` into HTML responses before `</head>`:
57+
58+
```typescript
59+
const { middleware, probeEndpoint, injectionMiddleware } = createDeviceRouter({
60+
storage: new MemoryStorageAdapter(),
61+
injectProbe: true,
62+
probeNonce: 'my-csp-nonce', // optional, for Content-Security-Policy
63+
});
64+
65+
app.use(injectionMiddleware); // before routes
66+
app.post('/device-router/probe', probeEndpoint);
67+
app.use(middleware);
68+
```
69+
70+
## Custom thresholds
71+
72+
Override default tier classification boundaries:
73+
74+
```typescript
75+
const { middleware, probeEndpoint } = createDeviceRouter({
76+
storage,
77+
thresholds: {
78+
cpu: { lowUpperBound: 4, midUpperBound: 8 },
79+
memory: { midUpperBound: 8 },
80+
},
81+
});
82+
```
83+
84+
## Options
85+
86+
| Option | Type | Default | Description |
87+
| ------------- | -------------------------------------- | ----------------- | ------------------------------------- |
88+
| `storage` | `StorageAdapter` | _(required)_ | Storage backend for profiles |
89+
| `cookieName` | `string` | `'dr_session'` | Session cookie name |
90+
| `cookiePath` | `string` | `'/'` | Cookie path |
91+
| `ttl` | `number` | `86400` (24h) | Profile TTL in seconds |
92+
| `probePath` | `string` || Custom probe endpoint path |
93+
| `thresholds` | `TierThresholds` | Built-in defaults | Custom tier classification thresholds |
94+
| `injectProbe` | `boolean` | `false` | Auto-inject probe into HTML |
95+
| `probeNonce` | `string \| ((req: Request) => string)` || CSP nonce for injected script |
96+
97+
## Exports
98+
99+
- `createDeviceRouter(options)` — All-in-one setup returning `{ middleware, probeEndpoint, injectionMiddleware? }`
100+
- `createMiddleware(options)` — Standalone middleware
101+
- `createProbeEndpoint(options)` — Standalone probe endpoint handler
102+
- `createInjectionMiddleware(options)` — Standalone probe injection middleware
103+
104+
## Compatibility
105+
106+
- Express 4.x and 5.x
107+
- Node.js >= 20
108+
109+
## License
110+
111+
MIT

packages/middleware-express/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@device-router/middleware-express",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"type": "module",
55
"license": "MIT",
66
"main": "./dist/index.js",
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# @device-router/middleware-fastify
2+
3+
Fastify plugin for [DeviceRouter](https://github.com/SimplyLiz/DeviceRouter). Adds device classification and rendering hints to every request.
4+
5+
## Installation
6+
7+
```bash
8+
pnpm add @device-router/middleware-fastify @device-router/storage @fastify/cookie
9+
```
10+
11+
For automatic probe injection:
12+
13+
```bash
14+
pnpm add @device-router/probe
15+
```
16+
17+
## Quick start
18+
19+
```typescript
20+
import Fastify from 'fastify';
21+
import cookie from '@fastify/cookie';
22+
import { createDeviceRouter } from '@device-router/middleware-fastify';
23+
import { MemoryStorageAdapter } from '@device-router/storage';
24+
25+
const app = Fastify();
26+
const { plugin, probeEndpoint } = createDeviceRouter({
27+
storage: new MemoryStorageAdapter(),
28+
});
29+
30+
await app.register(cookie);
31+
await app.register(plugin);
32+
33+
app.post('/device-router/probe', probeEndpoint);
34+
35+
app.get('/', (req, reply) => {
36+
const profile = req.deviceProfile;
37+
38+
if (profile?.hints.preferServerRendering) {
39+
return reply.send(renderSSR());
40+
}
41+
if (profile?.hints.deferHeavyComponents) {
42+
return reply.send(renderLite());
43+
}
44+
reply.send(renderFull());
45+
});
46+
47+
app.listen({ port: 3000 });
48+
```
49+
50+
## How it works
51+
52+
1. **Probe endpoint** receives device signals from the browser and stores a classified profile
53+
2. **Plugin** registers a `preHandler` hook that reads the session cookie, loads the profile from storage, and attaches it to `req.deviceProfile`
54+
3. Your route handlers use `req.deviceProfile.hints` and `req.deviceProfile.tiers` to adapt responses
55+
56+
## Probe auto-injection
57+
58+
Automatically inject the probe `<script>` into HTML responses:
59+
60+
```typescript
61+
const { plugin, probeEndpoint, injectionHook } = createDeviceRouter({
62+
storage: new MemoryStorageAdapter(),
63+
injectProbe: true,
64+
probeNonce: 'my-csp-nonce', // optional
65+
});
66+
```
67+
68+
When `injectProbe` is enabled, the plugin registers an `onSend` hook that injects the script before `</head>`.
69+
70+
## Custom thresholds
71+
72+
```typescript
73+
const { plugin, probeEndpoint } = createDeviceRouter({
74+
storage,
75+
thresholds: {
76+
cpu: { lowUpperBound: 4, midUpperBound: 8 },
77+
memory: { midUpperBound: 8 },
78+
},
79+
});
80+
```
81+
82+
## Options
83+
84+
| Option | Type | Default | Description |
85+
| ------------- | --------------------------------------------- | ----------------- | ----------------------------- |
86+
| `storage` | `StorageAdapter` | _(required)_ | Storage backend for profiles |
87+
| `cookieName` | `string` | `'dr_session'` | Session cookie name |
88+
| `cookiePath` | `string` | `'/'` | Cookie path |
89+
| `ttl` | `number` | `86400` (24h) | Profile TTL in seconds |
90+
| `thresholds` | `TierThresholds` | Built-in defaults | Custom tier thresholds |
91+
| `injectProbe` | `boolean` | `false` | Auto-inject probe into HTML |
92+
| `probePath` | `string` || Custom probe endpoint path |
93+
| `probeNonce` | `string \| ((req: FastifyRequest) => string)` || CSP nonce for injected script |
94+
95+
## Exports
96+
97+
- `createDeviceRouter(options)` — All-in-one setup returning `{ plugin, probeEndpoint, injectionHook? }`
98+
- `createMiddleware(options)` — Standalone preHandler hook
99+
- `createProbeEndpoint(options)` — Standalone probe endpoint handler
100+
- `createInjectionHook(options)` — Standalone onSend injection hook
101+
102+
## Compatibility
103+
104+
- Fastify 5.x
105+
- `@fastify/cookie` 11.x
106+
- Node.js >= 20
107+
108+
## License
109+
110+
MIT

packages/middleware-fastify/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@device-router/middleware-fastify",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"type": "module",
55
"license": "MIT",
66
"main": "./dist/index.js",

0 commit comments

Comments
 (0)