@webority/repo-checks
Repository checks for Webority React web portals and React Native apps. One command reads every
app package.json in the git repository and reports what the house rules say it must not have,
must have, and how it must be pinned and installed.
It also counts raw measures, raw colours, root font sizes and palette reads in every tracked style, markup and script file of the repository (bootstrap-ui.md → Tokens).
Use
pnpm add -D --save-exact @webority/repo-checks
pnpm exec webority-repo-checks # every app in the repository
pnpm exec webority-repo-checks apps/portal # one app folder
An app is a package.json whose dependencies hold react-native (mobile) or react-dom (web). A react-dom folder that also has react-native-web is a react-native-web demo: it gets the pnpm and Biome checks, not the portal rules. Biome and @webority/biome-config are read from the package.json beside the biome.json in effect (a nested config with "root": false defers to the one above), so a sub-folder linted by the root config does not repeat them. A workspace:, file: or link: reference is local and never counts as a lockstep version.
A package with agentation in its dependencies is a build tool, not an app. It and anything else (tooling roots, libraries) get only the lockfile check.
Every app also gets the Node checks: engines.node (from the app's package.json, else the one beside its pnpm-lock.yaml) must be >=24.15 <25, and the nearest .nvmrc must hold 24. A mobile app with an android/ folder gets the Android checks, and a web app's src/ the import checks.
The missing-export:<file> check runs the Biome installed beside the app or in a folder above it, so it needs @biomejs/biome installed; without one it reports nothing, and the required:@biomejs/biome finding fails the app.
| Finding | What it means |
|---|---|
banned:<name> | A dependency the rules retire or forbid (ESLint, Prettier, lint-staged, react-toastify, moment and the rest; the mobile list is the retired-libraries table) |
required:<name> | A dependency every app carries (Biome and the shared preset, the core and UI packages, Bootstrap and react-bootstrap on web, Crashlytics on mobile) |
not-exact:<name> | A ^ or ~ range. Web pins @webority/* and bootstrap exactly; a React Native app pins every dependency exactly |
bootstrap-version | Bootstrap other than 5.3.3 |
lockstep:<set> | Packages that publish together at one version are pinned at different versions |
pnpm:* | A committed package-lock.json or yarn.lock, no pnpm-lock.yaml, no devEngines.packageManager of { "name": "pnpm", "version": "<x.y.z>", "onFail": "error" } (a plain npm install fails, and pnpm refuses any other version), a leftover packageManager field (pnpm ignores it beside devEngines), or a pnpm-workspace.yaml without engineStrict: true and strictPeerDependencies: true |
biome:* | No Biome config, a config that does not extend @webority/biome-config/biome.jsonc, a formatter.lineEnding or root linter.rules, or (mobile) no __DEV__ global, either top-level or in an override whose includes cover the app folder |
catalogue-copy:<file> | A web file under src/components/ named like a @webority/ui-react export |
engines-node-floor:package.json | engines.node is missing or is not >=24.15 <25 |
engines-node-floor:.nvmrc | No .nvmrc at or above the app, or it does not hold 24 |
android-release-signing | android/app/build.gradle has no guard that throws a GradleException when a requested task name contains release and a signing setting or the upload key is missing |
android-edge-to-edge | android/gradle.properties sets edgeToEdgeEnabled=false |
rn-mainactivity-oncreate-bundle:<file> | A MainActivity.kt or .java passes a bundle to super.onCreate; pass null |
metro-bridge:<file> | A React Native app's entry file (package.json main, default index.js), root App.js or src/App.js nor any other source file of the app (outside node_modules, ios, android and build output) imports createMetroBridge from @webority/mobile-core/metro and assigns globalThis.__METRO_BRIDGE__ from a createMetroBridge(...) call inside if (__DEV__) in the same file; react-native-procedures.md → Debugging: the development bridge |
mobile-production-stamps:<file> | A mobile app's Android manifest or iOS Expo.plist points over-the-air updates at the Staging or Development feed, or android/gradle.properties carries a .staging or .development app id, left by mobile-tools prebuild. Run mobile-tools prebuild --env Production before committing |
spa-host-immutable-assets:<file> | A web app's web.config, staticwebapp.config.json, _headers or spa-host*.json sets Cache-Control with immutable |
stale-theme-name:<file> | A web style or source file reads a pre-0.89.0 theme name as a whole token (var(--radius), --color-primary-ink). No alias ships for these. Run webority upgrade 0.89.0 --path <folder> once, then webority brand. --color-primary-text is not flagged. Never recorded in the baseline |
lint:<file> | Biome's check, with warnings as errors, over the app folder: any error or warning. Never recorded in the baseline; fix the finding |
web-import-axios:<file> | A web source file under src/ imports axios; HTTP goes through @webority/web-core |
web-import-router:<file> | A web source file imports BrowserRouter or Routes from react-router or react-router-dom; routes are objects for createBrowserRouter |
web-import-env-branch:<file> | A web source file reads import.meta.env.DEV or import.meta.env.PROD; branch on AppSettings.ENVIRONMENT |
missing-export:<file> | A file imports a name its module does not export; Biome's React false alarms and unresolved aliases are not reported |
raw-measure:<file> | A px, rem or em literal, or a bare number for a weight or a leading, in padding, margin, gap, inset, a corner, font size, font weight or line height. Counted per file. |
raw-colour:<file> | A hex, an rgb() or hsl() with numbers, or a CSS named colour in a colour property or style key, where a colour meaning belongs. Counted per file. |
root-font-size:<file> | A font-size or font declared on html or :root. The type scale assumes the browser's root size. Counted per file. |
palette-read:<file> | A read of a raw palette step (var(--wui-<step>), or a phone palette. read) or of a Bootstrap theme colour (var(--bs-primary), var(--bs-primary-rgb) or Sass $primary, for the eight theme colours), where a colour meaning belongs. The message names the meaning to read. Counted per file. |
missing-canvas-key:<file> | A list item (the first JSX tag in a .map( callback) with no data-canvas key on web or testID on a phone, and no spread. Counted per file. |
repeated-canvas-key:<file> | A quoted data-canvas or testID value that a second tag in the same module repeats. Counted per file. |
raw-bootstrap:<file> | A .jsx, .tsx or .cshtml file writes a raw Bootstrap component class (btn, card, modal, table, badge, alert, form-control, form-select, form-check-input, pagination, nav-tabs, dropdown-menu, offcanvas, breadcrumb, progress, spinner-border, accordion); the message names the library component. A modifier such as btn-primary alone is not counted. Counted per file. |
library-class-restyle:<file> | A product .scss or .css selector names a class the library theme defines (a wui-* component class or an app-* shell, navigation or tab class), including as an ancestor (.my-page .wui-card-body, .app-shell.collapsed .app-sidebar .brand-logo); use a library prop or token, or a product class. A token file that only sets custom properties names no class and is not counted. Counted per file. |
shadowed-library-name:<file> | A .jsx or .tsx file imports a catalogue component from @webority/ui-react under another name and declares a component under the catalogue name (import { AppButton as LibraryAppButton } with function AppButton), so its call sites read as the library component. Use the library component and its props, or give the product component its own name. Counted per file. |
rename-wrapper:<file> | A re-export of @webority/ui-react, a component whose body only returns one catalogue component, or a _Name.cshtml partial that is one <app-*> element: import the library name. Counted per file. |
single-use-component:<file> | A file under src/components/<feature>/ (not layout/), or a shared partial under Pages/Shared/ or a *.Web.Common project, that fewer than two files use; move it next to the page that uses it. A layout is skipped: a _…Layout.cshtml file, or one that a _ViewStart.cshtml of the same project names with Layout = "<name>", because areas find it by that name. A Razor file whose first line is @* layering-allow single-use-component: <reason> *@ is skipped too; a mark with no reason is a finding. Counted per file. |
error-summary-title:<file> | A .cshtml <app-error-summary> sets a title with the old generic wording ("Please fix the following", "There were errors"); drop it, the library writes the standard heading. A computed title or one naming the failed action is not counted. Counted per file. |
route-internal-id:<file> | A route binds a record's internal number: a .cshtml @page or a C# [Route], [Http*] or Map* template whose id or ...Id parameter is constrained int or long, or an asp-route-* link that passes a record's .Id. Route by the public id ({publicId:guid}) instead. An untyped {id} is not counted. Counted per file. |
oversize:<file> | A .jsx, .tsx or .cshtml file over 600 lines (over 800 says to split it along its concerns). A warning only: it never fails, even outside the baseline. Counted per file. |
hook:drift | The root .husky/pre-commit is missing or differs from the shared hook this package ships (CRLF is ignored) |
tracked-case:<path> | A tracked file or folder whose name on disk differs only by letter case, a rename done by hand on a Mac that git never saw; a Linux checkout cannot find the import. Run the git mv the message gives. A folder is reported once. Always fails. |
Comment lines (//, /*, *) and .test. or .spec. files are not read by the import checks.
Token rules: counted per file
The four rules read every tracked .scss, .css, .cshtml, .jsx and .tsx file, and each .js or
.ts file that imports react-native. They skip a path with a node_modules, dist, generated,
vendor, __tests__ or __mocks__ segment, an email template project folder (<Product>.Email or
<Product>.Emails: email clients read no CSS variables, so inline pixels and colours are required there;
a plain Email folder of admin screens stays checked), a path containing wwwroot/lib/, a .generated. name,
a .min.css name, a .test. or .spec. script, and any path the repo lists under generated[].paths in
.webority/map.json (a trailing / names a folder; * matches within one segment). List a compiled
stylesheet there: it bundles the shared theme, so its library rules are not the product's code, and the
rules still read the SCSS it is built from.
raw-measure counts a non-zero px, rem or em literal, or a bare number for a weight or a leading, in
padding, margin, gap, inset, a corner, font size, font weight or line height: one finding per
declaration or style key. A zero, a keyword (auto, bold, normal), a token
(var(--wui-space-md)), a percentage or another unit, and a custom property or Sass variable
declaration are not counted.
raw-colour counts a hex, an rgb(), rgba(), hsl() or hsla() with numeric arguments, or a CSS
named colour, in a colour property or style key (including a fallback inside var(--color-x, #fff)).
transparent, currentColor, a meaning read, a custom property or Sass variable declaration, and
text inside a url() or a quoted string are not counted.
root-font-size counts every font-size or font declaration in a rule whose selector is html or
:root (with or without an attribute, class or pseudo-class), whatever its value.
palette-read counts each var(--wui-<step>) and each phone palette. read, plus each
var(--bs-<colour>), var(--bs-<colour>-rgb) and Sass $<colour> for Bootstrap's eight theme
colours: one finding per read, and a declaration is not a read.
missing-canvas-key and repeated-canvas-key read every script module that imports react-native,
@webority/mobile-ui or @webority/ui-react. The attribute is testID in a module that imports
react-native or @webority/mobile-ui, and data-canvas in any other. A spread on the item counts as
keyed, and a repeat adds one finding per tag after the first.
src/assets/styles/_home.scss:12: color: var(--wui-primary);
(read the meaning: var(--color-primary))
New rules start as warnings
lint, rn-mainactivity-oncreate-bundle, spa-host-immutable-assets and oversize print as warnings and do not fail until
REPO_CHECKS_ENFORCE names them: REPO_CHECKS_ENFORCE=lint,rn-mainactivity-oncreate-bundle, or all. A release
removes a rule from the warning list when no repository has a hit. --json carries the warnings under warnings.
Existing findings: a baseline that only shrinks
A repository adopting the checks records what it has today, once:
pnpm exec webority-repo-checks --baseline-init
That writes .webority/baselines/repo-checks-baseline.json, creating the folder: the dependency findings as
"<app folder> | <id>", and the code rules counted per file:
{
"version": 2,
"findings": ["apps/portal | banned:@webority/ui-elements", "samples/react-sandbox | required:@webority/web-core"],
"counts": { "palette-read:packages/theme/scss/wui/_buttons.scss": 3, "raw-measure:src/assets/styles/_home.scss": 14 }
}
From then on a finding in the baseline is reported as known and does not fail; any other finding
fails (exit 1). A count may only fall: a file above its recorded count fails and prints its lines,
and a file missing from the baseline is at 0, so a new or renamed file starts at 0. After fixing old
findings or lowering counts, --baseline-shrink drops the fixed findings and lowers each count to
its current value, deleting one that reaches 0; it never adds or raises one.
--baseline-accept <folder> --reason <text> records debt the owner has accepted: every current finding and count under that folder is added, and a count is raised to its current value, never lowered. Use it only for debt the owner has accepted, and give a reason that names the decision; the reason is kept in accepted in the baseline file, and --baseline-shrink keeps that entry.
--baseline-adopt <rule,...> records today's findings and counts for rules the baseline has never recorded, such as a rule a new release adds. It refuses a rule the baseline already records, so it never raises an existing entry, and names how many findings and counts it adopted per rule.
--baseline-init runs once: it refuses an existing file and folder arguments. When the file is the
earlier shape (no version), delete it and run --baseline-init once from the git root, then review
the diff before committing.
--json prints { root, new, known, stale } for tools; a count above its record appears in new
with its lines.
The shared pre-commit hook
Every repository runs the pre-commit hook this package ships. Adopt it once, from the git root:
pnpm exec webority-repo-checks --sync-hook
That writes .husky/pre-commit, creating .husky when it is missing, with LF line endings. A
multi-app repository installs it once, at the root. husky stays the installer, through the
prepare script. Remove lint-staged (pnpm remove lint-staged, and delete its config key): the
shared hook runs Biome itself. Put repo-only steps in .husky/pre-commit.local; the shared hook
runs that file after Biome, and --sync-hook never touches it.
hook:drift keeps the root copy equal to the shipped one.
Versioning
Published from the webority-ui monorepo in lockstep with the other @webority/* packages. Pin
an exact version; a new rule arrives when you bump it.