lintcn 0.3.0 → 0.4.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 CHANGED
@@ -1,3 +1,15 @@
1
+ ## 0.4.0
2
+
3
+ 1. **Simpler rule imports** — rules now import from `pkg/rule`, `pkg/utils`, etc. instead of `internal/rule`. The `internal/` child-module-path hack is gone. Your `.go` files use clean import paths:
4
+ ```go
5
+ import (
6
+ "github.com/typescript-eslint/tsgolint/pkg/rule"
7
+ "github.com/typescript-eslint/tsgolint/pkg/utils"
8
+ )
9
+ ```
10
+
11
+ 2. **Simpler codegen** — uses a tsgolint fork with `pkg/runner.Run()`, eliminating all regex surgery on main.go. The generated binary entry point is a 15-line template instead of a patched copy of tsgolint's main.go.
12
+
1
13
  ## 0.3.0
2
14
 
3
15
  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/dist/cache.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const DEFAULT_TSGOLINT_VERSION = "v0.9.2";
1
+ export declare const DEFAULT_TSGOLINT_VERSION = "a93604379da2631b70332a65bc47eb5ced689a3b";
2
2
  export declare function getCacheDir(): string;
3
3
  export declare function getTsgolintSourceDir(version: string): string;
4
4
  export declare function getBinDir(): string;
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAmBA,eAAO,MAAM,wBAAwB,WAAW,CAAA;AAOhD,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,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAuCD,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2D3E;AAED,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAQ/D"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,wBAAwB,6CAA6C,CAAA;AAQlF,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,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAqCD,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2D3E;AAED,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAQ/D"}
package/dist/cache.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Manage cached tsgolint source and compiled binaries.
2
- // Downloads tsgolint + typescript-go as tarballs from GitHub (no git required),
3
- // applies patches with `patch -p1`, and copies internal/collections.
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)
@@ -10,15 +10,15 @@ import os from 'node:os';
10
10
  import path from 'node:path';
11
11
  import { pipeline } from 'node:stream/promises';
12
12
  import { execAsync } from "./exec.js";
13
- // Pinned tsgolint version — updated with each lintcn release.
14
- // This ensures reproducible builds: every user on the same lintcn version
15
- // compiles rules against the same tsgolint API. Changing this is a conscious
16
- // decision tsgolint API changes can break user rules.
17
- export const DEFAULT_TSGOLINT_VERSION = 'v0.9.2';
18
- // Pinned typescript-go commit that tsgolint v0.9.2 depends on.
19
- // Found via `git ls-tree HEAD typescript-go` in the tsgolint repo.
13
+ // Pinned tsgolint fork commit — updated with each lintcn release.
14
+ // Uses remorses/tsgolint fork which exposes pkg/runner.Run() and
15
+ // moves internal/ packages to pkg/ for clean external imports.
16
+ export const DEFAULT_TSGOLINT_VERSION = 'a93604379da2631b70332a65bc47eb5ced689a3b';
17
+ // Pinned typescript-go base commit from microsoft/typescript-go (before patches).
18
+ // Patches from tsgolint/patches/ are applied on top during setup.
19
+ // This is the upstream commit the tsgolint submodule was forked from.
20
20
  // Must be updated when DEFAULT_TSGOLINT_VERSION changes.
21
- const TYPESCRIPT_GO_COMMIT = '2437fa43e85103d2a18e8e41e1a2a994d0708ccf';
21
+ const TYPESCRIPT_GO_COMMIT = '1b7eabe122e1575a0df9c77eccdf4e063c623224';
22
22
  export function getCacheDir() {
23
23
  return path.join(os.homedir(), '.cache', 'lintcn');
24
24
  }
@@ -43,7 +43,6 @@ async function downloadAndExtract(url, targetDir) {
43
43
  throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
44
44
  }
45
45
  fs.mkdirSync(targetDir, { recursive: true });
46
- // pipe through gunzip, then extract with tar (strip top-level directory)
47
46
  const tmpTarGz = path.join(os.tmpdir(), `lintcn-${Date.now()}.tar.gz`);
