lintcn 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -8
- package/README.md +74 -21
- package/dist/cache.d.ts +6 -2
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +66 -23
- package/dist/cli.js +11 -5
- package/dist/codegen.d.ts +1 -3
- package/dist/codegen.d.ts.map +1 -1
- package/dist/codegen.js +35 -18
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +112 -74
- package/dist/commands/clean.d.ts +2 -0
- package/dist/commands/clean.d.ts.map +1 -0
- package/dist/commands/clean.js +44 -0
- package/dist/commands/lint.d.ts.map +1 -1
- package/dist/commands/lint.js +25 -9
- package/dist/commands/remove.d.ts.map +1 -1
- package/dist/commands/remove.js +6 -14
- package/dist/discover.d.ts +3 -3
- package/dist/discover.d.ts.map +1 -1
- package/dist/discover.js +32 -23
- package/dist/hash.d.ts +7 -1
- package/dist/hash.d.ts.map +1 -1
- package/dist/hash.js +28 -25
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +4 -2
- package/src/cache.ts +75 -25
- package/src/cli.ts +12 -5
- package/src/codegen.ts +38 -18
- package/src/commands/add.ts +134 -70
- package/src/commands/clean.ts +48 -0
- package/src/commands/lint.ts +26 -9
- package/src/commands/remove.ts +6 -16
- package/src/discover.ts +39 -30
- package/src/hash.ts +33 -27
- package/src/index.ts +1 -1
- package/LICENSE +0 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,70 @@
|
|
|
1
|
-
## 0.
|
|
1
|
+
## 0.6.0
|
|
2
|
+
|
|
3
|
+
1. **Rules now live in subfolders** — each rule is its own Go package under `.lintcn/{rule_name}/`, replacing the old flat `.lintcn/*.go` layout. This eliminates the need to rename `options.go` and `schema.json` companions — they stay in the subfolder with their original names, and the Go package name matches the folder. `lintcn add` now fetches the entire rule folder automatically.
|
|
2
4
|
|
|
3
|
-
1. **Simpler rule imports** — rules now import from `pkg/rule`, `pkg/utils`, etc. instead of `internal/rule`. The `internal/` child-module-path hack is gone. Your `.go` files use clean import paths:
|
|
4
|
-
```go
|
|
5
|
-
import (
|
|
6
|
-
"github.com/typescript-eslint/tsgolint/pkg/rule"
|
|
7
|
-
"github.com/typescript-eslint/tsgolint/pkg/utils"
|
|
8
|
-
)
|
|
9
5
|
```
|
|
6
|
+
.lintcn/
|
|
7
|
+
no_floating_promises/
|
|
8
|
+
no_floating_promises.go
|
|
9
|
+
no_floating_promises_test.go
|
|
10
|
+
options.go ← original name, no renaming
|
|
11
|
+
schema.json
|
|
12
|
+
my_custom_rule/
|
|
13
|
+
my_custom_rule.go
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
2. **`lintcn add` fetches whole folders** — both folder URLs (`/tree/`) and file URLs (`/blob/`) now fetch every `.go` and `.json` file in the rule's directory. Passing a file URL auto-detects the parent folder:
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
```bash
|
|
19
|
+
# folder URL
|
|
20
|
+
lintcn add https://github.com/oxc-project/tsgolint/tree/main/internal/rules/no_floating_promises
|
|
21
|
+
|
|
22
|
+
# file URL — auto-fetches the whole folder
|
|
23
|
+
lintcn add https://github.com/oxc-project/tsgolint/blob/main/internal/rules/await_thenable/await_thenable.go
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
3. **Error for flat `.go` files in `.lintcn/`** — if leftover flat files from older versions are detected, lintcn now prints a clear migration error with instructions instead of silently ignoring them.
|
|
27
|
+
|
|
28
|
+
4. **Reproducible builds with `-trimpath`** — the Go binary is now built with `-trimpath`, stripping absolute paths from the output. Binaries are identical across machines for the same rule content + tsgolint version + platform.
|
|
29
|
+
|
|
30
|
+
5. **Faster cache hits** — Go version removed from the content hash. The compiled binary is a standalone executable with no Go runtime dependency, so the Go version used to build it doesn't affect correctness. Also excludes `_test.go` files from the hash since tests don't affect the binary.
|
|
31
|
+
|
|
32
|
+
6. **Go compilation output is live** — `go build` now inherits stdio, so compilation progress and errors stream directly to the terminal instead of being silently captured.
|
|
33
|
+
|
|
34
|
+
7. **First-build guidance** — on first compile (cold Go cache), lintcn explains the one-time 30s cost and shows which directories to cache in CI:
|
|
35
|
+
```
|
|
36
|
+
Compiling custom tsgolint binary (first build — may take 30s+ to compile dependencies)...
|
|
37
|
+
Subsequent builds will be fast (~1s). In CI, cache ~/.cache/lintcn/ and GOCACHE (run `go env GOCACHE`).
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
8. **GitHub Actions example** — README now includes a copy-paste workflow that caches the compiled binary. Subsequent CI runs take ~12s (vs ~4min cold):
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
- name: Cache lintcn binary + Go build cache
|
|
44
|
+
uses: actions/cache@v4
|
|
45
|
+
with:
|
|
46
|
+
path: |
|
|
47
|
+
~/.cache/lintcn
|
|
48
|
+
~/go/pkg
|
|
49
|
+
key: lintcn-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.lintcn/**/*.go') }}
|
|
50
|
+
restore-keys: lintcn-${{ runner.os }}-${{ runner.arch }}-
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 0.5.0
|
|
54
|
+
|
|
55
|
+
1. **Security fix — path traversal in `--tsgolint-version`** — the version flag is now validated against a strict pattern. Previously a value like `../../etc` could escape the cache directory.
|
|
56
|
+
|
|
57
|
+
2. **Fixed intermittent failures with concurrent `lintcn lint` runs** — build workspaces are now per-content-hash instead of shared. Two processes running simultaneously no longer corrupt each other's build.
|
|
58
|
+
|
|
59
|
+
3. **Cross-platform tar extraction** — replaced shell `tar` command with the npm `tar` package. Works on Windows without needing system tar.
|
|
60
|
+
|
|
61
|
+
4. **No more `patch` command required** — tsgolint downloads now use a fork with a clean `internal/runner.Run()` entry point. Zero modifications to existing tsgolint files; upstream syncs will never conflict.
|
|
62
|
+
|
|
63
|
+
5. **Downloads no longer hang** — 120s timeout on all GitHub tarball downloads.
|
|
64
|
+
|
|
65
|
+
6. **Fixed broken `.tsgolint` symlink** — `lintcn add` now correctly detects and recreates broken symlinks.
|
|
66
|
+
|
|
67
|
+
## 0.4.0
|
|
12
68
|
|
|
13
69
|
## 0.3.0
|
|
14
70
|
|
package/README.md
CHANGED
|
@@ -13,8 +13,11 @@ npm install -D lintcn
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
# Add a rule
|
|
17
|
-
npx lintcn add https://github.com/
|
|
16
|
+
# Add a rule folder from tsgolint
|
|
17
|
+
npx lintcn add https://github.com/oxc-project/tsgolint/tree/main/internal/rules/no_floating_promises
|
|
18
|
+
|
|
19
|
+
# Add by file URL (auto-fetches the whole folder)
|
|
20
|
+
npx lintcn add https://github.com/oxc-project/tsgolint/blob/main/internal/rules/await_thenable/await_thenable.go
|
|
18
21
|
|
|
19
22
|
# Lint your project
|
|
20
23
|
npx lintcn lint
|
|
@@ -26,21 +29,33 @@ npx lintcn lint --tsconfig tsconfig.build.json
|
|
|
26
29
|
npx lintcn list
|
|
27
30
|
|
|
28
31
|
# Remove a rule
|
|
29
|
-
npx lintcn remove no-
|
|
32
|
+
npx lintcn remove no-floating-promises
|
|
33
|
+
|
|
34
|
+
# Clean cached tsgolint source + binaries
|
|
35
|
+
npx lintcn clean
|
|
30
36
|
```
|
|
31
37
|
|
|
38
|
+
Browse all 50+ available built-in rules in the [tsgolint rules directory](https://github.com/oxc-project/tsgolint/tree/main/internal/rules).
|
|
39
|
+
|
|
32
40
|
## How it works
|
|
33
41
|
|
|
34
|
-
|
|
42
|
+
Each rule lives in its own subfolder under `.lintcn/`. You own the source — edit, customize, delete.
|
|
35
43
|
|
|
36
44
|
```
|
|
37
45
|
my-project/
|
|
38
46
|
├── .lintcn/
|
|
39
|
-
│ ├── .gitignore
|
|
40
|
-
│ ├──
|
|
41
|
-
│
|
|
47
|
+
│ ├── .gitignore ← ignores generated Go files
|
|
48
|
+
│ ├── no_floating_promises/
|
|
49
|
+
│ │ ├── no_floating_promises.go ← rule source (committed)
|
|
50
|
+
│ │ ├── no_floating_promises_test.go ← tests (committed)
|
|
51
|
+
│ │ ├── options.go ← rule options struct
|
|
52
|
+
│ │ └── schema.json ← options schema
|
|
53
|
+
│ ├── await_thenable/
|
|
54
|
+
│ │ ├── await_thenable.go
|
|
55
|
+
│ │ └── await_thenable_test.go
|
|
56
|
+
│ └── my_custom_rule/
|
|
57
|
+
│ └── my_custom_rule.go
|
|
42
58
|
├── src/
|
|
43
|
-
│ ├── index.ts
|
|
44
59
|
│ └── ...
|
|
45
60
|
├── tsconfig.json
|
|
46
61
|
└── package.json
|
|
@@ -48,22 +63,32 @@ my-project/
|
|
|
48
63
|
|
|
49
64
|
When you run `npx lintcn lint`, the CLI:
|
|
50
65
|
|
|
51
|
-
1. Scans `.lintcn
|
|
52
|
-
2. Generates a Go workspace with
|
|
66
|
+
1. Scans `.lintcn/*/` subfolders for rule definitions
|
|
67
|
+
2. Generates a Go workspace with your custom rules
|
|
53
68
|
3. Compiles a custom binary (cached — rebuilds only when rules change)
|
|
54
69
|
4. Runs the binary against your project
|
|
55
70
|
|
|
56
|
-
|
|
71
|
+
You can run `lintcn lint` from any subdirectory — it walks up to find `.lintcn/` and lints the cwd project.
|
|
72
|
+
|
|
73
|
+
## Writing custom rules
|
|
74
|
+
|
|
75
|
+
To help AI agents write and modify rules, install the lintcn skill:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx skills add remorses/lintcn
|
|
79
|
+
```
|
|
57
80
|
|
|
58
|
-
|
|
81
|
+
This gives your AI agent the full tsgolint rule API reference — AST visitors, type checker, reporting, fixes, and testing patterns.
|
|
59
82
|
|
|
60
|
-
|
|
83
|
+
Every rule lives in a subfolder under `.lintcn/` with the package name matching the folder:
|
|
61
84
|
|
|
62
85
|
```go
|
|
86
|
+
// .lintcn/no_unhandled_error/no_unhandled_error.go
|
|
87
|
+
|
|
63
88
|
// lintcn:name no-unhandled-error
|
|
64
89
|
// lintcn:description Disallow discarding Error-typed return values
|
|
65
90
|
|
|
66
|
-
package
|
|
91
|
+
package no_unhandled_error
|
|
67
92
|
|
|
68
93
|
import (
|
|
69
94
|
"github.com/microsoft/typescript-go/shim/ast"
|
|
@@ -134,7 +159,7 @@ void getUser("id")
|
|
|
134
159
|
```json
|
|
135
160
|
{
|
|
136
161
|
"devDependencies": {
|
|
137
|
-
"lintcn": "0.
|
|
162
|
+
"lintcn": "0.5.0"
|
|
138
163
|
}
|
|
139
164
|
}
|
|
140
165
|
```
|
|
@@ -145,17 +170,45 @@ Each lintcn release bundles a specific tsgolint version. Updating lintcn can cha
|
|
|
145
170
|
2. Run `npx lintcn build` after updating to verify your rules still compile
|
|
146
171
|
3. Fix any compilation errors before committing
|
|
147
172
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
173
|
+
## CI Setup
|
|
174
|
+
|
|
175
|
+
The first `lintcn lint` compiles a custom Go binary (~30s). Subsequent runs use the cached binary (<1s). Cache `~/.cache/lintcn/` and Go's build cache to keep CI fast.
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
# .github/workflows/lint.yml
|
|
179
|
+
name: Lint
|
|
180
|
+
on: [push, pull_request]
|
|
181
|
+
|
|
182
|
+
jobs:
|
|
183
|
+
lint:
|
|
184
|
+
runs-on: ubuntu-latest
|
|
185
|
+
steps:
|
|
186
|
+
- uses: actions/checkout@v4
|
|
187
|
+
|
|
188
|
+
- uses: actions/setup-node@v4
|
|
189
|
+
with:
|
|
190
|
+
node-version: 22
|
|
191
|
+
|
|
192
|
+
- name: Cache lintcn binary + Go build cache
|
|
193
|
+
uses: actions/cache@v4
|
|
194
|
+
with:
|
|
195
|
+
path: |
|
|
196
|
+
~/.cache/lintcn
|
|
197
|
+
~/go/pkg
|
|
198
|
+
key: lintcn-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.lintcn/**/*.go') }}
|
|
199
|
+
restore-keys: |
|
|
200
|
+
lintcn-${{ runner.os }}-${{ runner.arch }}-
|
|
201
|
+
|
|
202
|
+
- run: npm ci
|
|
203
|
+
- run: npx lintcn lint
|
|
152
204
|
```
|
|
153
205
|
|
|
206
|
+
The cache key includes a hash of your rule files — when rules change, the binary is recompiled. The `restore-keys` fallback ensures Go's build cache is still used even when rules change, so recompilation takes ~1s instead of 30s.
|
|
207
|
+
|
|
154
208
|
## Prerequisites
|
|
155
209
|
|
|
156
210
|
- **Node.js** — for the CLI
|
|
157
|
-
- **Go
|
|
158
|
-
- **Git** — for cloning tsgolint source on first build
|
|
211
|
+
- **Go** — for compiling rules (`go.dev/dl`)
|
|
159
212
|
|
|
160
213
|
Go is only needed for `lintcn lint` / `lintcn build`. Adding and listing rules works without Go.
|
|
161
214
|
|
package/dist/cache.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
export declare const DEFAULT_TSGOLINT_VERSION = "
|
|
1
|
+
export declare const DEFAULT_TSGOLINT_VERSION = "e945641eabec22993eda3e7c101692e80417e0ea";
|
|
2
|
+
/** Validate version string to prevent path traversal attacks.
|
|
3
|
+
* Only allows alphanumeric chars, dots, underscores, and hyphens. */
|
|
4
|
+
export declare function validateVersion(version: string): void;
|
|
2
5
|
export declare function getCacheDir(): string;
|
|
3
6
|
export declare function getTsgolintSourceDir(version: string): string;
|
|
4
7
|
export declare function getBinDir(): string;
|
|
5
8
|
export declare function getBinaryPath(contentHash: string): string;
|
|
6
|
-
|
|
9
|
+
/** Per-hash build directory to avoid races between concurrent lintcn processes. */
|
|
10
|
+
export declare function getBuildDir(contentHash: string): string;
|
|
7
11
|
export declare function ensureTsgolintSource(version: string): Promise<string>;
|
|
8
12
|
export declare function cachedBinaryExists(contentHash: string): boolean;
|
|
9
13
|
//# sourceMappingURL=cache.d.ts.map
|
package/dist/cache.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAoBA,eAAO,MAAM,wBAAwB,6CAA6C,CAAA;AAUlF;sEACsE;AACtE,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAOrD;AAED,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEvD;AA0DD,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuE3E;AAED,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAQ/D"}
|
package/dist/cache.js
CHANGED
|
@@ -4,21 +4,33 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Cache layout:
|
|
6
6
|
// ~/.cache/lintcn/tsgolint/<version>/ — extracted source (read-only)
|
|
7
|
+
// ~/.cache/lintcn/build/<content-hash>/ — per-hash build workspace (no race)
|
|
7
8
|
// ~/.cache/lintcn/bin/<content-hash> — compiled binaries
|
|
8
9
|
import fs from 'node:fs';
|
|
9
10
|
import os from 'node:os';
|
|
10
11
|
import path from 'node:path';
|
|
12
|
+
import crypto from 'node:crypto';
|
|
11
13
|
import { pipeline } from 'node:stream/promises';
|
|
14
|
+
import { extract } from 'tar';
|
|
12
15
|
import { execAsync } from "./exec.js";
|
|
13
16
|
// Pinned tsgolint fork commit — updated with each lintcn release.
|
|
14
|
-
// Uses remorses/tsgolint fork which
|
|
15
|
-
//
|
|
16
|
-
export const DEFAULT_TSGOLINT_VERSION = '
|
|
17
|
+
// Uses remorses/tsgolint fork which adds internal/runner.Run().
|
|
18
|
+
// Only 1 commit on top of upstream — zero modifications to existing files.
|
|
19
|
+
export const DEFAULT_TSGOLINT_VERSION = 'e945641eabec22993eda3e7c101692e80417e0ea';
|
|
17
20
|
// Pinned typescript-go base commit from microsoft/typescript-go (before patches).
|
|
18
21
|
// Patches from tsgolint/patches/ are applied on top during setup.
|
|
19
|
-
// This is the upstream commit the tsgolint submodule was forked from.
|
|
20
22
|
// Must be updated when DEFAULT_TSGOLINT_VERSION changes.
|
|
21
23
|
const TYPESCRIPT_GO_COMMIT = '1b7eabe122e1575a0df9c77eccdf4e063c623224';
|
|
24
|
+
// Strict pattern for version strings — prevents path traversal via ../
|
|
25
|
+
const VERSION_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
26
|
+
/** Validate version string to prevent path traversal attacks.
|
|
27
|
+
* Only allows alphanumeric chars, dots, underscores, and hyphens. */
|
|
28
|
+
export function validateVersion(version) {
|
|
29
|
+
if (!VERSION_PATTERN.test(version)) {
|
|
30
|
+
throw new Error(`Invalid tsgolint version "${version}". ` +
|
|
31
|
+
'Version must only contain alphanumeric characters, dots, underscores, and hyphens.');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
22
34
|
export function getCacheDir() {
|
|
23
35
|
return path.join(os.homedir(), '.cache', 'lintcn');
|
|
24
36
|
}
|
|
@@ -31,24 +43,47 @@ export function getBinDir() {
|
|
|
31
43
|
export function getBinaryPath(contentHash) {
|
|
32
44
|
return path.join(getBinDir(), contentHash);
|
|
33
45
|
}
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
/** Per-hash build directory to avoid races between concurrent lintcn processes. */
|
|
47
|
+
export function getBuildDir(contentHash) {
|
|
48
|
+
return path.join(getCacheDir(), 'build', contentHash);
|
|
36
49
|
}
|
|
37
50
|
/** Download a tarball from URL and extract it to targetDir.
|
|
51
|
+
* Uses the `tar` npm package for cross-platform support (no shell tar needed).
|
|
38
52
|
* GitHub tarballs have a top-level directory like `repo-ref/`,
|
|
39
53
|
* so we strip the first path component during extraction. */
|
|
40
54
|
async function downloadAndExtract(url, targetDir) {
|
|
41
|
-
const
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timeout = setTimeout(() => {
|
|
57
|
+
controller.abort(new Error(`Download timed out after 120s: ${url}`));
|
|
58
|
+
}, 120_000);
|
|
59
|
+
let response;
|
|
60
|
+
try {
|
|
61
|
+
response = await fetch(url, { signal: controller.signal });
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
clearTimeout(timeout);
|
|
65
|
+
}
|
|
42
66
|
if (!response.ok || !response.body) {
|
|
43
67
|
throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
|
|
44
68
|
}
|
|
45
|
-
|
|
46
|
-
const tmpTarGz = path.join(os.tmpdir(), `lintcn-${
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
// download to temp file with random suffix to avoid collisions
|
|
70
|
+
const tmpTarGz = path.join(os.tmpdir(), `lintcn-${crypto.randomBytes(8).toString('hex')}.tar.gz`);
|
|
71
|
+
try {
|
|
72
|
+
const fileStream = fs.createWriteStream(tmpTarGz);
|
|
73
|
+
// @ts-ignore ReadableStream vs NodeJS.ReadableStream mismatch
|
|
74
|
+
await pipeline(response.body, fileStream);
|
|
75
|
+
// extract with npm tar package (cross-platform, no shell tar needed)
|
|
76
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
77
|
+
await extract({
|
|
78
|
+
file: tmpTarGz,
|
|
79
|
+
cwd: targetDir,
|
|
80
|
+
strip: 1,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
// always clean up temp file
|
|
85
|
+
fs.rmSync(tmpTarGz, { force: true });
|
|
86
|
+
}
|
|
52
87
|
}
|
|
53
88
|
/** Apply git-format patches using `patch -p1` (no git required).
|
|
54
89
|
* Patches are standard unified diff format, `patch` ignores the git metadata. */
|
|
@@ -63,12 +98,17 @@ async function applyPatches(patchesDir, targetDir) {
|
|
|
63
98
|
return patches.length;
|
|
64
99
|
}
|
|
65
100
|
export async function ensureTsgolintSource(version) {
|
|
101
|
+
validateVersion(version);
|
|
66
102
|
const sourceDir = getTsgolintSourceDir(version);
|
|
67
103
|
const readyMarker = path.join(sourceDir, '.lintcn-ready');
|
|
68
104
|
if (fs.existsSync(readyMarker)) {
|
|
69
105
|
return sourceDir;
|
|
70
106
|
}
|
|
71
|
-
//
|
|
107
|
+
// Use a temp directory for the download, then atomic rename on success.
|
|
108
|
+
// This prevents concurrent processes from seeing partial state, and
|
|
109
|
+
// avoids the "non-empty dir on retry" problem.
|
|
110
|
+
const tmpDir = path.join(getCacheDir(), 'tsgolint', `.tmp-${version}-${crypto.randomBytes(4).toString('hex')}`);
|
|
111
|
+
// clean up any partial previous attempt
|
|
72
112
|
if (fs.existsSync(sourceDir)) {
|
|
73
113
|
fs.rmSync(sourceDir, { recursive: true });
|
|
74
114
|
}
|
|
@@ -76,14 +116,14 @@ export async function ensureTsgolintSource(version) {
|
|
|
76
116
|
// download tsgolint fork tarball
|
|
77
117
|
console.log(`Downloading tsgolint@${version.slice(0, 8)}...`);
|
|
78
118
|
const tsgolintUrl = `https://github.com/remorses/tsgolint/archive/${version}.tar.gz`;
|
|
79
|
-
await downloadAndExtract(tsgolintUrl,
|
|
119
|
+
await downloadAndExtract(tsgolintUrl, tmpDir);
|
|
80
120
|
// download typescript-go from microsoft (base commit before patches)
|
|
81
|
-
const tsGoDir = path.join(
|
|
121
|
+
const tsGoDir = path.join(tmpDir, 'typescript-go');
|
|
82
122
|
console.log('Downloading typescript-go...');
|
|
83
123
|
const tsGoUrl = `https://github.com/microsoft/typescript-go/archive/${TYPESCRIPT_GO_COMMIT}.tar.gz`;
|
|
84
124
|
await downloadAndExtract(tsGoUrl, tsGoDir);
|
|
85
125
|
// apply tsgolint's patches to typescript-go
|
|
86
|
-
const patchesDir = path.join(
|
|
126
|
+
const patchesDir = path.join(tmpDir, 'patches');
|
|
87
127
|
if (fs.existsSync(patchesDir)) {
|
|
88
128
|
const count = await applyPatches(patchesDir, tsGoDir);
|
|
89
129
|
if (count > 0) {
|
|
@@ -91,7 +131,7 @@ export async function ensureTsgolintSource(version) {
|
|
|
91
131
|
}
|
|
92
132
|
}
|
|
93
133
|
// copy internal/collections from typescript-go (required by tsgolint, done by `just init`)
|
|
94
|
-
const collectionsDir = path.join(
|
|
134
|
+
const collectionsDir = path.join(tmpDir, 'internal', 'collections');
|
|
95
135
|
const tsGoCollections = path.join(tsGoDir, 'internal', 'collections');
|
|
96
136
|
if (fs.existsSync(tsGoCollections)) {
|
|
97
137
|
fs.mkdirSync(collectionsDir, { recursive: true });
|
|
@@ -103,13 +143,16 @@ export async function ensureTsgolintSource(version) {
|
|
|
103
143
|
}
|
|
104
144
|
}
|
|
105
145
|
// write ready marker
|
|
106
|
-
fs.writeFileSync(
|
|
146
|
+
fs.writeFileSync(path.join(tmpDir, '.lintcn-ready'), new Date().toISOString());
|
|
147
|
+
// atomic rename: move completed dir to final location
|
|
148
|
+
fs.mkdirSync(path.dirname(sourceDir), { recursive: true });
|
|
149
|
+
fs.renameSync(tmpDir, sourceDir);
|
|
107
150
|
console.log('tsgolint source ready');
|
|
108
151
|
}
|
|
109
152
|
catch (err) {
|
|
110
|
-
// clean up partial
|
|
111
|
-
if (fs.existsSync(
|
|
112
|
-
fs.rmSync(
|
|
153
|
+
// clean up partial temp directory
|
|
154
|
+
if (fs.existsSync(tmpDir)) {
|
|
155
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
113
156
|
}
|
|
114
157
|
throw err;
|
|
115
158
|
}
|
package/dist/cli.js
CHANGED
|
@@ -7,16 +7,17 @@ import { addRule } from "./commands/add.js";
|
|
|
7
7
|
import { lint, buildBinary } from "./commands/lint.js";
|
|
8
8
|
import { listRules } from "./commands/list.js";
|
|
9
9
|
import { removeRule } from "./commands/remove.js";
|
|
10
|
+
import { clean } from "./commands/clean.js";
|
|
10
11
|
import { DEFAULT_TSGOLINT_VERSION } from "./cache.js";
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
12
13
|
const packageJson = require('../package.json');
|
|
13
14
|
const cli = goke('lintcn');
|
|
14
15
|
cli
|
|
15
|
-
.command('add <url>', 'Add a rule by URL. Fetches the
|
|
16
|
-
.example('# Add a rule
|
|
17
|
-
.example('lintcn add https://github.com/
|
|
18
|
-
.example('# Add
|
|
19
|
-
.example('lintcn add https://
|
|
16
|
+
.command('add <url>', 'Add a rule by GitHub URL. Fetches the whole folder into .lintcn/{rule}/')
|
|
17
|
+
.example('# Add a rule folder')
|
|
18
|
+
.example('lintcn add https://github.com/oxc-project/tsgolint/tree/main/internal/rules/no_floating_promises')
|
|
19
|
+
.example('# Add by file URL (auto-fetches the whole folder)')
|
|
20
|
+
.example('lintcn add https://github.com/oxc-project/tsgolint/blob/main/internal/rules/await_thenable/await_thenable.go')
|
|
20
21
|
.action(async (url) => {
|
|
21
22
|
await addRule(url);
|
|
22
23
|
});
|
|
@@ -67,6 +68,11 @@ cli
|
|
|
67
68
|
const binaryPath = await buildBinary({ rebuild: !!options.rebuild, tsgolintVersion });
|
|
68
69
|
console.log(binaryPath);
|
|
69
70
|
});
|
|
71
|
+
cli
|
|
72
|
+
.command('clean', 'Remove cached tsgolint source and compiled binaries to free disk space')
|
|
73
|
+
.action(() => {
|
|
74
|
+
clean();
|
|
75
|
+
});
|
|
70
76
|
cli.help();
|
|
71
77
|
cli.version(packageJson.version);
|
|
72
78
|
cli.parse();
|
package/dist/codegen.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { RuleMetadata } from './discover.ts';
|
|
2
2
|
/** Generate .lintcn/go.work and .lintcn/go.mod for editor/gopls support. */
|
|
3
3
|
export declare function generateEditorGoFiles(lintcnDir: string): void;
|
|
4
|
-
/** Generate build workspace for compiling the custom binary.
|
|
5
|
-
* With pkg/runner.Run(), the generated main.go is a static template —
|
|
6
|
-
* no regex surgery or file copying needed. */
|
|
4
|
+
/** Generate build workspace for compiling the custom binary. */
|
|
7
5
|
export declare function generateBuildWorkspace({ buildDir, tsgolintDir, lintcnDir, rules, }: {
|
|
8
6
|
buildDir: string;
|
|
9
7
|
tsgolintDir: string;
|
package/dist/codegen.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codegen.d.ts","sourceRoot":"","sources":["../src/codegen.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"codegen.d.ts","sourceRoot":"","sources":["../src/codegen.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AA4BjD,4EAA4E;AAC5E,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAmC7D;AAED,gEAAgE;AAChE,wBAAgB,sBAAsB,CAAC,EACrC,QAAQ,EACR,WAAW,EACX,SAAS,EACT,KAAK,GACN,EAAE;IACD,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,YAAY,EAAE,CAAA;CACtB,GAAG,IAAI,CA2CP"}
|
package/dist/codegen.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// Generate Go workspace files for building a custom tsgolint binary.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Uses internal/runner.Run() from the fork — codegen is just a static
|
|
3
|
+
// main.go template + go.work with shim replaces.
|
|
4
|
+
//
|
|
5
|
+
// Key: module names must be child paths of github.com/typescript-eslint/tsgolint
|
|
6
|
+
// so Go allows importing internal/ packages across the module boundary.
|
|
5
7
|
import fs from 'node:fs';
|
|
6
8
|
import path from 'node:path';
|
|
7
9
|
// Shim modules that need replace directives in go.work.
|
|
8
|
-
// These redirect module paths to local directories inside the tsgolint source.
|
|
9
10
|
const SHIM_MODULES = [
|
|
10
11
|
'ast',
|
|
11
12
|
'bundled',
|
|
@@ -22,6 +23,7 @@ const SHIM_MODULES = [
|
|
|
22
23
|
'vfs/cachedvfs',
|
|
23
24
|
'vfs/osvfs',
|
|
24
25
|
];
|
|
26
|
+
const TSGOLINT_MODULE = 'github.com/typescript-eslint/tsgolint';
|
|
25
27
|
function generateReplaceDirectives(tsgolintRelPath) {
|
|
26
28
|
return SHIM_MODULES.map((mod) => {
|
|
27
29
|
return `\tgithub.com/microsoft/typescript-go/shim/${mod} => ${tsgolintRelPath}/shim/${mod}`;
|
|
@@ -41,8 +43,9 @@ replace (
|
|
|
41
43
|
${generateReplaceDirectives('./.tsgolint')}
|
|
42
44
|
)
|
|
43
45
|
`;
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
+
// Module name is a child path of tsgolint — this is required so Go allows
|
|
47
|
+
// importing internal/ packages across the module boundary in a workspace.
|
|
48
|
+
const goMod = `module ${TSGOLINT_MODULE}/lintcn-rules
|
|
46
49
|
|
|
47
50
|
go 1.26
|
|
48
51
|
`;
|
|
@@ -59,9 +62,7 @@ go.sum
|
|
|
59
62
|
fs.writeFileSync(gitignorePath, gitignore);
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
|
-
/** Generate build workspace for compiling the custom binary.
|
|
63
|
-
* With pkg/runner.Run(), the generated main.go is a static template —
|
|
64
|
-
* no regex surgery or file copying needed. */
|
|
65
|
+
/** Generate build workspace for compiling the custom binary. */
|
|
65
66
|
export function generateBuildWorkspace({ buildDir, tsgolintDir, lintcnDir, rules, }) {
|
|
66
67
|
fs.mkdirSync(path.join(buildDir, 'wrapper'), { recursive: true });
|
|
67
68
|
// symlink tsgolint source
|
|
@@ -91,21 +92,37 @@ ${generateReplaceDirectives('./tsgolint')}
|
|
|
91
92
|
)
|
|
92
93
|
`;
|
|
93
94
|
fs.writeFileSync(path.join(buildDir, 'go.work'), goWork);
|
|
94
|
-
// wrapper
|
|
95
|
-
const wrapperGoMod = `module lintcn-wrapper
|
|
95
|
+
// wrapper module — child path of tsgolint for internal/ access
|
|
96
|
+
const wrapperGoMod = `module ${TSGOLINT_MODULE}/lintcn-wrapper
|
|
96
97
|
|
|
97
98
|
go 1.26
|
|
98
99
|
`;
|
|
99
100
|
fs.writeFileSync(path.join(buildDir, 'wrapper', 'go.mod'), wrapperGoMod);
|
|
100
|
-
// wrapper/main.go —
|
|
101
|
+
// wrapper/main.go — static template
|
|
101
102
|
const mainGo = generateMainGo(rules);
|
|
102
103
|
fs.writeFileSync(path.join(buildDir, 'wrapper', 'main.go'), mainGo);
|
|
103
104
|
}
|
|
104
|
-
/**
|
|
105
|
-
*
|
|
105
|
+
/** Sanitize a package name into a valid Go identifier for use as an import alias.
|
|
106
|
+
* Replaces hyphens/dots with underscores, prepends _ if starts with a digit. */
|
|
107
|
+
function toGoAlias(pkg) {
|
|
108
|
+
let alias = pkg.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
109
|
+
if (/^[0-9]/.test(alias)) {
|
|
110
|
+
alias = '_' + alias;
|
|
111
|
+
}
|
|
112
|
+
return alias;
|
|
113
|
+
}
|
|
114
|
+
/** Generate main.go that imports user rules and calls internal/runner.Run().
|
|
115
|
+
* Each rule subfolder is its own Go package, imported by package name. */
|
|
106
116
|
function generateMainGo(rules) {
|
|
117
|
+
// Deduplicate imports by package name (in case a subfolder has multiple rules)
|
|
118
|
+
const uniquePackages = [...new Set(rules.map((r) => { return r.packageName; }))];
|
|
119
|
+
const imports = uniquePackages.map((pkg) => {
|
|
120
|
+
const alias = toGoAlias(pkg);
|
|
121
|
+
return `\t${alias} "${TSGOLINT_MODULE}/lintcn-rules/${pkg}"`;
|
|
122
|
+
}).join('\n');
|
|
107
123
|
const ruleEntries = rules.map((r) => {
|
|
108
|
-
|
|
124
|
+
const alias = toGoAlias(r.packageName);
|
|
125
|
+
return `\t\t${alias}.${r.varName},`;
|
|
109
126
|
}).join('\n');
|
|
110
127
|
return `// Code generated by lintcn. DO NOT EDIT.
|
|
111
128
|
package main
|
|
@@ -113,9 +130,9 @@ package main
|
|
|
113
130
|
import (
|
|
114
131
|
\t"os"
|
|
115
132
|
|
|
116
|
-
\t"
|
|
117
|
-
\t"
|
|
118
|
-
|
|
133
|
+
\t"${TSGOLINT_MODULE}/internal/rule"
|
|
134
|
+
\t"${TSGOLINT_MODULE}/internal/runner"
|
|
135
|
+
${imports}
|
|
119
136
|
)
|
|
120
137
|
|
|
121
138
|
func main() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AAwHA,wBAAsB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4ExD"}
|