Skip to content

Commit a5fec9d

Browse files
committed
chore: examples, repo hygiene, attw patch, finalize migration plan
Hygiene: - Remove v1 example/index.html (referenced the deleted UMD bundle) and resources/{public,private}.png (referenced only by the old README screenshots). Examples (replace single example/ with examples/): - browser/ — vanilla <script type="module"> CDN import. - node-import/ — demonstrates NOT_A_BROWSER typed error path. - vite-react/ — Vite + React component using detectIncognito. Each example links to the local build via `link:../..` so it works without a workspace config. Tooling fix: - pnpm-patched @arethetypeswrong/core@0.18.2 to concat streaming Gunzip chunks instead of overwriting (upstream pattern only keeps the last empty chunk, breaking every package whose decompressed contents exceed one fflate buffer). Documented in MIGRATION_PLAN.md "Known limitations". Final verification (all green): - pnpm install --frozen-lockfile / typecheck / lint / test:coverage / build / validate:package (publint + attw) / size. - Coverage 98.17 % lines, 100 % functions, 90.09 % branches. - attw clean across node10 / node16 CJS / node16 ESM / bundler. - size-limit: 1.07 kB ESM / 1.18 kB CJS brotlied (budget 2 kB). - npm pack --dry-run: 10 files, 15 kB tarball / 80 kB unpacked. - Smoke-tested in a fresh project: ESM / CJS / TS-strict all import cleanly and the typed error crosses the module boundary.
1 parent 3ba2a57 commit a5fec9d

21 files changed

Lines changed: 369 additions & 64 deletions

MIGRATION_PLAN.md

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,30 @@ There is no test suite. Baseline coverage is **0 %**.
7373

7474
## Phase-by-phase plan
7575

