@casomoltd/tooling 0.29.0 → 0.30.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
@@ -30,8 +30,8 @@ Shared linting, formatting, commit config, and CLI tools for Casomo Ltd's repos.
30
30
  | `build-report` | Compile a Typst client report to PDF with the house template |
31
31
  | `render-figures` | Pre-render a design spec's mermaid figures to static SVG |
32
32
  | `spec-check` | Check a design spec's anchors, contents, figures and numbering |
33
- | `check-private-refs` | In a public repo, refuse a private tracker link or a name listed in `check-private-refs.config.mjs` |
34
- | `check-personal-data` | Refuse an email, National Insurance number, UK mobile number, date of birth or locally denylisted name; per-repo allowances in `check-personal-data.config.mjs` |
33
+ | `check-private-refs` | In a public repo, refuse a private tracker link or a name listed under `privateRefs` in `casomo.config.mjs` |
34
+ | `check-personal-data` | Refuse an email, National Insurance number, UK mobile number, date of birth or locally denylisted name; per-repo allowances under `personalData` in `casomo.config.mjs` |
35
35
 
36
36
  ## Install
37
37
 
@@ -16,16 +16,18 @@
16
16
  *
17
17
  * Deliberately configurable, because this file ships in a public package and
18
18
  * cannot name anybody's domains or reference formats. A consuming repo adds
19
- * its own in `check-personal-data.config.mjs`:
19
+ * its own under `personalData` in its `casomo.config.mjs`:
20
20
  *
21
21
  * export default {
22
- * allowEmail: [/@example-org\.com$/i], // addresses that are not people
23
- * rules: [ // repo-specific identifiers
24
- * {name: "case reference", re: /\bCASE-\d{4,}\b/g,
25
- * allow: (m) => m === "CASE-0000"},
26
- * ],
27
- * denylist: ".personal-data-denylist", // the default; gitignore it
28
- * allow: [/\.test\.mjs$/], // paths exempted, with care
22
+ * personalData: {
23
+ * allowEmail: [/@example-org\.com$/i], // addresses that are not people
24
+ * rules: [ // repo-specific identifiers
25
+ * {name: "case reference", re: /\bCASE-\d{4,}\b/g,
26
+ * allow: (m) => m === "CASE-0000"},
27
+ * ],
28
+ * denylist: ".personal-data-denylist", // the default; gitignore it
29
+ * allow: [/\.test\.mjs$/], // paths exempted, with care
30
+ * },
29
31
  * };
30
32
  *
31
33
  * Usage:
@@ -35,7 +37,7 @@
35
37
  import {execFileSync} from "node:child_process";
36
38
  import {existsSync, readFileSync, realpathSync, statSync} from "node:fs";
37
39
  import {pathToFileURL} from "node:url";
38
- import {loadConfig} from "./utils.mjs";
40
+ import {CASOMO_CONFIG_KEYS, loadCasomoConfig} from "./utils.mjs";
39
41
 
40
42
  /** Addresses in every repo's own tooling that name no person. */
