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,118 @@
|
|
|
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
|
+
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import { getLintcnDir } from '../paths.ts'
|
|
8
|
+
import { generateEditorGoFiles } from '../codegen.ts'
|
|
9
|
+
import { ensureTsgolintSource, DEFAULT_TSGOLINT_VERSION } from '../cache.ts'
|
|
10
|
+
|
|
11
|
+
function normalizeGithubUrl(url: string): string {
|
|
12
|
+
// Convert github.com/user/repo/blob/branch/path to raw.githubusercontent.com
|
|
13
|
+
const blobMatch = url.match(
|
|
14
|
+
/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/,
|
|
15
|
+
)
|
|
16
|
+
if (blobMatch) {
|
|
17
|
+
const [, owner, repo, branch, filePath] = blobMatch
|
|
18
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${filePath}`
|
|
19
|
+
}
|
|
20
|
+
return url
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function deriveTestUrl(rawUrl: string): string {
|
|
24
|
+
return rawUrl.replace(/\.go$/, '_test.go')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function fetchFile(url: string): Promise<string | null> {
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(url)
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
return await response.text()
|
|
34
|
+
} catch {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function rewritePackageName(content: string): string {
|
|
40
|
+
// Rewrite first package declaration to package lintcn
|
|
41
|
+
return content.replace(/^package\s+\w+/m, 'package lintcn')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ensureSourceComment(content: string, sourceUrl: string): string {
|
|
45
|
+
if (content.includes('// lintcn:source')) {
|
|
46
|
+
return content
|
|
47
|
+
}
|
|
48
|
+
// Insert source comment after the first lintcn: comment block, or at the very top
|
|
49
|
+
const lines = content.split('\n')
|
|
50
|
+
let insertIndex = 0
|
|
51
|
+
for (let i = 0; i < lines.length; i++) {
|
|
52
|
+
if (lines[i].startsWith('// lintcn:')) {
|
|
53
|
+
insertIndex = i + 1
|
|
54
|
+
} else if (insertIndex > 0) {
|
|
55
|
+
break
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
lines.splice(insertIndex, 0, `// lintcn:source ${sourceUrl}`)
|
|
59
|
+
return lines.join('\n')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function addRule(url: string): Promise<void> {
|
|
63
|
+
const rawUrl = normalizeGithubUrl(url)
|
|
64
|
+
|
|
65
|
+
console.log(`Fetching ${rawUrl}...`)
|
|
66
|
+
const content = await fetchFile(rawUrl)
|
|
67
|
+
if (!content) {
|
|
68
|
+
throw new Error(`Could not fetch rule from ${rawUrl}`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// validate it looks like a Go file with a rule
|
|
72
|
+
if (!content.includes('rule.Rule')) {
|
|
73
|
+
console.warn('Warning: no rule.Rule reference found in this file. Are you sure this is a tsgolint rule?')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// derive filename from URL
|
|
77
|
+
const urlPath = new URL(rawUrl).pathname
|
|
78
|
+
const fileName = path.basename(urlPath)
|
|
79
|
+
if (!fileName.endsWith('.go')) {
|
|
80
|
+
throw new Error(`URL must point to a .go file, got: ${fileName}`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const lintcnDir = getLintcnDir()
|
|
84
|
+
fs.mkdirSync(lintcnDir, { recursive: true })
|
|
85
|
+
|
|
86
|
+
// write the rule file
|
|
87
|
+
const filePath = path.join(lintcnDir, fileName)
|
|
88
|
+
if (fs.existsSync(filePath)) {
|
|
89
|
+
console.log(`Overwriting existing ${fileName}`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let processed = rewritePackageName(content)
|
|
93
|
+
processed = ensureSourceComment(processed, url)
|
|
94
|
+
fs.writeFileSync(filePath, processed)
|
|
95
|
+
console.log(`Added ${fileName}`)
|
|
96
|
+
|
|
97
|
+
// try to fetch matching test file
|
|
98
|
+
const testUrl = deriveTestUrl(rawUrl)
|
|
99
|
+
const testContent = await fetchFile(testUrl)
|
|
100
|
+
if (testContent) {
|
|
101
|
+
const testFileName = fileName.replace(/\.go$/, '_test.go')
|
|
102
|
+
const testProcessed = rewritePackageName(testContent)
|
|
103
|
+
fs.writeFileSync(path.join(lintcnDir, testFileName), testProcessed)
|
|
104
|
+
console.log(`Added ${testFileName}`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ensure .tsgolint source is available and generate editor support files
|
|
108
|
+
const tsgolintDir = await ensureTsgolintSource(DEFAULT_TSGOLINT_VERSION)
|
|
109
|
+
|
|
110
|
+
// create .tsgolint symlink inside .lintcn for gopls
|
|
111
|
+
const tsgolintLink = path.join(lintcnDir, '.tsgolint')
|
|
112
|
+
if (!fs.existsSync(tsgolintLink)) {
|
|
113
|
+
fs.symlinkSync(tsgolintDir, tsgolintLink)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
generateEditorGoFiles(lintcnDir)
|
|
117
|
+
console.log('Editor support files generated (go.work, go.mod)')
|
|
118
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import { spawn } from 'node:child_process'
|
|
6
|
+
import { getLintcnDir } from '../paths.ts'
|
|
7
|
+
import { discoverRules } from '../discover.ts'
|
|
8
|
+
import { generateBuildWorkspace } from '../codegen.ts'
|
|
9
|
+
import { ensureTsgolintSource, DEFAULT_TSGOLINT_VERSION, cachedBinaryExists, getBinaryPath, getBuildDir, getBinDir } from '../cache.ts'
|
|
10
|
+
import { computeContentHash } from '../hash.ts'
|
|
11
|
+
import { execAsync } from '../exec.ts'
|
|
12
|
+
|
|
13
|
+
async function checkGoInstalled(): Promise<void> {
|
|
14
|
+
try {
|
|
15
|
+
await execAsync('go', ['version'])
|
|
16
|
+
} catch {
|
|
17
|
+
throw new Error(
|
|
18
|
+
'Go 1.26+ is required to build rules.\n' +
|
|
19
|
+
'Install from https://go.dev/dl/',
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function buildBinary({
|
|
25
|
+
rebuild,
|
|
26
|
+
tsgolintVersion,
|
|
27
|
+
}: {
|
|
28
|
+
rebuild: boolean
|
|
29
|
+
tsgolintVersion: string
|
|
30
|
+
}): Promise<string> {
|
|
31
|
+
await checkGoInstalled()
|
|
32
|
+
|
|
33
|
+
const lintcnDir = getLintcnDir()
|
|
34
|
+
if (!fs.existsSync(lintcnDir)) {
|
|
35
|
+
throw new Error('No .lintcn/ directory found. Run `lintcn add <url>` first.')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const rules = discoverRules(lintcnDir)
|
|
39
|
+
if (rules.length === 0) {
|
|
40
|
+
throw new Error('No rules found in .lintcn/. Run `lintcn add <url>` to add rules.')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(`Found ${rules.length} custom rule${rules.length === 1 ? '' : 's'} (tsgolint ${tsgolintVersion})`)
|
|
44
|
+
|
|
45
|
+
// ensure tsgolint source
|
|
46
|
+
const tsgolintDir = await ensureTsgolintSource(tsgolintVersion)
|
|
47
|
+
|
|
48
|
+
// compute content hash
|
|
49
|
+
const contentHash = await computeContentHash({
|
|
50
|
+
lintcnDir,
|
|
51
|
+
tsgolintVersion,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// check cache
|
|
55
|
+
if (!rebuild && cachedBinaryExists(contentHash)) {
|
|
56
|
+
console.log('Using cached binary')
|
|
57
|
+
return getBinaryPath(contentHash)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// generate build workspace
|
|
61
|
+
const buildDir = getBuildDir()
|
|
62
|
+
console.log('Generating build workspace...')
|
|
63
|
+
generateBuildWorkspace({
|
|
64
|
+
buildDir,
|
|
65
|
+
tsgolintDir,
|
|
66
|
+
lintcnDir,
|
|
67
|
+
rules,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// compile
|
|
71
|
+
const binDir = getBinDir()
|
|
72
|
+
fs.mkdirSync(binDir, { recursive: true })
|
|
73
|
+
const binaryPath = getBinaryPath(contentHash)
|
|
74
|
+
|
|
75
|
+
console.log('Compiling custom tsgolint binary...')
|
|
76
|
+
await execAsync('go', ['build', '-o', binaryPath, './wrapper'], {
|
|
77
|
+
cwd: buildDir,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
console.log('Build complete')
|
|
81
|
+
return binaryPath
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function lint({
|
|
85
|
+
rebuild,
|
|
86
|
+
tsgolintVersion,
|
|
87
|
+
passthroughArgs,
|
|
88
|
+
}: {
|
|
89
|
+
rebuild: boolean
|
|
90
|
+
tsgolintVersion: string
|
|
91
|
+
passthroughArgs: string[]
|
|
92
|
+
}): Promise<number> {
|
|
93
|
+
const binaryPath = await buildBinary({ rebuild, tsgolintVersion })
|
|
94
|
+
|
|
95
|
+
// run the binary with passthrough args, inheriting stdio
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
const proc = spawn(binaryPath, passthroughArgs, {
|
|
98
|
+
stdio: 'inherit',
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
proc.on('error', (err) => {
|
|
102
|
+
console.error(`Failed to run binary: ${err.message}`)
|
|
103
|
+
resolve(1)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
proc.on('close', (code) => {
|
|
107
|
+
resolve(code ?? 1)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// lintcn list — list installed rules with metadata from .lintcn/
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import { getLintcnDir } from '../paths.ts'
|
|
5
|
+
import { discoverRules } from '../discover.ts'
|
|
6
|
+
|
|
7
|
+
export function listRules(): void {
|
|
8
|
+
const lintcnDir = getLintcnDir()
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(lintcnDir)) {
|
|
11
|
+
console.log('No .lintcn/ directory found. Run `lintcn add <url>` to add rules.')
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const rules = discoverRules(lintcnDir)
|
|
16
|
+
|
|
17
|
+
if (rules.length === 0) {
|
|
18
|
+
console.log('No rules installed. Run `lintcn add <url>` to add rules.')
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log('Installed rules:\n')
|
|
23
|
+
|
|
24
|
+
const maxNameLen = Math.max(...rules.map((r) => { return r.name.length }))
|
|
25
|
+
|
|
26
|
+
for (const rule of rules) {
|
|
27
|
+
const name = rule.name.padEnd(maxNameLen + 2)
|
|
28
|
+
const desc = rule.description || '(no description)'
|
|
29
|
+
console.log(` ${name}${desc}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(`\n${rules.length} rule${rules.length === 1 ? '' : 's'} installed`)
|
|
33
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// lintcn remove <name> — delete a rule and its test file from .lintcn/
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { getLintcnDir } from '../paths.ts'
|
|
6
|
+
import { discoverRules } from '../discover.ts'
|
|
7
|
+
|
|
8
|
+
export function removeRule(name: string): void {
|
|
9
|
+
const lintcnDir = getLintcnDir()
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(lintcnDir)) {
|
|
12
|
+
throw new Error('No .lintcn/ directory found.')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// match by lintcn:name metadata or by filename
|
|
16
|
+
const rules = discoverRules(lintcnDir)
|
|
17
|
+
const normalizedName = name.replace(/-/g, '_')
|
|
18
|
+
|
|
19
|
+
const match = rules.find((r) => {
|
|
20
|
+
return r.name === name || r.fileName.replace(/\.go$/, '') === normalizedName
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
if (!match) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Rule "${name}" not found. Run \`lintcn list\` to see installed rules.`,
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// delete rule file
|
|
30
|
+
const rulePath = path.join(lintcnDir, match.fileName)
|
|
31
|
+
fs.rmSync(rulePath)
|
|
32
|
+
console.log(`Removed ${match.fileName}`)
|
|
33
|
+
|
|
34
|
+
// delete test file if exists
|
|
35
|
+
const testFileName = match.fileName.replace(/\.go$/, '_test.go')
|
|
36
|
+
const testPath = path.join(lintcnDir, testFileName)
|
|
37
|
+
if (fs.existsSync(testPath)) {
|
|
38
|
+
fs.rmSync(testPath)
|
|
39
|
+
console.log(`Removed ${testFileName}`)
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/discover.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
|
|
7
|
+
export interface RuleMetadata {
|
|
8
|
+
/** kebab-case rule name from // lintcn:name or derived from filename */
|
|
9
|
+
name: string
|
|
10
|
+
/** one-line description from // lintcn:description */
|
|
11
|
+
description: string
|
|
12
|
+
/** original source URL from // lintcn:source */
|
|
13
|
+
source: string
|
|
14
|
+
/** exported Go variable name like NoFloatingPromisesRule */
|
|
15
|
+
varName: string
|
|
16
|
+
/** filename relative to .lintcn/ */
|
|
17
|
+
fileName: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const RULE_VAR_RE = /^var\s+(\w+)\s*=\s*rule\.Rule\s*\{/m
|
|
21
|
+
const METADATA_RE = /^\/\/\s*lintcn:(\w+)\s+(.+)$/gm
|
|
22
|
+
|
|
23
|
+
export function parseMetadata(content: string): Record<string, string> {
|
|
24
|
+
const meta: Record<string, string> = {}
|
|
25
|
+
for (const match of content.matchAll(METADATA_RE)) {
|
|
26
|
+
meta[match[1]] = match[2].trim()
|
|
27
|
+
}
|
|
28
|
+
return meta
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parseRuleVar(content: string): string | undefined {
|
|
32
|
+
const match = content.match(RULE_VAR_RE)
|
|
33
|
+
return match?.[1]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function discoverRules(lintcnDir: string): RuleMetadata[] {
|
|
37
|
+
if (!fs.existsSync(lintcnDir)) {
|
|
38
|
+
return []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const files = fs.readdirSync(lintcnDir).filter((f) => {
|
|
42
|
+
return f.endsWith('.go') && !f.endsWith('_test.go')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const rules: RuleMetadata[] = []
|
|
46
|
+
|
|
47
|
+
for (const fileName of files) {
|
|
48
|
+
const filePath = path.join(lintcnDir, fileName)
|
|
49
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
50
|
+
|
|
51
|
+
const varName = parseRuleVar(content)
|
|
52
|
+
if (!varName) {
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const meta = parseMetadata(content)
|
|
57
|
+
const baseName = fileName.replace(/\.go$/, '')
|
|
58
|
+
|
|
59
|
+
rules.push({
|
|
60
|
+
name: meta.name || baseName.replace(/_/g, '-'),
|
|
61
|
+
description: meta.description || '',
|
|
62
|
+
source: meta.source || '',
|
|
63
|
+
varName,
|
|
64
|
+
fileName,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return rules
|
|
69
|
+
}
|
package/src/exec.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Async process execution utility using spawn.
|
|
2
|
+
// Returns stdout/stderr as strings, rejects on non-zero exit code.
|
|
3
|
+
|
|
4
|
+
import { spawn } from 'node:child_process'
|
|
5
|
+
|
|
6
|
+
export interface ExecResult {
|
|
7
|
+
stdout: string
|
|
8
|
+
stderr: string
|
|
9
|
+
exitCode: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function execAsync(
|
|
13
|
+
command: string,
|
|
14
|
+
args: string[],
|
|
15
|
+
options?: { cwd?: string; stdio?: 'pipe' | 'inherit' },
|
|
16
|
+
): Promise<ExecResult> {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const proc = spawn(command, args, {
|
|
19
|
+
cwd: options?.cwd,
|
|
20
|
+
stdio: options?.stdio === 'inherit' ? 'inherit' : 'pipe',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
let stdout = ''
|
|
24
|
+
let stderr = ''
|
|
25
|
+
|
|
26
|
+
if (proc.stdout) {
|
|
27
|
+
proc.stdout.on('data', (data: Buffer) => {
|
|
28
|
+
stdout += data.toString()
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
if (proc.stderr) {
|
|
32
|
+
proc.stderr.on('data', (data: Buffer) => {
|
|
33
|
+
stderr += data.toString()
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
proc.on('error', (err) => {
|
|
38
|
+
reject(new Error(`Failed to execute ${command}: ${err.message}`, { cause: err }))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
proc.on('close', (code) => {
|
|
42
|
+
const exitCode = code ?? 1
|
|
43
|
+
if (exitCode !== 0 && options?.stdio !== 'inherit') {
|
|
44
|
+
reject(new Error(`${command} exited with code ${exitCode}\n${stderr}`))
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
resolve({ stdout, stderr, exitCode })
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
}
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
|
|
5
|
+
import crypto from 'node:crypto'
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
import { execAsync } from './exec.ts'
|
|
9
|
+
|
|
10
|
+
export async function computeContentHash({
|
|
11
|
+
lintcnDir,
|
|
12
|
+
tsgolintVersion,
|
|
13
|
+
}: {
|
|
14
|
+
lintcnDir: string
|
|
15
|
+
tsgolintVersion: string
|
|
16
|
+
}): Promise<string> {
|
|
17
|
+
const hash = crypto.createHash('sha256')
|
|
18
|
+
|
|
19
|
+
hash.update(`tsgolint:${tsgolintVersion}\n`)
|
|
20
|
+
hash.update(`platform:${process.platform}-${process.arch}\n`)
|
|
21
|
+
|
|
22
|
+
// add Go version
|
|
23
|
+
try {
|
|
24
|
+
const { stdout } = await execAsync('go', ['version'])
|
|
25
|
+
hash.update(`go:${stdout.trim()}\n`)
|
|
26
|
+
} catch {
|
|
27
|
+
hash.update('go:unknown\n')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// add all rule file contents in sorted order
|
|
31
|
+
const files = fs
|
|
32
|
+
.readdirSync(lintcnDir)
|
|
33
|
+
.filter((f) => {
|
|
34
|
+
return f.endsWith('.go')
|
|
35
|
+
})
|
|
36
|
+
.sort()
|
|
37
|
+
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
const content = fs.readFileSync(path.join(lintcnDir, file), 'utf-8')
|
|
40
|
+
hash.update(`file:${file}\n`)
|
|
41
|
+
hash.update(content)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return hash.digest('hex').slice(0, 16)
|
|
45
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1 +1,7 @@
|
|
|
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'
|