76-
- [x] **Phase 0** — Reconnaissance: write this plan, `DECISIONS.md`, `BREAKING_CHANGES.md`, the v2 branch.
77-
- [ ] **Phase 1** — Foundation: bump engines, write `tsconfig.json`, `package.json` with `exports` map, `files`, `.nvmrc`, `.node-version`, `browserslist`, `packageManager`.
78-
- [ ] **Phase 2** — Dependencies: rip out `webpack`, `babel`, `get-browser`. Add `typescript`, `tsup`, `vitest`, `happy-dom`, `@vitest/coverage-v8`, `expect-type`, `tinybench`, ESLint/Prettier stack, `publint`, `@arethetypeswrong/cli`, `size-limit`, changesets.
79-
- [ ] **Phase 3** — Rewrite source in TypeScript. New detection module. Typed errors. Strict types on public API.
80-
- [ ] **Phase 4** — Tooling: ESLint flat config, Prettier, tsup config, lint-staged + simple-git-hooks, full script set.
81-
- [ ] **Phase 5** — Tests + coverage gates + benchmark.
82-
- [ ] **Phase 6** — CI workflows + Changesets + branch-protection note.
83-
- [ ] **Phase 7** — README rewrite, TypeDoc site, supporting docs, issue/PR templates.
84-
- [ ] **Phase 8** — Hygiene: `.gitignore`, `.gitattributes`, examples, size-limit budget, fresh-install smoke test.
85-
- [ ] **Phase 9** — Final verification: install/typecheck/lint/test/build all green, `publint` + `attw` clean, smoke-test installation from `npm pack`.
76+
- [x] **Phase 0** — Reconnaissance + `MIGRATION_PLAN.md` / `DECISIONS.md` / `BREAKING_CHANGES.md`.
77+
- [x] **Phase 1** — Foundation: engines, `tsconfig.json` (strict + `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, `verbatimModuleSyntax`), `package.json` with `exports` map, files allowlist, `.nvmrc` / `.node-version`, browserslist, `packageManager`.
78+
- [x] **Phase 2** — Dependencies: removed webpack/babel/get-browser; modern stack installed; zero runtime deps.
79+
- [x] **Phase 3** — Source rewritten in TypeScript across `browser.ts`, `strategies.ts`, `detect.ts`, `errors.ts`, `types.ts`, `index.ts`.
80+
- [x] **Phase 4** — ESLint v9 flat + Prettier + tsup dual build, lint-staged + simple-git-hooks installed.
81+
- [x] **Phase 5** — Vitest suite, 49 tests, **98.17 % lines / 100 % functions / 90.09 % branches**, type-level tests with `expectTypeOf`, tinybench benchmarks.
82+
- [x] **Phase 6**`ci.yml` (3 × 3 matrix), `release.yml` (changesets + npm provenance via OIDC), `codeql.yml`, `docs.yml`, dependabot config.
83+
- [x] **Phase 7** — README rewrite, TypeDoc site builds, CONTRIBUTING / CODE_OF_CONDUCT / SECURITY / CHANGELOG, issue/PR templates, FUNDING.yml.
84+
- [x] **Phase 8** — Examples (`browser/`, `node-import/`, `vite-react/`), `.gitattributes`, size-limit budget enforced.
85+
- [x] **Phase 9** — Final verification ✓
86+
- `pnpm install --frozen-lockfile`
87+
- `pnpm typecheck` ✓ (zero errors)
88+
- `pnpm lint` ✓ (zero errors, zero warnings)
89+
- `pnpm test` ✓ (49/49)
90+
- `pnpm test:coverage` ✓ (above thresholds)
91+
- `pnpm build`
92+
- `pnpm validate:package` ✓ (publint "All good!" + attw "No problems found 🌟")
93+
- `pnpm size` ✓ (1.07 kB / 1.18 kB brotlied; budget 2 kB)
94+
- `npm pack --dry-run` ✓ (10 files; 15 kB / 80 kB unpacked)
95+
- Fresh-install smoke test ✓ (ESM / CJS / TS-strict all import cleanly from the tarball; typed error class crosses the module boundary)
96+
97+
## Known limitations
98+
99+
- **`attw` upstream bug**: versions 0.14 – 0.18 use a streaming-Gunzip pattern that only retains the last (empty) chunk, so any package whose decompressed contents exceed one fflate buffer fails with `Cannot read properties of undefined (reading 'filename')`. Worked around with a `pnpm patch` to `@arethetypeswrong/core` that concatenates all chunks. Upstream issue reference: <https://github.com/arethetypeswrong/arethetypeswrong.github.io>. Once an upstream fix lands, drop the patch.
86100

87101
## Risk register
88102

example/index.html

Lines changed: 0 additions & 41 deletions
This file was deleted.

examples/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
dist/
3+
*.tsbuildinfo

examples/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Examples
2+
3+
Three runnable examples demonstrating common usage:
4+
5+
| Path | What it shows |
6+
| ------------------------------- | ------------------------------------------------------------------------------- |
7+
| [`browser/`](./browser) | Vanilla `<script type="module">` loading from a CDN. |
8+
| [`node-import/`](./node-import) | Importing in Node — exists to demonstrate the typed `NOT_A_BROWSER` error path. |
9+
| [`vite-react/`](./vite-react) | React component using `isIncognito` inside a Vite app. |
10+
11+
Each example is independent — `cd examples/<name>` and follow that folder's
12+
README.

examples/browser/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Browser example
2+
3+
A single-file HTML page that imports `is-incognito-mode` from a CDN and renders
4+
the detection result.
5+
6+
## Run
7+
8+
Open `index.html` in a browser. Or serve it locally:
9+
10+
```sh
11+
npx serve .
12+
```
13+
14+
Then visit the URL in both a regular and an incognito window to compare.

examples/browser/index.html

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>is-incognito-mode · browser example</title>
7+
<style>
8+
:root {
9+
color-scheme: light dark;
10+
font-family:
11+
ui-sans-serif,
12+
system-ui,
13+
-apple-system,
14+
'Segoe UI',
15+
Roboto,
16+
sans-serif;
17+
}
18+
body {
19+
margin: 0;
20+
min-height: 100dvh;
21+
display: grid;
22+
place-items: center;
23+
padding: 2rem;
24+
}
25+
main {
26+
max-width: 36rem;
27+
text-align: center;
28+
}
29+
h1 {
30+
margin: 0 0 1rem;
31+
font-size: clamp(1.5rem, 3vw + 1rem, 2.5rem);
32+
font-weight: 600;
33+
}
34+
#verdict {
35+
font-size: clamp(1.25rem, 2vw + 0.5rem, 1.75rem);
36+
margin: 0 0 1rem;
37+
}
38+
#detail {
39+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
40+
font-size: 0.9rem;
41+
padding: 1rem;
42+
background: color-mix(in oklab, currentColor 8%, transparent);
43+
border-radius: 0.5rem;
44+
text-align: left;
45+
overflow-x: auto;
46+
}
47+
</style>
48+
</head>
49+
<body>
50+
<main>
51+
<h1>is-incognito-mode</h1>
52+
<p id="verdict">Probing…</p>
53+
<pre id="detail"></pre>
54+
</main>
55+
56+
<script type="module">
57+
import { detectIncognito } from 'https://esm.sh/is-incognito-mode@2';
58+
59+
const verdict = document.getElementById('verdict');
60+
const detail = document.getElementById('detail');
61+
62+
try {
63+
const result = await detectIncognito();
64+
verdict.textContent = result.isPrivate
65+
? 'You are in private / incognito mode.'
66+
: 'You are in a regular browsing window.';
67+
detail.textContent = JSON.stringify(result, null, 2);
68+
} catch (error) {
69+
verdict.textContent = 'Could not determine incognito state.';
70+
detail.textContent = `${error.name}: ${error.message}\n(${error.code})`;
71+
}
72+
</script>
73+
</body>
74+
</html>

examples/node-import/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Node import example
2+
3+
Demonstrates that the package imports cleanly in Node and emits the typed
4+
`NOT_A_BROWSER` error at runtime, so applications doing SSR can branch on it
5+
without string-matching messages.
6+
7+
```sh
8+
pnpm install
9+
node index.mjs
10+
```
11+
12+
Expected output:
13+
14+
```
15+
Got the expected typed error:
16+
name : IncognitoDetectionError
17+
code : NOT_A_BROWSER
18+
msg : is-incognito-mode can only run in a browser-like environment.
19+
```

examples/node-import/index.mjs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Demonstrates that importing in Node is fine, but invoking the detector
2+
// without a browser-like global throws the typed NOT_A_BROWSER error.
3+
import { detectIncognito, IncognitoDetectionError } from 'is-incognito-mode';
4+
5+
try {
6+
const result = await detectIncognito();
7+
console.log('Unexpected result:', result);
8+
} catch (error) {
9+
if (error instanceof IncognitoDetectionError) {
10+
console.log('Got the expected typed error:');
11+
console.log(' name :', error.name);
12+
console.log(' code :', error.code);
13+
console.log(' msg :', error.message);
14+
} else {
15+
throw error;
16+
}
17+
}

examples/node-import/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "is-incognito-mode-node-example",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"start": "node index.mjs"
8+
},
9+
"dependencies": {
10+
"is-incognito-mode": "link:../.."
11+
}
12+
}

examples/vite-react/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Vite + React example
2+
3+
A minimal React + Vite app using `detectIncognito` inside an effect.
4+
5+
## Run
6+
7+
From the monorepo root or this folder:
8+
9+
```sh
10+
cd examples/vite-react
11+
pnpm install
12+
pnpm dev
13+
```
14+
15+
Open the printed `http://localhost:5173` URL in both a regular and an
16+
incognito window to compare.

0 commit comments

Comments
 (0)