@marianmeres/scanner 1.0.1
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/AGENTS.md +144 -0
- package/API.md +696 -0
- package/CLAUDE.md +3 -0
- package/LICENSE +21 -0
- package/README.md +272 -0
- package/dist/camera-adapter.d.ts +31 -0
- package/dist/camera-adapter.js +96 -0
- package/dist/detector.d.ts +25 -0
- package/dist/detector.js +36 -0
- package/dist/mod.d.ts +5 -0
- package/dist/mod.js +5 -0
- package/dist/scan-image.d.ts +24 -0
- package/dist/scan-image.js +35 -0
- package/dist/scanner.d.ts +17 -0
- package/dist/scanner.js +594 -0
- package/dist/stage.d.ts +59 -0
- package/dist/stage.js +334 -0
- package/dist/types.d.ts +262 -0
- package/dist/types.js +28 -0
- package/package.json +40 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# @marianmeres/scanner — Agent Guide
|
|
2
|
+
|
|
3
|
+
## Quick Reference
|
|
4
|
+
|
|
5
|
+
- **Stack**: Deno, TypeScript, browser-targeted (DOM libs enabled)
|
|
6
|
+
- **Runtime dependencies**: `barcode-detector` (npm; zxing-cpp wasm ponyfill),
|
|
7
|
+
`@marianmeres/store` (reactive store, Svelte-compatible `subscribe`),
|
|
8
|
+
`@marianmeres/clog` (logging), `@marianmeres/mediaperms` (permission lifecycle
|
|
9
|
+
inside the default CameraAdapter)
|
|
10
|
+
- **Test**: `deno task test` | **Build example**: `deno task build:example` |
|
|
11
|
+
**Format**: `deno fmt`
|
|
12
|
+
|
|
13
|
+
## Project Structure
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
/src
|
|
17
|
+
mod.ts — Main entry: re-exports everything below (headless, no DOM construction)
|
|
18
|
+
types.ts — All public types/interfaces + ScannerErrorCode const
|
|
19
|
+
scanner.ts — createScanner() core (state, detection loop, start/stop lifecycle)
|
|
20
|
+
camera-adapter.ts — createDefaultCameraAdapter() (navigator.mediaDevices streams + mediaperms permission lifecycle), detectFacing()
|
|
21
|
+
detector.ts — createDefaultDetector() (barcode-detector ponyfill), toScanResult(), DEFAULT_FORMATS
|
|
22
|
+
scan-image.ts — scanImage() standalone still-image decode
|
|
23
|
+
stage.ts — Subpath export "./stage": createScannerStage() DOM viewfinder UI
|
|
24
|
+
/tests
|
|
25
|
+
_helpers.ts — Shared test doubles (mock adapter/detector/track/stream/video, recording logger)
|
|
26
|
+
scanner.test.ts — Core lifecycle/scan-loop tests (mock adapter + detector, no real browser APIs)
|
|
27
|
+
camera-adapter.test.ts — Default adapter tests (fake navigator.mediaDevices, injected fake mediaperms)
|
|
28
|
+
scan-image.test.ts — scanImage() mapping/throw/fetch tests
|
|
29
|
+
stage.test.ts — Stage DOM tests (@b-fuze/deno-dom + mock Scanner)
|
|
30
|
+
/scripts
|
|
31
|
+
build-npm.ts — NPM package build (@marianmeres/npmbuild; entryPoints: mod, stage)
|
|
32
|
+
SPEC.md — Agreed v1 spec (design rationale)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## What This Library Does
|
|
36
|
+
|
|
37
|
+
Framework-agnostic browser primitive for scanning QR/barcodes from a live camera
|
|
38
|
+
stream: headless controller (`createScanner`) with reactive state, single-shot and
|
|
39
|
+
continuous modes, torch + camera switching, standalone still-image decode
|
|
40
|
+
(`scanImage`), and a mountable viewfinder stage UI (`createScannerStage`, `/stage`
|
|
41
|
+
subpath).
|
|
42
|
+
|
|
43
|
+
Decoding always uses the `barcode-detector` ponyfill (zxing-cpp wasm) — the native
|
|
44
|
+
`BarcodeDetector` is deliberately NOT used. The ~1 MB wasm loads lazily from the
|
|
45
|
+
jsDelivr CDN by default; `wasmOverrides.locateFile` enables self-hosting (CSP).
|
|
46
|
+
|
|
47
|
+
## Key Concepts
|
|
48
|
+
|
|
49
|
+
| Concept | Description |
|
|
50
|
+
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| **CameraAdapter** | Injectable seam for camera acquisition; default wraps `navigator.mediaDevices` for streams/enumeration and delegates the permission lifecycle to `@marianmeres/mediaperms` (share an app-owned instance via `createDefaultCameraAdapter({ perms })`). |
|
|
52
|
+
| **Detector** | Injectable seam for the decode engine (W3C `BarcodeDetector`-shaped); default wraps the ponyfill. Mock in tests. |
|
|
53
|
+
| **Reactive state** | `@marianmeres/store` powers `subscribe()` (Svelte `$store` compatible, fires immediately) |
|
|
54
|
+
| **Never-throw** | Scanner methods never throw/reject — errors land in `state.error` (`ScannerErrorCode`) |
|
|
55
|
+
| **Stream retention** | Unlike mediaperms (probe + stop), the scanner RETAINS the `MediaStream` for the live preview; tracks stop on `stop()`/`destroy()` |
|
|
56
|
+
| **Headless/DOM split** | Core constructs no DOM (video element lazily, only when needed); all chrome lives in `stage.ts` |
|
|
57
|
+
|
|
58
|
+
## Public API
|
|
59
|
+
|
|
60
|
+
### Main entry (`@marianmeres/scanner`)
|
|
61
|
+
|
|
62
|
+
| Export | Type | Purpose |
|
|
63
|
+
| --------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
64
|
+
| `createScanner(config?)` | Factory | Main entry point, returns `Scanner` instance |
|
|
65
|
+
| `scanImage(source, options?)` | Function | Decode a still image (Blob/File/ImageData/img/canvas/url). THROWS on failure (documented exception to never-throw) |
|
|
66
|
+
| `createDefaultCameraAdapter(o?)` | Factory | Real browser adapter (`getUserMedia` streams + mediaperms permissions; `{ perms, permsConfig }` options) |
|
|
67
|
+
| `createDefaultDetector(options?)` | Factory | Ponyfill-backed `Detector` (formats + `wasmOverrides`) |
|
|
68
|
+
| `classifyAcquireError(e)` | Helper | Map a `getUserMedia` rejection to `ScannerError` |
|
|
69
|
+
| `detectFacing(label)` | Helper | Best-effort `"user"`/`"environment"`/`null` from device label |
|
|
70
|
+
| `toScanResult(d, timestamp?)` | Helper | Raw detection → public `ScanResult` |
|
|
71
|
+
| `ScannerErrorCode` | Const | Frozen object of typed `state.error.code` values |
|
|
72
|
+
| `DEFAULT_FORMATS` | Const | `["qr_code"]` |
|
|
73
|
+
|
|
74
|
+
### Stage subpath (`@marianmeres/scanner/stage`)
|
|
75
|
+
|
|
76
|
+
| Export | Type | Purpose |
|
|
77
|
+
| -------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
78
|
+
| `createScannerStage(scanner, options)` | Factory | Mounts viewfinder UI (video, overlay cutout, corner guides, scan line, opt-in cancel/torch/switch buttons) into a container. Returns `{ el, destroy }`. THROWS without DOM/container. |
|
|
79
|
+
|
|
80
|
+
### Scanner instance methods
|
|
81
|
+
|
|
82
|
+
| Method | Returns | Description |
|
|
83
|
+
| --------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
84
|
+
| `subscribe(cb)` | `() => void` | Reactive subscription (fires immediately) |
|
|
85
|
+
| `get()` | `ScannerState` | Current state snapshot |
|
|
86
|
+
| `start()` | `Promise<ScanResult \| null>` | Acquire camera + scan. Single: first hit; continuous: `null` when stopped. Concurrent calls coalesce. Never rejects. |
|
|
87
|
+
| `stop()` | `void` | Cancel: stop loop + tracks, resolve pending `start()` with `null` |
|
|
88
|
+
| `listCameras()` | `Promise<CameraInfo[]>` | Enumerate devices (also updates `state.cameras`) |
|
|
89
|
+
| `setCamera(deviceId)` | `Promise<void>` | Live-switch while scanning; otherwise applied on next `start()` |
|
|
90
|
+
| `setTorch(on)` | `Promise<boolean>` | `false` when unsupported/failed (never rejects) |
|
|
91
|
+
| `getVideo()` | `HTMLVideoElement \| null` | Preview element (lazily created; `null` without DOM) |
|
|
92
|
+
| `destroy()` | `void` | Release everything. Idempotent. |
|
|
93
|
+
|
|
94
|
+
## Critical Conventions
|
|
95
|
+
|
|
96
|
+
1. Use `globalThis` never `window` (Deno compatibility)
|
|
97
|
+
2. Scanner methods never throw — classify failures into `state.error` via
|
|
98
|
+
`classifyAcquireError` / `ScannerErrorCode`. Exceptions: standalone `scanImage()`
|
|
99
|
+
and the stage factory throw by design.
|
|
100
|
+
3. Permission denial (`NotAllowedError`) sets `state.permission = "denied"` +
|
|
101
|
+
a dedicated `PERMISSION_DENIED` error — but there is no built-in denial UI
|
|
102
|
+
(mediaperms territory).
|
|
103
|
+
4. Tests use injectable `adapter` + `detector` — never depend on real browser APIs;
|
|
104
|
+
real wasm decode runs only in the browser example, not CI.
|
|
105
|
+
5. `start()` caches an in-flight promise (concurrent calls coalesce); async init is
|
|
106
|
+
guarded by a `generation` counter — keep both when touching lifecycle code.
|
|
107
|
+
6. Detection loop: `requestVideoFrameCallback` → rAF → `setTimeout` fallback;
|
|
108
|
+
throttled by `scanIntervalMs`; skips while `document.visibilityState === "hidden"`
|
|
109
|
+
or `paused` (live camera switch); stops after 5 consecutive detector failures
|
|
110
|
+
(`DETECTOR_FAILED`).
|
|
111
|
+
7. The default detector is created lazily inside `start()` (wasm cost only when
|
|
112
|
+
used); `scanImage` creates its own unless given one.
|
|
113
|
+
8. Stage is a pure consumer of the headless controller — no scanner state lives in
|
|
114
|
+
`stage.ts`; destroying one never destroys the other.
|
|
115
|
+
9. Never auto-start on mount — `start()` must remain an explicit call (iOS Safari
|
|
116
|
+
gesture/permission rules).
|
|
117
|
+
10. Format: tabs, 90-char line width, 4-space indent width (`deno fmt`)
|
|
118
|
+
|
|
119
|
+
## Before Making Changes
|
|
120
|
+
|
|
121
|
+
- [ ] Read `SPEC.md` for design decisions and out-of-scope items
|
|
122
|
+
- [ ] Read `src/types.ts` first — all public contracts live there
|
|
123
|
+
- [ ] Check existing patterns (mediaperms is the architectural blueprint)
|
|
124
|
+
- [ ] Run `deno task test`
|
|
125
|
+
- [ ] Run `deno fmt` and `deno check src/mod.ts src/stage.ts`
|
|
126
|
+
|
|
127
|
+
## Platform Notes
|
|
128
|
+
|
|
129
|
+
- **HTTPS required**: `getUserMedia` exists only in secure contexts (localhost OK).
|
|
130
|
+
On insecure origins browsers typically omit `navigator.mediaDevices` entirely →
|
|
131
|
+
`NOT_SUPPORTED`; `INSECURE_CONTEXT` means getUserMedia threw `SecurityError`
|
|
132
|
+
(e.g. Permissions-Policy).
|
|
133
|
+
- **Camera labels are empty** until permission has been granted at least once; the
|
|
134
|
+
scanner re-enumerates after a successful `start()`.
|
|
135
|
+
- **Mobile browsers cannot open two cameras at once** — `setCamera()` stops the
|
|
136
|
+
current tracks before acquiring the new stream.
|
|
137
|
+
- **Torch** is a track capability (`getCapabilities().torch`) — effectively
|
|
138
|
+
Android/Chrome only; always feature-detected, `setTorch()` resolves `false`
|
|
139
|
+
elsewhere.
|
|
140
|
+
- **In-app WebView specifics are handled at the permission level only** — the
|
|
141
|
+
default adapter's mediaperms delegation brings platform detection and
|
|
142
|
+
webview-bridge awareness (pass `permsConfig` through
|
|
143
|
+
`createDefaultCameraAdapter`); stream acquisition quirks of exotic WebViews
|
|
144
|
+
remain untested.
|