typescript-bedrock 0.1.6 → 0.1.9
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 +1 -0
- package/files/.github/workflows/typescript.yml +1 -1
- package/files/.just/typescript.just +3 -3
- package/files/base/.github/workflows/base.yml +1 -1
- package/files/base/justfile +2 -1
- package/oxlintrc.json +15 -1
- package/package.json +1 -1
- package/src/adopt/rewrite-script.ts +2 -2
- package/src/adopt/template.ts +2 -2
- package/src/cli.ts +4 -0
- package/src/lint.ts +138 -0
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ Shared tooling for strict Bun and TypeScript 7 repos. It holds the configs each
|
|
|
14
14
|
| Command | Does |
|
|
15
15
|
| --- | --- |
|
|
16
16
|
| `typescript-bedrock adopt <target>` | Set up a repo. `--help` lists the options. |
|
|
17
|
+
| `typescript-bedrock lint [oxlint args]` | Run oxlint. A type-aware run takes the lock `/tmp/typescript-bedrock-lint.lock`, so one runs at a time on the machine and a second one prints that it waits |
|
|
17
18
|
| `typescript-bedrock guard` | Fail on suppression comments, other package managers, TypeScript drift, unsorted catalogs, package API without JSDoc, and files that differ from what `adopt` writes |
|
|
18
19
|
|
|
19
20
|
`adopt` writes `.just/typescript.just` and `.github/workflows/typescript.yml` beside the files every bedrock shares (`files/base`, a copy of `base/`). Git hooks, Markdown linting, and `AGENTS.md` come from the shared part; `just setup` points git at `.githooks`. `adopt` upserts its own `typescript-bedrock` block in `AGENTS.md`, `.gitignore`, `.gitattributes`, `.editorconfig`, and `.github/dependabot.yml` and keeps the rest of each file. It keeps every file it replaces or removes as `<file>.bak`, and a second `adopt` plans nothing.
|
|
@@ -19,13 +19,13 @@ format-check:
|
|
|
19
19
|
typecheck:
|
|
20
20
|
bun check
|
|
21
21
|
|
|
22
|
-
# Run oxlint. The base justfile lints Markdown.
|
|
22
|
+
# Run oxlint, one type-aware run at a time on the machine. The base justfile lints Markdown.
|
|
23
23
|
lint:
|
|
24
|
-
bun run
|
|
24
|
+
bun run typescript-bedrock lint
|
|
25
25
|
|
|
26
26
|
# Apply oxlint's fixes.
|
|
27
27
|
lint-fix:
|
|
28
|
-
bun run
|
|
28
|
+
bun run typescript-bedrock lint --fix
|
|
29
29
|
|
|
30
30
|
# Run the tests.
|
|
31
31
|
test:
|
package/files/base/justfile
CHANGED
|
@@ -10,10 +10,11 @@ mod? rust ".just/rust.just"
|
|
|
10
10
|
mod? swift ".just/swift.just"
|
|
11
11
|
mod? cpp ".just/cpp.just"
|
|
12
12
|
mod? jvm ".just/jvm.just"
|
|
13
|
+
mod? go ".just/go.just"
|
|
13
14
|
|
|
14
15
|
import? "project.just"
|
|
15
16
|
|
|
16
|
-
languages := "typescript rust swift cpp jvm"
|
|
17
|
+
languages := "typescript rust swift cpp jvm go"
|
|
17
18
|
|
|
18
19
|
# List the recipes.
|
|
19
20
|
default:
|
package/oxlintrc.json
CHANGED
|
@@ -36,6 +36,15 @@
|
|
|
36
36
|
}
|
|
37
37
|
],
|
|
38
38
|
"typescript/dot-notation": "error",
|
|
39
|
+
"typescript/no-misused-promises": [
|
|
40
|
+
"error",
|
|
41
|
+
{
|
|
42
|
+
"checksVoidReturn": {
|
|
43
|
+
"arguments": false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"typescript/strict-void-return": "off",
|
|
39
48
|
"typescript/prefer-readonly-parameter-types": "off",
|
|
40
49
|
"eslint/arrow-body-style": ["error", "as-needed"],
|
|
41
50
|
"eslint/grouped-accessor-pairs": "error",
|
|
@@ -133,7 +142,12 @@
|
|
|
133
142
|
"typescript/prefer-return-this-type": "error",
|
|
134
143
|
"typescript/prefer-string-starts-ends-with": "error",
|
|
135
144
|
"typescript/unified-signatures": "error",
|
|
136
|
-
"unicorn/catch-error-name":
|
|
145
|
+
"unicorn/catch-error-name": [
|
|
146
|
+
"error",
|
|
147
|
+
{
|
|
148
|
+
"ignore": ["^_?\\$$"]
|
|
149
|
+
}
|
|
150
|
+
],
|
|
137
151
|
"unicorn/consistent-date-clone": "error",
|
|
138
152
|
"unicorn/consistent-existence-index-check": "error",
|
|
139
153
|
"unicorn/error-message": "error",
|
package/package.json
CHANGED
|
@@ -60,11 +60,11 @@ const TOOLS: ReadonlyMap<string, (args: Tokens) => Tokens> = new Map<
|
|
|
60
60
|
([sub = "", ...args]) =>
|
|
61
61
|
sub === "format"
|
|
62
62
|
? ["bun", "fmt", ...(args.includes("--write") ? [] : ["--check"])]
|
|
63
|
-
: ["
|
|
63
|
+
: ["typescript-bedrock", "lint", ...withFix(args)],
|
|
64
64
|
],
|
|
65
65
|
["cross-env", (args) => rewriteCommand(args)],
|
|
66
66
|
["esno", runOnBun],
|
|
67
|
-
["eslint", (args) => ["
|
|
67
|
+
["eslint", (args) => ["typescript-bedrock", "lint", ...withFix(args)]],
|
|
68
68
|
["jest", (args) => ["bun", "test", ...args]],
|
|
69
69
|
["node", runOnBun],
|
|
70
70
|
["npm", (args) => packageManager("npm", args)],
|
package/src/adopt/template.ts
CHANGED
|
@@ -8,9 +8,9 @@ const SCRIPTS = {
|
|
|
8
8
|
format: "bun fmt",
|
|
9
9
|
"format:check": "bun fmt --check",
|
|
10
10
|
guard: "typescript-bedrock guard",
|
|
11
|
-
lint: "bun run --sequential --no-exit-on-error
|
|
11
|
+
lint: "bun run --sequential --no-exit-on-error 'typescript-bedrock lint' markdownlint-cli2",
|
|
12
12
|
"lint:fix":
|
|
13
|
-
"bun run --sequential --no-exit-on-error '
|
|
13
|
+
"bun run --sequential --no-exit-on-error 'typescript-bedrock lint --fix' 'markdownlint-cli2 --fix'",
|
|
14
14
|
test: "bun test --pass-with-no-tests",
|
|
15
15
|
typecheck: "bun check",
|
|
16
16
|
verify: "bun run --sequential format:check typecheck lint guard test",
|
package/src/cli.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { adopt } from "./adopt/command.ts";
|
|
3
3
|
import { guard } from "./guard.ts";
|
|
4
|
+
import { lint } from "./lint.ts";
|
|
4
5
|
|
|
5
6
|
const USAGE = `Usage: typescript-bedrock <command> [args]
|
|
6
7
|
|
|
7
8
|
Commands:
|
|
8
9
|
adopt <target> Set up a repo with typescript-bedrock. See \`adopt --help\`.
|
|
10
|
+
lint [oxlint args] Run oxlint. A type-aware run waits for the machine-wide lock,
|
|
11
|
+
so one runs at a time on this machine.
|
|
9
12
|
guard Report suppression comments, foreign lockfiles and scripts,
|
|
10
13
|
TypeScript drift, and unsorted catalogs, and files that differ
|
|
11
14
|
from what adopt writes.`;
|
|
@@ -13,6 +16,7 @@ Commands:
|
|
|
13
16
|
const COMMANDS: Record<string, (args: string[]) => Promise<number>> = {
|
|
14
17
|
adopt,
|
|
15
18
|
guard: () => guard(process.cwd()),
|
|
19
|
+
lint,
|
|
16
20
|
};
|
|
17
21
|
|
|
18
22
|
const [name = "", ...args] = Bun.argv.slice(2);
|
package/src/lint.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
/** Flags that print information and lint nothing, so they need no lock. */
|
|
7
|
+
const QUERY_FLAGS = new Set([
|
|
8
|
+
"-h",
|
|
9
|
+
"--help",
|
|
10
|
+
"-V",
|
|
11
|
+
"--version",
|
|
12
|
+
"--rules",
|
|
13
|
+
"--print-config",
|
|
14
|
+
"--init",
|
|
15
|
+
"--lsp",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const POLL_MS = 500;
|
|
19
|
+
/** A path every user and sandbox on the machine sees, so all runs share one lock. */
|
|
20
|
+
const LOCK_ROOT = process.platform === "win32" ? tmpdir() : "/tmp";
|
|
21
|
+
|
|
22
|
+
/** The directory that holds the machine-wide type-aware lint lock. */
|
|
23
|
+
export const LOCK_PATH = join(LOCK_ROOT, "typescript-bedrock-lint.lock");
|
|
24
|
+
|
|
25
|
+
/** Options of {@link withLock}. */
|
|
26
|
+
export interface LockOptions {
|
|
27
|
+
/** Receives the line that says why the run waits; prints to stderr by default. */
|
|
28
|
+
onWait?: (message: string) => void;
|
|
29
|
+
/** Milliseconds between two tries; 500 by default. */
|
|
30
|
+
pollMs?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether oxlint runs type-aware rules for these arguments. The shared config
|
|
35
|
+
* turns `typeAware` on, so every run lints with tsgolint except a query.
|
|
36
|
+
*/
|
|
37
|
+
export const isTypeAware = (args: readonly string[]): boolean =>
|
|
38
|
+
!args.some((arg) => QUERY_FLAGS.has(arg));
|
|
39
|
+
|
|
40
|
+
const ownerOf = async (path: string): Promise<number | undefined> => {
|
|
41
|
+
const text = await Bun.file(join(path, "pid"))
|
|
42
|
+
.text()
|
|
43
|
+
.catch(() => "");
|
|
44
|
+
const pid = Number(text);
|
|
45
|
+
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const isAlive = (pid: number): boolean => {
|
|
49
|
+
try {
|
|
50
|
+
process.kill(pid, 0);
|
|
51
|
+
return true;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// EPERM means the process exists and belongs to another user.
|
|
54
|
+
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const isExists = (error: unknown): boolean =>
|
|
59
|
+
error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
60
|
+
|
|
61
|
+
/** Takes the lock, or returns the pid that holds it. */
|
|
62
|
+
const tryAcquire = async (path: string): Promise<number | undefined> => {
|
|
63
|
+
try {
|
|
64
|
+
await mkdir(path);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (!isExists(error)) {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
const owner = await ownerOf(path);
|
|
70
|
+
if (owner !== undefined && !isAlive(owner)) {
|
|
71
|
+
await rm(path, { recursive: true, force: true });
|
|
72
|
+
return tryAcquire(path);
|
|
73
|
+
}
|
|
74
|
+
return owner ?? 0;
|
|
75
|
+
}
|
|
76
|
+
await Bun.write(join(path, "pid"), String(process.pid));
|
|
77
|
+
return undefined;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Runs `task` while holding the lock at `path`. A second caller waits, prints
|
|
82
|
+
* that it waits, and runs once the first finishes; a lock whose owner is gone
|
|
83
|
+
* is broken.
|
|
84
|
+
*/
|
|
85
|
+
export const withLock = async <T>(
|
|
86
|
+
path: string,
|
|
87
|
+
task: () => Promise<T>,
|
|
88
|
+
{ onWait = console.error, pollMs = POLL_MS }: LockOptions = {},
|
|
89
|
+
): Promise<T> => {
|
|
90
|
+
const acquire = async (announced: boolean): Promise<void> => {
|
|
91
|
+
const holder = await tryAcquire(path);
|
|
92
|
+
if (holder === undefined) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!announced) {
|
|
96
|
+
onWait(
|
|
97
|
+
`typescript-bedrock: waiting for the type-aware lint that process ${holder} runs; one runs at a time on this machine.`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
await Bun.sleep(pollMs);
|
|
101
|
+
return acquire(true);
|
|
102
|
+
};
|
|
103
|
+
await acquire(false);
|
|
104
|
+
try {
|
|
105
|
+
return await task();
|
|
106
|
+
} finally {
|
|
107
|
+
await rm(path, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const oxlintBin = (): string =>
|
|
112
|
+
join(
|
|
113
|
+
dirname(fileURLToPath(import.meta.resolve("oxlint/package.json"))),
|
|
114
|
+
"bin/oxlint",
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const runOxlint = (args: readonly string[]): Promise<number> => {
|
|
118
|
+
const child = Bun.spawn(["bun", oxlintBin(), ...args], {
|
|
119
|
+
stdin: "inherit",
|
|
120
|
+
stdout: "inherit",
|
|
121
|
+
stderr: "inherit",
|
|
122
|
+
});
|
|
123
|
+
const stop = () => {
|
|
124
|
+
child.kill();
|
|
125
|
+
};
|
|
126
|
+
process.once("SIGINT", stop);
|
|
127
|
+
process.once("SIGTERM", stop);
|
|
128
|
+
return child.exited;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Runs oxlint with `args`. A type-aware run takes the machine-wide lock first,
|
|
133
|
+
* because each one loads a whole TypeScript program into memory.
|
|
134
|
+
*/
|
|
135
|
+
export const lint = (args: readonly string[]): Promise<number> =>
|
|
136
|
+
isTypeAware(args)
|
|
137
|
+
? withLock(LOCK_PATH, () => runOxlint(args))
|
|
138
|
+
: runOxlint(args);
|