@shivaedev/quality 0.32.0 → 0.32.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +1 -1
  3. package/dist/hooks/forwarding.d.ts +7 -0
  4. package/dist/hooks/forwarding.d.ts.map +1 -0
  5. package/dist/hooks/forwarding.js +94 -0
  6. package/dist/hooks/forwarding.js.map +1 -0
  7. package/dist/hooks/install.d.ts.map +1 -1
  8. package/dist/hooks/install.js +2 -1
  9. package/dist/hooks/install.js.map +1 -1
  10. package/dist/hooks/shim.d.ts +1 -0
  11. package/dist/hooks/shim.d.ts.map +1 -1
  12. package/dist/hooks/shim.js +3 -1
  13. package/dist/hooks/shim.js.map +1 -1
  14. package/dist/hooks/status.d.ts +1 -1
  15. package/dist/hooks/status.d.ts.map +1 -1
  16. package/dist/hooks/status.js +8 -6
  17. package/dist/hooks/status.js.map +1 -1
  18. package/dist/hooks/uninstall.d.ts.map +1 -1
  19. package/dist/hooks/uninstall.js +3 -1
  20. package/dist/hooks/uninstall.js.map +1 -1
  21. package/dist/imports/packageExports.d.ts +4 -0
  22. package/dist/imports/packageExports.d.ts.map +1 -0
  23. package/dist/imports/packageExports.js +48 -0
  24. package/dist/imports/packageExports.js.map +1 -0
  25. package/dist/imports/publicModules.d.ts +4 -0
  26. package/dist/imports/publicModules.d.ts.map +1 -0
  27. package/dist/imports/publicModules.js +21 -0
  28. package/dist/imports/publicModules.js.map +1 -0
  29. package/dist/rules/imports/bundleInputs.d.ts.map +1 -1
  30. package/dist/rules/imports/bundleInputs.js +4 -50
  31. package/dist/rules/imports/bundleInputs.js.map +1 -1
  32. package/dist/rules/stories/supportImports.d.ts.map +1 -1
  33. package/dist/rules/stories/supportImports.js +9 -3
  34. package/dist/rules/stories/supportImports.js.map +1 -1
  35. package/docs/debt-and-hooks.md +3 -3
  36. package/package.json +3 -3
  37. package/src/hooks/forwarding.ts +99 -0
  38. package/src/hooks/install.ts +3 -1
  39. package/src/hooks/shim.ts +3 -1
  40. package/src/hooks/status.ts +10 -6
  41. package/src/hooks/uninstall.ts +3 -1
  42. package/src/imports/packageExports.ts +55 -0
  43. package/src/imports/publicModules.ts +27 -0
  44. package/src/rules/imports/bundleInputs.ts +4 -59
  45. package/src/rules/stories/supportImports.ts +10 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.32.1 - 2026-10-21
4
+
5
+ ### Changed
6
+
7
+ - Let story kits import any workspace package's exported modules through package names or aliases without listing them in `tests/support-imports.options.modules`. Keep the allowlist for additional internal modules; packages without exports retain their existing boundary.
8
+
9
+ ### Fixed
10
+
11
+ - Keep all global Git hooks running after `quality hooks install`, including hooks added later, and prevent recursive pre-commit chaining. Report installations that hide global hooks with a reinstall repair hint; uninstall leaves global hooks in effect.
12
+
3
13
  ## 0.32.0 - 2026-10-19
4
14
 
5
15
  ### Changed
package/README.md CHANGED
@@ -140,7 +140,7 @@ Quality's `tests/story-setup` rule reports top-level test functions named with t
140
140
 
141
141
  Specs keep expectations visible. `tests/spec-helpers` moves all module-level helper functions out of specs and into the kit; `tests/no-expect-in-support` moves `expect` calls into the spec. `tests/named-values` rejects numeric literals passed to `toBe` or `toHaveLength` in specs: assert the observed items or give the value a domain name. `tests/no-limit-imports` rejects importing `MAX_*` constants from a `limits` module into specs, so a story proves behavior instead of copying an implementation cap.
142
142
 
143
- `tests/support-imports` checks resolved repository imports from `test-support/` against `options.modules`, an allowlist of repository-relative module paths or glob patterns. Imports within test-support and imports of external packages remain available for the harness. Name only the public surface the kit drives, such as the host, schemas and queue definitions. The default empty list allows no production repository modules; aliases, dynamic imports and re-exports resolve through the same import graph as the other import rules.
143
+ `tests/support-imports` lets a kit drive any workspace package's exported modules, including its own package's exports through package names or `#` aliases. Export blocks keep internal modules closed. `options.modules` opens additional repository-relative module paths or glob patterns, such as an app's host, schemas and queue definitions. Packages without exports still need that allowlist. Imports within test-support and imports of external packages remain available for the harness; aliases, dynamic imports and re-exports resolve through the same import graph as the other import rules.
144
144
 
145
145
  `boundaries/queue-sql` rejects SQL mentioning the private `pgboss.` schema. Apps use their queue definitions and let workers process jobs; there is no queue-purge API by design. `boundaries/auth-sql` rejects SQL accessing Better Auth's `user`, `account`, `session` and `verification` tables. Apps use platform's auth API; `options.owners` names platform's own auth implementation and its migrations. These rules inspect SQL templates, literal query calls and query variables, and inventoried `.sql` files. They do not evaluate arbitrary runtime SQL construction.
146
146
 
