eslint-plugin-aerofortress 0.10.0

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.
Files changed (3) hide show
  1. package/README.md +48 -0
  2. package/index.cjs +1757 -0
  3. package/package.json +24 -0
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # eslint-plugin-aerofortress
2
+
3
+ The frontend harness — the front-side parallel of the backend's Roslyn analyzers (`AeroFortress.Framework.Doctor`). It polices the
4
+ MVVM seam of an AeroFortress screen so React Native + web stays **wired, not mocked**. Doctor-removable: delete the
5
+ plugin and the app still builds; you only lose enforcement.
6
+
7
+ ## Rules
8
+
9
+ | Rule | Code | Polices |
10
+ |---|---|---|
11
+ | `view-purity` | LZFE001 | A `*.view.tsx` renders only — no generated client / axios / react-query import (contract **types** are fine). |
12
+ | `data-door` | LZFE002 | The generated client has exactly two doors: a screen's `*.viewModel.ts`, and the auth/routing infra (`lib/session`, `lib/guards`). Re-exporting it (`export … from "client.gen"`) outside the doors is the laundering bypass — also flagged (type re-exports stay free). |
13
+ | `viewmodel-platform-agnostic` | LZFE009 | A `*.viewModel.ts` imports no `react-native` / `expo` (value **or** type) — the core stays shareable web↔mobile and testable in jsdom. |
14
+ | `test-colocated` | LZFE005 | Every `*.viewModel.ts` has a co-located `*.test.tsx` that `renderHook()`s it (unit tier — proof the data door mounts). |
15
+ | `view-integration-test` | LZFE006 | Every `*.view.tsx` has a co-located test that `render()`s the View (integration tier — proof the screen composes + mounts). |
16
+ | `no-mock` | LZFE003 | No mock/fixture/MSW import in production code (only under `*.test.*`). |
17
+ | `state-completeness` | LZFE010 | A `*.view.tsx` routes loading/error/empty through `<Resource>` — no raw `isPending`/`isError`/… (the booleans are the ViewModel's). |
18
+ | `i18n-completeness` | LZFE011 | Every locale catalog in a `*.i18n.ts` declares the same keys, compared as **flattened paths** (`empty.title`) so a key missing inside a nested group is caught too — a key in one locale but not its siblings is a silent untranslated string. |
19
+ | `design-tokens` | LZFE012 | No inline hex color in production code — colors come from a token; only the `theme`/`tokens`/`palette` definition files may hold hex. |
20
+ | `mutation-error-handled` | LZFE013 | A `*.viewModel.ts` mutation (`.mutate`/`.mutateAsync`) passes an `onError` — no silent failure (front-side of the backend's error_handling). An **empty** `onError: () => {}` is flagged too: that's the silent failure with paperwork. |
21
+ | `no-hardcoded-copy` | LZFE014 | A `*.view.tsx` has no hardcoded user-facing text — JSX text children (`>text<`) **and** copy props (`placeholder`/`title`/`label`/…) go through i18n `t()`. High-signal (`{t()}`/non-copy attrs never flagged). |
22
+ | `no-router-replace-in-effect` | LZFE015 | A redirect-on-state is a **declarative** `<Redirect>`/`<Navigate>` returned from render, never an imperative `router.replace`/`router.navigate`/`useNavigate()` call inside `useEffect` (a post-paint flash on TanStack; an infinite navigation/refetch loop on expo-router web). |
23
+ | `session-one-door` | LZFE016 | The session token is written through **one seam** (`lib/session`); a `*.viewModel`/`*.view` importing the token setter (`setAccessToken`/…) directly — **or writing a token-ish key straight to storage** (`localStorage`/`AsyncStorage`/`SecureStore.setItem("…token…", …)`) — is the scattered write that forgets the cache reset, and bounces the just-authenticated user back to login. |
24
+ | `guard-tristate` | LZFE017 | A route guard redirects on a **tri-state** `SessionState` (`loading \| authenticated \| anonymous`), never a raw `isAuthenticated` boolean (which reads "still loading" as "signed out"). The read-side twin of LZFE010. |
25
+ | `route-param-guard` | LZFE018 | A route reading a **required id param** (expo-router `useLocalSearchParams`) guards its absence — `if (!id) return <Redirect …/>` — so a param-less hit (bookmark / stale link) can't render a ghost screen on an empty id. |
26
+ | `safe-back` | LZFE019 | No bare `router.back()`/`history.back()` — on web a deep-linked screen has no in-app history, so it's a dead button. Route Back through a guarded helper (`safeBack`/`useGoBack`) that falls back to a parent. |
27
+ | `no-hardcoded-base-url` | LZFE020 | The API base URL comes from **configuration** (env `VITE_API_URL`/`EXPO_PUBLIC_API_URL`, a relative base, or an injected default), never a hardcoded host baked into `axios.create({ baseURL: "http://…" })` — a baked literal silently 404s when the backend runs on a different port. |
28
+ | `no-raw-html` | LZFE021 | No `dangerouslySetInnerHTML` outside the **one audited seam** (`lib/html`) — JSX escapes by construction; raw HTML is the XSS door, and if the app renders rich HTML the sanitizer lives in that seam, reviewable. |
29
+ | `no-open-redirect` | LZFE022 | Never navigate to a value that **arrived in the URL** (`router.replace(returnTo)` / `location.href = next` off `useLocalSearchParams`/`useSearch`) — the open-redirect phishing primitive. Map the param through an **allowlist** of known routes first. |
30
+ | `ui-door` | LZFE024 | A `*.view.tsx` renders **no host element** and carries **no `style`/`className`** — everything visual comes from `@/ui` (the app-owned kit, token-typed props). The `data-door` pattern applied to paint; a missing primitive is extended in `ui/`, never inlined. |
31
+ | `scale-only` | LZFE025 | No off-scale spacing/typography literal (`padding: 13`, `fontSize: "13px"`, Tailwind `p-[13px]`) outside `ui/`, the token files, and tests — rhythm comes from the `space`/`text` scales (DESIGN-CONVENTIONS.md). |
32
+ | `semantic-colors` | LZFE026 | No `rgb()/hsl()/oklch()` literal, no CSS named color in a color-ish style key, no value-import of the raw `palette` outside `ui/`, and no Tailwind palette-family utility (`bg-red-500`, `text-blue-600`) in a `className` outside `ui/` — color is a semantic role (`color.*`/theme). Completes `design-tokens` (LZFE012, the hex half). |
33
+
34
+ ### Routing rules — both routers, one shape
35
+
36
+ LZFE015–019 are the **routing harness**: the front-side parallel of the backend's slice rules, born from the two app pilots (Pauta on TanStack Router, Hostpoint on expo-router) converging on the same navigation bugs — a freshly-registered user bounced to login, ghost screens on missing params, dead Back buttons, effect-driven redirect loops. They police a **shape** (declarative redirect, tri-state session, guarded param, guarded back), so they recognize each router's idiom (`<Redirect>`/`router.replace`/`useLocalSearchParams` ↔ `<Navigate>`/`useNavigate()`/`Route.useParams`) without depending on either runtime — "ship the standard, not the adapter". The tri-state `SessionState` and the `safeBack` helper they steer toward live in the spine (`@aerofortress/react`).
37
+
38
+ ## Self-proving
39
+
40
+ `npm test` runs `index.test.cjs` — RuleTester cases that pin every rule on **both** edges: it must FIRE on the
41
+ violation it polices and PASS on the shapes it allows. A rule isn't done until both are pinned (the discipline the
42
+ framework applies to its own backend rules). CI runs this beside `dotnet build` and the spine's vitest suite.
43
+
44
+ ## Consuming it in an app
45
+
46
+ An app registers the plugin in its flat ESLint config and turns the rules on for its feature tree — see the
47
+ `frontend-sdk/eslint.config.mjs` in this repo, which lints the canonical `sample/`. Until this package is published to
48
+ npm, a consuming app (e.g. the Hostpoint dogfood) installs it from this repo; once published it is a normal devDep.