lintcn 0.3.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 +16 -0
- package/README.md +5 -4
- package/dist/cache.d.ts +6 -2
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +77 -36
- package/dist/codegen.d.ts +2 -15
- package/dist/codegen.d.ts.map +1 -1
- package/dist/codegen.js +39 -84
- 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 +86 -39
- package/src/codegen.ts +37 -97
- 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,3 +1,19 @@
|
|
|
1
|
+
## 0.5.0
|
|
2
|
+
|
|
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.
|
|
8
|
+
|
|
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
|
|
16
|
+
|
|
1
17
|
## 0.3.0
|
|
2
18
|
|
|
3
19
|
1. **Only custom rules run by default** — previously the binary included all 44 built-in tsgolint rules, producing thousands of noisy errors. Now only your `.lintcn/` rules run. True shadcn model: explicitly add each rule you want.
|
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
|
@@ -1,24 +1,36 @@
|
|
|
1
1
|
// Manage cached tsgolint source and compiled binaries.
|
|
2
|
-
// Downloads tsgolint + typescript-go as tarballs from GitHub
|
|
3
|
-
// applies patches
|
|
2
|
+
// Downloads tsgolint fork + typescript-go as tarballs from GitHub,
|
|
3
|
+
// applies tsgolint's patches to typescript-go, and copies collections.
|
|
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
|
-
// Pinned tsgolint
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
// Found via `git ls-tree HEAD typescript-go` in the tsgolint repo.
|
|
16
|
+
// Pinned tsgolint fork commit — updated with each lintcn release.
|
|
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';
|
|
20
|
+
// Pinned typescript-go base commit from microsoft/typescript-go (before patches).
|
|
21
|
+
// Patches from tsgolint/patches/ are applied on top during setup.
|
|
20
22
|
// Must be updated when DEFAULT_TSGOLINT_VERSION changes.
|
|
21
|
-
const TYPESCRIPT_GO_COMMIT = '
|
|
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,25 +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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
}
|
|
53
87
|
}
|
|
54
88
|
/** Apply git-format patches using `patch -p1` (no git required).
|
|
55
89
|
* Patches are standard unified diff format, `patch` ignores the git metadata. */
|
|
@@ -59,33 +93,37 @@ async function applyPatches(patchesDir, targetDir) {
|
|
|
59
93
|
.sort();
|
|
60
94
|
for (const patchFile of patches) {
|
|
61
95
|
const patchPath = path.join(patchesDir, patchFile);
|
|
62
|
-
// --batch silences interactive prompts, -f forces application
|
|
63
96
|
await execAsync('patch', ['-p1', '--batch', '-i', patchPath], { cwd: targetDir });
|
|
64
97
|
}
|
|
65
98
|
return patches.length;
|
|
66
99
|
}
|
|
67
100
|
export async function ensureTsgolintSource(version) {
|
|
101
|
+
validateVersion(version);
|
|
68
102
|
const sourceDir = getTsgolintSourceDir(version);
|
|
69
103
|
const readyMarker = path.join(sourceDir, '.lintcn-ready');
|
|
70
104
|
if (fs.existsSync(readyMarker)) {
|
|
71
105
|
return sourceDir;
|
|
72
106
|
}
|
|
73
|
-
//
|
|
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
|
|
74
112
|
if (fs.existsSync(sourceDir)) {
|
|
75
113
|
fs.rmSync(sourceDir, { recursive: true });
|
|
76
114
|
}
|
|
77
115
|
try {
|
|
78
|
-
// download tsgolint
|
|
79
|
-
console.log(`Downloading tsgolint@${version}...`);
|
|
80
|
-
const tsgolintUrl = `https://github.com/
|
|
81
|
-
await downloadAndExtract(tsgolintUrl,
|
|
82
|
-
// download typescript-go
|
|
83
|
-
const tsGoDir = path.join(
|
|
116
|
+
// download tsgolint fork tarball
|
|
117
|
+
console.log(`Downloading tsgolint@${version.slice(0, 8)}...`);
|
|
118
|
+
const tsgolintUrl = `https://github.com/remorses/tsgolint/archive/${version}.tar.gz`;
|
|
119
|
+
await downloadAndExtract(tsgolintUrl, tmpDir);
|
|
120
|
+
// download typescript-go from microsoft (base commit before patches)
|
|
121
|
+
const tsGoDir = path.join(tmpDir, 'typescript-go');
|
|
84
122
|
console.log('Downloading typescript-go...');
|
|
85
123
|
const tsGoUrl = `https://github.com/microsoft/typescript-go/archive/${TYPESCRIPT_GO_COMMIT}.tar.gz`;
|
|
86
124
|
await downloadAndExtract(tsGoUrl, tsGoDir);
|
|
87
|
-
// apply patches to typescript-go
|
|
88
|
-
const patchesDir = path.join(
|
|
125
|
+
// apply tsgolint's patches to typescript-go
|
|
126
|
+
const patchesDir = path.join(tmpDir, 'patches');
|
|
89
127
|
if (fs.existsSync(patchesDir)) {
|
|
90
128
|
const count = await applyPatches(patchesDir, tsGoDir);
|
|
91
129
|
if (count > 0) {
|
|
@@ -93,7 +131,7 @@ export async function ensureTsgolintSource(version) {
|
|
|
93
131
|
}
|
|
94
132
|
}
|
|
95
133
|
// copy internal/collections from typescript-go (required by tsgolint, done by `just init`)
|
|
96
|
-
const collectionsDir = path.join(
|
|
134
|
+
const collectionsDir = path.join(tmpDir, 'internal', 'collections');
|
|
97
135
|
const tsGoCollections = path.join(tsGoDir, 'internal', 'collections');
|
|
98
136
|
if (fs.existsSync(tsGoCollections)) {
|
|
99
137
|
fs.mkdirSync(collectionsDir, { recursive: true });
|
|
@@ -105,13 +143,16 @@ export async function ensureTsgolintSource(version) {
|
|
|
105
143
|
}
|
|
106
144
|
}
|
|
107
145
|
// write ready marker
|
|
108
|
-
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);
|
|
109
150
|
console.log('tsgolint source ready');
|
|
110
151
|
}
|
|
111
152
|
catch (err) {
|
|
112
|
-
// clean up partial
|
|
113
|
-
if (fs.existsSync(
|
|
114
|
-
fs.rmSync(
|
|
153
|
+
// clean up partial temp directory
|
|
154
|
+
if (fs.existsSync(tmpDir)) {
|
|
155
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
115
156
|
}
|
|
116
157
|
throw err;
|
|
117
158
|
}
|
package/dist/codegen.d.ts
CHANGED
|
@@ -1,24 +1,11 @@
|
|
|
1
1
|
import type { RuleMetadata } from './discover.ts';
|
|
2
|
-
/** Generate .lintcn/go.work and .lintcn/go.mod for editor/gopls support.
|
|
3
|
-
*
|
|
4
|
-
* Key learnings from testing:
|
|
5
|
-
* - Module name MUST be a child path of github.com/typescript-eslint/tsgolint
|
|
6
|
-
* so Go allows importing internal/ packages across the module boundary.
|
|
7
|
-
* - go.work must `use` both .tsgolint AND .tsgolint/typescript-go since
|
|
8
|
-
* tsgolint's own go.work (which does this) is ignored by the outer workspace.
|
|
9
|
-
* - go.mod should be minimal (no requires) — the workspace resolves everything. */
|
|
2
|
+
/** Generate .lintcn/go.work and .lintcn/go.mod for editor/gopls support. */
|
|
10
3
|
export declare function generateEditorGoFiles(lintcnDir: string): void;
|
|
11
|
-
/** Generate build workspace
|
|
12
|
-
* Instead of hardcoding the built-in rule list, we copy tsgolint's actual
|
|
13
|
-
* main.go and inject custom rule imports + entries. This way the generated
|
|
14
|
-
* code always matches the pinned tsgolint version. */
|
|
4
|
+
/** Generate build workspace for compiling the custom binary. */
|
|
15
5
|
export declare function generateBuildWorkspace({ buildDir, tsgolintDir, lintcnDir, rules, }: {
|
|
16
6
|
buildDir: string;
|
|
17
7
|
tsgolintDir: string;
|
|
18
8
|
lintcnDir: string;
|
|
19
9
|
rules: RuleMetadata[];
|
|
20
10
|
}): void;
|
|
21
|
-
/** Copy all supporting .go files from cmd/tsgolint/ into the wrapper dir.
|
|
22
|
-
* main.go is generated separately with custom rules injected. */
|
|
23
|
-
export declare function copyTsgolintCmdFiles(tsgolintDir: string, wrapperDir: string): void;
|
|
24
11
|
//# sourceMappingURL=codegen.d.ts.map
|
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,14 +1,12 @@
|
|
|
1
1
|
// Generate Go workspace files for building a custom tsgolint binary.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// build/wrapper/main.go — tsgolint main.go with custom rules appended
|
|
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.
|
|
8
7
|
import fs from 'node:fs';
|
|
9
8
|
import path from 'node:path';
|
|
10
|
-
//
|
|
11
|
-
// These redirect shim module paths to local directories inside the tsgolint source.
|
|
9
|
+
// Shim modules that need replace directives in go.work.
|
|
12
10
|
const SHIM_MODULES = [
|
|
13
11
|
'ast',
|
|
14
12
|
'bundled',
|
|
@@ -25,19 +23,13 @@ const SHIM_MODULES = [
|
|
|
25
23
|
'vfs/cachedvfs',
|
|
26
24
|
'vfs/osvfs',
|
|
27
25
|
];
|
|
26
|
+
const TSGOLINT_MODULE = 'github.com/typescript-eslint/tsgolint';
|
|
28
27
|
function generateReplaceDirectives(tsgolintRelPath) {
|
|
29
28
|
return SHIM_MODULES.map((mod) => {
|
|
30
29
|
return `\tgithub.com/microsoft/typescript-go/shim/${mod} => ${tsgolintRelPath}/shim/${mod}`;
|
|
31
30
|
}).join('\n');
|
|
32
31
|
}
|
|
33
|
-
/** Generate .lintcn/go.work and .lintcn/go.mod for editor/gopls support.
|
|
34
|
-
*
|
|
35
|
-
* Key learnings from testing:
|
|
36
|
-
* - Module name MUST be a child path of github.com/typescript-eslint/tsgolint
|
|
37
|
-
* so Go allows importing internal/ packages across the module boundary.
|
|
38
|
-
* - go.work must `use` both .tsgolint AND .tsgolint/typescript-go since
|
|
39
|
-
* tsgolint's own go.work (which does this) is ignored by the outer workspace.
|
|
40
|
-
* - go.mod should be minimal (no requires) — the workspace resolves everything. */
|
|
32
|
+
/** Generate .lintcn/go.work and .lintcn/go.mod for editor/gopls support. */
|
|
41
33
|
export function generateEditorGoFiles(lintcnDir) {
|
|
42
34
|
const goWork = `go 1.26
|
|
43
35
|
|
|
@@ -51,7 +43,9 @@ replace (
|
|
|
51
43
|
${generateReplaceDirectives('./.tsgolint')}
|
|
52
44
|
)
|
|
53
45
|
`;
|
|
54
|
-
|
|
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
|
|
55
49
|
|
|
56
50
|
go 1.26
|
|
57
51
|
`;
|
|
@@ -68,10 +62,7 @@ go.sum
|
|
|
68
62
|
fs.writeFileSync(gitignorePath, gitignore);
|
|
69
63
|
}
|
|
70
64
|
}
|
|
71
|
-
/** Generate build workspace
|
|
72
|
-
* Instead of hardcoding the built-in rule list, we copy tsgolint's actual
|
|
73
|
-
* main.go and inject custom rule imports + entries. This way the generated
|
|
74
|
-
* code always matches the pinned tsgolint version. */
|
|
65
|
+
/** Generate build workspace for compiling the custom binary. */
|
|
75
66
|
export function generateBuildWorkspace({ buildDir, tsgolintDir, lintcnDir, rules, }) {
|
|
76
67
|
fs.mkdirSync(path.join(buildDir, 'wrapper'), { recursive: true });
|
|
77
68
|
// symlink tsgolint source
|
|
@@ -86,7 +77,7 @@ export function generateBuildWorkspace({ buildDir, tsgolintDir, lintcnDir, rules
|
|
|
86
77
|
fs.rmSync(rulesLink, { recursive: true });
|
|
87
78
|
}
|
|
88
79
|
fs.symlinkSync(path.resolve(lintcnDir), rulesLink);
|
|
89
|
-
// go.work
|
|
80
|
+
// go.work
|
|
90
81
|
const goWork = `go 1.26
|
|
91
82
|
|
|
92
83
|
use (
|
|
@@ -101,73 +92,37 @@ ${generateReplaceDirectives('./tsgolint')}
|
|
|
101
92
|
)
|
|
102
93
|
`;
|
|
103
94
|
fs.writeFileSync(path.join(buildDir, 'go.work'), goWork);
|
|
104
|
-
// wrapper
|
|
105
|
-
|
|
106
|
-
// Adding explicit requires with v0.0.0 triggers Go proxy lookups that fail.
|
|
107
|
-
const wrapperGoMod = `module github.com/typescript-eslint/tsgolint/lintcn-wrapper
|
|
95
|
+
// wrapper module — child path of tsgolint for internal/ access
|
|
96
|
+
const wrapperGoMod = `module ${TSGOLINT_MODULE}/lintcn-wrapper
|
|
108
97
|
|
|
109
98
|
go 1.26
|
|
110
99
|
`;
|
|
111
100
|
fs.writeFileSync(path.join(buildDir, 'wrapper', 'go.mod'), wrapperGoMod);
|
|
112
|
-
//
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
// wrapper/main.go — copy from tsgolint and inject custom rules
|
|
116
|
-
const mainGo = generateMainGoFromSource(tsgolintDir, rules);
|
|
117
|
-
fs.writeFileSync(path.join(wrapperDir, 'main.go'), mainGo);
|
|
101
|
+
// wrapper/main.go — static template
|
|
102
|
+
const mainGo = generateMainGo(rules);
|
|
103
|
+
fs.writeFileSync(path.join(buildDir, 'wrapper', 'main.go'), mainGo);
|
|
118
104
|
}
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
* Everything else (printDiagnostic, runMain, headless) stays untouched. */
|
|
124
|
-
function generateMainGoFromSource(tsgolintDir, customRules) {
|
|
125
|
-
const mainGoPath = path.join(tsgolintDir, 'cmd', 'tsgolint', 'main.go');
|
|
126
|
-
const original = fs.readFileSync(mainGoPath, 'utf-8');
|
|
127
|
-
// 1. Remove built-in rule import lines, add lintcn import
|
|
128
|
-
const lines = original.split('\n');
|
|
129
|
-
const filtered = lines.filter((line) => {
|
|
130
|
-
return !line.includes('/internal/rules/');
|
|
131
|
-
});
|
|
132
|
-
// Insert lintcn import before the first shim import (microsoft/typescript-go)
|
|
133
|
-
const lintcnImport = `\tlintcn "github.com/typescript-eslint/tsgolint/lintcn-rules"`;
|
|
134
|
-
let shimImportIndex = -1;
|
|
135
|
-
for (let i = 0; i < filtered.length; i++) {
|
|
136
|
-
if (filtered[i].includes('microsoft/typescript-go/shim')) {
|
|
137
|
-
shimImportIndex = i;
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
if (shimImportIndex === -1) {
|
|
142
|
-
throw new Error('Failed to find shim import in tsgolint main.go. The source layout may have changed.');
|
|
143
|
-
}
|
|
144
|
-
if (customRules.length > 0) {
|
|
145
|
-
filtered.splice(shimImportIndex, 0, lintcnImport, '');
|
|
146
|
-
}
|
|
147
|
-
let mainGo = filtered.join('\n');
|
|
148
|
-
// 2. Replace allRules body with only custom entries
|
|
149
|
-
const customEntries = customRules.map((r) => {
|
|
150
|
-
return `\tlintcn.${r.varName},`;
|
|
105
|
+
/** Generate main.go that imports user rules and calls internal/runner.Run(). */
|
|
106
|
+
function generateMainGo(rules) {
|
|
107
|
+
const ruleEntries = rules.map((r) => {
|
|
108
|
+
return `\t\tlintcn.${r.varName},`;
|
|
151
109
|
}).join('\n');
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
110
|
+
return `// Code generated by lintcn. DO NOT EDIT.
|
|
111
|
+
package main
|
|
112
|
+
|
|
113
|
+
import (
|
|
114
|
+
\t"os"
|
|
115
|
+
|
|
116
|
+
\t"${TSGOLINT_MODULE}/internal/rule"
|
|
117
|
+
\t"${TSGOLINT_MODULE}/internal/runner"
|
|
118
|
+
\tlintcn "${TSGOLINT_MODULE}/lintcn-rules"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
func main() {
|
|
122
|
+
\trules := []rule.Rule{
|
|
123
|
+
${ruleEntries}
|
|
124
|
+
\t}
|
|
125
|
+
\tos.Exit(runner.Run(rules, os.Args[1:]))
|
|
162
126
|
}
|
|
163
|
-
|
|
164
|
-
* main.go is generated separately with custom rules injected. */
|
|
165
|
-
export function copyTsgolintCmdFiles(tsgolintDir, wrapperDir) {
|
|
166
|
-
const cmdDir = path.join(tsgolintDir, 'cmd', 'tsgolint');
|
|
167
|
-
const files = fs.readdirSync(cmdDir).filter((f) => {
|
|
168
|
-
return f.endsWith('.go') && f !== 'main.go' && !f.endsWith('_test.go');
|
|
169
|
-
});
|
|
170
|
-
for (const file of files) {
|
|
171
|
-
fs.copyFileSync(path.join(cmdDir, file), path.join(wrapperDir, file));
|
|
172
|
-
}
|
|
127
|
+
`;
|
|
173
128
|
}
|
|
@@ -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
|
@@ -1,28 +1,43 @@
|
|
|
1
1
|
// Manage cached tsgolint source and compiled binaries.
|
|
2
|
-
// Downloads tsgolint + typescript-go as tarballs from GitHub
|
|
3
|
-
// applies patches
|
|
2
|
+
// Downloads tsgolint fork + typescript-go as tarballs from GitHub,
|
|
3
|
+
// applies tsgolint's patches to typescript-go, and copies collections.
|
|
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'
|
|
13
|
-
import {
|
|
15
|
+
import { extract } from 'tar'
|
|
14
16
|
import { execAsync } from './exec.ts'
|
|
15
17
|
|
|
16
|
-
// Pinned tsgolint
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
export const DEFAULT_TSGOLINT_VERSION = 'v0.9.2'
|
|
18
|
+
// Pinned tsgolint fork commit — updated with each lintcn release.
|
|
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'
|
|
21
22
|
|
|
22
|
-
// Pinned typescript-go commit
|
|
23
|
-
//
|
|
23
|
+
// Pinned typescript-go base commit from microsoft/typescript-go (before patches).
|
|
24
|
+
// Patches from tsgolint/patches/ are applied on top during setup.
|
|
24
25
|
// Must be updated when DEFAULT_TSGOLINT_VERSION changes.
|
|
25
|
-
const TYPESCRIPT_GO_COMMIT = '
|
|
26
|
+
const TYPESCRIPT_GO_COMMIT = '1b7eabe122e1575a0df9c77eccdf4e063c623224'
|
|
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
|
+
}
|
|
26
41
|
|
|
27
42
|
export function getCacheDir(): string {
|
|
28
43
|
return path.join(os.homedir(), '.cache', 'lintcn')
|
|
@@ -40,29 +55,50 @@ export function getBinaryPath(contentHash: string): string {
|
|
|
40
55
|
return path.join(getBinDir(), contentHash)
|
|
41
56
|
}
|
|
42
57
|
|
|
43
|
-
|
|
44
|
-
|
|
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)
|
|
45
61
|
}
|
|
46
62
|
|
|
47
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).
|
|
48
65
|
* GitHub tarballs have a top-level directory like `repo-ref/`,
|
|
49
66
|
* so we strip the first path component during extraction. */
|
|
50
67
|
async function downloadAndExtract(url: string, targetDir: string): Promise<void> {
|
|
51
|
-
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
|
+
|
|
52
80
|
if (!response.ok || !response.body) {
|
|
53
81
|
throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`)
|
|
54
82
|
}
|
|
55
83
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
}
|
|
66
102
|
}
|
|
67
103
|
|
|
68
104
|
/** Apply git-format patches using `patch -p1` (no git required).
|
|
@@ -74,7 +110,6 @@ async function applyPatches(patchesDir: string, targetDir: string): Promise<numb
|
|
|
74
110
|
|
|
75
111
|
for (const patchFile of patches) {
|
|
76
112
|
const patchPath = path.join(patchesDir, patchFile)
|
|
77
|
-
// --batch silences interactive prompts, -f forces application
|
|
78
113
|
await execAsync('patch', ['-p1', '--batch', '-i', patchPath], { cwd: targetDir })
|
|
79
114
|
}
|
|
80
115
|
|
|
@@ -82,6 +117,8 @@ async function applyPatches(patchesDir: string, targetDir: string): Promise<numb
|
|
|
82
117
|
}
|
|
83
118
|
|
|
84
119
|
export async function ensureTsgolintSource(version: string): Promise<string> {
|
|
120
|
+
validateVersion(version)
|
|
121
|
+
|
|
85
122
|
const sourceDir = getTsgolintSourceDir(version)
|
|
86
123
|
const readyMarker = path.join(sourceDir, '.lintcn-ready')
|
|
87
124
|
|
|
@@ -89,25 +126,30 @@ export async function ensureTsgolintSource(version: string): Promise<string> {
|
|
|
89
126
|
return sourceDir
|
|
90
127
|
}
|
|
91
128
|
|
|
92
|
-
//
|
|
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
|
|
93
135
|
if (fs.existsSync(sourceDir)) {
|
|
94
136
|
fs.rmSync(sourceDir, { recursive: true })
|
|
95
137
|
}
|
|
96
138
|
|
|
97
139
|
try {
|
|
98
|
-
// download tsgolint
|
|
99
|
-
console.log(`Downloading tsgolint@${version}...`)
|
|
100
|
-
const tsgolintUrl = `https://github.com/
|
|
101
|
-
await downloadAndExtract(tsgolintUrl,
|
|
140
|
+
// download tsgolint fork tarball
|
|
141
|
+
console.log(`Downloading tsgolint@${version.slice(0, 8)}...`)
|
|
142
|
+
const tsgolintUrl = `https://github.com/remorses/tsgolint/archive/${version}.tar.gz`
|
|
143
|
+
await downloadAndExtract(tsgolintUrl, tmpDir)
|
|
102
144
|
|
|
103
|
-
// download typescript-go
|
|
104
|
-
const tsGoDir = path.join(
|
|
145
|
+
// download typescript-go from microsoft (base commit before patches)
|
|
146
|
+
const tsGoDir = path.join(tmpDir, 'typescript-go')
|
|
105
147
|
console.log('Downloading typescript-go...')
|
|
106
148
|
const tsGoUrl = `https://github.com/microsoft/typescript-go/archive/${TYPESCRIPT_GO_COMMIT}.tar.gz`
|
|
107
149
|
await downloadAndExtract(tsGoUrl, tsGoDir)
|
|
108
150
|
|
|
109
|
-
// apply patches to typescript-go
|
|
110
|
-
const patchesDir = path.join(
|
|
151
|
+
// apply tsgolint's patches to typescript-go
|
|
152
|
+
const patchesDir = path.join(tmpDir, 'patches')
|
|
111
153
|
if (fs.existsSync(patchesDir)) {
|
|
112
154
|
const count = await applyPatches(patchesDir, tsGoDir)
|
|
113
155
|
if (count > 0) {
|
|
@@ -116,7 +158,7 @@ export async function ensureTsgolintSource(version: string): Promise<string> {
|
|
|
116
158
|
}
|
|
117
159
|
|
|
118
160
|
// copy internal/collections from typescript-go (required by tsgolint, done by `just init`)
|
|
119
|
-
const collectionsDir = path.join(
|
|
161
|
+
const collectionsDir = path.join(tmpDir, 'internal', 'collections')
|
|
120
162
|
const tsGoCollections = path.join(tsGoDir, 'internal', 'collections')
|
|
121
163
|
if (fs.existsSync(tsGoCollections)) {
|
|
122
164
|
fs.mkdirSync(collectionsDir, { recursive: true })
|
|
@@ -129,12 +171,17 @@ export async function ensureTsgolintSource(version: string): Promise<string> {
|
|
|
129
171
|
}
|
|
130
172
|
|
|
131
173
|
// write ready marker
|
|
132
|
-
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
|
+
|
|
133
180
|
console.log('tsgolint source ready')
|
|
134
181
|
} catch (err) {
|
|
135
|
-
// clean up partial
|
|
136
|
-
if (fs.existsSync(
|
|
137
|
-
fs.rmSync(
|
|
182
|
+
// clean up partial temp directory
|
|
183
|
+
if (fs.existsSync(tmpDir)) {
|
|
184
|
+
fs.rmSync(tmpDir, { recursive: true })
|
|
138
185
|
}
|
|
139
186
|
throw err
|
|
140
187
|
}
|
package/src/codegen.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
// Generate Go workspace files for building a custom tsgolint binary.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// build/wrapper/main.go — tsgolint main.go with custom rules appended
|
|
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.
|
|
8
7
|
|
|
9
8
|
import fs from 'node:fs'
|
|
10
9
|
import path from 'node:path'
|
|
11
10
|
import type { RuleMetadata } from './discover.ts'
|
|
12
11
|
|
|
13
|
-
//
|
|
14
|
-
// These redirect shim module paths to local directories inside the tsgolint source.
|
|
12
|
+
// Shim modules that need replace directives in go.work.
|
|
15
13
|
const SHIM_MODULES = [
|
|
16
14
|
'ast',
|
|
17
15
|
'bundled',
|
|
@@ -29,20 +27,15 @@ const SHIM_MODULES = [
|
|
|
29
27
|
'vfs/osvfs',
|
|
30
28
|
] as const
|
|
31
29
|
|
|
30
|
+
const TSGOLINT_MODULE = 'github.com/typescript-eslint/tsgolint'
|
|
31
|
+
|
|
32
32
|
function generateReplaceDirectives(tsgolintRelPath: string): string {
|
|
33
33
|
return SHIM_MODULES.map((mod) => {
|
|
34
34
|
return `\tgithub.com/microsoft/typescript-go/shim/${mod} => ${tsgolintRelPath}/shim/${mod}`
|
|
35
35
|
}).join('\n')
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/** Generate .lintcn/go.work and .lintcn/go.mod for editor/gopls support.
|
|
39
|
-
*
|
|
40
|
-
* Key learnings from testing:
|
|
41
|
-
* - Module name MUST be a child path of github.com/typescript-eslint/tsgolint
|
|
42
|
-
* so Go allows importing internal/ packages across the module boundary.
|
|
43
|
-
* - go.work must `use` both .tsgolint AND .tsgolint/typescript-go since
|
|
44
|
-
* tsgolint's own go.work (which does this) is ignored by the outer workspace.
|
|
45
|
-
* - go.mod should be minimal (no requires) — the workspace resolves everything. */
|
|
38
|
+
/** Generate .lintcn/go.work and .lintcn/go.mod for editor/gopls support. */
|
|
46
39
|
export function generateEditorGoFiles(lintcnDir: string): void {
|
|
47
40
|
const goWork = `go 1.26
|
|
48
41
|
|
|
@@ -57,7 +50,9 @@ ${generateReplaceDirectives('./.tsgolint')}
|
|
|
57
50
|
)
|
|
58
51
|
`
|
|
59
52
|
|
|
60
|
-
|
|
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
|
|
61
56
|
|
|
62
57
|
go 1.26
|
|
63
58
|
`
|
|
@@ -78,10 +73,7 @@ go.sum
|
|
|
78
73
|
}
|
|
79
74
|
}
|
|
80
75
|
|
|
81
|
-
/** Generate build workspace
|
|
82
|
-
* Instead of hardcoding the built-in rule list, we copy tsgolint's actual
|
|
83
|
-
* main.go and inject custom rule imports + entries. This way the generated
|
|
84
|
-
* code always matches the pinned tsgolint version. */
|
|
76
|
+
/** Generate build workspace for compiling the custom binary. */
|
|
85
77
|
export function generateBuildWorkspace({
|
|
86
78
|
buildDir,
|
|
87
79
|
tsgolintDir,
|
|
@@ -109,7 +101,7 @@ export function generateBuildWorkspace({
|
|
|
109
101
|
}
|
|
110
102
|
fs.symlinkSync(path.resolve(lintcnDir), rulesLink)
|
|
111
103
|
|
|
112
|
-
// go.work
|
|
104
|
+
// go.work
|
|
113
105
|
const goWork = `go 1.26
|
|
114
106
|
|
|
115
107
|
use (
|
|
@@ -125,92 +117,40 @@ ${generateReplaceDirectives('./tsgolint')}
|
|
|
125
117
|
`
|
|
126
118
|
fs.writeFileSync(path.join(buildDir, 'go.work'), goWork)
|
|
127
119
|
|
|
128
|
-
// wrapper
|
|
129
|
-
|
|
130
|
-
// Adding explicit requires with v0.0.0 triggers Go proxy lookups that fail.
|
|
131
|
-
const wrapperGoMod = `module github.com/typescript-eslint/tsgolint/lintcn-wrapper
|
|
120
|
+
// wrapper module — child path of tsgolint for internal/ access
|
|
121
|
+
const wrapperGoMod = `module ${TSGOLINT_MODULE}/lintcn-wrapper
|
|
132
122
|
|
|
133
123
|
go 1.26
|
|
134
124
|
`
|
|
135
125
|
fs.writeFileSync(path.join(buildDir, 'wrapper', 'go.mod'), wrapperGoMod)
|
|
136
126
|
|
|
137
|
-
//
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// wrapper/main.go — copy from tsgolint and inject custom rules
|
|
142
|
-
const mainGo = generateMainGoFromSource(tsgolintDir, rules)
|
|
143
|
-
fs.writeFileSync(path.join(wrapperDir, 'main.go'), mainGo)
|
|
127
|
+
// wrapper/main.go — static template
|
|
128
|
+
const mainGo = generateMainGo(rules)
|
|
129
|
+
fs.writeFileSync(path.join(buildDir, 'wrapper', 'main.go'), mainGo)
|
|
144
130
|
}
|
|
145
131
|
|
|
146
|
-
/**
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
* Everything else (printDiagnostic, runMain, headless) stays untouched. */
|
|
151
|
-
function generateMainGoFromSource(tsgolintDir: string, customRules: RuleMetadata[]): string {
|
|
152
|
-
const mainGoPath = path.join(tsgolintDir, 'cmd', 'tsgolint', 'main.go')
|
|
153
|
-
const original = fs.readFileSync(mainGoPath, 'utf-8')
|
|
154
|
-
|
|
155
|
-
// 1. Remove built-in rule import lines, add lintcn import
|
|
156
|
-
const lines = original.split('\n')
|
|
157
|
-
const filtered = lines.filter((line) => {
|
|
158
|
-
return !line.includes('/internal/rules/')
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
// Insert lintcn import before the first shim import (microsoft/typescript-go)
|
|
162
|
-
const lintcnImport = `\tlintcn "github.com/typescript-eslint/tsgolint/lintcn-rules"`
|
|
163
|
-
let shimImportIndex = -1
|
|
164
|
-
for (let i = 0; i < filtered.length; i++) {
|
|
165
|
-
if (filtered[i].includes('microsoft/typescript-go/shim')) {
|
|
166
|
-
shimImportIndex = i
|
|
167
|
-
break
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
if (shimImportIndex === -1) {
|
|
171
|
-
throw new Error(
|
|
172
|
-
'Failed to find shim import in tsgolint main.go. The source layout may have changed.',
|
|
173
|
-
)
|
|
174
|
-
}
|
|
175
|
-
if (customRules.length > 0) {
|
|
176
|
-
filtered.splice(shimImportIndex, 0, lintcnImport, '')
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
let mainGo = filtered.join('\n')
|
|
180
|
-
|
|
181
|
-
// 2. Replace allRules body with only custom entries
|
|
182
|
-
const customEntries = customRules.map((r) => {
|
|
183
|
-
return `\tlintcn.${r.varName},`
|
|
132
|
+
/** Generate main.go that imports user rules and calls internal/runner.Run(). */
|
|
133
|
+
function generateMainGo(rules: RuleMetadata[]): string {
|
|
134
|
+
const ruleEntries = rules.map((r) => {
|
|
135
|
+
return `\t\tlintcn.${r.varName},`
|
|
184
136
|
}).join('\n')
|
|
185
137
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
throw new Error(
|
|
189
|
-
'Failed to find allRules slice in tsgolint main.go. The source layout may have changed.',
|
|
190
|
-
)
|
|
191
|
-
}
|
|
138
|
+
return `// Code generated by lintcn. DO NOT EDIT.
|
|
139
|
+
package main
|
|
192
140
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
`var allRules = []rule.Rule{\n${customEntries}\n}`,
|
|
196
|
-
)
|
|
141
|
+
import (
|
|
142
|
+
\t"os"
|
|
197
143
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
144
|
+
\t"${TSGOLINT_MODULE}/internal/rule"
|
|
145
|
+
\t"${TSGOLINT_MODULE}/internal/runner"
|
|
146
|
+
\tlintcn "${TSGOLINT_MODULE}/lintcn-rules"
|
|
147
|
+
)
|
|
202
148
|
|
|
203
|
-
|
|
149
|
+
func main() {
|
|
150
|
+
\trules := []rule.Rule{
|
|
151
|
+
${ruleEntries}
|
|
152
|
+
\t}
|
|
153
|
+
\tos.Exit(runner.Run(rules, os.Args[1:]))
|
|
204
154
|
}
|
|
205
|
-
|
|
206
|
-
/** Copy all supporting .go files from cmd/tsgolint/ into the wrapper dir.
|
|
207
|
-
* main.go is generated separately with custom rules injected. */
|
|
208
|
-
export function copyTsgolintCmdFiles(tsgolintDir: string, wrapperDir: string): void {
|
|
209
|
-
const cmdDir = path.join(tsgolintDir, 'cmd', 'tsgolint')
|
|
210
|
-
const files = fs.readdirSync(cmdDir).filter((f) => {
|
|
211
|
-
return f.endsWith('.go') && f !== 'main.go' && !f.endsWith('_test.go')
|
|
212
|
-
})
|
|
213
|
-
for (const file of files) {
|
|
214
|
-
fs.copyFileSync(path.join(cmdDir, file), path.join(wrapperDir, file))
|
|
215
|
-
}
|
|
155
|
+
`
|
|
216
156
|
}
|
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.
|