@webority/repo-checks 0.51.2 → 0.52.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/README.md CHANGED
@@ -12,7 +12,7 @@ pnpm exec webority-repo-checks # every app in the repository
12
12
  pnpm exec webority-repo-checks apps/portal # one app folder
13
13
  ```
14
14
 
15
- An app is a `package.json` whose `dependencies` hold `react-native` (mobile) or `react-dom` (web).
15
+ 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.
16
16
  Anything else (tooling roots, libraries) gets only the lockfile check.
17
17
 
18
18
  | Finding | What it means |
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
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
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{};
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`]],demo:[]},ee={web:[...p,[`@mui/`,h],[`@chakra-ui/`,h],[`@emotion/`,`react.md: no CSS-in-JS`],[`@radix-ui/`,m],[`@react-aria/`,m]],mobile:[...p],demo:[]},te={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`]],demo:[]},ne={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`]],demo:[[`@biomejs/biome`,`react.md: Biome is mandatory`],[`@webority/biome-config`,`react.md: every repo extends the shared Biome preset`]]},re=`5.3.3`,ie=[[`@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`]],ae=`@webority/biome-config/biome.jsonc`,oe=`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(`.`),y=e=>e.split(`\\`).join(`/`),b=(e,t)=>y(c(e,t))||`.`;function x(e){try{return JSON.parse(n(e,`utf8`))}catch{return null}}function S(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 C(e){let t=S(e,[`rev-parse`,`--show-toplevel`]);return t?t.trim():null}function w(e,t){let n=S(e,[`ls-files`,`-z`,`--`,...t.flatMap(e=>[e,`:(glob)**/${e}`])]);return n?[...new Set(n.split(`\0`).filter(Boolean))]:[]}function se(e){let t=e?.dependencies??{};return t[`react-native`]?`mobile`:t[`react-dom`]?e?.devDependencies?.[`react-native-web`]||t[`react-native-web`]?`demo`:`web`:null}var ce=e=>/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(e)||/^(?:workspace:\*|file:|link:)/.test(e),T=new Set([`@biomejs/biome`,`@webority/biome-config`]),E=e=>/^(?:workspace:|file:|link:)/.test(e);function le(e,t,n){let r=[],i={...e.dependencies,...e.devDependencies},a=Object.keys(i);for(let[e,n]of v[t])i[e]&&r.push([`banned:${e}`,`"${e}" is a dependency. ${n}.`]);for(let[e,n]of ee[t])for(let t of a.filter(t=>t.startsWith(e)))r.push([`banned:${t}`,`"${t}" is a dependency. ${n}.`]);for(let[e,n,a]of te[t])i[e]&&i[n]&&r.push([`banned:${e}`,`"${e}" is a dependency beside ${n}. ${a}.`]);for(let[e,a]of ne[t])(T.has(e)?n:i)[e]||r.push([`required:${e}`,`"${e}" is not a dependency. ${a}.`]);let o=t===`mobile`?a:a.filter(e=>e.startsWith(`@webority/`)||e===`bootstrap`);for(let e of o){let t=String(i[e]);ce(t)||r.push([`not-exact:${e}`,`"${e}": "${t}" is a range. Pin the version the lockfile holds, without ^ or ~.`])}i.bootstrap&&String(i.bootstrap).replace(/^[\^~]/,``)!==`5.3.3`&&r.push([`bootstrap-version`,`bootstrap is "${i.bootstrap}". bootstrap-ui.md: every surface pins ${re}.`]);for(let e of ie){let t=e.filter(e=>i[e]&&!E(String(i[e])));if(new Set(t.map(e=>String(i[e]).replace(/^[\^~]/,``))).size>1){let n=t.map(e=>`${e}@${i[e]}`).join(`, `);r.push([`lockstep:${e[0]}`,`${n} differ. These publish together at one version; move every pin in the set together.`])}}return r}function D(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 O(e,t){let r=e,i=null;for(;;){let e=D(r,t,[`biome.json`,`biome.jsonc`]);if(!e)return i;i=o(e);let a;try{a=d(n(e,`utf8`))}catch{return i}let s=o(i);if(a.root!==!1||i===t||s===i)return i;r=s}}function k(e,t,n){let r=O(e,t),i=r&&r!==e?x(s(r,`package.json`)):null;return i?{...i.dependencies,...i.devDependencies}:n}function A(t,r){let i=[],a=D(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=x(s(c,`package.json`))??x(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 j(e,t,r){let i=D(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 "${ae}". 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 ue(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(oe)}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=ue(t),i=[];for(let e of M(n)){let n=a(e).split(`.`)[0];if(r.has(n)){let r=y(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=>b(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 w(t,[`package-lock.json`,`yarn.lock`])){let t=y(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(w(t,[`package.json`]).map(e=>b(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=x(s(n,`package.json`)),o=se(a);if(!o)continue;let l=A(n,t).filter(([t])=>!i.some(n=>n.pkg===e&&n.id===t));c(e,[...le(a,o,k(n,t,{...a.dependencies,...a.devDependencies})),...l,...j(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=C(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 de(){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=de(),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)),fe=new Set(R.map(e=>c(U,l(process.cwd(),e)).split(`\\`).join(`/`)||`.`)),pe=e=>!R.length||fe.has(e.split(` `)[0]),Z=[...Y].filter(e=>!X.has(e)&&pe(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,6 @@
1
1
  {
2
2
  "name": "@webority/repo-checks",
3
- "version": "0.51.2",
3
+ "version": "0.52.0",
4
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
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -24,6 +24,7 @@
24
24
  "vite": "^8.0.12"
25
25
  },
26
26
  "scripts": {
27
- "build": "vite build"
27
+ "build": "vite build",
28
+ "test": "node --test \"test/*.test.js\""
28
29
  }
29
30
  }