@webority/repo-checks 0.0.0-stage → 0.48.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.
package/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Copyright (c) Webority Technologies. All rights reserved.
2
+
3
+ This software and its source code are proprietary and confidential to
4
+ Webority Technologies. Use is restricted to Webority products and teams under
5
+ internal license. Redistribution outside Webority Technologies is prohibited.
package/README.md CHANGED
@@ -1,3 +1,47 @@
1
- # Temporary Holding Version
1
+ # @webority/repo-checks
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Repository checks for Webority React web portals and React Native apps. One command reads every
4
+ app `package.json` in the git repository and reports what the house rules say it must not have,
5
+ must have, and how it must be pinned and installed.
6
+
7
+ ## Use
8
+
9
+ ```bash
10
+ pnpm add -D --save-exact @webority/repo-checks
11
+ pnpm exec webority-repo-checks # every app in the repository
12
+ pnpm exec webority-repo-checks apps/portal # one app folder
13
+ ```
14
+
15
+ An app is a `package.json` whose `dependencies` hold `react-native` (mobile) or `react-dom` (web).
16
+ Anything else (tooling roots, libraries) gets only the lockfile check.
17
+
18
+ | Finding | What it means |
19
+ |---|---|
20
+ | `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) |
21
+ | `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) |
22
+ | `not-exact:<name>` | A `^` or `~` range. Web pins `@webority/*` and `bootstrap` exactly; a React Native app pins every dependency exactly |
23
+ | `bootstrap-version` | Bootstrap other than 5.3.3 |
24
+ | `lockstep:<set>` | Packages that publish together at one version are pinned at different versions |
25
+ | `pnpm:*` | A committed `package-lock.json` or `yarn.lock`, no `pnpm-lock.yaml`, no `packageManager: pnpm@…`, or a `pnpm-workspace.yaml` without `engineStrict: true` and `strictPeerDependencies: true` |
26
+ | `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 |
27
+ | `catalogue-copy:<file>` | A web file under `src/components/` named like a `@webority/ui-react` export |
28
+
29
+ ## Existing findings: a baseline that only shrinks
30
+
31
+ A repository adopting the checks records what it has today, once:
32
+
33
+ ```bash
34
+ pnpm exec webority-repo-checks --baseline-init
35
+ ```
36
+
37
+ That writes `.webority/repo-checks-baseline.json`. From then on a finding in the baseline is
38
+ reported as known and does not fail; any other finding fails (exit 1). After fixing old findings,
39
+ `--baseline-shrink` drops the entries that no longer occur. Nothing adds an entry to an existing
40
+ baseline: `--baseline-init` refuses when the file exists.
41
+
42
+ `--json` prints `{ root, new, known, stale }` for tools.
43
+
44
+ ## Versioning
45
+
46
+ Published from the `webority-ui` monorepo in lockstep with the other `@webority/*` packages. Pin
47
+ an exact version; a new rule arrives when you bump it.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,writeFileSync as i}from"node:fs";import{basename as a,dirname as o,join as s,relative as c,resolve as l}from"node:path";import{spawnSync as u}from"node:child_process";function d(e){let t=``,n=!1;for(let r=0;r<e.length;r+=1){let i=e[r];if(n){t+=i,i===`\\`?(t+=e[r+1]??``,r+=1):i===`"`&&(n=!1);continue}if(i===`"`)n=!0,t+=i;else if(i===`/`&&e[r+1]===`/`){for(;r<e.length&&e[r]!==`
3
+ `;)r+=1;t+=`
4
+ `}else if(i===`/`&&e[r+1]===`*`){for(r+=2;r<e.length&&!(e[r]===`*`&&e[r+1]===`/`);)r+=1;r+=1}else t+=i}return JSON.parse(t.replace(/,(\s*[}\]])/g,`$1`))}var f=[[`eslint`,`react.md: Biome is the only lint and format tool`],[`@eslint/js`,`react.md: Biome is the only lint and format tool`],[`prettier`,`react.md: Biome is the only lint and format tool`],[`lint-staged`,`react.md: the shared pre-commit hook replaces lint-staged`]],p=[[`@typescript-eslint/`,`react.md: no TypeScript toolchain residue`],[`eslint-plugin-`,`react.md: Biome is the only lint and format tool`],[`eslint-config-`,`react.md: Biome is the only lint and format tool`],[`prettier-plugin-`,`react.md: Biome is the only lint and format tool`]],m=`bootstrap-ui.md: hard interactive components come from @webority/ui-react`,h=`react.md: Bootstrap 5.3.3 is the only UI framework`,g=`react.md: dates format through Intl and the web-core formatters`,_=`react-native.md: retired; Intl and the mobile-core formatters`,v={web:[...f,[`typescript`,`react.md: JavaScript only, never TypeScript`],[`sass`,`react.md: the SCSS compiler is sass-embedded, never the pure-JS sass`],[`react-toastify`,`react.md: notifications are toast from @webority/ui-react`],[`downshift`,m],[`react-day-picker`,m],[`react-aria`,m],[`react-aria-components`,m],[`@webority/ui-elements`,`bootstrap-ui.md: products use the App* wrappers, and ui-react brings the elements itself`],[`crypto-js`,`react.md: stored tokens are never encrypted client-side`],[`tailwindcss`,h],[`antd`,h],[`styled-components`,`react.md: no CSS-in-JS`],[`bootstrap-icons`,`bootstrap-ui.md: icons are AppIcon from @webority/ui-react`],[`iconsax-react`,`ui-components.md: icons are AppIcon from @webority/ui-react`],[`moment`,g],[`moment-timezone`,g],[`dayjs`,g],[`date-fns`,g]],mobile:[...f,[`typescript`,`react-native.md: app code is JavaScript`],[`@types/react`,`react-native.md: app code is JavaScript`],[`@react-native/typescript-config`,`react-native.md: app code is JavaScript`],[`react-native-permissions`,`react-native.md: retired; each Expo module requests its own permission`],[`moment`,_],[`moment-timezone`,_],[`dayjs`,_],[`react-native-fast-image`,`react-native.md: retired; expo-image`],[`@d11/react-native-fast-image`,`react-native.md: retired; expo-image`],[`formik`,`react-native.md: retired; react-hook-form`],[`yup`,`react-native.md: retired; zod with zodResolver`],[`react-native-status-bar-height`,`react-native.md: retired; react-native-safe-area-context insets`],[`react-native-flash-message`,`react-native.md: retired; the mobile-ui toast`],[`react-native-toast-message`,`react-native.md: retired; the mobile-ui toast`],[`prop-types`,`react-native.md: retired; defaults in the parameter destructure`],[`@react-navigation/stack`,`react-native.md: retired; @react-navigation/native-stack`],[`react-native-version-check`,`react-native.md: retired; the server-driven minimum version`],[`@react-native-firebase/messaging`,`react-native.md: push is expo-notifications through mobile-core`],[`@notifee/react-native`,`react-native.md: push is expo-notifications through mobile-core`],[`patch-package`,`react-native-toolchain.md: patch with pnpm patch, never patch-package`]]},y={web:[...p,[`@mui/`,h],[`@chakra-ui/`,h],[`@emotion/`,`react.md: no CSS-in-JS`],[`@radix-ui/`,m],[`@react-aria/`,m]],mobile:[...p]},ee={web:[[`axios-retry`,`@webority/web-core`,`react.md: retry lives only in @webority/web-core`]],mobile:[[`axios-retry`,`@webority/mobile-core`,`react.md: retry lives only in @webority/mobile-core`]]},te={web:[[`@biomejs/biome`,`react.md: Biome is mandatory`],[`@webority/biome-config`,`react.md: every repo extends the shared Biome preset`],[`@webority/web-core`,`react.md: clients, auth, errors and logging come from web-core`],[`@webority/ui-react`,`react.md: App* components come from the catalogue`],[`@webority/theme`,`bootstrap-ui.md: look and tokens come from the shared theme`],[`bootstrap`,`bootstrap-ui.md: Bootstrap 5.3.3 on every surface`],[`react-bootstrap`,`bootstrap-ui.md: react-bootstrap is a dependency from project init`],[`sass-embedded`,`react.md: the SCSS compiler is sass-embedded`]],mobile:[[`@biomejs/biome`,`react-native.md: Biome is mandatory`],[`@webority/biome-config`,`react.md: every repo extends the shared Biome preset`],[`@webority/mobile-core`,`react-native.md: apps consume the shared mobile library`],[`@webority/mobile-ui`,`react-native.md: apps consume the shared mobile library`],[`@react-native-firebase/app`,`react-native.md: Crashlytics in every app`],[`@react-native-firebase/crashlytics`,`react-native.md: Crashlytics in every app`]]},ne=`5.3.3`,re=[[`@webority/theme`,`@webority/ui-elements`,`@webority/ui-react`,`@webority/support-react`,`@webority/chat-react`,`@webority/web-core`,`@webority/biome-config`,`@webority/repo-checks`],[`@webority/mobile-core`,`@webority/mobile-ui`,`@webority/mobile-support`,`@webority/mobile-tools`]],ie=`@webority/biome-config/biome.jsonc`,ae=`AppAccordion.AppActionIcon.AppAlert.AppAsyncBlock.AppAutocomplete.AppAvatar.AppAvatarGroup.AppBadge.AppBanner.AppBreadcrumbs.AppButton.AppCard.AppCheckbox.AppChip.AppChoiceCard.AppChoiceChip.AppColorField.AppColumnMenu.AppCommandPalette.AppCompareTable.AppConfirmDialog.AppConsentBanner.AppCopyButton.AppCountrySelect.AppDataTable.AppDatePicker.AppDateRangePicker.AppDescriptions.AppDropdown.AppEmptyState.AppErrorSummary.AppFileAttachment.AppFilePicker.AppFileViewer.AppFloatingDock.AppFormActions.AppFormRow.AppFormSection.AppIcon.AppInfoList.AppInfoTip.AppInput.AppKpiTile.AppLink.AppLogoPicker.AppMeter.AppModal.AppMultiSelect.AppNumberInput.AppOtpInput.AppPageHeader.AppPagination.AppPhoneNumberInput.AppPopover.AppPricingCard.AppProgress.AppQrCode.AppRadio.AppRadioGroup.AppRepeater.AppRequiredMark.AppResourceCard.AppResult.AppScrollbar.AppSearch.AppSegmented.AppSelect.AppSelectMini.AppShell.AppSidebarMenu.AppSkeleton.AppSkeletonRows.AppSlider.AppSpinner.AppSplash.AppStatusBadge.AppStepper.AppSwitch.AppTableEmptyState.AppTablePagination.AppTableToolbar.AppTabs.AppTagInput.AppTextarea.AppThemeToggle.AppTimeline.AppTimePicker.AppToast.AppToolbar.AppTooltip.AppWizard.AppWorkspaceSwitcher`.split(`.`),b=e=>e.split(`\\`).join(`/`),x=(e,t)=>b(c(e,t))||`.`;function S(e){try{return JSON.parse(n(e,`utf8`))}catch{return null}}function C(e,t){let n=u(`git`,t,{cwd:e,encoding:`utf8`,windowsHide:!0,maxBuffer:64*1024*1024});return n.status===0?n.stdout:null}function w(e){let t=C(e,[`rev-parse`,`--show-toplevel`]);return t?t.trim():null}function T(e,t){let n=C(e,[`ls-files`,`-z`,`--`,...t.flatMap(e=>[e,`:(glob)**/${e}`])]);return n?[...new Set(n.split(`\0`).filter(Boolean))]:[]}function oe(e){let t=e?.dependencies??{};return t[`react-native`]?`mobile`:t[`react-dom`]?`web`:null}var E=e=>/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(e)||/^(?:workspace:\*|file:|link:)/.test(e);function D(e,t){let n=[],r={...e.dependencies,...e.devDependencies},i=Object.keys(r);for(let[e,i]of v[t])r[e]&&n.push([`banned:${e}`,`"${e}" is a dependency. ${i}.`]);for(let[e,r]of y[t])for(let t of i.filter(t=>t.startsWith(e)))n.push([`banned:${t}`,`"${t}" is a dependency. ${r}.`]);for(let[e,i,a]of ee[t])r[e]&&r[i]&&n.push([`banned:${e}`,`"${e}" is a dependency beside ${i}. ${a}.`]);for(let[e,i]of te[t])r[e]||n.push([`required:${e}`,`"${e}" is not a dependency. ${i}.`]);let a=t===`mobile`?i:i.filter(e=>e.startsWith(`@webority/`)||e===`bootstrap`);for(let e of a){let t=String(r[e]);E(t)||n.push([`not-exact:${e}`,`"${e}": "${t}" is a range. Pin the version the lockfile holds, without ^ or ~.`])}r.bootstrap&&String(r.bootstrap).replace(/^[\^~]/,``)!==`5.3.3`&&n.push([`bootstrap-version`,`bootstrap is "${r.bootstrap}". bootstrap-ui.md: every surface pins ${ne}.`]);for(let e of re){let t=e.filter(e=>r[e]&&!String(r[e]).startsWith(`workspace:`));if(new Set(t.map(e=>String(r[e]).replace(/^[\^~]/,``))).size>1){let i=t.map(e=>`${e}@${r[e]}`).join(`, `);n.push([`lockstep:${e[0]}`,`${i} differ. These publish together at one version; move every pin in the set together.`])}}return n}function O(t,n,r){let i=t;for(;;){for(let t of r)if(e(s(i,t)))return s(i,t);if(i===n)return null;let t=o(i);if(t===i)return null;i=t}}function k(t,r){let i=[],a=O(t,r,[`pnpm-lock.yaml`]);a||i.push([`pnpm:no-lockfile`,`No pnpm-lock.yaml in this folder or above it. react.md: every JS repo installs with pnpm.`]);let c=a?o(a):t,l=S(s(c,`package.json`))??S(s(t,`package.json`));/^pnpm@/.test(l?.packageManager??``)||i.push([`pnpm:package-manager`,`package.json has no "packageManager": "pnpm@<version>". react.md: Package manager.`]);let u=s(c,`pnpm-workspace.yaml`);if(!e(u))i.push([`pnpm:no-workspace-file`,`No pnpm-workspace.yaml beside the lockfile. react.md: it holds engineStrict, strictPeerDependencies and allowBuilds.`]);else{let e=n(u,`utf8`);/^engineStrict:\s*true\s*$/m.test(e)||i.push([`pnpm:engine-strict`,`pnpm-workspace.yaml lacks engineStrict: true. react.md: a Node mismatch must fail the install.`]),/^strictPeerDependencies:\s*true\s*$/m.test(e)||i.push([`pnpm:strict-peers`,`pnpm-workspace.yaml lacks strictPeerDependencies: true. react.md: a peer conflict must fail the install.`])}for(let n of[`package-lock.json`,`yarn.lock`])e(s(t,n))&&i.push([`pnpm:${n}`,`${n} sits in this folder. react.md: pnpm only; seed pnpm-lock.yaml with pnpm import, then delete it.`]);return i}function A(e,t,r){let i=O(e,t,[`biome.json`,`biome.jsonc`]);if(!i)return[[`biome:missing`,`No biome.json in this folder or above it. react.md: one committed root biome.json.`]];let o;try{o=d(n(i,`utf8`))}catch{return[[`biome:unreadable`,`${a(i)} does not parse.`]]}let s=[];return[].concat(o.extends??[]).some(e=>e===`@webority/biome-config/biome.jsonc`||String(e).endsWith(`biome-config/biome.jsonc`))||s.push([`biome:extends`,`${a(i)} does not extend "${ie}". react.md: every repo extends the shared preset.`]),o.formatter?.lineEnding&&s.push([`biome:line-ending`,`${a(i)} sets formatter.lineEnding. react.md: inherit it from the shared preset, never set it.`]),o.linter?.rules&&Object.keys(o.linter.rules).length>0&&s.push([`biome:root-rules`,`${a(i)} sets linter.rules at the root. react.md: never a rule's severity at the root; a per-path exception goes in overrides.`]),r===`mobile`&&!(o.javascript?.globals??[]).includes(`__DEV__`)&&s.push([`biome:dev-global`,`${a(i)} lacks "__DEV__" in javascript.globals. react-native.md: Debugging and agent access.`]),s}function j(e){let t=s(e,`node_modules`,`@webority`,`ui-react`);try{let e=JSON.parse(n(s(t,`package.json`),`utf8`)),r=n(s(t,typeof e.module==`string`?e.module:typeof e.main==`string`?e.main:`dist/index.js`),`utf8`),i=new Set([...r.matchAll(/App[A-Z][A-Za-z0-9]*/g)].map(e=>e[0]));if(i.size>0)return i}catch{}return new Set(ae)}function M(e){return r(e,{withFileTypes:!0}).flatMap(t=>t.isDirectory()?M(s(e,t.name)):[s(e,t.name)])}function N(t){let n=s(t,`src`,`components`);if(!e(n))return[];let r=j(t),i=[];for(let e of M(n)){let n=a(e).split(`.`)[0];if(r.has(n)){let r=b(c(t,e));i.push([`catalogue-copy:${r}`,`${r} is named like the @webority/ui-react export ${n}. bootstrap-ui.md: use the catalogue component, never a local copy.`])}}return i}function P(t,n=null){let r=n?new Set(n.map(e=>x(t,e))):null,i=[],c=(e,t)=>{for(let[n,r]of t)i.push({pkg:e,id:n,message:r})};for(let e of T(t,[`package-lock.json`,`yarn.lock`])){let t=b(o(e))||`.`;r&&!r.has(t)||c(t,[[`pnpm:${a(e)}`,`${a(e)} is committed. react.md: pnpm only; seed pnpm-lock.yaml with pnpm import, then delete it.`]])}let l=new Set(T(t,[`package.json`]).map(e=>x(t,s(t,o(e)))));for(let n of r??[])e(s(t,n,`package.json`))&&l.add(n);for(let e of l){if(r&&!r.has(e))continue;let n=s(t,e),a=S(s(n,`package.json`)),o=oe(a);if(!o)continue;let l=k(n,t).filter(([t])=>!i.some(n=>n.pkg===e&&n.id===t));c(e,[...D(a,o),...l,...A(n,t,o),...o===`web`?N(n):[]])}return i}var F=`.webority/repo-checks-baseline.json`,I=process.argv.slice(2),L=new Set(I.filter(e=>e.startsWith(`--`))),R=I.filter(e=>!e.startsWith(`--`)),z=e=>process.stdout.write(`${e}\n`),B=e=>process.stderr.write(`${e}\n`),V=new Set([`--json`,`--baseline-init`,`--baseline-shrink`]),H=[...L].filter(e=>!V.has(e));(H.length||L.has(`--baseline-init`)&&L.has(`--baseline-shrink`))&&(B(`repo-checks: unknown or conflicting options ${H.join(` `)}`.trim()),B(`usage: webority-repo-checks [--json] [--baseline-init | --baseline-shrink] [app folder ...]`),process.exit(2));var U=w(process.cwd());U||(B(`repo-checks: not inside a git repository.`),process.exit(2));var W=P(U,R.length?R.map(e=>l(process.cwd(),e)):null).sort((e,t)=>e.pkg===t.pkg?0:e.pkg<t.pkg?-1:1),G=e=>`${e.pkg}\t${e.id}`,K=s(U,F);function se(){if(!e(K))return null;try{return JSON.parse(n(K,`utf8`))}catch{B(`repo-checks: ${F} does not parse.`),process.exit(2)}}function q(e){let n={};for(let[t,r]of e.map(e=>e.split(` `)).sort())(n[t]??=[]).push(r);t(o(K),{recursive:!0}),i(K,`${JSON.stringify(n,null,4)}\n`)}var J=se(),Y=new Set(Object.entries(J??{}).flatMap(([e,t])=>t.map(t=>`${e}\t${t}`)));L.has(`--baseline-init`)&&(J&&(B(`repo-checks: ${F} already exists. It only shrinks: fix a new finding, or run --baseline-shrink after fixing old ones.`),process.exit(2)),q(W.map(G)),z(`repo-checks: recorded ${W.length} existing finding(s) in ${F}.`),process.exit(0));var X=new Set(W.map(G)),ce=new Set(R.map(e=>c(U,l(process.cwd(),e)).split(`\\`).join(`/`)||`.`)),le=e=>!R.length||ce.has(e.split(` `)[0]),Z=[...Y].filter(e=>!X.has(e)&&le(e));L.has(`--baseline-shrink`)&&(J||(B(`repo-checks: no ${F} to shrink.`),process.exit(2)),R.length&&(B(`repo-checks: --baseline-shrink reads the whole repository; run it without folder arguments.`),process.exit(2)),q([...Y].filter(e=>X.has(e))),z(`repo-checks: dropped ${Z.length} fixed entr${Z.length===1?`y`:`ies`} from ${F}.`),process.exit(0));var Q=W.filter(e=>!Y.has(G(e))),$=W.filter(e=>Y.has(G(e)));if(L.has(`--json`)&&(z(JSON.stringify({root:U,new:Q,known:$,stale:Z.map(e=>e.split(` `))},null,2)),process.exit(+!!Q.length)),Q.length){B(`repo-checks: ${Q.length} new finding(s)`);let e=null;for(let t of Q)t.pkg!==e&&(e=t.pkg,B(` ${e}`)),B(` - ${t.id}: ${t.message}`)}else z(`repo-checks: no new findings.`);if($.length&&z(`repo-checks: ${$.length} known finding(s) recorded in ${F}.`),Z.length){z(`repo-checks: ${Z.length} baseline entr${Z.length===1?`y is`:`ies are`} fixed; drop them with webority-repo-checks --baseline-shrink:`);for(let e of Z)z(` ${e.replace(` `,` `)}`)}process.exit(+!!Q.length);export{};
package/package.json CHANGED
@@ -1,6 +1,29 @@
1
1
  {
2
2
  "name": "@webority/repo-checks",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.48.0",
4
+ "description": "Repository checks for Webority React web portals and React Native apps: banned and required dependencies, pnpm only, exact pins, and the shared Biome preset. Run before a push; existing findings sit in a baseline that only shrinks.",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org/"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "bin": {
15
+ "webority-repo-checks": "./dist/repo-checks.js"
16
+ },
17
+ "exports": {
18
+ "./package.json": "./package.json"
19
+ },
20
+ "engines": {
21
+ "node": ">=24.15 <25"
22
+ },
23
+ "devDependencies": {
24
+ "vite": "^8.0.12"
25
+ },
26
+ "scripts": {
27
+ "build": "vite build"
28
+ }
6
29
  }