lintcn 0.0.1 → 0.2.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 +31 -0
- package/LICENSE +21 -0
- package/README.md +157 -5
- package/dist/cache.d.ts +9 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +88 -0
- package/dist/cli.js +71 -3
- package/dist/codegen.d.ts +18 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.js +607 -0
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +101 -0
- package/dist/commands/lint.d.ts +10 -0
- package/dist/commands/lint.d.ts.map +1 -0
- package/dist/commands/lint.js +78 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +24 -0
- package/dist/commands/remove.d.ts +2 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +31 -0
- package/dist/discover.d.ts +16 -0
- package/dist/discover.d.ts.map +1 -0
- package/dist/discover.js +44 -0
- package/dist/exec.d.ts +10 -0
- package/dist/exec.d.ts.map +1 -0
- package/dist/exec.js +34 -0
- package/dist/hash.d.ts +5 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +33 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/paths.d.ts +2 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +5 -0
- package/package.json +12 -9
- package/src/cache.ts +106 -0
- package/src/cli.ts +80 -2
- package/src/codegen.ts +640 -0
- package/src/commands/add.ts +118 -0
- package/src/commands/lint.ts +110 -0
- package/src/commands/list.ts +33 -0
- package/src/commands/remove.ts +41 -0
- package/src/discover.ts +69 -0
- package/src/exec.ts +50 -0
- package/src/hash.ts +45 -0
- package/src/index.ts +7 -1
- package/src/paths.ts +7 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// lintcn add <url> — fetch a .go rule file by URL and copy into .lintcn/
|
|
2
|
+
// Also tries to fetch matching _test.go file from the same directory.
|
|
3
|
+
// Normalizes GitHub blob URLs to raw URLs automatically.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { getLintcnDir } from "../paths.js";
|
|
7
|
+
import { generateEditorGoFiles } from "../codegen.js";
|
|
8
|
+
import { ensureTsgolintSource, DEFAULT_TSGOLINT_VERSION } from "../cache.js";
|
|
9
|
+
function normalizeGithubUrl(url) {
|
|
10
|
+
// Convert github.com/user/repo/blob/branch/path to raw.githubusercontent.com
|
|
11
|
+
const blobMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/);
|
|
12
|
+
if (blobMatch) {
|
|
13
|
+
const [, owner, repo, branch, filePath] = blobMatch;
|
|
14
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${filePath}`;
|
|
15
|
+
}
|
|
16
|
+
return url;
|
|
17
|
+
}
|
|
18
|
+
function deriveTestUrl(rawUrl) {
|
|
19
|
+
return rawUrl.replace(/\.go$/, '_test.go');
|
|
20
|
+
}
|
|
21
|
+
async function fetchFile(url) {
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetch(url);
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return await response.text();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function rewritePackageName(content) {
|
|
34
|
+
// Rewrite first package declaration to package lintcn
|
|
35
|
+
return content.replace(/^package\s+\w+/m, 'package lintcn');
|
|
36
|
+
}
|
|
37
|
+
function ensureSourceComment(content, sourceUrl) {
|
|
38
|
+
if (content.includes('// lintcn:source')) {
|
|
39
|
+
return content;
|
|
40
|
+
}
|
|
41
|
+
// Insert source comment after the first lintcn: comment block, or at the very top
|
|
42
|
+
const lines = content.split('\n');
|
|
43
|
+
let insertIndex = 0;
|
|
44
|
+
for (let i = 0; i < lines.length; i++) {
|
|
45
|
+
if (lines[i].startsWith('// lintcn:')) {
|
|
46
|
+
insertIndex = i + 1;
|
|
47
|
+
}
|
|
48
|
+
else if (insertIndex > 0) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
lines.splice(insertIndex, 0, `// lintcn:source ${sourceUrl}`);
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
export async function addRule(url) {
|
|
56
|
+
const rawUrl = normalizeGithubUrl(url);
|
|
57
|
+
console.log(`Fetching ${rawUrl}...`);
|
|
58
|
+
const content = await fetchFile(rawUrl);
|
|
59
|
+
if (!content) {
|
|
60
|
+
throw new Error(`Could not fetch rule from ${rawUrl}`);
|
|
61
|
+
}
|
|
62
|
+
// validate it looks like a Go file with a rule
|
|
63
|
+
if (!content.includes('rule.Rule')) {
|
|
64
|
+
console.warn('Warning: no rule.Rule reference found in this file. Are you sure this is a tsgolint rule?');
|
|
65
|
+
}
|
|
66
|
+
// derive filename from URL
|
|
67
|
+
const urlPath = new URL(rawUrl).pathname;
|
|
68
|
+
const fileName = path.basename(urlPath);
|
|
69
|
+
if (!fileName.endsWith('.go')) {
|
|
70
|
+
throw new Error(`URL must point to a .go file, got: ${fileName}`);
|
|
71
|
+
}
|
|
72
|
+
const lintcnDir = getLintcnDir();
|
|
73
|
+
fs.mkdirSync(lintcnDir, { recursive: true });
|
|
74
|
+
// write the rule file
|
|
75
|
+
const filePath = path.join(lintcnDir, fileName);
|
|
76
|
+
if (fs.existsSync(filePath)) {
|
|
77
|
+
console.log(`Overwriting existing ${fileName}`);
|
|
78
|
+
}
|
|
79
|
+
let processed = rewritePackageName(content);
|
|
80
|
+
processed = ensureSourceComment(processed, url);
|
|
81
|
+
fs.writeFileSync(filePath, processed);
|
|
82
|
+
console.log(`Added ${fileName}`);
|
|
83
|
+
// try to fetch matching test file
|
|
84
|
+
const testUrl = deriveTestUrl(rawUrl);
|
|
85
|
+
const testContent = await fetchFile(testUrl);
|
|
86
|
+
if (testContent) {
|
|
87
|
+
const testFileName = fileName.replace(/\.go$/, '_test.go');
|
|
88
|
+
const testProcessed = rewritePackageName(testContent);
|
|
89
|
+
fs.writeFileSync(path.join(lintcnDir, testFileName), testProcessed);
|
|
90
|
+
console.log(`Added ${testFileName}`);
|
|
91
|
+
}
|
|
92
|
+
// ensure .tsgolint source is available and generate editor support files
|
|
93
|
+
const tsgolintDir = await ensureTsgolintSource(DEFAULT_TSGOLINT_VERSION);
|
|
94
|
+
// create .tsgolint symlink inside .lintcn for gopls
|
|
95
|
+
const tsgolintLink = path.join(lintcnDir, '.tsgolint');
|
|
96
|
+
if (!fs.existsSync(tsgolintLink)) {
|
|
97
|
+
fs.symlinkSync(tsgolintDir, tsgolintLink);
|
|
98
|
+
}
|
|
99
|
+
generateEditorGoFiles(lintcnDir);
|
|
100
|
+
console.log('Editor support files generated (go.work, go.mod)');
|
|
101
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function buildBinary({ rebuild, tsgolintVersion, }: {
|
|
2
|
+
rebuild: boolean;
|
|
3
|
+
tsgolintVersion: string;
|
|
4
|
+
}): Promise<string>;
|
|
5
|
+
export declare function lint({ rebuild, tsgolintVersion, passthroughArgs, }: {
|
|
6
|
+
rebuild: boolean;
|
|
7
|
+
tsgolintVersion: string;
|
|
8
|
+
passthroughArgs: string[];
|
|
9
|
+
}): Promise<number>;
|
|
10
|
+
//# sourceMappingURL=lint.d.ts.map
|
|
@@ -0,0 +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,CAoDlB;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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// lintcn lint — build a custom tsgolint binary and run it against the project.
|
|
2
|
+
// Handles Go workspace generation, compilation with caching, and execution.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { getLintcnDir } from "../paths.js";
|
|
6
|
+
import { discoverRules } from "../discover.js";
|
|
7
|
+
import { generateBuildWorkspace } from "../codegen.js";
|
|
8
|
+
import { ensureTsgolintSource, cachedBinaryExists, getBinaryPath, getBuildDir, getBinDir } from "../cache.js";
|
|
9
|
+
import { computeContentHash } from "../hash.js";
|
|
10
|
+
import { execAsync } from "../exec.js";
|
|
11
|
+
async function checkGoInstalled() {
|
|
12
|
+
try {
|
|
13
|
+
await execAsync('go', ['version']);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new Error('Go 1.26+ is required to build rules.\n' +
|
|
17
|
+
'Install from https://go.dev/dl/');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function buildBinary({ rebuild, tsgolintVersion, }) {
|
|
21
|
+
await checkGoInstalled();
|
|
22
|
+
const lintcnDir = getLintcnDir();
|
|
23
|
+
if (!fs.existsSync(lintcnDir)) {
|
|
24
|
+
throw new Error('No .lintcn/ directory found. Run `lintcn add <url>` first.');
|
|
25
|
+
}
|
|
26
|
+
const rules = discoverRules(lintcnDir);
|
|
27
|
+
if (rules.length === 0) {
|
|
28
|
+
throw new Error('No rules found in .lintcn/. Run `lintcn add <url>` to add rules.');
|
|
29
|
+
}
|
|
30
|
+
console.log(`Found ${rules.length} custom rule${rules.length === 1 ? '' : 's'} (tsgolint ${tsgolintVersion})`);
|
|
31
|
+
// ensure tsgolint source
|
|
32
|
+
const tsgolintDir = await ensureTsgolintSource(tsgolintVersion);
|
|
33
|
+
// compute content hash
|
|
34
|
+
const contentHash = await computeContentHash({
|
|
35
|
+
lintcnDir,
|
|
36
|
+
tsgolintVersion,
|
|
37
|
+
});
|
|
38
|
+
// check cache
|
|
39
|
+
if (!rebuild && cachedBinaryExists(contentHash)) {
|
|
40
|
+
console.log('Using cached binary');
|
|
41
|
+
return getBinaryPath(contentHash);
|
|
42
|
+
}
|
|
43
|
+
// generate build workspace
|
|
44
|
+
const buildDir = getBuildDir();
|
|
45
|
+
console.log('Generating build workspace...');
|
|
46
|
+
generateBuildWorkspace({
|
|
47
|
+
buildDir,
|
|
48
|
+
tsgolintDir,
|
|
49
|
+
lintcnDir,
|
|
50
|
+
rules,
|
|
51
|
+
});
|
|
52
|
+
// compile
|
|
53
|
+
const binDir = getBinDir();
|
|
54
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
55
|
+
const binaryPath = getBinaryPath(contentHash);
|
|
56
|
+
console.log('Compiling custom tsgolint binary...');
|
|
57
|
+
await execAsync('go', ['build', '-o', binaryPath, './wrapper'], {
|
|
58
|
+
cwd: buildDir,
|
|
59
|
+
});
|
|
60
|
+
console.log('Build complete');
|
|
61
|
+
return binaryPath;
|
|
62
|
+
}
|
|
63
|
+
export async function lint({ rebuild, tsgolintVersion, passthroughArgs, }) {
|
|
64
|
+
const binaryPath = await buildBinary({ rebuild, tsgolintVersion });
|
|
65
|
+
// run the binary with passthrough args, inheriting stdio
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const proc = spawn(binaryPath, passthroughArgs, {
|
|
68
|
+
stdio: 'inherit',
|
|
69
|
+
});
|
|
70
|
+
proc.on('error', (err) => {
|
|
71
|
+
console.error(`Failed to run binary: ${err.message}`);
|
|
72
|
+
resolve(1);
|
|
73
|
+
});
|
|
74
|
+
proc.on('close', (code) => {
|
|
75
|
+
resolve(code ?? 1);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/commands/list.ts"],"names":[],"mappings":"AAMA,wBAAgB,SAAS,IAAI,IAAI,CA0BhC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// lintcn list — list installed rules with metadata from .lintcn/
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { getLintcnDir } from "../paths.js";
|
|
4
|
+
import { discoverRules } from "../discover.js";
|
|
5
|
+
export function listRules() {
|
|
6
|
+
const lintcnDir = getLintcnDir();
|
|
7
|
+
if (!fs.existsSync(lintcnDir)) {
|
|
8
|
+
console.log('No .lintcn/ directory found. Run `lintcn add <url>` to add rules.');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const rules = discoverRules(lintcnDir);
|
|
12
|
+
if (rules.length === 0) {
|
|
13
|
+
console.log('No rules installed. Run `lintcn add <url>` to add rules.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
console.log('Installed rules:\n');
|
|
17
|
+
const maxNameLen = Math.max(...rules.map((r) => { return r.name.length; }));
|
|
18
|
+
for (const rule of rules) {
|
|
19
|
+
const name = rule.name.padEnd(maxNameLen + 2);
|
|
20
|
+
const desc = rule.description || '(no description)';
|
|
21
|
+
console.log(` ${name}${desc}`);
|
|
22
|
+
}
|
|
23
|
+
console.log(`\n${rules.length} rule${rules.length === 1 ? '' : 's'} installed`);
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remove.d.ts","sourceRoot":"","sources":["../../src/commands/remove.ts"],"names":[],"mappings":"AAOA,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAiC7C"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// lintcn remove <name> — delete a rule and its test file from .lintcn/
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getLintcnDir } from "../paths.js";
|
|
5
|
+
import { discoverRules } from "../discover.js";
|
|
6
|
+
export function removeRule(name) {
|
|
7
|
+
const lintcnDir = getLintcnDir();
|
|
8
|
+
if (!fs.existsSync(lintcnDir)) {
|
|
9
|
+
throw new Error('No .lintcn/ directory found.');
|
|
10
|
+
}
|
|
11
|
+
// match by lintcn:name metadata or by filename
|
|
12
|
+
const rules = discoverRules(lintcnDir);
|
|
13
|
+
const normalizedName = name.replace(/-/g, '_');
|
|
14
|
+
const match = rules.find((r) => {
|
|
15
|
+
return r.name === name || r.fileName.replace(/\.go$/, '') === normalizedName;
|
|
16
|
+
});
|
|
17
|
+
if (!match) {
|
|
18
|
+
throw new Error(`Rule "${name}" not found. Run \`lintcn list\` to see installed rules.`);
|
|
19
|
+
}
|
|
20
|
+
// delete rule file
|
|
21
|
+
const rulePath = path.join(lintcnDir, match.fileName);
|
|
22
|
+
fs.rmSync(rulePath);
|
|
23
|
+
console.log(`Removed ${match.fileName}`);
|
|
24
|
+
// delete test file if exists
|
|
25
|
+
const testFileName = match.fileName.replace(/\.go$/, '_test.go');
|
|
26
|
+
const testPath = path.join(lintcnDir, testFileName);
|
|
27
|
+
if (fs.existsSync(testPath)) {
|
|
28
|
+
fs.rmSync(testPath);
|
|
29
|
+
console.log(`Removed ${testFileName}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface RuleMetadata {
|
|
2
|
+
/** kebab-case rule name from // lintcn:name or derived from filename */
|
|
3
|
+
name: string;
|
|
4
|
+
/** one-line description from // lintcn:description */
|
|
5
|
+
description: string;
|
|
6
|
+
/** original source URL from // lintcn:source */
|
|
7
|
+
source: string;
|
|
8
|
+
/** exported Go variable name like NoFloatingPromisesRule */
|
|
9
|
+
varName: string;
|
|
10
|
+
/** filename relative to .lintcn/ */
|
|
11
|
+
fileName: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function parseMetadata(content: string): Record<string, string>;
|
|
14
|
+
export declare function parseRuleVar(content: string): string | undefined;
|
|
15
|
+
export declare function discoverRules(lintcnDir: string): RuleMetadata[];
|
|
16
|
+
//# sourceMappingURL=discover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../src/discover.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAA;IACZ,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAA;IACnB,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAA;IACd,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAA;IACf,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAA;CACjB;AAKD,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMrE;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGhE;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,EAAE,CAiC/D"}
|
package/dist/discover.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Scan .lintcn/*.go files for rule.Rule variables and lintcn: metadata comments.
|
|
2
|
+
// Returns structured info about each discovered rule for codegen and list display.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
const RULE_VAR_RE = /^var\s+(\w+)\s*=\s*rule\.Rule\s*\{/m;
|
|
6
|
+
const METADATA_RE = /^\/\/\s*lintcn:(\w+)\s+(.+)$/gm;
|
|
7
|
+
export function parseMetadata(content) {
|
|
8
|
+
const meta = {};
|
|
9
|
+
for (const match of content.matchAll(METADATA_RE)) {
|
|
10
|
+
meta[match[1]] = match[2].trim();
|
|
11
|
+
}
|
|
12
|
+
return meta;
|
|
13
|
+
}
|
|
14
|
+
export function parseRuleVar(content) {
|
|
15
|
+
const match = content.match(RULE_VAR_RE);
|
|
16
|
+
return match?.[1];
|
|
17
|
+
}
|
|
18
|
+
export function discoverRules(lintcnDir) {
|
|
19
|
+
if (!fs.existsSync(lintcnDir)) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const files = fs.readdirSync(lintcnDir).filter((f) => {
|
|
23
|
+
return f.endsWith('.go') && !f.endsWith('_test.go');
|
|
24
|
+
});
|
|
25
|
+
const rules = [];
|
|
26
|
+
for (const fileName of files) {
|
|
27
|
+
const filePath = path.join(lintcnDir, fileName);
|
|
28
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
29
|
+
const varName = parseRuleVar(content);
|
|
30
|
+
if (!varName) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const meta = parseMetadata(content);
|
|
34
|
+
const baseName = fileName.replace(/\.go$/, '');
|
|
35
|
+
rules.push({
|
|
36
|
+
name: meta.name || baseName.replace(/_/g, '-'),
|
|
37
|
+
description: meta.description || '',
|
|
38
|
+
source: meta.source || '',
|
|
39
|
+
varName,
|
|
40
|
+
fileName,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return rules;
|
|
44
|
+
}
|
package/dist/exec.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ExecResult {
|
|
2
|
+
stdout: string;
|
|
3
|
+
stderr: string;
|
|
4
|
+
exitCode: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function execAsync(command: string, args: string[], options?: {
|
|
7
|
+
cwd?: string;
|
|
8
|
+
stdio?: 'pipe' | 'inherit';
|
|
9
|
+
}): Promise<ExecResult>;
|
|
10
|
+
//# sourceMappingURL=exec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../src/exec.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,wBAAgB,SAAS,CACvB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACrD,OAAO,CAAC,UAAU,CAAC,CAkCrB"}
|
package/dist/exec.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Async process execution utility using spawn.
|
|
2
|
+
// Returns stdout/stderr as strings, rejects on non-zero exit code.
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
export function execAsync(command, args, options) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const proc = spawn(command, args, {
|
|
7
|
+
cwd: options?.cwd,
|
|
8
|
+
stdio: options?.stdio === 'inherit' ? 'inherit' : 'pipe',
|
|
9
|
+
});
|
|
10
|
+
let stdout = '';
|
|
11
|
+
let stderr = '';
|
|
12
|
+
if (proc.stdout) {
|
|
13
|
+
proc.stdout.on('data', (data) => {
|
|
14
|
+
stdout += data.toString();
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
if (proc.stderr) {
|
|
18
|
+
proc.stderr.on('data', (data) => {
|
|
19
|
+
stderr += data.toString();
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
proc.on('error', (err) => {
|
|
23
|
+
reject(new Error(`Failed to execute ${command}: ${err.message}`, { cause: err }));
|
|
24
|
+
});
|
|
25
|
+
proc.on('close', (code) => {
|
|
26
|
+
const exitCode = code ?? 1;
|
|
27
|
+
if (exitCode !== 0 && options?.stdio !== 'inherit') {
|
|
28
|
+
reject(new Error(`${command} exited with code ${exitCode}\n${stderr}`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
resolve({ stdout, stderr, exitCode });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
package/dist/hash.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AASA,wBAAsB,kBAAkB,CAAC,EACvC,SAAS,EACT,eAAe,GAChB,EAAE;IACD,SAAS,EAAE,MAAM,CAAA;IACjB,eAAe,EAAE,MAAM,CAAA;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CA6BlB"}
|
package/dist/hash.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Content hash for binary caching.
|
|
2
|
+
// Combines tsgolint version, rule file contents, Go version, and platform
|
|
3
|
+
// into a single SHA-256 hash used as the cached binary filename.
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { execAsync } from "./exec.js";
|
|
8
|
+
export async function computeContentHash({ lintcnDir, tsgolintVersion, }) {
|
|
9
|
+
const hash = crypto.createHash('sha256');
|
|
10
|
+
hash.update(`tsgolint:${tsgolintVersion}\n`);
|
|
11
|
+
hash.update(`platform:${process.platform}-${process.arch}\n`);
|
|
12
|
+
// add Go version
|
|
13
|
+
try {
|
|
14
|
+
const { stdout } = await execAsync('go', ['version']);
|
|
15
|
+
hash.update(`go:${stdout.trim()}\n`);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
hash.update('go:unknown\n');
|
|
19
|
+
}
|
|
20
|
+
// add all rule file contents in sorted order
|
|
21
|
+
const files = fs
|
|
22
|
+
.readdirSync(lintcnDir)
|
|
23
|
+
.filter((f) => {
|
|
24
|
+
return f.endsWith('.go');
|
|
25
|
+
})
|
|
26
|
+
.sort();
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
const content = fs.readFileSync(path.join(lintcnDir, file), 'utf-8');
|
|
29
|
+
hash.update(`file:${file}\n`);
|
|
30
|
+
hash.update(content);
|
|
31
|
+
}
|
|
32
|
+
return hash.digest('hex').slice(0, 16);
|
|
33
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { discoverRules, parseMetadata, parseRuleVar } from './discover.ts';
|
|
2
|
+
export type { RuleMetadata } from './discover.ts';
|
|
3
|
+
export { addRule } from './commands/add.ts';
|
|
4
|
+
export { lint, buildBinary } from './commands/lint.ts';
|
|
5
|
+
export { listRules } from './commands/list.ts';
|
|
6
|
+
export { removeRule } from './commands/remove.ts';
|
|
7
|
+
export { DEFAULT_TSGOLINT_VERSION } from './cache.ts';
|
|
2
8
|
//# 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,
|
|
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"}
|
package/dist/index.js
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { discoverRules, parseMetadata, parseRuleVar } from "./discover.js";
|
|
2
|
+
export { addRule } from "./commands/add.js";
|
|
3
|
+
export { lint, buildBinary } from "./commands/lint.js";
|
|
4
|
+
export { listRules } from "./commands/list.js";
|
|
5
|
+
export { removeRule } from "./commands/remove.js";
|
|
6
|
+
export { DEFAULT_TSGOLINT_VERSION } from "./cache.js";
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAIA,wBAAgB,YAAY,IAAI,MAAM,CAErC"}
|
package/dist/paths.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lintcn",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -24,12 +24,9 @@
|
|
|
24
24
|
"files": [
|
|
25
25
|
"src",
|
|
26
26
|
"dist",
|
|
27
|
-
"README.md"
|
|
27
|
+
"README.md",
|
|
28
|
+
"CHANGELOG.md"
|
|
28
29
|
],
|
|
29
|
-
"scripts": {
|
|
30
|
-
"build": "rm -rf dist *.tsbuildinfo && tsc && chmod +x dist/cli.js",
|
|
31
|
-
"prepublishOnly": "pnpm build"
|
|
32
|
-
},
|
|
33
30
|
"keywords": [
|
|
34
31
|
"lint",
|
|
35
32
|
"linter",
|
|
@@ -53,7 +50,13 @@
|
|
|
53
50
|
},
|
|
54
51
|
"license": "MIT",
|
|
55
52
|
"devDependencies": {
|
|
56
|
-
"
|
|
57
|
-
"
|
|
53
|
+
"@types/node": "^22.0.0",
|
|
54
|
+
"typescript": "5.8.2"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"goke": "^6.3.0"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "rm -rf dist *.tsbuildinfo && tsc && chmod +x dist/cli.js"
|
|
58
61
|
}
|
|
59
|
-
}
|
|
62
|
+
}
|
package/src/cache.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Manage cached tsgolint source clone and compiled binaries.
|
|
2
|
+
// Cache lives in ~/.cache/lintcn/ with structure:
|
|
3
|
+
// tsgolint/<version>/ — cloned tsgolint source (read-only)
|
|
4
|
+
// bin/<content-hash> — compiled binaries
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import os from 'node:os'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { execAsync } from './exec.ts'
|
|
10
|
+
|
|
11
|
+
// Pinned tsgolint version — updated with each lintcn release.
|
|
12
|
+
// This ensures reproducible builds: every user on the same lintcn version
|
|
13
|
+
// compiles rules against the same tsgolint API. Changing this is a conscious
|
|
14
|
+
// decision — tsgolint API changes can break user rules.
|
|
15
|
+
export const DEFAULT_TSGOLINT_VERSION = 'v0.9.2'
|
|
16
|
+
|
|
17
|
+
export function getCacheDir(): string {
|
|
18
|
+
return path.join(os.homedir(), '.cache', 'lintcn')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getTsgolintSourceDir(version: string): string {
|
|
22
|
+
return path.join(getCacheDir(), 'tsgolint', version)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getBinDir(): string {
|
|
26
|
+
return path.join(getCacheDir(), 'bin')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getBinaryPath(contentHash: string): string {
|
|
30
|
+
return path.join(getBinDir(), contentHash)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getBuildDir(): string {
|
|
34
|
+
return path.join(getCacheDir(), 'build')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function ensureTsgolintSource(version: string): Promise<string> {
|
|
38
|
+
const sourceDir = getTsgolintSourceDir(version)
|
|
39
|
+
const readyMarker = path.join(sourceDir, '.lintcn-ready')
|
|
40
|
+
|
|
41
|
+
if (fs.existsSync(readyMarker)) {
|
|
42
|
+
return sourceDir
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log(`Cloning tsgolint@${version}...`)
|
|
46
|
+
|
|
47
|
+
fs.mkdirSync(sourceDir, { recursive: true })
|
|
48
|
+
|
|
49
|
+
// clone with depth 1 for speed — --branch works with tags and branches
|
|
50
|
+
const cloneArgs = [
|
|
51
|
+
'clone', '--depth', '1',
|
|
52
|
+
'--branch', version,
|
|
53
|
+
'--recurse-submodules', '--shallow-submodules',
|
|
54
|
+
'https://github.com/oxc-project/tsgolint.git', sourceDir,
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
await execAsync('git', cloneArgs)
|
|
58
|
+
|
|
59
|
+
// apply patches if they exist
|
|
60
|
+
const patchesDir = path.join(sourceDir, 'patches')
|
|
61
|
+
if (fs.existsSync(patchesDir)) {
|
|
62
|
+
const patches = fs.readdirSync(patchesDir).filter((f) => {
|
|
63
|
+
return f.endsWith('.patch')
|
|
64
|
+
}).sort()
|
|
65
|
+
|
|
66
|
+
if (patches.length > 0) {
|
|
67
|
+
console.log(`Applying ${patches.length} patches...`)
|
|
68
|
+
const patchPaths = patches.map((p) => {
|
|
69
|
+
return path.join('..', 'patches', p)
|
|
70
|
+
})
|
|
71
|
+
await execAsync('git', ['am', '--3way', '--no-gpg-sign', ...patchPaths], {
|
|
72
|
+
cwd: path.join(sourceDir, 'typescript-go'),
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// copy internal/collections from typescript-go (required by tsgolint, done by `just init`)
|
|
78
|
+
const collectionsDir = path.join(sourceDir, 'internal', 'collections')
|
|
79
|
+
const tsGoCollections = path.join(sourceDir, 'typescript-go', 'internal', 'collections')
|
|
80
|
+
if (!fs.existsSync(collectionsDir) && fs.existsSync(tsGoCollections)) {
|
|
81
|
+
fs.mkdirSync(collectionsDir, { recursive: true })
|
|
82
|
+
const files = fs.readdirSync(tsGoCollections).filter((f) => {
|
|
83
|
+
return f.endsWith('.go') && !f.endsWith('_test.go')
|
|
84
|
+
})
|
|
85
|
+
for (const file of files) {
|
|
86
|
+
fs.copyFileSync(path.join(tsGoCollections, file), path.join(collectionsDir, file))
|
|
87
|
+
}
|
|
88
|
+
console.log(`Copied ${files.length} collection files`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// write ready marker
|
|
92
|
+
fs.writeFileSync(readyMarker, new Date().toISOString())
|
|
93
|
+
console.log('tsgolint source ready')
|
|
94
|
+
|
|
95
|
+
return sourceDir
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function cachedBinaryExists(contentHash: string): boolean {
|
|
99
|
+
const binPath = getBinaryPath(contentHash)
|
|
100
|
+
try {
|
|
101
|
+
fs.accessSync(binPath, fs.constants.X_OK)
|
|
102
|
+
return true
|
|
103
|
+
} catch {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
}
|