@webority/repo-checks 0.76.0 → 0.77.1
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 +17 -0
- package/dist/repo-checks.js +57 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ Anything else (tooling roots, libraries) gets only the lockfile check.
|
|
|
25
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
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
27
|
| `catalogue-copy:<file>` | A web file under `src/components/` named like a `@webority/ui-react` export |
|
|
28
|
+
| `hook:drift` | The root `.husky/pre-commit` is missing or differs from the shared hook this package ships (CRLF is ignored) |
|
|
28
29
|
|
|
29
30
|
## Existing findings: a baseline that only shrinks
|
|
30
31
|
|
|
@@ -41,6 +42,22 @@ baseline: `--baseline-init` refuses when the file exists.
|
|
|
41
42
|
|
|
42
43
|
`--json` prints `{ root, new, known, stale }` for tools.
|
|
43
44
|
|
|
45
|
+
## The shared pre-commit hook
|
|
46
|
+
|
|
47
|
+
Every repository runs the pre-commit hook this package ships. Adopt it once, from the git root:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pnpm exec webority-repo-checks --sync-hook
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
That writes `.husky/pre-commit`, creating `.husky` when it is missing, with LF line endings. A
|
|
54
|
+
multi-app repository installs it once, at the root. husky stays the installer, through the
|
|
55
|
+
`prepare` script. Remove lint-staged (`pnpm remove lint-staged`, and delete its config key): the
|
|
56
|
+
shared hook runs Biome itself. Put repo-only steps in `.husky/pre-commit.local`; the shared hook
|
|
57
|
+
runs that file after Biome, and `--sync-hook` never touches it.
|
|
58
|
+
|
|
59
|
+
`hook:drift` keeps the root copy equal to the shipped one.
|
|
60
|
+
|
|
44
61
|
## Versioning
|
|
45
62
|
|
|
46
63
|
Published from the `webority-ui` monorepo in lockstep with the other `@webority/*` packages. Pin
|
package/dist/repo-checks.js
CHANGED
|
@@ -1,4 +1,59 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{
|
|
2
|
+
import{chmodSync as e,existsSync as t,mkdirSync as n,readFileSync as r,readdirSync as i,writeFileSync as a}from"node:fs";import{basename as o,dirname as s,join as c,relative as l,resolve as u}from"node:path";import{spawnSync as ee}from"node:child_process";var d=`# Fast only. The expensive verification is \`corepack pnpm check\`, run by hand before pushing
|
|
3
|
+
# (CLAUDE.md -> Before pushing). A slow pre-commit gets bypassed within a day, and a hook people bypass is worse than none because
|
|
4
|
+
# it manufactures confidence without providing it.
|
|
5
|
+
#
|
|
6
|
+
# It WRITES rather than merely reporting. A rejected commit leaves the index exactly as it was
|
|
7
|
+
# (that is git, not the hook), so the files you just staged are still staged. Stage a second,
|
|
8
|
+
# unrelated set of paths for the retry and the commit silently takes BOTH.
|
|
9
|
+
#
|
|
10
|
+
# A formatting complaint is therefore fixed and re-staged instead of thrown back, and only a
|
|
11
|
+
# genuine violation, one Biome cannot safely fix, stops the commit. That is also what react.md
|
|
12
|
+
# prescribes (\`biome check --write --staged\`).
|
|
13
|
+
#
|
|
14
|
+
# EXCEPT when something is PARTIALLY staged (a file with both staged and unstaged hunks).
|
|
15
|
+
# There, re-staging the written file would sweep in the hunks you deliberately held back,
|
|
16
|
+
# turning a formatting convenience into a commit of unfinished work. That case keeps the
|
|
17
|
+
# read-only behaviour and explains the trap instead of springing it.
|
|
18
|
+
|
|
19
|
+
staged=$(git diff --cached --name-only --diff-filter=ACMR)
|
|
20
|
+
partial=$(printf '%s\\n' "$staged" "$(git diff --name-only)" | sort | uniq -d)
|
|
21
|
+
|
|
22
|
+
# The binary directly, not \`pnpm exec\`: pnpm checks installed dependencies against the manifests
|
|
23
|
+
# before every exec, so a commit staging a package.json change ran a full install in this hook.
|
|
24
|
+
biome="$(git rev-parse --show-toplevel)/node_modules/.bin/biome"
|
|
25
|
+
if [ ! -x "$biome" ]; then
|
|
26
|
+
echo "pre-commit: Biome is not installed. Install the dependencies at the repository root, then commit again."
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
explain() {
|
|
31
|
+
echo ""
|
|
32
|
+
echo "pre-commit: biome rejected the staged content — nothing was committed."
|
|
33
|
+
echo ""
|
|
34
|
+
echo " Your files are STILL STAGED. If you now 'git add' other paths for the retry,"
|
|
35
|
+
echo " this commit will take those too. Re-stage only what you meant to commit."
|
|
36
|
+
echo ""
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if [ -n "$partial" ]; then
|
|
40
|
+
echo "pre-commit: partially-staged file(s) — checking without writing, so held-back hunks stay held back:"
|
|
41
|
+
printf ' %s\\n' $partial
|
|
42
|
+
"$biome" check --staged --no-errors-on-unmatched || { explain; exit 1; }
|
|
43
|
+
else
|
|
44
|
+
"$biome" check --write --staged --no-errors-on-unmatched || { explain; exit 1; }
|
|
45
|
+
# Re-stage ONLY paths already in the index, so a sibling session's in-progress files
|
|
46
|
+
# can never be swept in (concurrency.md). \`git add -A\` here would be exactly that bug.
|
|
47
|
+
git update-index --again
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# A repo's own fast checks live in .husky/pre-commit.local, so this file stays identical in every
|
|
51
|
+
# repo. It runs after Biome, on the same staged set, and its failure stops the commit.
|
|
52
|
+
local_checks="$(git rev-parse --show-toplevel)/.husky/pre-commit.local"
|
|
53
|
+
if [ -f "$local_checks" ]; then
|
|
54
|
+
sh "$local_checks" || exit 1
|
|
55
|
+
fi
|
|
56
|
+
`;function f(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
57
|
`;)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`]],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 n of w(t,[`package-lock.json`,`yarn.lock`])){if(!e(s(t,n)))continue;let i=y(o(n))||`.`;r&&!r.has(i)||c(i,[[`pnpm:${a(n)}`,`${a(n)} 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{};
|
|
58
|
+
`}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 p=[[`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`]],m=[[`@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`]],h=`bootstrap-ui.md: hard interactive components come from @webority/ui-react`,g=`react.md: Bootstrap 5.3.3 is the only UI framework`,_=`react.md: dates format through Intl and the web-core formatters`,v=`react-native.md: retired; Intl and the mobile-core formatters`,y={web:[...p,[`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`,h],[`react-day-picker`,h],[`react-aria`,h],[`react-aria-components`,h],[`@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`,g],[`antd`,g],[`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`,_],[`moment-timezone`,_],[`dayjs`,_],[`date-fns`,_]],mobile:[...p,[`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`,v],[`moment-timezone`,v],[`dayjs`,v],[`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:[]},b={web:[...m,[`@mui/`,g],[`@chakra-ui/`,g],[`@emotion/`,`react.md: no CSS-in-JS`],[`@radix-ui/`,h],[`@react-aria/`,h]],mobile:[...m],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`,x=`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(`.`),S=e=>e.split(`\\`).join(`/`),C=(e,t)=>S(l(e,t))||`.`;function w(e){try{return JSON.parse(r(e,`utf8`))}catch{return null}}function T(e,t){let n=ee(`git`,t,{cwd:e,encoding:`utf8`,windowsHide:!0,maxBuffer:64*1024*1024});return n.status===0?n.stdout:null}function oe(e){let t=T(e,[`rev-parse`,`--show-toplevel`]);return t?t.trim():null}function E(e,t){let n=T(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 D=e=>/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(e)||/^(?:workspace:\*|file:|link:)/.test(e),O=new Set([`@biomejs/biome`,`@webority/biome-config`]),ce=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 y[t])i[e]&&r.push([`banned:${e}`,`"${e}" is a dependency. ${n}.`]);for(let[e,n]of b[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])(O.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]);D(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]&&!ce(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 k(e,n,r){let i=e;for(;;){for(let e of r)if(t(c(i,e)))return c(i,e);if(i===n)return null;let e=s(i);if(e===i)return null;i=e}}function A(e,t){let n=e,i=null;for(;;){let e=k(n,t,[`biome.json`,`biome.jsonc`]);if(!e)return i;i=s(e);let a;try{a=f(r(e,`utf8`))}catch{return i}let o=s(i);if(a.root!==!1||i===t||o===i)return i;n=o}}function ue(e,t,n){let r=A(e,t),i=r&&r!==e?w(c(r,`package.json`)):null;return i?{...i.dependencies,...i.devDependencies}:n}function de(e,n){let i=[],a=k(e,n,[`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 o=a?s(a):e,l=w(c(o,`package.json`))??w(c(e,`package.json`));/^pnpm@/.test(l?.packageManager??``)||i.push([`pnpm:package-manager`,`package.json has no "packageManager": "pnpm@<version>". react.md: Package manager.`]);let u=c(o,`pnpm-workspace.yaml`);if(!t(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=r(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`])t(c(e,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 fe(e,t,n){let i=k(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 a;try{a=f(r(i,`utf8`))}catch{return[[`biome:unreadable`,`${o(i)} does not parse.`]]}let s=[];return[].concat(a.extends??[]).some(e=>e===`@webority/biome-config/biome.jsonc`||String(e).endsWith(`biome-config/biome.jsonc`))||s.push([`biome:extends`,`${o(i)} does not extend "${ae}". react.md: every repo extends the shared preset.`]),a.formatter?.lineEnding&&s.push([`biome:line-ending`,`${o(i)} sets formatter.lineEnding. react.md: inherit it from the shared preset, never set it.`]),a.linter?.rules&&Object.keys(a.linter.rules).length>0&&s.push([`biome:root-rules`,`${o(i)} sets linter.rules at the root. react.md: never a rule's severity at the root; a per-path exception goes in overrides.`]),n===`mobile`&&!(a.javascript?.globals??[]).includes(`__DEV__`)&&s.push([`biome:dev-global`,`${o(i)} lacks "__DEV__" in javascript.globals. react-native.md: Debugging and agent access.`]),s}function pe(e){let t=c(e,`node_modules`,`@webority`,`ui-react`);try{let e=JSON.parse(r(c(t,`package.json`),`utf8`)),n=r(c(t,typeof e.module==`string`?e.module:typeof e.main==`string`?e.main:`dist/index.js`),`utf8`),i=new Set([...n.matchAll(/App[A-Z][A-Za-z0-9]*/g)].map(e=>e[0]));if(i.size>0)return i}catch{}return new Set(x)}function j(e){return i(e,{withFileTypes:!0}).flatMap(t=>t.isDirectory()?j(c(e,t.name)):[c(e,t.name)])}function me(e){let n=c(e,`src`,`components`);if(!t(n))return[];let r=pe(e),i=[];for(let t of j(n)){let n=o(t).split(`.`)[0];if(r.has(n)){let r=S(l(e,t));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}var M=`.husky/pre-commit`;function N(e){let n=c(e,M),i=t(n)?r(n,`utf8`).replace(/\r\n/g,`
|
|
59
|
+
`):null;return i===d?[]:[[`hook:drift`,`${M} ${i===null?`is missing`:`differs from the shared hook`}. Run pnpm exec webority-repo-checks --sync-hook from the git root; repo-only steps go in .husky/pre-commit.local.`]]}function P(e,n=null){let r=n?new Set(n.map(t=>C(e,t))):null,i=[],a=(e,t)=>{for(let[n,r]of t)i.push({pkg:e,id:n,message:r})};(!r||r.has(`.`))&&a(`.`,N(e));for(let n of E(e,[`package-lock.json`,`yarn.lock`])){if(!t(c(e,n)))continue;let i=S(s(n))||`.`;r&&!r.has(i)||a(i,[[`pnpm:${o(n)}`,`${o(n)} is committed. react.md: pnpm only; seed pnpm-lock.yaml with pnpm import, then delete it.`]])}let l=new Set(E(e,[`package.json`]).map(t=>C(e,c(e,s(t)))));for(let n of r??[])t(c(e,n,`package.json`))&&l.add(n);for(let t of l){if(r&&!r.has(t))continue;let n=c(e,t),o=w(c(n,`package.json`)),s=se(o);if(!s)continue;let l=de(n,e).filter(([e])=>!i.some(n=>n.pkg===t&&n.id===e));a(t,[...le(o,s,ue(n,e,{...o.dependencies,...o.devDependencies})),...l,...fe(n,e,s),...s===`web`?me(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=L.has(`--sync-hook`),B=e=>process.stdout.write(`${e}\n`),V=e=>process.stderr.write(`${e}\n`),he=new Set([`--json`,`--baseline-init`,`--baseline-shrink`,`--sync-hook`]),H=[...L].filter(e=>!he.has(e));(H.length||L.has(`--baseline-init`)&&L.has(`--baseline-shrink`)||z&&(L.size>1||R.length))&&(V(`repo-checks: unknown or conflicting options ${H.join(` `)}`.trim()),V(`usage: webority-repo-checks [--json] [--baseline-init | --baseline-shrink] [app folder ...]`),V(` webority-repo-checks --sync-hook`),process.exit(2));var U=oe(process.cwd());if(U||(V(`repo-checks: not inside a git repository.`),process.exit(2)),z){let t=c(U,M);n(s(t),{recursive:!0}),a(t,d),e(t,493),B(`repo-checks: wrote ${t}`),process.exit(0)}var W=P(U,R.length?R.map(e=>u(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=c(U,F);function ge(){if(!t(K))return null;try{return JSON.parse(r(K,`utf8`))}catch{V(`repo-checks: ${F} does not parse.`),process.exit(2)}}function q(e){let t={};for(let[n,r]of e.map(e=>e.split(` `)).sort())(t[n]??=[]).push(r);n(s(K),{recursive:!0}),a(K,`${JSON.stringify(t,null,4)}\n`)}var J=ge(),Y=new Set(Object.entries(J??{}).flatMap(([e,t])=>t.map(t=>`${e}\t${t}`)));L.has(`--baseline-init`)&&(J&&(V(`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)),B(`repo-checks: recorded ${W.length} existing finding(s) in ${F}.`),process.exit(0));var X=new Set(W.map(G)),_e=new Set(R.map(e=>l(U,u(process.cwd(),e)).split(`\\`).join(`/`)||`.`)),ve=e=>!R.length||_e.has(e.split(` `)[0]),Z=[...Y].filter(e=>!X.has(e)&&ve(e));L.has(`--baseline-shrink`)&&(J||(V(`repo-checks: no ${F} to shrink.`),process.exit(2)),R.length&&(V(`repo-checks: --baseline-shrink reads the whole repository; run it without folder arguments.`),process.exit(2)),q([...Y].filter(e=>X.has(e))),B(`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`)&&(B(JSON.stringify({root:U,new:Q,known:$,stale:Z.map(e=>e.split(` `))},null,2)),process.exit(+!!Q.length)),Q.length){V(`repo-checks: ${Q.length} new finding(s)`);let e=null;for(let t of Q)t.pkg!==e&&(e=t.pkg,V(` ${e}`)),V(` - ${t.id}: ${t.message}`)}else B(`repo-checks: no new findings.`);if($.length&&B(`repo-checks: ${$.length} known finding(s) recorded in ${F}.`),Z.length){B(`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)B(` ${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.
|
|
3
|
+
"version": "0.77.1",
|
|
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",
|