48
47
  const fileStream = fs.createWriteStream(tmpTarGz);
49
48
  // @ts-ignore ReadableStream vs NodeJS.ReadableStream mismatch
@@ -59,7 +58,6 @@ async function applyPatches(patchesDir, targetDir) {
59
58
  .sort();
60
59
  for (const patchFile of patches) {
61
60
  const patchPath = path.join(patchesDir, patchFile);
62
- // --batch silences interactive prompts, -f forces application
63
61
  await execAsync('patch', ['-p1', '--batch', '-i', patchPath], { cwd: targetDir });
64
62
  }
65
63
  return patches.length;
@@ -75,16 +73,16 @@ export async function ensureTsgolintSource(version) {
75
73
  fs.rmSync(sourceDir, { recursive: true });
76
74
  }
77
75
  try {
78
- // download tsgolint source tarball
79
- console.log(`Downloading tsgolint@${version}...`);
80
- const tsgolintUrl = `https://github.com/oxc-project/tsgolint/archive/refs/tags/${version}.tar.gz`;
76
+ // download tsgolint fork tarball
77
+ console.log(`Downloading tsgolint@${version.slice(0, 8)}...`);
78
+ const tsgolintUrl = `https://github.com/remorses/tsgolint/archive/${version}.tar.gz`;
81
79
  await downloadAndExtract(tsgolintUrl, sourceDir);
82
- // download typescript-go source tarball into tsgolint/typescript-go/
80
+ // download typescript-go from microsoft (base commit before patches)
83
81
  const tsGoDir = path.join(sourceDir, 'typescript-go');
84
82
  console.log('Downloading typescript-go...');
85
83
  const tsGoUrl = `https://github.com/microsoft/typescript-go/archive/${TYPESCRIPT_GO_COMMIT}.tar.gz`;
86
84
  await downloadAndExtract(tsGoUrl, tsGoDir);
87
- // apply patches to typescript-go
85
+ // apply tsgolint's patches to typescript-go
88
86
  const patchesDir = path.join(sourceDir, 'patches');
89
87
  if (fs.existsSync(patchesDir)) {
90
88
  const count = await applyPatches(patchesDir, tsGoDir);
package/dist/codegen.d.ts CHANGED
@@ -1,24 +1,13 @@
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 in cache dir for compiling the custom binary.
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.
5
+ * With pkg/runner.Run(), the generated main.go is a static template
6
+ * no regex surgery or file copying needed. */
15
7
  export declare function generateBuildWorkspace({ buildDir, tsgolintDir, lintcnDir, rules, }: {
16
8
  buildDir: string;
17
9
  tsgolintDir: string;
18
10
  lintcnDir: string;
19
11
  rules: RuleMetadata[];
20
12
  }): 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
13
  //# sourceMappingURL=codegen.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"codegen.d.ts","sourceRoot":"","sources":["../src/codegen.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AA2BjD;;;;;;;oFAOoF;AACpF,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAiC7D;AAED;;;uDAGuD;AACvD,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,CAiDP;AA8DD;kEACkE;AAClE,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAQlF"}
1
+ {"version":3,"file":"codegen.d.ts","sourceRoot":"","sources":["../src/codegen.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AA2BjD,4EAA4E;AAC5E,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAkC7D;AAED;;+CAE+C;AAC/C,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,11 @@
1
1
  // Generate Go workspace files for building a custom tsgolint binary.
2
- // Creates:
3
- // .lintcn/go.work workspace for gopls (editor support)
4
- // .lintcn/go.mod — module declaration
5
- // build/go.work — build workspace in cache dir
6
- // build/wrapper/go.mod — wrapper module
7
- // build/wrapper/main.go — tsgolint main.go with custom rules appended
2
+ // With the fork (remorses/tsgolint) exposing pkg/runner.Run(), codegen is
3
+ // minimal: a 10-line main.go template + go.work with shim replaces.
4
+ // No regex surgery, no file copying, no fragile string manipulation.
8
5
  import fs from 'node:fs';
9
6
  import path from 'node:path';
10
- // All replace directives needed from tsgolint's go.mod.
11
- // These redirect shim module paths to local directories inside the tsgolint source.
7
+ // Shim modules that need replace directives in go.work.
8
+ // These redirect module paths to local directories inside the tsgolint source.
12
9
  const SHIM_MODULES = [
13
10
  'ast',
14
11
  'bundled',
@@ -30,14 +27,7 @@ function generateReplaceDirectives(tsgolintRelPath) {
30
27
  return `\tgithub.com/microsoft/typescript-go/shim/${mod} => ${tsgolintRelPath}/shim/${mod}`;
31
28
  }).join('\n');
32
29
  }
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. */
30
+ /** Generate .lintcn/go.work and .lintcn/go.mod for editor/gopls support. */
41
31
  export function generateEditorGoFiles(lintcnDir) {
42
32
  const goWork = `go 1.26
43
33
 
@@ -51,7 +41,8 @@ replace (
51
41
  ${generateReplaceDirectives('./.tsgolint')}
52
42
  )
53
43
  `;
54
- const goMod = `module github.com/typescript-eslint/tsgolint/lintcn-rules
44
+ // No child-path hack needed — pkg/ is public, any module name works
45
+ const goMod = `module lintcn-rules
55
46
 
56
47
  go 1.26
57
48
  `;
@@ -68,10 +59,9 @@ go.sum
68
59
  fs.writeFileSync(gitignorePath, gitignore);
69
60
  }
70
61
  }
71
- /** Generate build workspace in cache dir for compiling the custom binary.
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. */
62
+ /** Generate build workspace for compiling the custom binary.
63
+ * With pkg/runner.Run(), the generated main.go is a static template
64
+ * no regex surgery or file copying needed. */
75
65
  export function generateBuildWorkspace({ buildDir, tsgolintDir, lintcnDir, rules, }) {
76
66
  fs.mkdirSync(path.join(buildDir, 'wrapper'), { recursive: true });
77
67
  // symlink tsgolint source
@@ -86,7 +76,7 @@ export function generateBuildWorkspace({ buildDir, tsgolintDir, lintcnDir, rules
86
76
  fs.rmSync(rulesLink, { recursive: true });
87
77
  }
88
78
  fs.symlinkSync(path.resolve(lintcnDir), rulesLink);
89
- // go.work — must include typescript-go submodule and use child module paths
79
+ // go.work
90
80
  const goWork = `go 1.26
91
81
 
92
82
  use (
@@ -101,73 +91,38 @@ ${generateReplaceDirectives('./tsgolint')}
101
91
  )
102
92
  `;
103
93
  fs.writeFileSync(path.join(buildDir, 'go.work'), goWork);
104
- // wrapper/go.mod — must be child path of tsgolint for internal/ access.
105
- // Minimal: no require block. The workspace resolves all dependencies.
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
94
+ // wrapper/go.mod — simple module name, no child-path hack needed
95
+ const wrapperGoMod = `module lintcn-wrapper
108
96
 
109
97
  go 1.26
110
98
  `;
111
99
  fs.writeFileSync(path.join(buildDir, 'wrapper', 'go.mod'), wrapperGoMod);
112
- // copy all supporting .go files from cmd/tsgolint/ (headless, payload, etc.)
113
- const wrapperDir = path.join(buildDir, 'wrapper');
114
- copyTsgolintCmdFiles(tsgolintDir, wrapperDir);
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);
100
+ // wrapper/main.go simple template, no regex or string surgery
101
+ const mainGo = generateMainGo(rules);
102
+ fs.writeFileSync(path.join(buildDir, 'wrapper', 'main.go'), mainGo);
118
103
  }
119
- /** Copy tsgolint's main.go and transform it to only include custom rules.
120
- * Two targeted string operations on the copied source:
121
- * 1. Remove all /internal/rules/ import lines (built-in rule packages)
122
- * 2. Replace allRules body with only custom lintcn.* entries
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},`;
104
+ /** Generate a minimal main.go that imports user rules and calls runner.Run().
105
+ * This is a static template no copying or patching of tsgolint source. */
106
+ function generateMainGo(rules) {
107
+ const ruleEntries = rules.map((r) => {
108
+ return `\t\tlintcn.${r.varName},`;
151
109
  }).join('\n');
152
- const allRulesPattern = /var allRules = \[]rule\.Rule\{[^}]*\}/s;
153
- if (!allRulesPattern.test(mainGo)) {
154
- throw new Error('Failed to find allRules slice in tsgolint main.go. The source layout may have changed.');
155
- }
156
- mainGo = mainGo.replace(allRulesPattern, `var allRules = []rule.Rule{\n${customEntries}\n}`);
157
- // assertion: verify custom rules are present
158
- if (customRules.length > 0 && !mainGo.includes(`lintcn.${customRules[0].varName}`)) {
159
- throw new Error('Custom rule injection verification failed.');
160
- }
161
- return mainGo;
110
+ return `// Code generated by lintcn. DO NOT EDIT.
111
+ package main
112
+
113
+ import (
114
+ \t"os"
115
+
116
+ \t"github.com/typescript-eslint/tsgolint/pkg/rule"
117
+ \t"github.com/typescript-eslint/tsgolint/pkg/runner"
118
+ \tlintcn "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
- /** Copy all supporting .go files from cmd/tsgolint/ into the wrapper dir.
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lintcn",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",
package/src/cache.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Manage cached tsgolint source and compiled binaries.
2
- // Downloads tsgolint + typescript-go as tarballs from GitHub (no git required),
3
- // applies patches with `patch -p1`, and copies internal/collections.
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)
@@ -10,19 +10,18 @@ import fs from 'node:fs'
10
10
  import os from 'node:os'
11
11
  import path from 'node:path'
12
12
  import { pipeline } from 'node:stream/promises'
13
- import { createGunzip } from 'node:zlib'
14
13
  import { execAsync } from './exec.ts'
15
14
 
16
- // Pinned tsgolint version — updated with each lintcn release.
17
- // This ensures reproducible builds: every user on the same lintcn version
18
- // compiles rules against the same tsgolint API. Changing this is a conscious
19
- // decision tsgolint API changes can break user rules.
20
- export const DEFAULT_TSGOLINT_VERSION = 'v0.9.2'
15
+ // Pinned tsgolint fork commit — updated with each lintcn release.
16
+ // Uses remorses/tsgolint fork which exposes pkg/runner.Run() and
17
+ // moves internal/ packages to pkg/ for clean external imports.
18
+ export const DEFAULT_TSGOLINT_VERSION = 'a93604379da2631b70332a65bc47eb5ced689a3b'
21
19
 
22
- // Pinned typescript-go commit that tsgolint v0.9.2 depends on.
23
- // Found via `git ls-tree HEAD typescript-go` in the tsgolint repo.
20
+ // Pinned typescript-go base commit from microsoft/typescript-go (before patches).
21
+ // Patches from tsgolint/patches/ are applied on top during setup.
22
+ // This is the upstream commit the tsgolint submodule was forked from.
24
23
  // Must be updated when DEFAULT_TSGOLINT_VERSION changes.
25
- const TYPESCRIPT_GO_COMMIT = '2437fa43e85103d2a18e8e41e1a2a994d0708ccf'
24
+ const TYPESCRIPT_GO_COMMIT = '1b7eabe122e1575a0df9c77eccdf4e063c623224'
26
25
 
27
26
  export function getCacheDir(): string {
28
27
  return path.join(os.homedir(), '.cache', 'lintcn')
@@ -55,7 +54,6 @@ async function downloadAndExtract(url: string, targetDir: string): Promise<void>
55
54
 
56
55
  fs.mkdirSync(targetDir, { recursive: true })
57
56
 
58
- // pipe through gunzip, then extract with tar (strip top-level directory)
59
57
  const tmpTarGz = path.join(os.tmpdir(), `lintcn-${Date.now()}.tar.gz`)
60
58
  const fileStream = fs.createWriteStream(tmpTarGz)
61
59
  // @ts-ignore ReadableStream vs NodeJS.ReadableStream mismatch
@@ -74,7 +72,6 @@ async function applyPatches(patchesDir: string, targetDir: string): Promise<numb
74
72
 
75
73
  for (const patchFile of patches) {
76
74
  const patchPath = path.join(patchesDir, patchFile)
77
- // --batch silences interactive prompts, -f forces application
78
75
  await execAsync('patch', ['-p1', '--batch', '-i', patchPath], { cwd: targetDir })
79
76
  }
80
77
 
@@ -95,18 +92,18 @@ export async function ensureTsgolintSource(version: string): Promise<string> {
95
92
  }
96
93
 
97
94
  try {
98
- // download tsgolint source tarball
99
- console.log(`Downloading tsgolint@${version}...`)
100
- const tsgolintUrl = `https://github.com/oxc-project/tsgolint/archive/refs/tags/${version}.tar.gz`
95
+ // download tsgolint fork tarball
96
+ console.log(`Downloading tsgolint@${version.slice(0, 8)}...`)
97
+ const tsgolintUrl = `https://github.com/remorses/tsgolint/archive/${version}.tar.gz`
101
98
  await downloadAndExtract(tsgolintUrl, sourceDir)
102
99
 
103
- // download typescript-go source tarball into tsgolint/typescript-go/
100
+ // download typescript-go from microsoft (base commit before patches)
104
101
  const tsGoDir = path.join(sourceDir, 'typescript-go')
105
102
  console.log('Downloading typescript-go...')
106
103
  const tsGoUrl = `https://github.com/microsoft/typescript-go/archive/${TYPESCRIPT_GO_COMMIT}.tar.gz`
107
104
  await downloadAndExtract(tsGoUrl, tsGoDir)
108
105
 
109
- // apply patches to typescript-go
106
+ // apply tsgolint's patches to typescript-go
110
107
  const patchesDir = path.join(sourceDir, 'patches')
111
108
  if (fs.existsSync(patchesDir)) {
112
109
  const count = await applyPatches(patchesDir, tsGoDir)
package/src/codegen.ts CHANGED
@@ -1,17 +1,14 @@
1
1
  // Generate Go workspace files for building a custom tsgolint binary.
2
- // Creates:
3
- // .lintcn/go.work workspace for gopls (editor support)
4
- // .lintcn/go.mod — module declaration
5
- // build/go.work — build workspace in cache dir
6
- // build/wrapper/go.mod — wrapper module
7
- // build/wrapper/main.go — tsgolint main.go with custom rules appended
2
+ // With the fork (remorses/tsgolint) exposing pkg/runner.Run(), codegen is
3
+ // minimal: a 10-line main.go template + go.work with shim replaces.
4
+ // No regex surgery, no file copying, no fragile string manipulation.
8
5
 
9
6
  import fs from 'node:fs'
10
7
  import path from 'node:path'
11
8
  import type { RuleMetadata } from './discover.ts'
12
9
 
13
- // All replace directives needed from tsgolint's go.mod.
14
- // These redirect shim module paths to local directories inside the tsgolint source.
10
+ // Shim modules that need replace directives in go.work.
11
+ // These redirect module paths to local directories inside the tsgolint source.
15
12
  const SHIM_MODULES = [
16
13
  'ast',
17
14
  'bundled',
@@ -35,14 +32,7 @@ function generateReplaceDirectives(tsgolintRelPath: string): string {
35
32
  }).join('\n')
36
33
  }
37
34
 
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. */
35
+ /** Generate .lintcn/go.work and .lintcn/go.mod for editor/gopls support. */
46
36
  export function generateEditorGoFiles(lintcnDir: string): void {
47
37
  const goWork = `go 1.26
48
38
 
@@ -57,7 +47,8 @@ ${generateReplaceDirectives('./.tsgolint')}
57
47
  )
58
48
  `
59
49
 
60
- const goMod = `module github.com/typescript-eslint/tsgolint/lintcn-rules
50
+ // No child-path hack needed — pkg/ is public, any module name works
51
+ const goMod = `module lintcn-rules
61
52
 
62
53
  go 1.26
63
54
  `
@@ -78,10 +69,9 @@ go.sum
78
69
  }
79
70
  }
80
71
 
81
- /** Generate build workspace in cache dir for compiling the custom binary.
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. */
72
+ /** Generate build workspace for compiling the custom binary.
73
+ * With pkg/runner.Run(), the generated main.go is a static template
74
+ * no regex surgery or file copying needed. */
85
75
  export function generateBuildWorkspace({
86
76
  buildDir,
87
77
  tsgolintDir,
@@ -109,7 +99,7 @@ export function generateBuildWorkspace({
109
99
  }
110
100
  fs.symlinkSync(path.resolve(lintcnDir), rulesLink)
111
101
 
112
- // go.work — must include typescript-go submodule and use child module paths
102
+ // go.work
113
103
  const goWork = `go 1.26
114
104
 
115
105
  use (
@@ -125,92 +115,41 @@ ${generateReplaceDirectives('./tsgolint')}
125
115
  `
126
116
  fs.writeFileSync(path.join(buildDir, 'go.work'), goWork)
127
117
 
128
- // wrapper/go.mod — must be child path of tsgolint for internal/ access.
129
- // Minimal: no require block. The workspace resolves all dependencies.
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
118
+ // wrapper/go.mod — simple module name, no child-path hack needed
119
+ const wrapperGoMod = `module lintcn-wrapper
132
120
 
133
121
  go 1.26
134
122
  `
135
123
  fs.writeFileSync(path.join(buildDir, 'wrapper', 'go.mod'), wrapperGoMod)
136
124
 
137
- // copy all supporting .go files from cmd/tsgolint/ (headless, payload, etc.)
138
- const wrapperDir = path.join(buildDir, 'wrapper')
139
- copyTsgolintCmdFiles(tsgolintDir, wrapperDir)
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)
125
+ // wrapper/main.go simple template, no regex or string surgery
126
+ const mainGo = generateMainGo(rules)
127
+ fs.writeFileSync(path.join(buildDir, 'wrapper', 'main.go'), mainGo)
144
128
  }
145
129
 
146
- /** Copy tsgolint's main.go and transform it to only include custom rules.
147
- * Two targeted string operations on the copied source:
148
- * 1. Remove all /internal/rules/ import lines (built-in rule packages)
149
- * 2. Replace allRules body with only custom lintcn.* entries
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},`
130
+ /** Generate a minimal main.go that imports user rules and calls runner.Run().
131
+ * This is a static template no copying or patching of tsgolint source. */
132
+ function generateMainGo(rules: RuleMetadata[]): string {
133
+ const ruleEntries = rules.map((r) => {
134
+ return `\t\tlintcn.${r.varName},`
184
135
  }).join('\n')
185
136
 
186
- const allRulesPattern = /var allRules = \[]rule\.Rule\{[^}]*\}/s
187
- if (!allRulesPattern.test(mainGo)) {
188
- throw new Error(
189
- 'Failed to find allRules slice in tsgolint main.go. The source layout may have changed.',
190
- )
191
- }
137
+ return `// Code generated by lintcn. DO NOT EDIT.
138
+ package main
192
139
 
193
- mainGo = mainGo.replace(
194
- allRulesPattern,
195
- `var allRules = []rule.Rule{\n${customEntries}\n}`,
196
- )
140
+ import (
141
+ \t"os"
197
142
 
198
- // assertion: verify custom rules are present
199
- if (customRules.length > 0 && !mainGo.includes(`lintcn.${customRules[0].varName}`)) {
200
- throw new Error('Custom rule injection verification failed.')
201
- }
143
+ \t"github.com/typescript-eslint/tsgolint/pkg/rule"
144
+ \t"github.com/typescript-eslint/tsgolint/pkg/runner"
145
+ \tlintcn "lintcn-rules"
146
+ )
202
147
 
203
- return mainGo
148
+ func main() {
149
+ \trules := []rule.Rule{
150
+ ${ruleEntries}
151
+ \t}
152
+ \tos.Exit(runner.Run(rules, os.Args[1:]))
204
153
  }
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
- }
154
+ `
216
155
  }