lintcn 0.4.0 → 0.5.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 +13 -9
- package/README.md +5 -4
- package/dist/cache.d.ts +6 -2
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +66 -23
- package/dist/codegen.d.ts +1 -3
- package/dist/codegen.d.ts.map +1 -1
- package/dist/codegen.js +17 -17
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +10 -3
- package/dist/commands/lint.d.ts.map +1 -1
- package/dist/commands/lint.js +6 -5
- 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/codegen.ts +18 -17
- package/src/commands/add.ts +9 -3
- package/src/commands/lint.ts +6 -5
- package/src/index.ts +1 -1
- package/LICENSE +0 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
## 0.
|
|
1
|
+
## 0.5.0
|
|
2
2
|
|
|
3
|
-
1. **
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
)
|
|
9
|
-
```
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
3. **Cross-platform tar extraction** — replaced shell `tar` command with the npm `tar` package. Works on Windows without needing system tar.
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
5. **Downloads no longer hang** — 120s timeout on all GitHub tarball downloads.
|
|
12
|
+
|
|
13
|
+
6. **Fixed broken `.tsgolint` symlink** — `lintcn add` now correctly detects and recreates broken symlinks.
|
|
14
|
+
|
|
15
|
+
## 0.4.0
|
|
12
16
|
|
|
13
17
|
## 0.3.0
|
|
14
18
|
|
package/README.md
CHANGED
|
@@ -49,10 +49,12 @@ my-project/
|
|
|
49
49
|
When you run `npx lintcn lint`, the CLI:
|
|
50
50
|
|
|
51
51
|
1. Scans `.lintcn/*.go` for rule definitions
|
|
52
|
-
2. Generates a Go workspace with
|
|
52
|
+
2. Generates a Go workspace with your custom rules
|
|
53
53
|
3. Compiles a custom binary (cached — rebuilds only when rules change)
|
|
54
54
|
4. Runs the binary against your project
|
|
55
55
|
|
|
56
|
+
You can run `lintcn lint` from any subdirectory — it walks up to find `.lintcn/` and lints the cwd project.
|
|
57
|
+
|
|
56
58
|
## Writing a rule
|
|
57
59
|
|
|
58
60
|
Every rule is a Go file with `package lintcn` that exports a `rule.Rule` variable.
|
|
@@ -134,7 +136,7 @@ void getUser("id")
|
|
|
134
136
|
```json
|
|
135
137
|
{
|
|
136
138
|
"devDependencies": {
|
|
137
|
-
"lintcn": "0.
|
|
139
|
+
"lintcn": "0.4.0"
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
142
|
```
|
|
@@ -154,8 +156,7 @@ npx lintcn lint --tsgolint-version v0.10.0
|
|
|
154
156
|
## Prerequisites
|
|
155
157
|
|
|
156
158
|
- **Node.js** — for the CLI
|
|
157
|
-
- **Go
|
|
158
|
-
- **Git** — for cloning tsgolint source on first build
|
|
159
|
+
- **Go** — for compiling rules (`go.dev/dl`)
|
|
159
160
|
|
|
160
161
|
Go is only needed for `lintcn lint` / `lintcn build`. Adding and listing rules works without Go.
|
|
161
162
|
|
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/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,18 +92,17 @@ ${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
|
-
/** Generate
|
|
105
|
-
* This is a static template — no copying or patching of tsgolint source. */
|
|
105
|
+
/** Generate main.go that imports user rules and calls internal/runner.Run(). */
|
|
106
106
|
function generateMainGo(rules) {
|
|
107
107
|
const ruleEntries = rules.map((r) => {
|
|
108
108
|
return `\t\tlintcn.${r.varName},`;
|
|
@@ -113,9 +113,9 @@ package main
|
|
|
113
113
|
import (
|
|
114
114
|
\t"os"
|
|
115
115
|
|
|
116
|
-
\t"
|
|
117
|
-
\t"
|
|
118
|
-
\tlintcn "lintcn-rules"
|
|
116
|
+
\t"${TSGOLINT_MODULE}/internal/rule"
|
|
117
|
+
\t"${TSGOLINT_MODULE}/internal/runner"
|
|
118
|
+
\tlintcn "${TSGOLINT_MODULE}/lintcn-rules"
|
|
119
119
|
)
|
|
120
120
|
|
|
121
121
|
func main() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AA+EA,wBAAsB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AA+EA,wBAAsB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA2DxD"}
|
package/dist/commands/add.js
CHANGED
|
@@ -104,11 +104,18 @@ export async function addRule(url) {
|
|
|
104
104
|
}
|
|
105
105
|
// ensure .tsgolint source is available and generate editor support files
|
|
106
106
|
const tsgolintDir = await ensureTsgolintSource(DEFAULT_TSGOLINT_VERSION);
|
|
107
|
-
// create .tsgolint symlink inside .lintcn for gopls
|
|
107
|
+
// create .tsgolint symlink inside .lintcn for gopls.
|
|
108
|
+
// Use lstatSync to detect broken symlinks (existsSync returns false for broken links)
|
|
108
109
|
const tsgolintLink = path.join(lintcnDir, '.tsgolint');
|
|
109
|
-
|
|
110
|
-
fs.
|
|
110
|
+
try {
|
|
111
|
+
fs.lstatSync(tsgolintLink);
|
|
112
|
+
// exists (possibly broken) — remove and recreate
|
|
113
|
+
fs.rmSync(tsgolintLink, { force: true });
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// doesn't exist at all
|
|
111
117
|
}
|
|
118
|
+
fs.symlinkSync(tsgolintDir, tsgolintLink);
|
|
112
119
|
generateEditorGoFiles(lintcnDir);
|
|
113
120
|
console.log('Editor support files generated (go.work, go.mod)');
|
|
114
121
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../../src/commands/lint.ts"],"names":[],"mappings":"AAuBA,wBAAsB,WAAW,CAAC,EAChC,OAAO,EACP,eAAe,GAChB,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../../src/commands/lint.ts"],"names":[],"mappings":"AAuBA,wBAAsB,WAAW,CAAC,EAChC,OAAO,EACP,eAAe,GAChB,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAkDlB;AAED,wBAAsB,IAAI,CAAC,EACzB,OAAO,EACP,eAAe,EACf,eAAe,GAChB,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,EAAE,CAAA;CAC1B,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBlB"}
|
package/dist/commands/lint.js
CHANGED
|
@@ -5,7 +5,7 @@ import { spawn } from 'node:child_process';
|
|
|
5
5
|
import { requireLintcnDir } from "../paths.js";
|
|
6
6
|
import { discoverRules } from "../discover.js";
|
|
7
7
|
import { generateBuildWorkspace } from "../codegen.js";
|
|
8
|
-
import { ensureTsgolintSource, cachedBinaryExists, getBinaryPath, getBuildDir, getBinDir } from "../cache.js";
|
|
8
|
+
import { ensureTsgolintSource, validateVersion, cachedBinaryExists, getBinaryPath, getBuildDir, getBinDir } from "../cache.js";
|
|
9
9
|
import { computeContentHash } from "../hash.js";
|
|
10
10
|
import { execAsync } from "../exec.js";
|
|
11
11
|
async function checkGoInstalled() {
|
|
@@ -13,18 +13,19 @@ async function checkGoInstalled() {
|
|
|
13
13
|
await execAsync('go', ['version']);
|
|
14
14
|
}
|
|
15
15
|
catch {
|
|
16
|
-
throw new Error('Go
|
|
16
|
+
throw new Error('Go is required to build rules.\n' +
|
|
17
17
|
'Install from https://go.dev/dl/');
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
export async function buildBinary({ rebuild, tsgolintVersion, }) {
|
|
21
|
+
validateVersion(tsgolintVersion);
|
|
21
22
|
await checkGoInstalled();
|
|
22
23
|
const lintcnDir = requireLintcnDir();
|
|
23
24
|
const rules = discoverRules(lintcnDir);
|
|
24
25
|
if (rules.length === 0) {
|
|
25
26
|
throw new Error('No rules found in .lintcn/. Run `lintcn add <url>` to add rules.');
|
|
26
27
|
}
|
|
27
|
-
console.log(`Found ${rules.length} custom rule${rules.length === 1 ? '' : 's'} (tsgolint ${tsgolintVersion})`);
|
|
28
|
+
console.log(`Found ${rules.length} custom rule${rules.length === 1 ? '' : 's'} (tsgolint ${tsgolintVersion.slice(0, 8)})`);
|
|
28
29
|
// ensure tsgolint source
|
|
29
30
|
const tsgolintDir = await ensureTsgolintSource(tsgolintVersion);
|
|
30
31
|
// compute content hash
|
|
@@ -37,8 +38,8 @@ export async function buildBinary({ rebuild, tsgolintVersion, }) {
|
|
|
37
38
|
console.log('Using cached binary');
|
|
38
39
|
return getBinaryPath(contentHash);
|
|
39
40
|
}
|
|
40
|
-
// generate build workspace
|
|
41
|
-
const buildDir = getBuildDir();
|
|
41
|
+
// generate build workspace (per-hash dir to avoid races between concurrent processes)
|
|
42
|
+
const buildDir = getBuildDir(contentHash);
|
|
42
43
|
console.log('Generating build workspace...');
|
|
43
44
|
generateBuildWorkspace({
|
|
44
45
|
buildDir,
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,6 @@ export { addRule } from './commands/add.ts';
|
|
|
4
4
|
export { lint, buildBinary } from './commands/lint.ts';
|
|
5
5
|
export { listRules } from './commands/list.ts';
|
|
6
6
|
export { removeRule } from './commands/remove.ts';
|
|
7
|
-
export { DEFAULT_TSGOLINT_VERSION } from './cache.ts';
|
|
7
|
+
export { DEFAULT_TSGOLINT_VERSION, validateVersion } from './cache.ts';
|
|
8
8
|
export { findLintcnDir, getLintcnDir, requireLintcnDir } from './paths.ts';
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC1E,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAC3C,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC1E,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAC3C,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,wBAAwB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AACtE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -3,5 +3,5 @@ export { addRule } from "./commands/add.js";
|
|
|
3
3
|
export { lint, buildBinary } from "./commands/lint.js";
|
|
4
4
|
export { listRules } from "./commands/list.js";
|
|
5
5
|
export { removeRule } from "./commands/remove.js";
|
|
6
|
-
export { DEFAULT_TSGOLINT_VERSION } from "./cache.js";
|
|
6
|
+
export { DEFAULT_TSGOLINT_VERSION, validateVersion } from "./cache.js";
|
|
7
7
|
export { findLintcnDir, getLintcnDir, requireLintcnDir } from "./paths.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lintcn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The shadcn for type-aware TypeScript lint rules. Browse, pick, and copy rules into your project.",
|
|
6
6
|
"bin": "dist/cli.js",
|
|
@@ -51,11 +51,13 @@
|
|
|
51
51
|
"license": "MIT",
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/node": "^22.0.0",
|
|
54
|
+
"@types/tar": "^7.0.87",
|
|
54
55
|
"typescript": "5.8.2"
|
|
55
56
|
},
|
|
56
57
|
"dependencies": {
|
|
57
58
|
"find-up": "^8.0.0",
|
|
58
|
-
"goke": "^6.3.0"
|
|
59
|
+
"goke": "^6.3.0",
|
|
60
|
+
"tar": "^7.5.12"
|
|
59
61
|
},
|
|
60
62
|
"scripts": {
|
|
61
63
|
"build": "rm -rf dist *.tsbuildinfo && tsc && chmod +x dist/cli.js"
|
package/src/cache.ts
CHANGED
|
@@ -4,25 +4,41 @@
|
|
|
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
|
|
|
9
10
|
import fs from 'node:fs'
|
|
10
11
|
import os from 'node:os'
|
|
11
12
|
import path from 'node:path'
|
|
13
|
+
import crypto from 'node:crypto'
|
|
12
14
|
import { pipeline } from 'node:stream/promises'
|
|
15
|
+
import { extract } from 'tar'
|
|
13
16
|
import { execAsync } from './exec.ts'
|
|
14
17
|
|
|
15
18
|
// Pinned tsgolint fork commit — updated with each lintcn release.
|
|
16
|
-
// Uses remorses/tsgolint fork which
|
|
17
|
-
//
|
|
18
|
-
export const DEFAULT_TSGOLINT_VERSION = '
|
|
19
|
+
// Uses remorses/tsgolint fork which adds internal/runner.Run().
|
|
20
|
+
// Only 1 commit on top of upstream — zero modifications to existing files.
|
|
21
|
+
export const DEFAULT_TSGOLINT_VERSION = 'e945641eabec22993eda3e7c101692e80417e0ea'
|
|
19
22
|
|
|
20
23
|
// Pinned typescript-go base commit from microsoft/typescript-go (before patches).
|
|
21
24
|
// Patches from tsgolint/patches/ are applied on top during setup.
|
|
22
|
-
// This is the upstream commit the tsgolint submodule was forked from.
|
|
23
25
|
// Must be updated when DEFAULT_TSGOLINT_VERSION changes.
|
|
24
26
|
const TYPESCRIPT_GO_COMMIT = '1b7eabe122e1575a0df9c77eccdf4e063c623224'
|
|
25
27
|
|
|
28
|
+
// Strict pattern for version strings — prevents path traversal via ../
|
|
29
|
+
const VERSION_PATTERN = /^[a-zA-Z0-9._-]+$/
|
|
30
|
+
|
|
31
|
+
/** Validate version string to prevent path traversal attacks.
|
|
32
|
+
* Only allows alphanumeric chars, dots, underscores, and hyphens. */
|
|
33
|
+
export function validateVersion(version: string): void {
|
|
34
|
+
if (!VERSION_PATTERN.test(version)) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Invalid tsgolint version "${version}". ` +
|
|
37
|
+
'Version must only contain alphanumeric characters, dots, underscores, and hyphens.',
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
export function getCacheDir(): string {
|
|
27
43
|
return path.join(os.homedir(), '.cache', 'lintcn')
|
|
28
44
|
}
|
|
@@ -39,28 +55,50 @@ export function getBinaryPath(contentHash: string): string {
|
|
|
39
55
|
return path.join(getBinDir(), contentHash)
|
|
40
56
|
}
|
|
41
57
|
|
|
42
|
-
|
|
43
|
-
|
|
58
|
+
/** Per-hash build directory to avoid races between concurrent lintcn processes. */
|
|
59
|
+
export function getBuildDir(contentHash: string): string {
|
|
60
|
+
return path.join(getCacheDir(), 'build', contentHash)
|
|
44
61
|
}
|
|
45
62
|
|
|
46
63
|
/** Download a tarball from URL and extract it to targetDir.
|
|
64
|
+
* Uses the `tar` npm package for cross-platform support (no shell tar needed).
|
|
47
65
|
* GitHub tarballs have a top-level directory like `repo-ref/`,
|
|
48
66
|
* so we strip the first path component during extraction. */
|
|
49
67
|
async function downloadAndExtract(url: string, targetDir: string): Promise<void> {
|
|
50
|
-
const
|
|
68
|
+
const controller = new AbortController()
|
|
69
|
+
const timeout = setTimeout(() => {
|
|
70
|
+
controller.abort(new Error(`Download timed out after 120s: ${url}`))
|
|
71
|
+
}, 120_000)
|
|
72
|
+
|
|
73
|
+
let response: Response
|
|
74
|
+
try {
|
|
75
|
+
response = await fetch(url, { signal: controller.signal })
|
|
76
|
+
} finally {
|
|
77
|
+
clearTimeout(timeout)
|
|
78
|
+
}
|
|
79
|
+
|
|
51
80
|
if (!response.ok || !response.body) {
|
|
52
81
|
throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`)
|
|
53
82
|
}
|
|
54
83
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
84
|
+
// download to temp file with random suffix to avoid collisions
|
|
85
|
+
const tmpTarGz = path.join(os.tmpdir(), `lintcn-${crypto.randomBytes(8).toString('hex')}.tar.gz`)
|
|
86
|
+
try {
|
|
87
|
+
const fileStream = fs.createWriteStream(tmpTarGz)
|
|
88
|
+
// @ts-ignore ReadableStream vs NodeJS.ReadableStream mismatch
|
|
89
|
+
await pipeline(response.body, fileStream)
|
|
90
|
+
|
|
91
|
+
// extract with npm tar package (cross-platform, no shell tar needed)
|
|
92
|
+
fs.mkdirSync(targetDir, { recursive: true })
|
|
93
|
+
await extract({
|
|
94
|
+
file: tmpTarGz,
|
|
95
|
+
cwd: targetDir,
|
|
96
|
+
strip: 1,
|
|
97
|
+
})
|
|
98
|
+
} finally {
|
|
99
|
+
// always clean up temp file
|
|
100
|
+
fs.rmSync(tmpTarGz, { force: true })
|
|
101
|
+
}
|
|
64
102
|
}
|
|
65
103
|
|
|
66
104
|
/** Apply git-format patches using `patch -p1` (no git required).
|
|
@@ -79,6 +117,8 @@ async function applyPatches(patchesDir: string, targetDir: string): Promise<numb
|
|
|
79
117
|
}
|
|
80
118
|
|
|
81
119
|
export async function ensureTsgolintSource(version: string): Promise<string> {
|
|
120
|
+
validateVersion(version)
|
|
121
|
+
|
|
82
122
|
const sourceDir = getTsgolintSourceDir(version)
|
|
83
123
|
const readyMarker = path.join(sourceDir, '.lintcn-ready')
|
|
84
124
|
|
|
@@ -86,7 +126,12 @@ export async function ensureTsgolintSource(version: string): Promise<string> {
|
|
|
86
126
|
return sourceDir
|
|
87
127
|
}
|
|
88
128
|
|
|
89
|
-
//
|
|
129
|
+
// Use a temp directory for the download, then atomic rename on success.
|
|
130
|
+
// This prevents concurrent processes from seeing partial state, and
|
|
131
|
+
// avoids the "non-empty dir on retry" problem.
|
|
132
|
+
const tmpDir = path.join(getCacheDir(), 'tsgolint', `.tmp-${version}-${crypto.randomBytes(4).toString('hex')}`)
|
|
133
|
+
|
|
134
|
+
// clean up any partial previous attempt
|
|
90
135
|
if (fs.existsSync(sourceDir)) {
|
|
91
136
|
fs.rmSync(sourceDir, { recursive: true })
|
|
92
137
|
}
|
|
@@ -95,16 +140,16 @@ export async function ensureTsgolintSource(version: string): Promise<string> {
|
|
|
95
140
|
// download tsgolint fork tarball
|
|
96
141
|
console.log(`Downloading tsgolint@${version.slice(0, 8)}...`)
|
|
97
142
|
const tsgolintUrl = `https://github.com/remorses/tsgolint/archive/${version}.tar.gz`
|
|
98
|
-
await downloadAndExtract(tsgolintUrl,
|
|
143
|
+
await downloadAndExtract(tsgolintUrl, tmpDir)
|
|
99
144
|
|
|
100
145
|
// download typescript-go from microsoft (base commit before patches)
|
|
101
|
-
const tsGoDir = path.join(
|
|
146
|
+
const tsGoDir = path.join(tmpDir, 'typescript-go')
|
|
102
147
|
console.log('Downloading typescript-go...')
|
|
103
148
|
const tsGoUrl = `https://github.com/microsoft/typescript-go/archive/${TYPESCRIPT_GO_COMMIT}.tar.gz`
|
|
104
149
|
await downloadAndExtract(tsGoUrl, tsGoDir)
|
|
105
150
|
|
|
106
151
|
// apply tsgolint's patches to typescript-go
|
|
107
|
-
const patchesDir = path.join(
|
|
152
|
+
const patchesDir = path.join(tmpDir, 'patches')
|
|
108
153
|
if (fs.existsSync(patchesDir)) {
|
|
109
154
|
const count = await applyPatches(patchesDir, tsGoDir)
|
|
110
155
|
if (count > 0) {
|
|
@@ -113,7 +158,7 @@ export async function ensureTsgolintSource(version: string): Promise<string> {
|
|
|
113
158
|
}
|
|
114
159
|
|
|
115
160
|
// copy internal/collections from typescript-go (required by tsgolint, done by `just init`)
|
|
116
|
-
const collectionsDir = path.join(
|
|
161
|
+
const collectionsDir = path.join(tmpDir, 'internal', 'collections')
|
|
117
162
|
const tsGoCollections = path.join(tsGoDir, 'internal', 'collections')
|
|
118
163
|
if (fs.existsSync(tsGoCollections)) {
|
|
119
164
|
fs.mkdirSync(collectionsDir, { recursive: true })
|
|
@@ -126,12 +171,17 @@ export async function ensureTsgolintSource(version: string): Promise<string> {
|
|
|
126
171
|
}
|
|
127
172
|
|
|
128
173
|
// write ready marker
|
|
129
|
-
fs.writeFileSync(
|
|
174
|
+
fs.writeFileSync(path.join(tmpDir, '.lintcn-ready'), new Date().toISOString())
|
|
175
|
+
|
|
176
|
+
// atomic rename: move completed dir to final location
|
|
177
|
+
fs.mkdirSync(path.dirname(sourceDir), { recursive: true })
|
|
178
|
+
fs.renameSync(tmpDir, sourceDir)
|
|
179
|
+
|
|
130
180
|
console.log('tsgolint source ready')
|
|
131
181
|
} catch (err) {
|
|
132
|
-
// clean up partial
|
|
133
|
-
if (fs.existsSync(
|
|
134
|
-
fs.rmSync(
|
|
182
|
+
// clean up partial temp directory
|
|
183
|
+
if (fs.existsSync(tmpDir)) {
|
|
184
|
+
fs.rmSync(tmpDir, { recursive: true })
|
|
135
185
|
}
|
|
136
186
|
throw err
|
|
137
187
|
}
|
package/src/codegen.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
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
|
|
|
6
8
|
import fs from 'node:fs'
|
|
7
9
|
import path from 'node:path'
|
|
8
10
|
import type { RuleMetadata } from './discover.ts'
|
|
9
11
|
|
|
10
12
|
// Shim modules that need replace directives in go.work.
|
|
11
|
-
// These redirect module paths to local directories inside the tsgolint source.
|
|
12
13
|
const SHIM_MODULES = [
|
|
13
14
|
'ast',
|
|
14
15
|
'bundled',
|
|
@@ -26,6 +27,8 @@ const SHIM_MODULES = [
|
|
|
26
27
|
'vfs/osvfs',
|
|
27
28
|
] as const
|
|
28
29
|
|
|
30
|
+
const TSGOLINT_MODULE = 'github.com/typescript-eslint/tsgolint'
|
|
31
|
+
|
|
29
32
|
function generateReplaceDirectives(tsgolintRelPath: string): string {
|
|
30
33
|
return SHIM_MODULES.map((mod) => {
|
|
31
34
|
return `\tgithub.com/microsoft/typescript-go/shim/${mod} => ${tsgolintRelPath}/shim/${mod}`
|
|
@@ -47,8 +50,9 @@ ${generateReplaceDirectives('./.tsgolint')}
|
|
|
47
50
|
)
|
|
48
51
|
`
|
|
49
52
|
|
|
50
|
-
//
|
|
51
|
-
|
|
53
|
+
// Module name is a child path of tsgolint — this is required so Go allows
|
|
54
|
+
// importing internal/ packages across the module boundary in a workspace.
|
|
55
|
+
const goMod = `module ${TSGOLINT_MODULE}/lintcn-rules
|
|
52
56
|
|
|
53
57
|
go 1.26
|
|
54
58
|
`
|
|
@@ -69,9 +73,7 @@ go.sum
|
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
/** Generate build workspace for compiling the custom binary.
|
|
73
|
-
* With pkg/runner.Run(), the generated main.go is a static template —
|
|
74
|
-
* no regex surgery or file copying needed. */
|
|
76
|
+
/** Generate build workspace for compiling the custom binary. */
|
|
75
77
|
export function generateBuildWorkspace({
|
|
76
78
|
buildDir,
|
|
77
79
|
tsgolintDir,
|
|
@@ -115,20 +117,19 @@ ${generateReplaceDirectives('./tsgolint')}
|
|
|
115
117
|
`
|
|
116
118
|
fs.writeFileSync(path.join(buildDir, 'go.work'), goWork)
|
|
117
119
|
|
|
118
|
-
// wrapper
|
|
119
|
-
const wrapperGoMod = `module lintcn-wrapper
|
|
120
|
+
// wrapper module — child path of tsgolint for internal/ access
|
|
121
|
+
const wrapperGoMod = `module ${TSGOLINT_MODULE}/lintcn-wrapper
|
|
120
122
|
|
|
121
123
|
go 1.26
|
|
122
124
|
`
|
|
123
125
|
fs.writeFileSync(path.join(buildDir, 'wrapper', 'go.mod'), wrapperGoMod)
|
|
124
126
|
|
|
125
|
-
// wrapper/main.go —
|
|
127
|
+
// wrapper/main.go — static template
|
|
126
128
|
const mainGo = generateMainGo(rules)
|
|
127
129
|
fs.writeFileSync(path.join(buildDir, 'wrapper', 'main.go'), mainGo)
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
/** Generate
|
|
131
|
-
* This is a static template — no copying or patching of tsgolint source. */
|
|
132
|
+
/** Generate main.go that imports user rules and calls internal/runner.Run(). */
|
|
132
133
|
function generateMainGo(rules: RuleMetadata[]): string {
|
|
133
134
|
const ruleEntries = rules.map((r) => {
|
|
134
135
|
return `\t\tlintcn.${r.varName},`
|
|
@@ -140,9 +141,9 @@ package main
|
|
|
140
141
|
import (
|
|
141
142
|
\t"os"
|
|
142
143
|
|
|
143
|
-
\t"
|
|
144
|
-
\t"
|
|
145
|
-
\tlintcn "lintcn-rules"
|
|
144
|
+
\t"${TSGOLINT_MODULE}/internal/rule"
|
|
145
|
+
\t"${TSGOLINT_MODULE}/internal/runner"
|
|
146
|
+
\tlintcn "${TSGOLINT_MODULE}/lintcn-rules"
|
|
146
147
|
)
|
|
147
148
|
|
|
148
149
|
func main() {
|
package/src/commands/add.ts
CHANGED
|
@@ -122,11 +122,17 @@ export async function addRule(url: string): Promise<void> {
|
|
|
122
122
|
// ensure .tsgolint source is available and generate editor support files
|
|
123
123
|
const tsgolintDir = await ensureTsgolintSource(DEFAULT_TSGOLINT_VERSION)
|
|
124
124
|
|
|
125
|
-
// create .tsgolint symlink inside .lintcn for gopls
|
|
125
|
+
// create .tsgolint symlink inside .lintcn for gopls.
|
|
126
|
+
// Use lstatSync to detect broken symlinks (existsSync returns false for broken links)
|
|
126
127
|
const tsgolintLink = path.join(lintcnDir, '.tsgolint')
|
|
127
|
-
|
|
128
|
-
fs.
|
|
128
|
+
try {
|
|
129
|
+
fs.lstatSync(tsgolintLink)
|
|
130
|
+
// exists (possibly broken) — remove and recreate
|
|
131
|
+
fs.rmSync(tsgolintLink, { force: true })
|
|
132
|
+
} catch {
|
|
133
|
+
// doesn't exist at all
|
|
129
134
|
}
|
|
135
|
+
fs.symlinkSync(tsgolintDir, tsgolintLink)
|
|
130
136
|
|
|
131
137
|
generateEditorGoFiles(lintcnDir)
|
|
132
138
|
console.log('Editor support files generated (go.work, go.mod)')
|
package/src/commands/lint.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { spawn } from 'node:child_process'
|
|
|
6
6
|
import { requireLintcnDir } from '../paths.ts'
|
|
7
7
|
import { discoverRules } from '../discover.ts'
|
|
8
8
|
import { generateBuildWorkspace } from '../codegen.ts'
|
|
9
|
-
import { ensureTsgolintSource,
|
|
9
|
+
import { ensureTsgolintSource, validateVersion, cachedBinaryExists, getBinaryPath, getBuildDir, getBinDir } from '../cache.ts'
|
|
10
10
|
import { computeContentHash } from '../hash.ts'
|
|
11
11
|
import { execAsync } from '../exec.ts'
|
|
12
12
|
|
|
@@ -15,7 +15,7 @@ async function checkGoInstalled(): Promise<void> {
|
|
|
15
15
|
await execAsync('go', ['version'])
|
|
16
16
|
} catch {
|
|
17
17
|
throw new Error(
|
|
18
|
-
'Go
|
|
18
|
+
'Go is required to build rules.\n' +
|
|
19
19
|
'Install from https://go.dev/dl/',
|
|
20
20
|
)
|
|
21
21
|
}
|
|
@@ -28,6 +28,7 @@ export async function buildBinary({
|
|
|
28
28
|
rebuild: boolean
|
|
29
29
|
tsgolintVersion: string
|
|
30
30
|
}): Promise<string> {
|
|
31
|
+
validateVersion(tsgolintVersion)
|
|
31
32
|
await checkGoInstalled()
|
|
32
33
|
|
|
33
34
|
const lintcnDir = requireLintcnDir()
|
|
@@ -37,7 +38,7 @@ export async function buildBinary({
|
|
|
37
38
|
throw new Error('No rules found in .lintcn/. Run `lintcn add <url>` to add rules.')
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
console.log(`Found ${rules.length} custom rule${rules.length === 1 ? '' : 's'} (tsgolint ${tsgolintVersion})`)
|
|
41
|
+
console.log(`Found ${rules.length} custom rule${rules.length === 1 ? '' : 's'} (tsgolint ${tsgolintVersion.slice(0, 8)})`)
|
|
41
42
|
|
|
42
43
|
// ensure tsgolint source
|
|
43
44
|
const tsgolintDir = await ensureTsgolintSource(tsgolintVersion)
|
|
@@ -54,8 +55,8 @@ export async function buildBinary({
|
|
|
54
55
|
return getBinaryPath(contentHash)
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
// generate build workspace
|
|
58
|
-
const buildDir = getBuildDir()
|
|
58
|
+
// generate build workspace (per-hash dir to avoid races between concurrent processes)
|
|
59
|
+
const buildDir = getBuildDir(contentHash)
|
|
59
60
|
console.log('Generating build workspace...')
|
|
60
61
|
generateBuildWorkspace({
|
|
61
62
|
buildDir,
|
package/src/index.ts
CHANGED
|
@@ -4,5 +4,5 @@ export { addRule } from './commands/add.ts'
|
|
|
4
4
|
export { lint, buildBinary } from './commands/lint.ts'
|
|
5
5
|
export { listRules } from './commands/list.ts'
|
|
6
6
|
export { removeRule } from './commands/remove.ts'
|
|
7
|
-
export { DEFAULT_TSGOLINT_VERSION } from './cache.ts'
|
|
7
|
+
export { DEFAULT_TSGOLINT_VERSION, validateVersion } from './cache.ts'
|
|
8
8
|
export { findLintcnDir, getLintcnDir, requireLintcnDir } from './paths.ts'
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Kimaki
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|