@howells/lint 0.1.3 → 0.1.5
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 +60 -3
- package/bin/howells-lint-strict.mjs +42 -0
- package/bin/howells-workspace-check.mjs +25 -0
- package/bin/howells-workspace-fix.mjs +6 -0
- package/bin/run-manypkg-command.mjs +40 -0
- package/bin/workspace-preflight.mjs +91 -0
- package/package.json +33 -26
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ The goal is not to invent a second lint philosophy. The goal is to:
|
|
|
6
6
|
|
|
7
7
|
- pin a single `@biomejs/biome` version
|
|
8
8
|
- pin a single `ultracite` version
|
|
9
|
+
- pin a single `@manypkg/cli` version for monorepo consistency checks
|
|
9
10
|
- give every consumer the same small preset matrix
|
|
10
11
|
- discourage repo-local overrides unless the project has a genuinely unique constraint
|
|
11
12
|
|
|
@@ -60,24 +61,46 @@ Next.js app:
|
|
|
60
61
|
|
|
61
62
|
## Binaries
|
|
62
63
|
|
|
63
|
-
Installers only need `@howells/lint` as a direct dependency. Use the package binaries instead of adding `@biomejs/biome` or `
|
|
64
|
+
Installers only need `@howells/lint` as a direct dependency. Use the package binaries instead of adding `@biomejs/biome`, `ultracite`, or `@manypkg/cli` separately:
|
|
64
65
|
|
|
65
66
|
- `howells-biome` proxies to the pinned Biome binary
|
|
66
67
|
- `howells-ultracite` proxies to the pinned Ultracite binary
|
|
67
68
|
- `howells-lint` defaults to `biome check .`
|
|
69
|
+
- `howells-lint-strict` runs the high-signal Biome security, correctness, and suspicious lint rules
|
|
68
70
|
- `howells-format` defaults to `biome check . --write`
|
|
71
|
+
- `howells-workspace-check` validates root workspace hygiene, then runs `manypkg check`
|
|
72
|
+
- `howells-workspace-fix` defaults to `manypkg fix`
|
|
69
73
|
|
|
70
74
|
Example scripts:
|
|
71
75
|
|
|
72
76
|
```json
|
|
73
77
|
{
|
|
74
78
|
"scripts": {
|
|
75
|
-
"lint": "howells-lint",
|
|
76
|
-
"lint:fix": "howells-format"
|
|
79
|
+
"lint": "howells-lint .",
|
|
80
|
+
"lint:fix": "howells-format .",
|
|
81
|
+
"lint:strict": "howells-lint-strict ."
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
```
|
|
80
85
|
|
|
86
|
+
Keep `lint` non-mutating. Put all `--write` behavior in `lint:fix` or `format` so CI and local checks have the same semantics.
|
|
87
|
+
|
|
88
|
+
Monorepo root scripts should compose package linting with workspace validation:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"scripts": {
|
|
93
|
+
"lint": "turbo run lint && howells-workspace-check",
|
|
94
|
+
"lint:fix": "turbo run lint:fix && howells-workspace-fix",
|
|
95
|
+
"lint:strict": "turbo run lint:strict"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`howells-workspace-check` expects workspace roots to declare `packageManager: "pnpm@..."`, require Node 20+ in `engines.node`, and keep `pnpm-workspace.yaml` present when using workspace package directories.
|
|
101
|
+
|
|
102
|
+
CI should call `pnpm lint` or `pnpm check` so these root checks are not bypassed by a direct `turbo lint` command.
|
|
103
|
+
|
|
81
104
|
Prefer explicit script targets over config churn when the only difference is scope:
|
|
82
105
|
|
|
83
106
|
```json
|
|
@@ -96,6 +119,40 @@ Prefer explicit script targets over config churn when the only difference is sco
|
|
|
96
119
|
- If multiple repos need the same exception, add or adjust a preset here.
|
|
97
120
|
- If a repo needs framework-specific linting, choose the matching preset instead of layering rules manually.
|
|
98
121
|
- Prefer inline `biome-ignore` comments for truly isolated exceptions over broad config overrides.
|
|
122
|
+
- Keep package `lint` scripts read-only; use `lint:fix` for formatting and safe writes.
|
|
123
|
+
- Prefer `howells-lint .` over raw `biome check` or long target lists unless a package has a real scope constraint.
|
|
124
|
+
|
|
125
|
+
## Claude Code Hooks
|
|
126
|
+
|
|
127
|
+
Add this to `.claude/settings.json` so files are formatted on edit and linted on session end:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"hooks": {
|
|
132
|
+
"PostToolUse": [
|
|
133
|
+
{
|
|
134
|
+
"matcher": "Edit|Write",
|
|
135
|
+
"hooks": [
|
|
136
|
+
{
|
|
137
|
+
"type": "command",
|
|
138
|
+
"command": "jq -r '.tool_input.file_path' | { read file_path; case \"$file_path\" in *.js|*.ts|*.jsx|*.tsx|*.json|*.jsonc|*.css|*.graphql) howells-format \"$file_path\" 2>/dev/null || true ;; esac; }"
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
],
|
|
143
|
+
"Stop": [
|
|
144
|
+
{
|
|
145
|
+
"hooks": [
|
|
146
|
+
{
|
|
147
|
+
"type": "command",
|
|
148
|
+
"command": "git diff --name-only --diff-filter=d HEAD | grep -E '\\.(js|ts|jsx|tsx|json|jsonc|css|graphql)$' | xargs howells-format 2>/dev/null || true"
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
99
156
|
|
|
100
157
|
## Upstream
|
|
101
158
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runPackageBin } from "./run-package-bin.mjs";
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const passthroughOptions = new Set(["--help", "-h", "--version", "-V"]);
|
|
7
|
+
const targets =
|
|
8
|
+
args.length === 0 || args[0].startsWith("-") ? [".", ...args] : args;
|
|
9
|
+
|
|
10
|
+
const strictRuleOptions = [
|
|
11
|
+
"--only=security",
|
|
12
|
+
"--skip=security/noSecrets",
|
|
13
|
+
"--only=correctness/noConstAssign",
|
|
14
|
+
"--only=correctness/noUnreachable",
|
|
15
|
+
"--only=correctness/noInvalidConstructorSuper",
|
|
16
|
+
"--only=correctness/noSetterReturn",
|
|
17
|
+
"--only=correctness/noUnsafeFinally",
|
|
18
|
+
"--only=correctness/noUnsafeOptionalChaining",
|
|
19
|
+
"--only=correctness/noGlobalObjectCalls",
|
|
20
|
+
"--only=correctness/noSelfAssign",
|
|
21
|
+
"--only=correctness/noSwitchDeclarations",
|
|
22
|
+
"--only=suspicious/noDebugger",
|
|
23
|
+
"--only=suspicious/noDoubleEquals",
|
|
24
|
+
"--only=suspicious/noExplicitAny",
|
|
25
|
+
"--only=suspicious/noCatchAssign",
|
|
26
|
+
"--only=suspicious/noFunctionAssign",
|
|
27
|
+
"--only=suspicious/noGlobalAssign",
|
|
28
|
+
"--only=suspicious/noRedeclare",
|
|
29
|
+
"--only=suspicious/noSparseArray",
|
|
30
|
+
"--only=suspicious/noVar",
|
|
31
|
+
"--only=suspicious/noDuplicateCase",
|
|
32
|
+
"--only=suspicious/noDuplicateObjectKeys",
|
|
33
|
+
"--only=suspicious/noDuplicateParameters",
|
|
34
|
+
"--only=suspicious/noFallthroughSwitchClause",
|
|
35
|
+
"--only=suspicious/noFocusedTests",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const resolvedArgs = passthroughOptions.has(args[0])
|
|
39
|
+
? args
|
|
40
|
+
: ["lint", ...strictRuleOptions, ...targets];
|
|
41
|
+
|
|
42
|
+
runPackageBin("@biomejs/biome", "biome", resolvedArgs);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { handleManypkgMetadataCommand } from "./run-manypkg-command.mjs";
|
|
4
|
+
import { runPackageBin } from "./run-package-bin.mjs";
|
|
5
|
+
import { runWorkspacePreflight } from "./workspace-preflight.mjs";
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
|
|
9
|
+
if (handleManypkgMetadataCommand("check", args)) {
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const errors = runWorkspacePreflight();
|
|
14
|
+
|
|
15
|
+
if (errors.length > 0) {
|
|
16
|
+
console.error("Workspace hygiene check failed:");
|
|
17
|
+
|
|
18
|
+
for (const error of errors) {
|
|
19
|
+
console.error(`- ${error}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
runPackageBin("@manypkg/cli", "manypkg", ["check", ...args]);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { runPackageBin } from "./run-package-bin.mjs";
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const helpOptions = new Set(["--help", "-h"]);
|
|
8
|
+
const versionOptions = new Set(["--version", "-V"]);
|
|
9
|
+
|
|
10
|
+
export function printManypkgCommandHelp(command) {
|
|
11
|
+
console.log(`Usage: howells-workspace-${command} [options]\n`);
|
|
12
|
+
console.log(`Runs: manypkg ${command} [options]`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function printManypkgCliVersion() {
|
|
16
|
+
const { version } = require("@manypkg/cli/package.json");
|
|
17
|
+
console.log(version);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function handleManypkgMetadataCommand(command, args) {
|
|
21
|
+
if (helpOptions.has(args[0])) {
|
|
22
|
+
printManypkgCommandHelp(command);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (versionOptions.has(args[0])) {
|
|
27
|
+
printManypkgCliVersion();
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function runManypkgCommand(command, args) {
|
|
35
|
+
if (handleManypkgMetadataCommand(command, args)) {
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
runPackageBin("@manypkg/cli", "manypkg", [command, ...args]);
|
|
40
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const workspaceDirs = ["apps", "packages", "services", "workers", "examples"];
|
|
5
|
+
|
|
6
|
+
function readRootPackageJson() {
|
|
7
|
+
const packageJsonPath = join(process.cwd(), "package.json");
|
|
8
|
+
|
|
9
|
+
if (!existsSync(packageJsonPath)) {
|
|
10
|
+
return {
|
|
11
|
+
errors: ["root package.json is missing"],
|
|
12
|
+
packageJson: undefined,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
return {
|
|
18
|
+
errors: [],
|
|
19
|
+
packageJson: JSON.parse(readFileSync(packageJsonPath, "utf8")),
|
|
20
|
+
};
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return {
|
|
23
|
+
errors: [`root package.json could not be parsed: ${error.message}`],
|
|
24
|
+
packageJson: undefined,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hasWorkspaceChildren(directory) {
|
|
30
|
+
const directoryPath = join(process.cwd(), directory);
|
|
31
|
+
|
|
32
|
+
if (!existsSync(directoryPath)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return readdirSync(directoryPath, { withFileTypes: true }).some((entry) => {
|
|
37
|
+
return (
|
|
38
|
+
entry.isDirectory() &&
|
|
39
|
+
existsSync(join(directoryPath, entry.name, "package.json"))
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasLikelyWorkspaceLayout(packageJson) {
|
|
45
|
+
return (
|
|
46
|
+
Boolean(packageJson?.workspaces) || workspaceDirs.some(hasWorkspaceChildren)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isNode20Engine(range) {
|
|
51
|
+
if (typeof range !== "string") {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const normalizedRange = range.replaceAll(/\s+/g, "");
|
|
56
|
+
|
|
57
|
+
if (/(^|[<>=~^|])1[0-9](\.|[^0-9]|$)/.test(normalizedRange)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return />=?20(\.|[^0-9]|$)|\^20(\.|[^0-9]|$)|~20(\.|[^0-9]|$)/.test(
|
|
62
|
+
normalizedRange,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function runWorkspacePreflight() {
|
|
67
|
+
const { errors, packageJson } = readRootPackageJson();
|
|
68
|
+
|
|
69
|
+
if (!packageJson) {
|
|
70
|
+
return errors;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof packageJson.packageManager !== "string") {
|
|
74
|
+
errors.push("root package.json must declare packageManager");
|
|
75
|
+
} else if (!packageJson.packageManager.startsWith("pnpm@")) {
|
|
76
|
+
errors.push("root package.json packageManager must use pnpm");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!isNode20Engine(packageJson.engines?.node)) {
|
|
80
|
+
errors.push("root package.json engines.node must require Node 20+");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
hasLikelyWorkspaceLayout(packageJson) &&
|
|
85
|
+
!existsSync(join(process.cwd(), "pnpm-workspace.yaml"))
|
|
86
|
+
) {
|
|
87
|
+
errors.push("pnpm-workspace.yaml is required for workspace projects");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return errors;
|
|
91
|
+
}
|
package/package.json
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
2
|
+
"name": "@howells/lint",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Pinned Biome and Ultracite presets for Howells projects.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20.19"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"biome/*.json",
|
|
11
|
+
"bin/*.mjs",
|
|
12
|
+
"README.md",
|
|
13
|
+
"MIGRATIONS.md"
|
|
14
|
+
],
|
|
15
|
+
"bin": {
|
|
16
|
+
"howells-biome": "bin/howells-biome.mjs",
|
|
17
|
+
"howells-lint": "bin/howells-lint.mjs",
|
|
18
|
+
"howells-lint-strict": "bin/howells-lint-strict.mjs",
|
|
19
|
+
"howells-format": "bin/howells-format.mjs",
|
|
20
|
+
"howells-ultracite": "bin/howells-ultracite.mjs",
|
|
21
|
+
"howells-workspace-check": "bin/howells-workspace-check.mjs",
|
|
22
|
+
"howells-workspace-fix": "bin/howells-workspace-fix.mjs"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@biomejs/biome": "2.4.12",
|
|
26
|
+
"@manypkg/cli": "^0.25.1",
|
|
27
|
+
"ultracite": "7.6.0"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
"./package.json": "./package.json",
|
|
31
|
+
"./biome/core": "./biome/core.json",
|
|
32
|
+
"./biome/react": "./biome/react.json",
|
|
33
|
+
"./biome/next": "./biome/next.json"
|
|
34
|
+
}
|
|
28
35
|
}
|