@zgeoff/chex 0.2.1 → 0.2.3

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
@@ -82,8 +82,8 @@ skipped file back:
82
82
 
83
83
  `chex init` writes the `extends` line and a
84
84
  [`chex-baseline` job](../../docs/architecture/pre-push-baseline.md#a-checkout-without-the-baseline)
85
- into `lefthook.yml`, and adds a `check:docs` script of `chex prose length links skill` when the repo
86
- has none, and changes nothing on a second run. `chex ci-plan` plans a run of `bun-checks`.
85
+ into `lefthook.yml`, and adds a `check:docs` script of `chex prose length links readmes skill` when
86
+ the repo has none, and changes nothing on a second run. `chex ci-plan` plans a run of `bun-checks`.
87
87
 
88
88
  ## The pre-push baseline
89
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zgeoff/chex",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Run each shared docs and repo check as one subcommand, with the lefthook pre-push baseline that calls them",
5
5
  "keywords": [
6
6
  "checks",
@@ -2,7 +2,7 @@ import { isRecord } from '@zgeoff/std';
2
2
 
3
3
  // The baseline's jobs run chex, the related tests through rig, and each job under diet's memory cap.
4
4
  export const baselinePackages = ['@zgeoff/chex', '@zgeoff/rig', '@zgeoff/diet'] as const;
5
- export const docsScript = 'chex prose length links skill';
5
+ export const docsScript = 'chex prose length links readmes skill';
6
6
 
7
7
  export type PackageOutcome =
8
8
  | { readonly kind: 'missing'; readonly packages: readonly string[] }
@@ -8,16 +8,31 @@ interface UnneededOverridesInput {
8
8
  }
9
9
 
10
10
  export function collectUnneededOverrides(input: UnneededOverridesInput): DependencyGateFinding[] {
11
- return Object.entries(input.overrides).flatMap(([name, pin]) => {
11
+ return Object.entries(input.overrides).flatMap(([key, pin]) => {
12
+ const target = parseOverrideKey(key);
13
+
14
+ if (target === null) {
15
+ return [
16
+ {
17
+ path: 'package.json',
18
+ entry: key,
19
+ problem: 'bun ignores an override key with more than one parent level',
20
+ },
21
+ ];
22
+ }
23
+
12
24
  const ranges = input.dependents
13
- .filter((dependent) => dependent.name === name)
14
- .map((dependent) => resolveCatalogRange(input, name, dependent.range));
25
+ .filter(
26
+ (dependent) =>
27
+ dependent.name === target.name && isDependentOfParent(dependent, target.parent),
28
+ )
29
+ .map((dependent) => resolveCatalogRange(input, target.name, dependent.range));
15
30
 
16
31
  if (ranges.length === 0) {
17
32
  return [
18
33
  {
19
34
  path: 'package.json',
20
- entry: name,
35
+ entry: key,
21
36
  problem: 'no package in bun.lock depends on the overridden package',
22
37
  },
23
38
  ];
@@ -27,7 +42,7 @@ export function collectUnneededOverrides(input: UnneededOverridesInput): Depende
27
42
  (range) => !isSemverRange(range) || !Bun.semver.satisfies(pin, range),
28
43
  );
29
44
 
30
- const isPinTheCatalogVersion = input.catalog[name] === pin;
45
+ const isPinTheCatalogVersion = input.catalog[target.name] === pin;
31
46
 
32
47
  if (isPinOutsideARange || isPinTheCatalogVersion) {
33
48
  return [];
@@ -36,13 +51,74 @@ export function collectUnneededOverrides(input: UnneededOverridesInput): Depende
36
51
  return [
37
52
  {
38
53
  path: 'package.json',
39
- entry: name,
54
+ entry: key,
40
55
  problem: `every range in bun.lock already admits ${pin}, so the override changes nothing`,
41
56
  },
42
57
  ];
43
58
  });
44
59
  }
45
60
 
61
+ interface PackageSpec {
62
+ readonly name: string;
63
+ readonly range: string | null;
64
+ }
65
+
66
+ interface OverrideTarget {
67
+ readonly name: string;
68
+ readonly parent: PackageSpec | null;
69
+ }
70
+
71
+ // Bun splits a key at the first `>` that follows no space, `|`, `@` or `:`, so the `>` of a range
72
+ // such as `left-pad@>=1` stays in the range.
73
+ const parentSeparatorPattern = /[^ |@:]>/u;
74
+
75
+ // A child range, as in `terser@4`, narrows the dependents bun applies the key to. The check keeps
76
+ // every dependent of the name, so a narrower key can only look more needed, never less.
77
+ function parseOverrideKey(key: string): OverrideTarget | null {
78
+ const separator = parentSeparatorPattern.exec(key);
79
+
80
+ if (separator === null) {
81
+ return { name: parsePackageSpec(key).name, parent: null };
82
+ }
83
+
84
+ const separatorIndex = separator.index + 1;
85
+ const child = key.slice(separatorIndex + 1).trim();
86
+
87
+ if (parentSeparatorPattern.test(child)) {
88
+ return null;
89
+ }
90
+
91
+ return {
92
+ name: parsePackageSpec(child).name,
93
+ parent: parsePackageSpec(key.slice(0, separatorIndex).trim()),
94
+ };
95
+ }
96
+
97
+ // A scoped name starts with `@`, so the range starts at the next `@`.
98
+ function parsePackageSpec(spec: string): PackageSpec {
99
+ const rangeIndex = spec.indexOf('@', 1);
100
+
101
+ if (rangeIndex === -1) {
102
+ return { name: spec, range: null };
103
+ }
104
+
105
+ return { name: spec.slice(0, rangeIndex), range: spec.slice(rangeIndex + 1) };
106
+ }
107
+
108
+ function isDependentOfParent(dependent: LockfileDependent, parent: PackageSpec | null): boolean {
109
+ if (parent === null) {
110
+ return true;
111
+ }
112
+
113
+ const id = parsePackageSpec(dependent.dependent);
114
+
115
+ if (id.name !== parent.name) {
116
+ return false;
117
+ }
118
+
119
+ return parent.range === null || id.range === null || Bun.semver.satisfies(id.range, parent.range);
120
+ }
121
+
46
122
  // bun.lock keeps a workspace's `catalog:` reference as it is written in its package.json.
47
123
  function resolveCatalogRange(input: UnneededOverridesInput, name: string, range: string): string {
48
124
  if (!range.startsWith('catalog:')) {
@@ -32,11 +32,24 @@ const workspaceFields = [
32
32
  ] as const;
33
33
 
34
34
  function collectWorkspaceDependents(path: string, workspace: unknown): LockfileDependent[] {
35
- const dependent = path === '' ? 'the root workspace' : path;
35
+ const dependent = buildWorkspaceID(path, workspace);
36
36
 
37
37
  return collectDeclaredRanges(dependent, workspace, workspaceFields);
38
38
  }
39
39
 
40
+ // A nested override key names its parent by package name, whether the parent is a workspace or a
41
+ // registry package, so a workspace with a name takes it in place of its path.
42
+ function buildWorkspaceID(path: string, workspace: unknown): string {
43
+ const name = isRecord(workspace) ? workspace['name'] : undefined;
44
+ const version = isRecord(workspace) ? workspace['version'] : undefined;
45
+
46
+ if (typeof name !== 'string') {
47
+ return path === '' ? 'the root workspace' : path;
48
+ }
49
+
50
+ return typeof version === 'string' ? `${name}@${version}` : name;
51
+ }
52
+
40
53
  function collectDeclaredRanges(
41
54
  dependent: string,
42
55
  metadata: unknown,
@@ -21,13 +21,55 @@ export function parseRootManifest(text: string): RootManifest {
21
21
  const audit = isRecord(scripts) ? scripts['audit'] : undefined;
22
22
 
23
23
  return {
24
- overrides: parseVersions(manifest['overrides'], 'overrides'),
24
+ overrides: parseOverrides(manifest['overrides']),
25
25
  catalog: parseVersions(catalog, 'workspaces.catalog'),
26
26
  catalogs: parseNamedCatalogs(catalogs),
27
27
  auditScript: typeof audit === 'string' ? audit : '',
28
28
  };
29
29
  }
30
30
 
31
+ function parseOverrides(value: unknown): Record<string, string> {
32
+ if (value === undefined) {
33
+ return {};
34
+ }
35
+
36
+ if (!isRecord(value)) {
37
+ throw new TypeError('package.json sets overrides to a value that is not an object');
38
+ }
39
+
40
+ return Object.fromEntries(collectOverrideEntries('', value));
41
+ }
42
+
43
+ // Bun reads `{ "a": { ".": "1.0.0", "b": "2.0.0" } }` as the keys `a` and `a>b`.
44
+ function collectOverrideEntries(
45
+ parentKey: string,
46
+ overrides: Record<string, unknown>,
47
+ ): [string, string][] {
48
+ return Object.entries(overrides).flatMap(([name, value]): [string, string][] => {
49
+ const key = buildOverrideKey(parentKey, name);
50
+
51
+ if (typeof value === 'string') {
52
+ return [[key, value]];
53
+ }
54
+
55
+ if (isRecord(value)) {
56
+ return collectOverrideEntries(key, value);
57
+ }
58
+
59
+ throw new TypeError(
60
+ `package.json sets the overrides entry for ${key} to a value that is not a version`,
61
+ );
62
+ });
63
+ }
64
+
65
+ function buildOverrideKey(parentKey: string, name: string): string {
66
+ if (name === '.') {
67
+ return parentKey;
68
+ }
69
+
70
+ return parentKey === '' ? name : `${parentKey}>${name}`;
71
+ }
72
+
31
73
  function parseVersions(value: unknown, field: string): Record<string, string> {
32
74
  if (value === undefined) {
33
75
  return {};