@@ -0,0 +1,7 @@
1
+ import { Effect, FileSystem } from "effect";
2
+ import { SetupFailure } from "#failure.ts";
3
+ export declare function forwardingPath(file: string): string;
4
+ export declare const globalHooksPath: (cwd: string) => Effect.Effect<string, SetupFailure, import("effect/process/ChildProcessSpawner").ChildProcessSpawner>;
5
+ export declare const installForwarding: (file: string) => Effect.Effect<string, import("#inventory/filesystem.ts").FilesystemFailure | import("effect/PlatformError").PlatformError | SetupFailure, FileSystem.FileSystem>;
6
+ export declare const removeForwarding: (file: string) => Effect.Effect<void, import("#inventory/filesystem.ts").FilesystemFailure | import("effect/PlatformError").PlatformError, FileSystem.FileSystem>;
7
+ //# sourceMappingURL=forwarding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forwarding.d.ts","sourceRoot":"","sources":["../../src/hooks/forwarding.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAqC3C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,eAAO,MAAM,eAAe,wHAM1B,CAAC;AAoBH,eAAO,MAAM,iBAAiB,oLAc5B,CAAC;AAEH,eAAO,MAAM,gBAAgB,mKAa3B,CAAC"}
@@ -0,0 +1,94 @@
1
+ import { dirname, join } from "node:path";
2
+ import { Effect, FileSystem } from "effect";
3
+ import { SetupFailure } from "#failure.ts";
4
+ import { git } from "#git/command.ts";
5
+ import { readOptionalText } from "#inventory/filesystem.ts";
6
+ import { shellWords } from "./command.js";
7
+ import { isHookShim, OWNER } from "./shim.js";
8
+ const HOOKS = [
9
+ "applypatch-msg",
10
+ "pre-applypatch",
11
+ "post-applypatch",
12
+ "pre-commit",
13
+ "pre-merge-commit",
14
+ "prepare-commit-msg",
15
+ "commit-msg",
16
+ "post-commit",
17
+ "pre-rebase",
18
+ "post-checkout",
19
+ "post-merge",
20
+ "pre-push",
21
+ "pre-receive",
22
+ "update",
23
+ "proc-receive",
24
+ "post-receive",
25
+ "post-update",
26
+ "reference-transaction",
27
+ "push-to-checkout",
28
+ "pre-auto-gc",
29
+ "post-rewrite",
30
+ "sendemail-validate",
31
+ "fsmonitor-watchman",
32
+ "p4-changelist",
33
+ "p4-prepare-changelist",
34
+ "p4-post-changelist",
35
+ "p4-pre-submit",
36
+ "post-index-change",
37
+ ];
38
+ export function forwardingPath(file) {
39
+ return join(dirname(file), "quality");
40
+ }
41
+ export const globalHooksPath = Effect.fn("Hooks.globalHooksPath")(function* (cwd) {
42
+ const global = yield* git(cwd, ["config", "--global", "--includes", "--type=path", "--get", "core.hooksPath"]);
43
+ if (global.code !== 0 && global.code !== 1) {
44
+ return yield* new SetupFailure({ message: `cannot read global core.hooksPath: ${global.stderr.trim()}` });
45
+ }
46
+ return global.stdout.trim();
47
+ });
48
+ function forwarder(file, name) {
49
+ return [
50
+ "#!/bin/sh",
51
+ OWNER,
52
+ ...(name === "pre-commit"
53
+ ? [`exec ${shellWords([file])} "$@"`]
54
+ : [
55
+ 'global_path="$(git config --global --includes --type=path --get core.hooksPath)"',
56
+ '[ -n "$global_path" ] || exit 0',
57
+ `global_hook="$global_path/${name}"`,
58
+ 'if [ -x "$global_hook" ] && ! [ "$global_hook" -ef "$0" ]; then',
59
+ '\texec "$global_hook" "$@"',
60
+ "fi",
61
+ ]),
62
+ "",
63
+ ].join("\n");
64
+ }
65
+ export const installForwarding = Effect.fn("Hooks.installForwarding")(function* (file) {
66
+ const fs = yield* FileSystem.FileSystem;
67
+ const path = forwardingPath(file);
68
+ yield* fs.makeDirectory(path, { recursive: true });
69
+ for (const name of HOOKS) {
70
+ const target = join(path, name);
71
+ const existing = yield* readOptionalText(target);
72
+ if (existing !== undefined && !isHookShim(existing)) {
73
+ return yield* new SetupFailure({ message: `cannot replace ${target}: it is not Quality's hook.` });
74
+ }
75
+ yield* fs.writeFileString(target, forwarder(file, name));
76
+ yield* fs.chmod(target, 0o755);
77
+ }
78
+ return path;
79
+ });
80
+ export const removeForwarding = Effect.fn("Hooks.removeForwarding")(function* (file) {
81
+ const fs = yield* FileSystem.FileSystem;
82
+ const path = forwardingPath(file);
83
+ for (const name of HOOKS) {
84
+ const target = join(path, name);
85
+ const existing = yield* readOptionalText(target);
86
+ if (existing !== undefined && isHookShim(existing)) {
87
+ yield* fs.remove(target);
88
+ }
89
+ }
90
+ if ((yield* fs.exists(path)) && (yield* fs.readDirectory(path)).length === 0) {
91
+ yield* fs.remove(path, { recursive: true });
92
+ }
93
+ });
94
+ //# sourceMappingURL=forwarding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forwarding.js","sourceRoot":"","sources":["../../src/hooks/forwarding.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/hooks/install.ts"],"names":[],"mappings":"AACA,OAAO,EAAW,MAAM,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAGrD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,EAA6B,KAAK,MAAM,EAAc,MAAM,cAAc,CAAC;AAKlF,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAoBD,eAAO,MAAM,WAAW,uJA8BtB,CAAC"}
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/hooks/install.ts"],"names":[],"mappings":"AACA,OAAO,EAAW,MAAM,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAGrD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,EAA6B,KAAK,MAAM,EAAc,MAAM,cAAc,CAAC;AAMlF,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAoBD,eAAO,MAAM,WAAW,uJA+BtB,CAAC"}
@@ -6,6 +6,7 @@ import { SetupFailure } from "#failure.ts";
6
6
  import { gitOrFail } from "#git/command.ts";
7
7
  import { readOptionalText } from "#inventory/filesystem.ts";
8
8
  import { commandWords, hookCommand, shellWords } from "./command.js";
9
+ import { globalHooksPath, installForwarding } from "./forwarding.js";
9
10
  import { hookLocation } from "./location.js";
10
11
  import { hookShim, isHookShim } from "./shim.js";
11
12
  import { globalHookStatus } from "./status.js";
