react-threat-map 0.2.1 → 0.2.3

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/DECISIONS.md CHANGED
@@ -247,24 +247,8 @@ Every runtime dependency, and why it earns its place:
247
247
  Both are declared `external`, so consumers dedupe them against their own copies of d3
248
248
  rather than shipping a second one.
249
249
 
250
- `react` is a **peer** dependency (>=16.14.0), never bundled.
251
-
252
- The floor is 16.14.0, not 16.8.0 (hooks) as one might expect: the compiled output uses
253
- the automatic JSX runtime, so it imports `react/jsx-runtime`, and that entry point first
254
- ships in **16.14.0**. On 16.8–16.13 the package installs and then fails at bundle time on
255
- an unresolved module, so those versions must stay outside the range.
256
-
257
- Nothing in the library needs more than that floor — it uses only `useState`, `useEffect`,
258
- `useRef`, `useMemo`, and `useCallback`. The range was originally `>=18` for no recorded
259
- reason, which excluded working consumers on React 16.14/17; widened in 0.2.1 after
260
- verifying typecheck and a mount/re-render/unmount render pass on 16.14, 17, 18, and 19.
261
-
262
- Supporting that range is also why the public types import `CSSProperties`, `MouseEvent`,
263
- `ReactElement`, and `RefObject` **by name** from `react` rather than reaching through the
264
- `React` UMD global or the global `JSX` namespace. A bare `JSX.Element` in an emitted `.d.ts`
265
- resolves against whatever `@types/react` the consumer has — and `@types/react@19` removed
266
- the global `JSX` namespace, so it broke React 19 consumers compiling with
267
- `skipLibCheck: false`. Named type imports are identical across `@types/react` 16 to 19.
250
+ `react` is a **peer** dependency, never bundled. The supported range and the reasoning
251
+ behind its floor are in [§7](#7-react-version-support-1614-and-up).
268
252
 
269
253
  Deliberately **not** dependencies: no state manager, no styling library, no CSS-in-JS, no
270
254
  animation library, no `d3-selection`/`d3-zoom`/`d3-scale`. The library ships zero CSS —
@@ -346,3 +330,150 @@ lines — silently erase every self-directed threat on the map.
346
330
  Consumers upgrading from 0.1.x see new animated loops wherever their feed contains
347
331
  same-region attacks that previously rendered as static dots. This is why the change went
348
332
  out as a minor bump rather than a patch.
333
+
334
+ ---
335
+
336
+ ## 7. React version support: 16.14 and up
337
+
338
+ **Chosen:** `peerDependencies: { react: ">=16.14.0" }`, covering React 16.14, 17, 18, and 19.
339
+
340
+ ### Why the floor is 16.14.0 and not 16.8.0
341
+
342
+ 16.8.0 is the obvious guess, since that is where hooks landed and hooks are all this library
343
+ uses. It is wrong. The compiled output uses the **automatic JSX runtime**, so it imports
344
+ `react/jsx-runtime`, and that entry point first ships in **16.14.0**.
345
+
346
+ The failure mode is what makes this worth recording: on 16.8–16.13 the package *installs
347
+ cleanly* and then fails at the consumer's bundler on an unresolved module. A range of
348
+ `>=16.8.0` would be a promise that npm accepts and webpack rejects, which is the worst
349
+ place for the error to surface. If the JSX transform is ever changed back to `classic`, the
350
+ floor can drop to 16.8.0 — those two facts move together.
351
+
352
+ ### Why not the original `>=18.0.0`
353
+
354
+ There was no recorded reason for it, and it was wrong in both directions: it excluded React
355
+ 16.14/17 consumers the library works fine on, and it *included* React 19, which was broken
356
+ (below). Nothing here needs more than the floor — the library uses only `useState`,
357
+ `useEffect`, `useRef`, `useMemo`, and `useCallback`.
358
+
359
+ Widened in 0.2.1, as a patch: broadening a peer range cannot break an existing consumer.
360
+
361
+ ### Why the public types import from `react` by name
362
+
363
+ Supporting a wide range is why `CSSProperties`, `MouseEvent`, `ReactElement`, and
364
+ `RefObject` are imported **by name** rather than reached through the `React` UMD global or
365
+ the global `JSX` namespace.
366
+
367
+ A bare `JSX.Element` in an emitted `.d.ts` resolves against whatever `@types/react` the
368
+ consumer happens to have, which is not a thing this library controls. `@types/react@19`
369
+ removed the global `JSX` namespace, so the 0.2.0 type surface failed for React 19 consumers
370
+ compiling with `skipLibCheck: false`:
371
+
372
+ ```
373
+ dist/index.d.ts(40,76): error TS2503: Cannot find namespace 'JSX'.
374
+ ```
375
+
376
+ Those four names are exported identically by `@types/react` 16 through 19. The rule is
377
+ enforced by `tests/react-compat.test.ts`, which rejects `React.*` and `JSX.*` in `src/`.
378
+
379
+ ### Tradeoff accepted
380
+
381
+ **The test suite does not run against the floor.** `@testing-library/react@16` requires
382
+ React 18+, so every test here executes on 18. Covering 16.14 properly means a second
383
+ devDependency tree and a CI matrix, which was judged not worth it for a library using five
384
+ hooks. A green suite is therefore *not* evidence that the floor works.
385
+
386
+ What closes most of the gap is static: `tests/react-compat.test.ts` asserts that every name
387
+ imported from `react` appears on an allowlist checked against 16.14 by hand, so reaching
388
+ for `useSyncExternalStore`, `useId`, `useTransition`, `useDeferredValue`, or `use` fails the
389
+ build rather than shipping. It cannot catch behavioural differences — only API existence.
390
+
391
+ The one behavioural difference worth knowing: React 16/17 do not batch `setState` outside
392
+ event handlers, so the `ResizeObserver`, `matchMedia`, and geo-load callbacks each trigger
393
+ their own render there. Every one of those call sites issues a single `setState`, so the
394
+ practical cost is nil — but a future hook that sets two pieces of state in one async
395
+ callback would render twice on the floor and once on 18+.
396
+
397
+ ### Known behavioural differences on the floor
398
+
399
+ Each of these is invisible to a suite running on 18, so each carries a guard that fails on
400
+ any version. Append to this list rather than fixing quietly — the entries are the only
401
+ record of what the static allowlist cannot reach.
402
+
403
+ **1. Numeric style values.** React appends `px` to numeric `style` values unless the property
404
+ is on its unitless allowlist, and `aspectRatio` only joined that list in React 18. The
405
+ wrapper's fallback ratio was emitted as a number, so on 16.14/17 it serialized to
406
+ `aspect-ratio: 2px` — invalid, dropped by the browser, wrapper collapsed to zero height,
407
+ absolutely-positioned canvases gated off, **map entirely invisible**. It affected every
408
+ projection, not just custom ones, and the whole no-explicit-`height` default path.
409
+
410
+ Found by a consumer on React 16.14, not by CI — precisely the gap this section predicted.
411
+ Fixed by `aspectRatioStyleFor` in `src/render/projection.ts`, which stringifies deliberately;
412
+ guarded by `tests/render/projection.test.ts`, which asserts the emitted **type** rather than
413
+ the rendered value, since the rendered value is correct on 18 either way.
414
+
415
+ The general rule this implies: **any numeric value handed to `style` must be checked against
416
+ React 17's unitless allowlist, or stringified.** `lineHeight` and `flexGrow` are on that list
417
+ and safe; `scale`, `translate`, `rotate` and `inset` are not.
418
+
419
+ ---
420
+
421
+ ## 8. Package entry points: per-condition types, plus a `typesVersions` fallback
422
+
423
+ **Chosen:** `exports` declares types *inside* each of the `import` and `require`
424
+ conditions, and `typesVersions` maps the `geo` subpath for resolvers that ignore `exports`
425
+ entirely.
426
+
427
+ This is not decoration. 0.2.1 shipped with two independent faults here, both invisible to
428
+ every other check in the repo: the suite imports from `src/`, and the smoke test exercises
429
+ one resolution mode. A package can be entirely correct at runtime and still fail to
430
+ typecheck for a large share of its consumers.
431
+
432
+ ### `moduleResolution: "node"` ignores `exports`, so the subpath had no types
433
+
434
+ Under node10 resolution — still the default for any project that has not opted in, which
435
+ is disproportionately the React 16/17 projects the floor in §7 deliberately invites —
436
+ `react-threat-map/geo` resolved to nothing:
437
+
438
+ ```
439
+ error TS2307: Cannot find module 'react-threat-map/geo' or its corresponding type
440
+ declarations. There are types at '.../dist/geo.d.ts', but this result could not be
441
+ resolved under your current 'moduleResolution' setting.
442
+ ```
443
+
444
+ `typesVersions` is the only mechanism node10 honours for subpath types, so it is the fix.
445
+
446
+ The workaround a consumer reaches for first is a trap worth naming, because it *looks*
447
+ like it works: `react-threat-map/dist/geo` typechecks fine, then fails at runtime with
448
+ `ERR_PACKAGE_PATH_NOT_EXPORTED`, since the deep path is not in `exports`. Webpack 5
449
+ enforces the same restriction. Neither specifier satisfied both the compiler and the
450
+ bundler — which is why this needed fixing in the library rather than working around
451
+ downstream.
452
+
453
+ ### One `types` per subpath made the package masquerade as ESM
454
+
455
+ The map previously declared a single `"types": "./dist/index.d.ts"` covering both
456
+ conditions. Because this package is `"type": "module"`, that `.d.ts` is read as an ES
457
+ module — so a **CommonJS** consumer on `node16`/`nodenext` was told the types were ESM
458
+ while the runtime correctly handed it `dist/index.cjs`:
459
+
460
+ ```
461
+ error TS1479: The current file is a CommonJS module whose imports will produce 'require'
462
+ calls; however, the referenced file is an ECMAScript module...
463
+ ```
464
+
465
+ tsup was *already emitting* `dist/index.d.cts` and `dist/geo.d.cts`. The exports map simply
466
+ never pointed at them, so they shipped in every tarball as dead weight. Declaring types
467
+ per-condition is what makes the dual build actually dual.
468
+
469
+ ### Tradeoff accepted
470
+
471
+ `typesVersions` is a legacy mechanism superseded by `exports`, and it duplicates the `geo`
472
+ mapping in a second place. Adding a subpath therefore means editing both, and forgetting
473
+ one breaks only node10 consumers — the group least likely to be on the maintainer's
474
+ machine. It is kept because node10 offers no alternative, and dropping it would mean
475
+ telling React 16/17 consumers to change their `tsconfig` to install a library that
476
+ otherwise supports them.
477
+
478
+ `npm run check:exports` (arethetypeswrong) runs in CI and covers every entry point against
479
+ every resolution mode. It is what would have caught both faults before 0.2.1 went out.
package/dist/index.cjs CHANGED
@@ -345,6 +345,9 @@ function defaultHeightFor(spec, width) {
345
345
  function aspectRatioFor(spec) {
346
346
  return 1 / ratioFor(spec);
347
347
  }
348
+ function aspectRatioStyleFor(spec) {
349
+ return String(aspectRatioFor(spec));
350
+ }
348
351
  var SPHERE = { type: "Sphere" };
349
352
  function createProjection(spec, width, height) {
350
353
  const projection = typeof spec === "string" ? (factories[spec] ?? factories.naturalEarth1)() : spec;
@@ -1326,8 +1329,9 @@ function ThreatMap(props) {
1326
1329
  // which gates off the canvases, which keeps it collapsed. `aspect-ratio`
1327
1330
  // is inert the moment anything else determines the height — a CSS class,
1328
1331
  // a flex parent, or the consumer's own `style` below — so it sets a
1329
- // floor without taking the decision away from them.
1330
- ...heightProp !== void 0 ? { height: heightProp } : { aspectRatio: aspectRatioFor(projectionProp) },
1332
+ // floor without taking the decision away from them. The value is a string
1333
+ // for React 16.14/17's sake; see aspectRatioStyleFor.
1334
+ ...heightProp !== void 0 ? { height: heightProp } : { aspectRatio: aspectRatioStyleFor(projectionProp) },
1331
1335
  ...style
1332
1336
  },
1333
1337
  role: "img",
package/dist/index.js CHANGED
@@ -344,6 +344,9 @@ function defaultHeightFor(spec, width) {
344
344
  function aspectRatioFor(spec) {
345
345
  return 1 / ratioFor(spec);
346
346
  }
347
+ function aspectRatioStyleFor(spec) {
348
+ return String(aspectRatioFor(spec));
349
+ }
347
350
  var SPHERE = { type: "Sphere" };
348
351
  function createProjection(spec, width, height) {
349
352
  const projection = typeof spec === "string" ? (factories[spec] ?? factories.naturalEarth1)() : spec;
@@ -1325,8 +1328,9 @@ function ThreatMap(props) {
1325
1328
  // which gates off the canvases, which keeps it collapsed. `aspect-ratio`
1326
1329
  // is inert the moment anything else determines the height — a CSS class,
1327
1330
  // a flex parent, or the consumer's own `style` below — so it sets a
1328
- // floor without taking the decision away from them.
1329
- ...heightProp !== void 0 ? { height: heightProp } : { aspectRatio: aspectRatioFor(projectionProp) },
1331
+ // floor without taking the decision away from them. The value is a string
1332
+ // for React 16.14/17's sake; see aspectRatioStyleFor.
1333
+ ...heightProp !== void 0 ? { height: heightProp } : { aspectRatio: aspectRatioStyleFor(projectionProp) },
1330
1334
  ...style
1331
1335
  },
1332
1336
  role: "img",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-threat-map",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "A React component that renders animated cyberattack threats on a static world map, with intelligent per-region aggregation and first-class US state support.",
5
5
  "keywords": [
6
6
  "react",
@@ -29,16 +29,33 @@
29
29
  "main": "./dist/index.cjs",
30
30
  "module": "./dist/index.js",
31
31
  "types": "./dist/index.d.ts",
32
+ "typesVersions": {
33
+ "*": {
34
+ "geo": [
35
+ "./dist/geo.d.ts"
36
+ ]
37
+ }
38
+ },
32
39
  "exports": {
33
40
  ".": {
34
- "types": "./dist/index.d.ts",
35
- "import": "./dist/index.js",
36
- "require": "./dist/index.cjs"
41
+ "import": {
42
+ "types": "./dist/index.d.ts",
43
+ "default": "./dist/index.js"
44
+ },
45
+ "require": {
46
+ "types": "./dist/index.d.cts",
47
+ "default": "./dist/index.cjs"
48
+ }
37
49
  },
38
50
  "./geo": {
39
- "types": "./dist/geo.d.ts",
40
- "import": "./dist/geo.js",
41
- "require": "./dist/geo.cjs"
51
+ "import": {
52
+ "types": "./dist/geo.d.ts",
53
+ "default": "./dist/geo.js"
54
+ },
55
+ "require": {
56
+ "types": "./dist/geo.d.cts",
57
+ "default": "./dist/geo.cjs"
58
+ }
42
59
  },
43
60
  "./package.json": "./package.json"
44
61
  },
@@ -56,6 +73,7 @@
56
73
  "build": "npm run geo && tsup",
57
74
  "dev": "tsup --watch",
58
75
  "typecheck": "tsc --noEmit",
76
+ "check:exports": "attw --pack .",
59
77
  "test": "vitest run",
60
78
  "test:watch": "vitest",
61
79
  "test:coverage": "vitest run --coverage",
@@ -71,6 +89,7 @@
71
89
  "topojson-client": "^3.1.0"
72
90
  },
73
91
  "devDependencies": {
92
+ "@arethetypeswrong/cli": "^0.18.5",
74
93
  "@testing-library/jest-dom": "^6.9.1",
75
94
  "@testing-library/react": "^16.1.0",
76
95
  "@types/d3-geo": "^3.1.0",