41
43
  const ALLOWED_EMAIL = [
@@ -217,9 +219,7 @@ export function scan(fileList, read, rules, names) {
217
219
  }
218
220
 
219
221
  async function main() {
220
- const mod = await loadConfig("check-personal-data.config.mjs");
221
- // `loadConfig` returns the module, so the config is its default export.
222
- const config = mod?.default ?? {};
222
+ const config = await loadCasomoConfig(CASOMO_CONFIG_KEYS.personalData);
223
223
  const denylist = config.denylist ?? ".personal-data-denylist";
224
224
  const rules = [...identityRules(config.allowEmail), ...(config.rules ?? [])];
225
225
  const names = denied(denylist);
@@ -21,21 +21,24 @@
21
21
  * a public package, so it cannot itself name anybody's private repos,
22
22
  * boards or hosts — that would be the disclosure it exists to prevent.
23
23
  * The defaults cover hosted trackers that are private by default; a
24
- * consuming repo adds its own names in `check-private-refs.config.mjs`:
24
+ * consuming repo adds its own names under `privateRefs` in its
25
+ * `casomo.config.mjs`:
25
26
  *
26
27
  * export default {
27
- * visibility: 'public', // optional; skips the gh lookup
28
- * forbidden: [ // strings or RegExp
29
- * {pattern: /internal\.example\.net/, why: 'internal hostname'},
30
- * ],
31
- * allow: [/CHANGELOG\.md$/], // paths exempted, with care
28
+ * privateRefs: {
29
+ * visibility: 'public', // optional; skips the gh lookup
30
+ * forbidden: [ // strings or RegExp
31
+ * {pattern: /internal\.example\.net/, why: 'internal hostname'},
32
+ * ],
33
+ * allow: [/CHANGELOG\.md$/], // paths exempted, with care
34
+ * },
32
35
  * };
33
36
  */
34
37
 
35
38
  import {execFileSync} from "node:child_process";
36
39
  import fs from "node:fs";
37
40
  import path from "node:path";
38
- import {loadConfig} from "./utils.mjs";
41
+ import {CASOMO_CONFIG_KEYS, loadCasomoConfig} from "./utils.mjs";
39
42
 
40
43
  /**
41
44
  * Hosted trackers whose links are private by default.
@@ -95,11 +98,7 @@ function toMatchers(entries) {
95
98
  }
96
99
 
97
100
  async function main() {
98
- const mod = await loadConfig("check-private-refs.config.mjs");
99
- // `loadConfig` returns the MODULE, so the config is its default
100
- // export. Reading the module object itself silently found no
101
- // patterns and reported a clean repo.
102
- const config = mod?.default ?? {};
101
+ const config = await loadCasomoConfig(CASOMO_CONFIG_KEYS.privateRefs);
103
102
  const visibility = repoVisibility(config.visibility);
104
103
 
105
104
  if (visibility === null) {
@@ -109,7 +108,7 @@ async function main() {
109
108
  console.warn(
110
109
  "check-private-refs: SKIPPED — could not determine repo "
111
110
  + "visibility (is `gh` installed and authenticated?). Set "
112
- + "`visibility` in check-private-refs.config.mjs to be explicit.",
111
+ + "`privateRefs.visibility` in casomo.config.mjs to be explicit.",
113
112
  );
114
113
  return;
115
114
  }
package/bin/utils.mjs CHANGED
@@ -39,13 +39,55 @@ export function openPath(target) {
39
39
  }
40
40
 
41
41
  /**
42
- * Load an optional per-repo config file from cwd.
43
- *
44
- * Each CLI tool looks for `<tool>.config.mjs` (e.g.
45
- * `readability.config.mjs`) in the consumer's project root.
42
+ * Load an optional per-repo config file from `dir` (the working directory by
43
+ * default): the module, or `{}` where there is no file. The file-level loader
44
+ * under `loadCasomoConfig`, which is where a gate's settings belong; a tool
45
+ * still reading its own `<tool>.config.mjs` (`readability`) calls this.
46
46
  */
47
- export async function loadConfig(filename) {
48
- const cfgPath = path.join(process.cwd(), filename);
47
+ export async function loadConfig(filename, dir = process.cwd()) {
48
+ const cfgPath = path.join(dir, filename);
49
49
  if (!fs.existsSync(cfgPath)) return {};
50
50
  return import(pathToFileURL(cfgPath).href);
51
51
  }
52
+
53
+ /** The keys a repo's `casomo.config.mjs` may hold, one per gate. */
54
+ export const CASOMO_CONFIG_KEYS = {
55
+ personalData: "personalData",
56
+ privateRefs: "privateRefs",
57
+ };
58
+
59
+ /** The files each gate read before `casomo.config.mjs`, by the key their
60
+ * settings now go under. A repo still holding one would have its settings
61
+ * ignored, so their presence fails the run. */
62
+ const RETIRED_CONFIGS = {
63
+ "check-personal-data.config.mjs": CASOMO_CONFIG_KEYS.personalData,
64
+ "check-private-refs.config.mjs": CASOMO_CONFIG_KEYS.privateRefs,
65
+ };
66
+
67
+ /**
68
+ * One gate's settings from the repo's `casomo.config.mjs`, by the key it
69
+ * reads: every gate a repo configures is configured in that one file, so a
70
+ * repo carries one config rather than one per tool. An absent file or key
71
+ * reads as no settings; a retired per-tool file, or a key no gate reads, is
72
+ * refused, because either would otherwise read as no settings too and a
73
+ * gate would pass on less than it was given.
74
+ */
75
+ export async function loadCasomoConfig(key, dir = process.cwd()) {
76
+ for (const [file, moveTo] of Object.entries(RETIRED_CONFIGS)) {
77
+ if (fs.existsSync(path.join(dir, file))) {
78
+ throw new Error(`${file} is no longer read: move its settings under `
79
+ + `\`${moveTo}\` in casomo.config.mjs and delete it`);
80
+ }
81
+ }
82
+ const mod = await loadConfig("casomo.config.mjs", dir);
83
+ // The settings are the module's default export; the module object itself
84
+ // holds no gate key and would read as no settings.
85
+ const config = mod?.default ?? {};
86
+ const known = Object.values(CASOMO_CONFIG_KEYS);
87
+ const unknown = Object.keys(config).filter((k) => !known.includes(k));
88
+ if (unknown.length) {
89
+ throw new Error(`casomo.config.mjs holds ${unknown.join(", ")}, which no `
90
+ + `gate reads; the keys are ${known.join(", ")}`);
91
+ }
92
+ return config[key] ?? {};
93
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@casomoltd/tooling",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Casomo's shared dev config: ESLint, Prettier, commitlint, knip & jscpd presets and dev CLIs (git hooks, readability, screenshots, design specs)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -23,7 +23,7 @@
23
23
  "scripts": {
24
24
  "check": "node bin/check-gates.mjs && node bin/verify-pack.mjs && npm run check:private-refs && npm run check:personal-data && npm run typecheck && npm run test && npm run lint:md",
25
25
  "typecheck": "tsc --noEmit -p hooks/tsconfig.json",
26
- "test": "node hooks/test.mts && node bin/check-gates.test.mjs && node bin/check-personal-data.test.mjs && node commitlint/index.test.mjs",
26
+ "test": "node hooks/test.mts && node bin/check-gates.test.mjs && node bin/check-personal-data.test.mjs && node bin/casomo-config.test.mjs && node commitlint/index.test.mjs",
27
27
  "lint:md": "remark --frail --quiet --no-stdout --rc-path bin/skills-lint.config.mjs skills agents docs",
28
28
  "build": "echo 'no-op: source-only repo, nothing to build'",
29
29
  "prepare": "husky || true",