@@ -45,7 +46,7 @@ export const installHook = Effect.fn("Hooks.installHook")(function* (install) {
45
46
  const done = foreign ? `replaced ${location.file}, which was not quality's hook` : `installed the pre-commit hook at ${location.file}`;
46
47
  yield* Console.log(`quality: ${done}; each commit runs \`${words}\`.`);
47
48
  }
48
- const hooks = dirname(location.file);
49
+ const hooks = (yield* globalHooksPath(config.root)) === "" ? dirname(location.file) : yield* installForwarding(location.file).pipe(Effect.mapError(failed));
49
50
  yield* gitOrFail(config.root, ["config", "--local", "core.hooksPath", hooks]);
50
51
  yield* Console.log(`quality: set repository-local core.hooksPath to ${hooks}.`);
51
52
  yield* globalHookStatus(location);
@@ -1 +1 @@
1
- {"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/hooks/install.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/hooks/install.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { type HookCommand } from "./command.ts";
2
+ export declare const OWNER = "# Installed by @shivaedev/quality: `quality hooks install` writes it, `quality hooks uninstall` removes it.";
2
3
  export declare function isHookShim(text: string): boolean;
3
4
  export declare function hookShim(directory: string, command: HookCommand): string;
4
5
  //# sourceMappingURL=shim.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"shim.d.ts","sourceRoot":"","sources":["../../src/hooks/shim.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAc,MAAM,cAAc,CAAC;AAI5D,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAID,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,MAAM,CA2BxE"}
1
+ {"version":3,"file":"shim.d.ts","sourceRoot":"","sources":["../../src/hooks/shim.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAc,MAAM,cAAc,CAAC;AAE5D,eAAO,MAAM,KAAK,gHAAgH,CAAC;AAEnI,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAID,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,MAAM,CA6BxE"}
@@ -1,5 +1,5 @@
1
1
  import { shellWords } from "./command.js";
2
- const OWNER = "# Installed by @shivaedev/quality: `quality hooks install` writes it, `quality hooks uninstall` removes it.";
2
+ export const OWNER = "# Installed by @shivaedev/quality: `quality hooks install` writes it, `quality hooks uninstall` removes it.";
3
3
  export function isHookShim(text) {
4
4
  return text.split("\n").includes(OWNER);
5
5
  }
@@ -11,6 +11,8 @@ export function hookShim(directory, command) {
11
11
  return [
12
12
  "#!/bin/sh",
13
13
  OWNER,
14
+ 'if [ -n "$QUALITY_PRE_COMMIT_ACTIVE" ]; then exit 0; fi',
15
+ "export QUALITY_PRE_COMMIT_ACTIVE=1",
14
16
  'top="$(git rev-parse --show-toplevel)" || exit 2',
15
17
  'if [ -n "$GIT_DIR" ] && [ -z "$GIT_WORK_TREE" ]; then export GIT_WORK_TREE="$top"; fi',
16
18
  'cd "$top" || exit 2',
@@ -1 +1 @@
1
- {"version":3,"file":"shim.js","sourceRoot":"","sources":["../../src/hooks/shim.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,UAAU,EAAE,MAAM,cAAc,CAAC;AAE5D,MAAM,KAAK,GAAG,6GAA6G,CAAC;AAE5H,MAAM,UAAU,UAAU,CAAC,IAAY;IACtC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACzC,CAAC;AAED,uHAAuH;AACvH,gJAAgJ;AAChJ,MAAM,UAAU,QAAQ,CAAC,SAAiB,EAAE,OAAoB;IAC/D,MAAM,MAAM,GAAG,SAAS,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC;IACrE,MAAM,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC;IAChH,OAAO;QACN,WAAW;QACX,KAAK;QACL,kDAAkD;QAClD,uFAAuF;QACvF,qBAAqB;QACrB,UAAU;QACV,kFAAkF;QAClF,gCAAgC;QAChC,yCAAyC;QACzC,mEAAmE;QACnE,sCAAsC;QACtC,MAAM;QACN,IAAI;QACJ,YAAY,MAAM,YAAY;QAC9B,WAAW,UAAU,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE;QAC3C,8BAA8B;QAC9B,sKAAsK;QACtK,UAAU;QACV,IAAI;QACJ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,eAAe;QAChC,gBAAgB;QAChB,EAAE;KACF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"shim.js","sourceRoot":"","sources":["../../src/hooks/shim.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,UAAU,EAAE,MAAM,cAAc,CAAC;AAE5D,MAAM,CAAC,MAAM,KAAK,GAAG,6GAA6G,CAAC;AAEnI,MAAM,UAAU,UAAU,CAAC,IAAY;IACtC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACzC,CAAC;AAED,uHAAuH;AACvH,gJAAgJ;AAChJ,MAAM,UAAU,QAAQ,CAAC,SAAiB,EAAE,OAAoB;IAC/D,MAAM,MAAM,GAAG,SAAS,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC;IACrE,MAAM,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC;IAChH,OAAO;QACN,WAAW;QACX,KAAK;QACL,yDAAyD;QACzD,oCAAoC;QACpC,kDAAkD;QAClD,uFAAuF;QACvF,qBAAqB;QACrB,UAAU;QACV,kFAAkF;QAClF,gCAAgC;QAChC,yCAAyC;QACzC,mEAAmE;QACnE,sCAAsC;QACtC,MAAM;QACN,IAAI;QACJ,YAAY,MAAM,YAAY;QAC9B,WAAW,UAAU,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE;QAC3C,8BAA8B;QAC9B,sKAAsK;QACtK,UAAU;QACV,IAAI;QACJ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,eAAe;QAChC,gBAAgB;QAChB,EAAE;KACF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { Effect } from "effect";
2
2
  import { GateFailed, SetupFailure } from "#failure.ts";
3
3
  import { type HookLocation } from "./location.ts";
4
- export declare const globalHookStatus: (location: HookLocation) => Effect.Effect<undefined, SetupFailure, import("effect/process/ChildProcessSpawner").ChildProcessSpawner>;
4
+ export declare const globalHookStatus: (location: HookLocation) => Effect.Effect<void, SetupFailure, import("effect/process/ChildProcessSpawner").ChildProcessSpawner>;
5
5
  export declare const hookStatus: (cwd: string) => Effect.Effect<undefined, GateFailed | SetupFailure, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | import("effect/FileSystem").FileSystem>;
6
6
  //# sourceMappingURL=status.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/hooks/status.ts"],"names":[],"mappings":"AAGA,OAAO,EAAW,MAAM,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGvD,OAAO,EAAE,KAAK,YAAY,EAAgB,MAAM,eAAe,CAAC;AAchE,eAAO,MAAM,gBAAgB,sIAc3B,CAAC;AAEH,eAAO,MAAM,UAAU,iLAYrB,CAAC"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/hooks/status.ts"],"names":[],"mappings":"AAGA,OAAO,EAAW,MAAM,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIvD,OAAO,EAAE,KAAK,YAAY,EAAgB,MAAM,eAAe,CAAC;AAchE,eAAO,MAAM,gBAAgB,iIAU3B,CAAC;AAEH,eAAO,MAAM,UAAU,iLAmBrB,CAAC"}
@@ -3,8 +3,9 @@ import { access, realpath } from "node:fs/promises";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { Console, Effect } from "effect";
5
5
  import { GateFailed, SetupFailure } from "#failure.ts";
6
- import { git, gitOrFail } from "#git/command.ts";
6
+ import { gitOrFail } from "#git/command.ts";
7
7
  import { readOptionalText } from "#inventory/filesystem.ts";
8
+ import { forwardingPath, globalHooksPath } from "./forwarding.js";
8
9
  import { hookLocation } from "./location.js";
9
10
  import { isHookShim } from "./shim.js";
10
11
  function executable(file) {
@@ -19,11 +20,7 @@ function executable(file) {
19
20
  });
20
21
  }
21
22
  export const globalHookStatus = Effect.fn("Hooks.globalHookStatus")(function* (location) {
22
- const global = yield* git(location.toplevel, ["config", "--global", "--includes", "--type=path", "--get", "core.hooksPath"]);
23
- if (global.code !== 0 && global.code !== 1) {
24
- return yield* new SetupFailure({ message: `cannot read global core.hooksPath: ${global.stderr.trim()}` });
25
- }
26
- const path = global.stdout.trim();
23
+ const path = yield* globalHooksPath(location.toplevel);
27
24
  const hook = resolve(location.toplevel, path, "pre-commit");
28
25
  const target = path === "" ? undefined : yield* executable(hook);
29
26
  const own = yield* executable(location.file);
@@ -39,6 +36,11 @@ export const hookStatus = Effect.fn("Hooks.hookStatus")(function* (cwd) {
39
36
  yield* Console.error(`quality: the effective hooks path ${dirname(path)} does not run Quality's pre-commit hook.\nhelp: run \`quality hooks install\` to check commits.`);
40
37
  return yield* new GateFailed();
41
38
  }
39
+ const global = yield* globalHooksPath(cwd);
40
+ if (global !== "" && dirname(path) !== resolve(location.toplevel, global) && dirname(path) !== forwardingPath(location.file)) {
41
+ yield* Console.error("quality: the repository-local core.hooksPath hides global hooks other than pre-commit.\nhelp: run `quality hooks install` to keep all global hooks running.");
42
+ return yield* new GateFailed();
43
+ }
42
44
  yield* Console.log(`quality: git runs Quality's pre-commit hook at ${path}.`);
43
45
  yield* globalHookStatus(location);
44
46
  });
@@ -1 +1 @@
1
- {"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/hooks/status.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/hooks/status.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"uninstall.d.ts","sourceRoot":"","sources":["../../src/hooks/uninstall.ts"],"names":[],"mappings":"AACA,OAAO,EAAW,MAAM,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAM3C,eAAO,MAAM,aAAa,8IAgBxB,CAAC"}
1
+ {"version":3,"file":"uninstall.d.ts","sourceRoot":"","sources":["../../src/hooks/uninstall.ts"],"names":[],"mappings":"AACA,OAAO,EAAW,MAAM,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAO3C,eAAO,MAAM,aAAa,8IAiBxB,CAAC"}
@@ -3,6 +3,7 @@ import { Console, Effect, FileSystem } from "effect";
3
3
  import { SetupFailure } from "#failure.ts";
4
4
  import { git, gitOrFail } from "#git/command.ts";
5
5
  import { readOptionalText } from "#inventory/filesystem.ts";
6
+ import { forwardingPath, removeForwarding } from "./forwarding.js";
6
7
  import { hookLocation } from "./location.js";
7
8
  import { isHookShim } from "./shim.js";
8
9
  export const uninstallHook = Effect.fn("Hooks.uninstallHook")(function* (cwd) {
@@ -17,7 +18,8 @@ export const uninstallHook = Effect.fn("Hooks.uninstallHook")(function* (cwd) {
17
18
  }
18
19
  yield* Effect.mapError(fs.remove(file), (error) => new SetupFailure({ message: `cannot remove ${file}: ${error.message}` }));
19
20
  const local = yield* git(cwd, ["config", "--local", "--get", "core.hooksPath"]);
20
- if (local.stdout.trim() === dirname(file)) {
21
+ yield* removeForwarding(file).pipe(Effect.mapError((error) => new SetupFailure({ message: error.message })));
22
+ if ([dirname(file), forwardingPath(file)].includes(local.stdout.trim())) {
21
23
  yield* gitOrFail(cwd, ["config", "--local", "--unset-all", "core.hooksPath"]);
22
24
  }
23
25
  yield* Console.log(`quality: removed the pre-commit hook at ${file}.`);
@@ -1 +1 @@
1
- {"version":3,"file":"uninstall.js","sourceRoot":"","sources":["../../src/hooks/uninstall.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,EAAE,CAAC,qBAAqB,CAAC,CAAC,QAAQ,CAAC,EAAE,GAAW;IACnF,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;IACxC,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,YAAY,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAC7H,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC5B,OAAO,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,2CAA2C,IAAI,GAAG,CAAC,CAAC;IAC/E,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,iBAAiB,IAAI,wCAAwC,CAAC,CAAC;IAC5F,CAAC;IACD,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,YAAY,CAAC,EAAE,OAAO,EAAE,iBAAiB,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;IAC7H,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC;IAChF,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,KAAK,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,gBAAgB,CAAC,CAAC,CAAC;IAC/E,CAAC;IACD,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,2CAA2C,IAAI,GAAG,CAAC,CAAC;AACxE,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"uninstall.js","sourceRoot":"","sources":["../../src/hooks/uninstall.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,EAAE,CAAC,qBAAqB,CAAC,CAAC,QAAQ,CAAC,EAAE,GAAW;IACnF,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;IACxC,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,YAAY,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAC7H,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC5B,OAAO,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,2CAA2C,IAAI,GAAG,CAAC,CAAC;IAC/E,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,iBAAiB,IAAI,wCAAwC,CAAC,CAAC;IAC5F,CAAC;IACD,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,YAAY,CAAC,EAAE,OAAO,EAAE,iBAAiB,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;IAC7H,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC;IAChF,KAAK,CAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,YAAY,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7G,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;QACzE,KAAK,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,gBAAgB,CAAC,CAAC,CAAC;IAC/E,CAAC;IACD,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,2CAA2C,IAAI,GAAG,CAAC,CAAC;AACxE,CAAC,CAAC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare function blockedExport(exports: unknown, subpath: string): boolean;
2
+ export declare function exportTargets(value: unknown): string[];
3
+ export declare function exportedModule(exports: unknown, local: string): boolean;
4
+ //# sourceMappingURL=packageExports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"packageExports.d.ts","sourceRoot":"","sources":["../../src/imports/packageExports.ts"],"names":[],"mappings":"AAAA,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAqBxE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAKtD;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAwBvE"}
@@ -0,0 +1,48 @@
1
+ export function blockedExport(exports, subpath) {
2
+ if (typeof exports !== "object" || exports === null || Array.isArray(exports)) {
3
+ return false;
4
+ }
5
+ const entries = Object.entries(exports);
6
+ const exact = entries.find(([key]) => key === subpath);
7
+ if (exact !== undefined) {
8
+ return exact[1] === null;
9
+ }
10
+ const matching = entries.filter(([key]) => {
11
+ const [prefix = "", suffix, ...rest] = key.split("*");
12
+ return (suffix !== undefined
13
+ && rest.length === 0
14
+ && subpath.length > prefix.length + suffix.length
15
+ && subpath.startsWith(prefix)
16
+ && subpath.endsWith(suffix));
17
+ });
18
+ const best = matching.toSorted(([left], [right]) => right.indexOf("*") - left.indexOf("*") || right.length - left.length)[0];
19
+ return best?.[1] === null;
20
+ }
21
+ export function exportTargets(value) {
22
+ if (typeof value === "string") {
23
+ return [value];
24
+ }
25
+ return typeof value === "object" && value !== null ? Object.values(value).flatMap(exportTargets) : [];
26
+ }
27
+ export function exportedModule(exports, local) {
28
+ if (typeof exports !== "object" || exports === null) {
29
+ return false;
30
+ }
31
+ const paths = [
32
+ `./${local}`,
33
+ `./${local.replace(/^src\//u, "dist/").replace(/\.(?<mode>[cm]?)tsx?$/u, ".$<mode>js")}`,
34
+ `./${local.replace(/^src\//u, "dist/").replace(/\.(?<mode>[cm]?)tsx?$/u, ".d.$<mode>ts")}`,
35
+ ];
36
+ return Object.entries(exports).some(([key, value]) => exportTargets(value).some((target) => paths.some((path) => {
37
+ const [prefix = "", suffix] = target.split("*");
38
+ if (suffix === undefined) {
39
+ return path === target && !blockedExport(exports, key);
40
+ }
41
+ if (!(path.startsWith(prefix) && path.endsWith(suffix)) || path.length <= prefix.length + suffix.length) {
42
+ return false;
43
+ }
44
+ const star = path.slice(prefix.length, path.length - suffix.length);
45
+ return !blockedExport(exports, key.replace("*", star));
46
+ })));
47
+ }
48
+ //# sourceMappingURL=packageExports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"packageExports.js","sourceRoot":"","sources":["../../src/imports/packageExports.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { RuleInputs } from "#rule.ts";
2
+ import type { ImportGraph } from "./graph.ts";
3
+ export declare function publicModules(inputs: RuleInputs, graph: ImportGraph): Promise<ReadonlySet<string>>;
4
+ //# sourceMappingURL=publicModules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publicModules.d.ts","sourceRoot":"","sources":["../../src/imports/publicModules.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAI9C,wBAAsB,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAoBxG"}
@@ -0,0 +1,21 @@
1
+ import { posix } from "node:path";
2
+ import { exportedModule } from "./packageExports.js";
3
+ import { packageOf } from "./workspace.js";
4
+ export async function publicModules(inputs, graph) {
5
+ const exports = new Map(await Promise.all(graph.packages.map(async (owner) => {
6
+ const manifest = JSON.parse((await inputs.readText(posix.join(owner.directory, "package.json"))) ?? "");
7
+ return [owner, manifest.exports];
8
+ })));
9
+ const modules = new Set();
10
+ for (const edge of graph.edges) {
11
+ if (edge.to.kind !== "file") {
12
+ continue;
13
+ }
14
+ const owner = packageOf(graph.packages, edge.to.path);
15
+ if (owner !== undefined && exportedModule(exports.get(owner), posix.relative(owner.directory, edge.to.path))) {
16
+ modules.add(edge.to.path);
17
+ }
18
+ }
19
+ return modules;
20
+ }
21
+ //# sourceMappingURL=publicModules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publicModules.js","sourceRoot":"","sources":["../../src/imports/publicModules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAGlC,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAkB,EAAE,KAAkB;IACzE,MAAM,OAAO,GAAG,IAAI,GAAG,CACtB,MAAM,OAAO,CAAC,GAAG,CAChB,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QAClC,MAAM,QAAQ,GAAmC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACxI,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAU,CAAC;IAC3C,CAAC,CAAC,CACF,CACD,CAAC;IACF,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QAChC,IAAI,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC7B,SAAS;QACV,CAAC;QACD,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QACtD,IAAI,KAAK,KAAK,SAAS,IAAI,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAC9G,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;IACF,CAAC;IACD,OAAO,OAAO,CAAC;AAChB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"bundleInputs.d.ts","sourceRoot":"","sources":["../../../src/rules/imports/bundleInputs.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAa,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAgEzE,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO;;;EA0BrI"}
1
+ {"version":3,"file":"bundleInputs.d.ts","sourceRoot":"","sources":["../../../src/rules/imports/bundleInputs.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,EAAa,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAQzE,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO;;;EA0BrI"}
@@ -1,55 +1,9 @@
1
1
  import { posix } from "node:path";
2
2
  import { outgoing } from "#imports/fences/reach.ts";
3
+ import { blockedExport, exportedModule, exportTargets } from "#imports/packageExports.ts";
3
4
  import { packageOf } from "#imports/workspace.ts";
4
- function blocked(exports, subpath) {
5
- if (typeof exports !== "object" || exports === null || Array.isArray(exports)) {
6
- return false;
7
- }
8
- const entries = Object.entries(exports);
9
- const exact = entries.find(([key]) => key === subpath);
10
- if (exact !== undefined) {
11
- return exact[1] === null;
12
- }
13
- const matching = entries.filter(([key]) => {
14
- const [prefix = "", suffix, ...rest] = key.split("*");
15
- return (suffix !== undefined
16
- && rest.length === 0
17
- && subpath.length > prefix.length + suffix.length
18
- && subpath.startsWith(prefix)
19
- && subpath.endsWith(suffix));
20
- });
21
- const best = matching.toSorted(([left], [right]) => right.indexOf("*") - left.indexOf("*") || right.length - left.length)[0];
22
- return best?.[1] === null;
23
- }
24
- function targets(value) {
25
- if (typeof value === "string") {
26
- return [value];
27
- }
28
- return typeof value === "object" && value !== null ? Object.values(value).flatMap(targets) : [];
29
- }
30
- function exported(exports, local) {
31
- if (typeof exports !== "object" || exports === null) {
32
- return false;
33
- }
34
- const paths = [
35
- `./${local}`,
36
- `./${local.replace(/^src\//u, "dist/").replace(/\.(?<mode>[cm]?)tsx?$/u, ".$<mode>js")}`,
37
- `./${local.replace(/^src\//u, "dist/").replace(/\.(?<mode>[cm]?)tsx?$/u, ".d.$<mode>ts")}`,
38
- ];
39
- return Object.entries(exports).some(([key, value]) => targets(value).some((target) => paths.some((path) => {
40
- const [prefix = "", suffix] = target.split("*");
41
- if (suffix === undefined) {
42
- return path === target && !blocked(exports, key);
43
- }
44
- if (!(path.startsWith(prefix) && path.endsWith(suffix)) || path.length <= prefix.length + suffix.length) {
45
- return false;
46
- }
47
- const star = path.slice(prefix.length, path.length - suffix.length);
48
- return !blocked(exports, key.replace("*", star));
49
- })));
50
- }
51
5
  function executable(manifest, local) {
52
- const binTargets = targets(Reflect.get(manifest, "bin"));
6
+ const binTargets = exportTargets(Reflect.get(manifest, "bin"));
53
7
  const built = local.replace(/^src\//u, "dist/").replace(/\.(?<mode>[cm]?)tsx?$/u, ".$<mode>js");
54
8
  return binTargets.some((target) => typeof target === "string" && [local, built].includes(posix.normalize(target)));
55
9
  }
@@ -58,8 +12,8 @@ export function bundleInputs(graph, owner, manifest, developmentFile) {
58
12
  const privateFiles = new Set(modules.filter((path) => {
59
13
  const local = posix.relative(owner.directory, path);
60
14
  return (local.startsWith("src/")
61
- && blocked(Reflect.get(manifest, "exports"), `./${local.slice(4)}`)
62
- && !exported(Reflect.get(manifest, "exports"), local));
15
+ && blockedExport(Reflect.get(manifest, "exports"), `./${local.slice(4)}`)
16
+ && !exportedModule(Reflect.get(manifest, "exports"), local));
63
17
  }));
64
18
  const queue = modules.filter((path) => executable(manifest, posix.relative(owner.directory, path)) || !(developmentFile(path) || privateFiles.has(path)));
65
19
  const runtime = new Set(queue);
@@ -1 +1 @@
1
- {"version":3,"file":"bundleInputs.js","sourceRoot":"","sources":["../../../src/rules/imports/bundleInputs.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bundleInputs.js","sourceRoot":"","sources":["../../../src/rules/imports/bundleInputs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEpD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC1F,OAAO,EAAE,SAAS,EAAyB,MAAM,uBAAuB,CAAC;AAEzE,SAAS,UAAU,CAAC,QAAgB,EAAE,KAAa;IAClD,MAAM,UAAU,GAAG,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;IAC/D,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,wBAAwB,EAAE,YAAY,CAAC,CAAC;IAChG,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACpH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAkB,EAAE,KAAuB,EAAE,QAAgB,EAAE,eAA0C;IACrI,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,KAAK,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC;IAC7I,MAAM,YAAY,GAAG,IAAI,GAAG,CAC3B,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;QACvB,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QACpD,OAAO,CACN,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC;eACrB,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE,KAAK,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;eACtE,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE,KAAK,CAAC,CAC3D,CAAC;IACH,CAAC,CAAC,CACF,CAAC;IACF,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAC3B,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAC3H,CAAC;IACF,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC/B,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACpC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YAC1C,IAAI,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC3D,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;gBAC1B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;YAC1B,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC;AAClC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"supportImports.d.ts","sourceRoot":"","sources":["../../../src/rules/stories/supportImports.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,cAAc;;EAoBzB,CAAC"}
1
+ {"version":3,"file":"supportImports.d.ts","sourceRoot":"","sources":["../../../src/rules/stories/supportImports.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,cAAc;;EA0BzB,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { Effect, Schema } from "effect";
2
2
  import { importGraph } from "#imports/graph.ts";
3
+ import { publicModules } from "#imports/publicModules.ts";
3
4
  import { isTestSupport } from "#naming/testName.ts";
4
5
  import { defineRule } from "#rule.ts";
5
6
  import { matchesAny } from "./matchesAny.js";
@@ -12,16 +13,21 @@ export const supportImports = defineRule({
12
13
  return [];
13
14
  }
14
15
  const graph = await importGraph(inputs);
16
+ const exported = await publicModules(inputs, graph);
15
17
  return graph.edges
16
- .filter((edge) => isTestSupport(edge.from) && edge.to.kind === "file" && !isTestSupport(edge.to.path) && !matchesAny(edge.to.path, options.modules))
18
+ .filter((edge) => isTestSupport(edge.from)
19
+ && edge.to.kind === "file"
20
+ && !isTestSupport(edge.to.path)
21
+ && !exported.has(edge.to.path)
22
+ && !matchesAny(edge.to.path, options.modules))
17
23
  .map((edge) => ({
18
24
  file: edge.from,
19
25
  line: edge.line,
20
- message: `Test-support imports "${edge.specifier}" outside its allowed public surface. Drive the app through its host, schemas or queue definitions; list those public module paths in tests/support-imports.options.modules in the repository's quality config.`,
26
+ message: `Test-support imports "${edge.specifier}" outside its allowed public surface. Drive the app through its host, schemas or queue definitions, or a workspace package's exports; list additional internal module paths in tests/support-imports.options.modules in the repository's quality config.`,
21
27
  subject: edge.specifier,
22
28
  }));
23
29
  },
24
- description: "Test-support reaches repository modules only through the public surface named by the repository's module allowlist.",
30
+ description: "Test-support reaches repository modules through workspace package exports and the repository's additional module allowlist.",
25
31
  id: "tests/support-imports",
26
32
  options: Schema.toStandardSchemaV1(SupportImportsOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
27
33
  });
@@ -1 +1 @@
1
- {"version":3,"file":"supportImports.js","sourceRoot":"","sources":["../../../src/rules/stories/supportImports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,MAAM,qBAAqB,GAAG,MAAM,CAAC,MAAM,CAAC;IAC3C,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;CAC5F,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,UAAU,CAAC;IACxC,KAAK,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE;QACvC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAC9D,OAAO,EAAE,CAAC;QACX,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;QACxC,OAAO,KAAK,CAAC,KAAK;aAChB,MAAM,CACN,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAC3I;aACA,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,OAAO,EAAE,yBAAyB,IAAI,CAAC,SAAS,iNAAiN;YACjQ,OAAO,EAAE,IAAI,CAAC,SAAS;SACvB,CAAC,CAAC,CAAC;IACN,CAAC;IACD,WAAW,EAAE,qHAAqH;IAClI,EAAE,EAAE,uBAAuB;IAC3B,OAAO,EAAE,MAAM,CAAC,kBAAkB,CAAC,qBAAqB,EAAE,EAAE,YAAY,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,EAAE,CAAC;CACzH,CAAC,CAAC"}
1
+ {"version":3,"file":"supportImports.js","sourceRoot":"","sources":["../../../src/rules/stories/supportImports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,MAAM,qBAAqB,GAAG,MAAM,CAAC,MAAM,CAAC;IAC3C,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;CAC5F,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,UAAU,CAAC;IACxC,KAAK,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE;QACvC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAC9D,OAAO,EAAE,CAAC;QACX,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;QACxC,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QACpD,OAAO,KAAK,CAAC,KAAK;aAChB,MAAM,CACN,CAAC,IAAI,EAAE,EAAE,CACR,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC;eACrB,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,MAAM;eACvB,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;eAC5B,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;eAC3B,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAC9C;aACA,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,OAAO,EAAE,yBAAyB,IAAI,CAAC,SAAS,0PAA0P;YAC1S,OAAO,EAAE,IAAI,CAAC,SAAS;SACvB,CAAC,CAAC,CAAC;IACN,CAAC;IACD,WAAW,EAAE,6HAA6H;IAC1I,EAAE,EAAE,uBAAuB;IAC3B,OAAO,EAAE,MAAM,CAAC,kBAAkB,CAAC,qBAAqB,EAAE,EAAE,YAAY,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,EAAE,CAAC;CACzH,CAAC,CAAC"}
@@ -79,8 +79,8 @@ help: fix what the output above reports, then commit again; `quality hooks pre-c
79
79
 
80
80
  The hook is a short script in the repository's common git directory, `.git/hooks/pre-commit`, which every worktree of the repository shares. It changes to the root of the worktree that commits, then to the config's folder in it, and runs that worktree's quality with that worktree's config, so a branch that changes the checks commits under its own rules. It runs quality the way `hooks install` ran: when that was a file inside the repository, such as a workspace's own source, it runs `node` with the same Node options on that file; otherwise it runs `node_modules/.bin/quality` in the config's folder. When that file does not exist, as in a new worktree whose dependencies are not installed yet, the hook blocks the commit and says to install them. A config file not named `quality.config.ts` is passed with `--config`.
81
81
 
82
- `hooks install` sets repository-local `core.hooksPath` to the common git directory's `hooks` folder and prints that path. It never changes global or system git configuration. At commit time, the hook reads global `core.hooksPath` and runs its executable `pre-commit` hook from the committing worktree's root, before Quality's checks. Relative global paths resolve from that root; a global path pointing to Quality's own hook is not chained. Quality still runs when the global hook fails, and either failure blocks the commit. Installation reports whether there is an executable global hook to chain.
82
+ `hooks install` sets repository-local `core.hooksPath` and prints that path. Without a global hooks path, it uses the common git directory's `hooks` folder. With a global `core.hooksPath`, it uses `hooks/quality`, whose forwarding hooks preserve every Git hook in the global directory, including hooks added later, with its arguments, stdin and exit status. It never changes global or system git configuration. At commit time, Quality's pre-commit shim reads global `core.hooksPath` and runs its executable `pre-commit` hook from the committing worktree's root, before Quality's checks. Relative global paths resolve from that root; a global path pointing to Quality's own hook is not chained. A re-entry marker prevents a global hook that chains `.git/hooks/pre-commit` from running Quality recursively. Quality still runs when the global hook fails, and either failure blocks the commit. Installation reports whether there is an executable global pre-commit hook to chain.
83
83
 
84
- `quality hooks status` reports whether git's effective hooks path runs an executable Quality pre-commit hook, and exits 1 with an installation hint when it does not. It also reports global chaining when Quality is active.
84
+ `quality hooks status` reports whether git's effective hooks path runs an executable Quality pre-commit hook, and exits 1 with an installation hint when it does not. It also reports global chaining when Quality is active. A local hooks path that runs Quality but hides other global hooks fails status with a `quality hooks install` repair hint; reinstalling replaces that layout with forwarding hooks.
85
85
 
86
- A pre-commit hook that quality did not write is kept: `hooks install` warns, says how to call quality from it, and exits 0 without setting the hooks path. `--force` replaces it. `quality hooks uninstall` removes quality's hook and keeps any other. It removes the repository-local hooks path only when that setting still points to Quality's hooks directory, letting global hooks take effect again.
86
+ A pre-commit hook that quality did not write is kept: `hooks install` warns, says how to call quality from it, and exits 0 without setting the hooks path. `--force` replaces it. `quality hooks uninstall` removes quality's hook and keeps any other. It removes Quality's forwarding hooks. It removes the repository-local hooks path only when that setting still points to one of Quality's hooks directories, letting global hooks take effect again.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/quality",
3
- "version": "0.32.0",
3
+ "version": "0.32.1",
4
4
  "description": "Repository quality gate with typed rules, a baseline of existing debt and a registry of reasoned exceptions",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/quality#readme",
6
6
  "bugs": {
@@ -68,13 +68,13 @@
68
68
  "@types/node": "24.19.2",
69
69
  "@typescript/native": "npm:typescript@7.0.2",
70
70
  "effect": "4.0.4",
71
- "vitest": "5.0.3"
71
+ "vitest": "5.0.4"
72
72
  },
73
73
  "peerDependencies": {
74
74
  "@effect/platform-node": "4.0.4",
75
75
  "@effect/platform-node-shared": "4.0.4",
76
76
  "effect": "4.0.4",
77
- "vitest": "5.0.3"
77
+ "vitest": "5.0.4"
78
78
  },
79
79
  "peerDependenciesMeta": {
80
80
  "vitest": {
@@ -0,0 +1,99 @@
1
+ import { dirname, join } from "node:path";
2
+ import { Effect, FileSystem } from "effect";
3
+ import { SetupFailure } from "#failure.ts";
4
+ import { git } from "#git/command.ts";
5
+ import { readOptionalText } from "#inventory/filesystem.ts";
6
+ import { shellWords } from "./command.ts";
7
+ import { isHookShim, OWNER } from "./shim.ts";
8
+
9
+ const HOOKS = [
10
+ "applypatch-msg",
11
+ "pre-applypatch",
12
+ "post-applypatch",
13
+ "pre-commit",
14
+ "pre-merge-commit",
15
+ "prepare-commit-msg",
16
+ "commit-msg",
17
+ "post-commit",
18
+ "pre-rebase",
19
+ "post-checkout",
20
+ "post-merge",
21
+ "pre-push",
22
+ "pre-receive",
23
+ "update",
24
+ "proc-receive",
25
+ "post-receive",
26
+ "post-update",
27
+ "reference-transaction",
28
+ "push-to-checkout",
29
+ "pre-auto-gc",
30
+ "post-rewrite",
31
+ "sendemail-validate",
32
+ "fsmonitor-watchman",
33
+ "p4-changelist",
34
+ "p4-prepare-changelist",
35
+ "p4-post-changelist",
36
+ "p4-pre-submit",
37
+ "post-index-change",
38
+ ];
39
+
40
+ export function forwardingPath(file: string): string {
41
+ return join(dirname(file), "quality");
42
+ }
43
+
44
+ export const globalHooksPath = Effect.fn("Hooks.globalHooksPath")(function* (cwd: string) {
45
+ const global = yield* git(cwd, ["config", "--global", "--includes", "--type=path", "--get", "core.hooksPath"]);
46
+ if (global.code !== 0 && global.code !== 1) {
47
+ return yield* new SetupFailure({ message: `cannot read global core.hooksPath: ${global.stderr.trim()}` });
48
+ }
49
+ return global.stdout.trim();
50
+ });
51
+
52
+ function forwarder(file: string, name: string): string {
53
+ return [
54
+ "#!/bin/sh",
55
+ OWNER,
56
+ ...(name === "pre-commit"
57
+ ? [`exec ${shellWords([file])} "$@"`]
58
+ : [
59
+ 'global_path="$(git config --global --includes --type=path --get core.hooksPath)"',
60
+ '[ -n "$global_path" ] || exit 0',
61
+ `global_hook="$global_path/${name}"`,
62
+ 'if [ -x "$global_hook" ] && ! [ "$global_hook" -ef "$0" ]; then',
63
+ '\texec "$global_hook" "$@"',
64
+ "fi",
65
+ ]),
66
+ "",
67
+ ].join("\n");
68
+ }
69
+
70
+ export const installForwarding = Effect.fn("Hooks.installForwarding")(function* (file: string) {
71
+ const fs = yield* FileSystem.FileSystem;
72
+ const path = forwardingPath(file);
73
+ yield* fs.makeDirectory(path, { recursive: true });
74
+ for (const name of HOOKS) {
75
+ const target = join(path, name);
76
+ const existing = yield* readOptionalText(target);
77
+ if (existing !== undefined && !isHookShim(existing)) {
78
+ return yield* new SetupFailure({ message: `cannot replace ${target}: it is not Quality's hook.` });
79
+ }
80
+ yield* fs.writeFileString(target, forwarder(file, name));
81
+ yield* fs.chmod(target, 0o755);
82
+ }
83
+ return path;
84
+ });
85
+
86
+ export const removeForwarding = Effect.fn("Hooks.removeForwarding")(function* (file: string) {
87
+ const fs = yield* FileSystem.FileSystem;
88
+ const path = forwardingPath(file);
89
+ for (const name of HOOKS) {
90
+ const target = join(path, name);
91
+ const existing = yield* readOptionalText(target);
92
+ if (existing !== undefined && isHookShim(existing)) {
93
+ yield* fs.remove(target);
94
+ }
95
+ }
96
+ if ((yield* fs.exists(path)) && (yield* fs.readDirectory(path)).length === 0) {
97
+ yield* fs.remove(path, { recursive: true });
98
+ }
99
+ });
@@ -6,6 +6,7 @@ import { SetupFailure } from "#failure.ts";
6
6
  import { gitOrFail } from "#git/command.ts";
7
7
  import { readOptionalText } from "#inventory/filesystem.ts";
8
8
  import { commandWords, hookCommand, type Launch, shellWords } from "./command.ts";
9
+ import { globalHooksPath, installForwarding } from "./forwarding.ts";
9
10
  import { hookLocation } from "./location.ts";
10
11
  import { hookShim, isHookShim } from "./shim.ts";
11
12
  import { globalHookStatus } from "./status.ts";
@@ -61,7 +62,8 @@ export const installHook = Effect.fn("Hooks.installHook")(function* (install: Ho
61
62
  const done = foreign ? `replaced ${location.file}, which was not quality's hook` : `installed the pre-commit hook at ${location.file}`;
62
63
  yield* Console.log(`quality: ${done}; each commit runs \`${words}\`.`);
63
64
  }
64
- const hooks = dirname(location.file);
65
+ const hooks =
66
+ (yield* globalHooksPath(config.root)) === "" ? dirname(location.file) : yield* installForwarding(location.file).pipe(Effect.mapError(failed));
65
67
  yield* gitOrFail(config.root, ["config", "--local", "core.hooksPath", hooks]);
66
68
  yield* Console.log(`quality: set repository-local core.hooksPath to ${hooks}.`);
67
69
  yield* globalHookStatus(location);
package/src/hooks/shim.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type HookCommand, shellWords } from "./command.ts";
2
2
 
3
- const OWNER = "# Installed by @shivaedev/quality: `quality hooks install` writes it, `quality hooks uninstall` removes it.";
3
+ export const OWNER = "# Installed by @shivaedev/quality: `quality hooks install` writes it, `quality hooks uninstall` removes it.";
4
4
 
5
5
  export function isHookShim(text: string): boolean {
6
6
  return text.split("\n").includes(OWNER);
@@ -14,6 +14,8 @@ export function hookShim(directory: string, command: HookCommand): string {
14
14
  return [
15
15
  "#!/bin/sh",
16
16
  OWNER,
17
+ 'if [ -n "$QUALITY_PRE_COMMIT_ACTIVE" ]; then exit 0; fi',
18
+ "export QUALITY_PRE_COMMIT_ACTIVE=1",
17
19
  'top="$(git rev-parse --show-toplevel)" || exit 2',
18
20
  'if [ -n "$GIT_DIR" ] && [ -z "$GIT_WORK_TREE" ]; then export GIT_WORK_TREE="$top"; fi',
19
21
  'cd "$top" || exit 2',
@@ -3,8 +3,9 @@ import { access, realpath } from "node:fs/promises";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { Console, Effect } from "effect";
5
5
  import { GateFailed, SetupFailure } from "#failure.ts";
6
- import { git, gitOrFail } from "#git/command.ts";
6
+ import { gitOrFail } from "#git/command.ts";
7
7
  import { readOptionalText } from "#inventory/filesystem.ts";
8
+ import { forwardingPath, globalHooksPath } from "./forwarding.ts";
8
9
  import { type HookLocation, hookLocation } from "./location.ts";
9
10
  import { isHookShim } from "./shim.ts";
10
11
 
@@ -20,11 +21,7 @@ function executable(file: string) {
20
21
  }
21
22
 
22
23
  export const globalHookStatus = Effect.fn("Hooks.globalHookStatus")(function* (location: HookLocation) {
23
- const global = yield* git(location.toplevel, ["config", "--global", "--includes", "--type=path", "--get", "core.hooksPath"]);
24
- if (global.code !== 0 && global.code !== 1) {
25
- return yield* new SetupFailure({ message: `cannot read global core.hooksPath: ${global.stderr.trim()}` });
26
- }
27
- const path = global.stdout.trim();
24
+ const path = yield* globalHooksPath(location.toplevel);
28
25
  const hook = resolve(location.toplevel, path, "pre-commit");
29
26
  const target = path === "" ? undefined : yield* executable(hook);
30
27
  const own = yield* executable(location.file);
@@ -45,6 +42,13 @@ export const hookStatus = Effect.fn("Hooks.hookStatus")(function* (cwd: string)
45
42
  );
46
43
  return yield* new GateFailed();
47
44
  }
45
+ const global = yield* globalHooksPath(cwd);
46
+ if (global !== "" && dirname(path) !== resolve(location.toplevel, global) && dirname(path) !== forwardingPath(location.file)) {
47
+ yield* Console.error(
48
+ "quality: the repository-local core.hooksPath hides global hooks other than pre-commit.\nhelp: run `quality hooks install` to keep all global hooks running.",
49
+ );
50
+ return yield* new GateFailed();
51
+ }
48
52
  yield* Console.log(`quality: git runs Quality's pre-commit hook at ${path}.`);
49
53
  yield* globalHookStatus(location);
50
54
  });
@@ -3,6 +3,7 @@ import { Console, Effect, FileSystem } from "effect";
3
3
  import { SetupFailure } from "#failure.ts";
4
4
  import { git, gitOrFail } from "#git/command.ts";
5
5
  import { readOptionalText } from "#inventory/filesystem.ts";
6
+ import { forwardingPath, removeForwarding } from "./forwarding.ts";
6
7
  import { hookLocation } from "./location.ts";
7
8
  import { isHookShim } from "./shim.ts";
8
9
 
@@ -18,7 +19,8 @@ export const uninstallHook = Effect.fn("Hooks.uninstallHook")(function* (cwd: st
18
19
  }
19
20
  yield* Effect.mapError(fs.remove(file), (error) => new SetupFailure({ message: `cannot remove ${file}: ${error.message}` }));
20
21
  const local = yield* git(cwd, ["config", "--local", "--get", "core.hooksPath"]);
21
- if (local.stdout.trim() === dirname(file)) {
22
+ yield* removeForwarding(file).pipe(Effect.mapError((error) => new SetupFailure({ message: error.message })));
23
+ if ([dirname(file), forwardingPath(file)].includes(local.stdout.trim())) {
22
24
  yield* gitOrFail(cwd, ["config", "--local", "--unset-all", "core.hooksPath"]);
23
25
  }
24
26
  yield* Console.log(`quality: removed the pre-commit hook at ${file}.`);
@@ -0,0 +1,55 @@
1
+ export function blockedExport(exports: unknown, subpath: string): boolean {
2
+ if (typeof exports !== "object" || exports === null || Array.isArray(exports)) {
3
+ return false;
4
+ }
5
+ const entries = Object.entries(exports);
6
+ const exact = entries.find(([key]) => key === subpath);
7
+ if (exact !== undefined) {
8
+ return exact[1] === null;
9
+ }
10
+ const matching = entries.filter(([key]) => {
11
+ const [prefix = "", suffix, ...rest] = key.split("*");
12
+ return (
13
+ suffix !== undefined
14
+ && rest.length === 0
15
+ && subpath.length > prefix.length + suffix.length
16
+ && subpath.startsWith(prefix)
17
+ && subpath.endsWith(suffix)
18
+ );
19
+ });
20
+ const best = matching.toSorted(([left], [right]) => right.indexOf("*") - left.indexOf("*") || right.length - left.length)[0];
21
+ return best?.[1] === null;
22
+ }
23
+
24
+ export function exportTargets(value: unknown): string[] {
25
+ if (typeof value === "string") {
26
+ return [value];
27
+ }
28
+ return typeof value === "object" && value !== null ? Object.values(value).flatMap(exportTargets) : [];
29
+ }
30
+
31
+ export function exportedModule(exports: unknown, local: string): boolean {
32
+ if (typeof exports !== "object" || exports === null) {
33
+ return false;
34
+ }
35
+ const paths = [
36
+ `./${local}`,
37
+ `./${local.replace(/^src\//u, "dist/").replace(/\.(?<mode>[cm]?)tsx?$/u, ".$<mode>js")}`,
38
+ `./${local.replace(/^src\//u, "dist/").replace(/\.(?<mode>[cm]?)tsx?$/u, ".d.$<mode>ts")}`,
39
+ ];
40
+ return Object.entries(exports).some(([key, value]) =>
41
+ exportTargets(value).some((target) =>
42
+ paths.some((path) => {
43
+ const [prefix = "", suffix] = target.split("*");
44
+ if (suffix === undefined) {
45
+ return path === target && !blockedExport(exports, key);
46
+ }
47
+ if (!(path.startsWith(prefix) && path.endsWith(suffix)) || path.length <= prefix.length + suffix.length) {
48
+ return false;
49
+ }
50
+ const star = path.slice(prefix.length, path.length - suffix.length);
51
+ return !blockedExport(exports, key.replace("*", star));
52
+ }),
53
+ ),
54
+ );
55
+ }
@@ -0,0 +1,27 @@
1
+ import { posix } from "node:path";
2
+ import type { RuleInputs } from "#rule.ts";
3
+ import type { ImportGraph } from "./graph.ts";
4
+ import { exportedModule } from "./packageExports.ts";
5
+ import { packageOf } from "./workspace.ts";
6
+
7
+ export async function publicModules(inputs: RuleInputs, graph: ImportGraph): Promise<ReadonlySet<string>> {
8
+ const exports = new Map(
9
+ await Promise.all(
10
+ graph.packages.map(async (owner) => {
11
+ const manifest: { readonly exports?: unknown } = JSON.parse((await inputs.readText(posix.join(owner.directory, "package.json"))) ?? "");
12
+ return [owner, manifest.exports] as const;
13
+ }),
14
+ ),
15
+ );
16
+ const modules = new Set<string>();
17
+ for (const edge of graph.edges) {
18
+ if (edge.to.kind !== "file") {
19
+ continue;
20
+ }
21
+ const owner = packageOf(graph.packages, edge.to.path);
22
+ if (owner !== undefined && exportedModule(exports.get(owner), posix.relative(owner.directory, edge.to.path))) {
23
+ modules.add(edge.to.path);
24
+ }
25
+ }
26
+ return modules;
27
+ }
@@ -1,66 +1,11 @@
1
1
  import { posix } from "node:path";
2
2
  import { outgoing } from "#imports/fences/reach.ts";
3
3
  import type { ImportGraph } from "#imports/graph.ts";
4
+ import { blockedExport, exportedModule, exportTargets } from "#imports/packageExports.ts";
4
5
  import { packageOf, type WorkspacePackage } from "#imports/workspace.ts";
5
6
 
6
- function blocked(exports: unknown, subpath: string): boolean {
7
- if (typeof exports !== "object" || exports === null || Array.isArray(exports)) {
8
- return false;
9
- }
10
- const entries = Object.entries(exports);
11
- const exact = entries.find(([key]) => key === subpath);
12
- if (exact !== undefined) {
13
- return exact[1] === null;
14
- }
15
- const matching = entries.filter(([key]) => {
16
- const [prefix = "", suffix, ...rest] = key.split("*");
17
- return (
18
- suffix !== undefined
19
- && rest.length === 0
20
- && subpath.length > prefix.length + suffix.length
21
- && subpath.startsWith(prefix)
22
- && subpath.endsWith(suffix)
23
- );
24
- });
25
- const best = matching.toSorted(([left], [right]) => right.indexOf("*") - left.indexOf("*") || right.length - left.length)[0];
26
- return best?.[1] === null;
27
- }
28
-
29
- function targets(value: unknown): string[] {
30
- if (typeof value === "string") {
31
- return [value];
32
- }
33
- return typeof value === "object" && value !== null ? Object.values(value).flatMap(targets) : [];
34
- }
35
-
36
- function exported(exports: unknown, local: string): boolean {
37
- if (typeof exports !== "object" || exports === null) {
38
- return false;
39
- }
40
- const paths = [
41
- `./${local}`,
42
- `./${local.replace(/^src\//u, "dist/").replace(/\.(?<mode>[cm]?)tsx?$/u, ".$<mode>js")}`,
43
- `./${local.replace(/^src\//u, "dist/").replace(/\.(?<mode>[cm]?)tsx?$/u, ".d.$<mode>ts")}`,
44
- ];
45
- return Object.entries(exports).some(([key, value]) =>
46
- targets(value).some((target) =>
47
- paths.some((path) => {
48
- const [prefix = "", suffix] = target.split("*");
49
- if (suffix === undefined) {
50
- return path === target && !blocked(exports, key);
51
- }
52
- if (!(path.startsWith(prefix) && path.endsWith(suffix)) || path.length <= prefix.length + suffix.length) {
53
- return false;
54
- }
55
- const star = path.slice(prefix.length, path.length - suffix.length);
56
- return !blocked(exports, key.replace("*", star));
57
- }),
58
- ),
59
- );
60
- }
61
-
62
7
  function executable(manifest: object, local: string): boolean {
63
- const binTargets = targets(Reflect.get(manifest, "bin"));
8
+ const binTargets = exportTargets(Reflect.get(manifest, "bin"));
64
9
  const built = local.replace(/^src\//u, "dist/").replace(/\.(?<mode>[cm]?)tsx?$/u, ".$<mode>js");
65
10
  return binTargets.some((target) => typeof target === "string" && [local, built].includes(posix.normalize(target)));
66
11
  }
@@ -72,8 +17,8 @@ export function bundleInputs(graph: ImportGraph, owner: WorkspacePackage, manife
72
17
  const local = posix.relative(owner.directory, path);
73
18
  return (
74
19
  local.startsWith("src/")
75
- && blocked(Reflect.get(manifest, "exports"), `./${local.slice(4)}`)
76
- && !exported(Reflect.get(manifest, "exports"), local)
20
+ && blockedExport(Reflect.get(manifest, "exports"), `./${local.slice(4)}`)
21
+ && !exportedModule(Reflect.get(manifest, "exports"), local)
77
22
  );
78
23
  }),
79
24
  );
@@ -1,5 +1,6 @@
1
1
  import { Effect, Schema } from "effect";
2
2
  import { importGraph } from "#imports/graph.ts";
3
+ import { publicModules } from "#imports/publicModules.ts";
3
4
  import { isTestSupport } from "#naming/testName.ts";
4
5
  import { defineRule } from "#rule.ts";
5
6
  import { matchesAny } from "./matchesAny.ts";
@@ -14,18 +15,24 @@ export const supportImports = defineRule({
14
15
  return [];
15
16
  }
16
17
  const graph = await importGraph(inputs);
18
+ const exported = await publicModules(inputs, graph);
17
19
  return graph.edges
18
20
  .filter(
19
- (edge) => isTestSupport(edge.from) && edge.to.kind === "file" && !isTestSupport(edge.to.path) && !matchesAny(edge.to.path, options.modules),
21
+ (edge) =>
22
+ isTestSupport(edge.from)
23
+ && edge.to.kind === "file"
24
+ && !isTestSupport(edge.to.path)
25
+ && !exported.has(edge.to.path)
26
+ && !matchesAny(edge.to.path, options.modules),
20
27
  )
21
28
  .map((edge) => ({
22
29
  file: edge.from,
23
30
  line: edge.line,
24
- message: `Test-support imports "${edge.specifier}" outside its allowed public surface. Drive the app through its host, schemas or queue definitions; list those public module paths in tests/support-imports.options.modules in the repository's quality config.`,
31
+ message: `Test-support imports "${edge.specifier}" outside its allowed public surface. Drive the app through its host, schemas or queue definitions, or a workspace package's exports; list additional internal module paths in tests/support-imports.options.modules in the repository's quality config.`,
25
32
  subject: edge.specifier,
26
33
  }));
27
34
  },
28
- description: "Test-support reaches repository modules only through the public surface named by the repository's module allowlist.",
35
+ description: "Test-support reaches repository modules through workspace package exports and the repository's additional module allowlist.",
29
36
  id: "tests/support-imports",
30
37
  options: Schema.toStandardSchemaV1(SupportImportsOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
31
38
  });