lintcn 0.0.1 → 0.1.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +133 -5
  4. package/dist/cache.d.ts +9 -0
  5. package/dist/cache.d.ts.map +1 -0
  6. package/dist/cache.js +84 -0
  7. package/dist/cli.js +65 -3
  8. package/dist/codegen.d.ts +18 -0
  9. package/dist/codegen.d.ts.map +1 -0
  10. package/dist/codegen.js +607 -0
  11. package/dist/commands/add.d.ts +2 -0
  12. package/dist/commands/add.d.ts.map +1 -0
  13. package/dist/commands/add.js +101 -0
  14. package/dist/commands/lint.d.ts +8 -0
  15. package/dist/commands/lint.d.ts.map +1 -0
  16. package/dist/commands/lint.js +78 -0
  17. package/dist/commands/list.d.ts +2 -0
  18. package/dist/commands/list.d.ts.map +1 -0
  19. package/dist/commands/list.js +24 -0
  20. package/dist/commands/remove.d.ts +2 -0
  21. package/dist/commands/remove.d.ts.map +1 -0
  22. package/dist/commands/remove.js +31 -0
  23. package/dist/discover.d.ts +16 -0
  24. package/dist/discover.d.ts.map +1 -0
  25. package/dist/discover.js +44 -0
  26. package/dist/exec.d.ts +10 -0
  27. package/dist/exec.d.ts.map +1 -0
  28. package/dist/exec.js +34 -0
  29. package/dist/hash.d.ts +5 -0
  30. package/dist/hash.d.ts.map +1 -0
  31. package/dist/hash.js +33 -0
  32. package/dist/index.d.ts +6 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +5 -1
  35. package/dist/paths.d.ts +2 -0
  36. package/dist/paths.d.ts.map +1 -0
  37. package/dist/paths.js +5 -0
  38. package/package.json +12 -9
  39. package/src/cache.ts +102 -0
  40. package/src/cli.ts +74 -2
  41. package/src/codegen.ts +640 -0
  42. package/src/commands/add.ts +118 -0
  43. package/src/commands/lint.ts +102 -0
  44. package/src/commands/list.ts +33 -0
  45. package/src/commands/remove.ts +41 -0
  46. package/src/discover.ts +69 -0
  47. package/src/exec.ts +50 -0
  48. package/src/hash.ts +45 -0
  49. package/src/index.ts +6 -1
  50. package/src/paths.ts +7 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ ## 0.1.0
2
+
3
+ 1. **Initial release** — CLI for adding type-aware TypeScript lint rules as Go files to your project:
4
+
5
+ ```bash
6
+ npx lintcn add https://github.com/user/repo/blob/main/rules/no_unhandled_error.go
7
+ npx lintcn lint
8
+ ```
9
+
10
+ 2. **`lintcn add <url>`** — fetch a `.go` rule file by URL into `.lintcn/`. Normalizes GitHub blob URLs to raw URLs automatically. Also fetches the matching `_test.go` if present. Rewrites the package declaration to `package lintcn` and injects a `// lintcn:source` comment.
11
+
12
+ 3. **`lintcn lint`** — builds a custom tsgolint binary (all 50+ built-in rules + your custom rules) and runs it against the project. Binary is cached by SHA-256 content hash — rebuilds only when rules change.
13
+
14
+ 4. **`lintcn build`** — build the custom binary without running it. Prints the binary path.
15
+
16
+ 5. **`lintcn list`** — list installed rules with descriptions parsed from `// lintcn:` metadata comments.
17
+
18
+ 6. **`lintcn remove <name>`** — delete a rule and its test file from `.lintcn/`.
19
+
20
+ 7. **Editor/LSP support** — generates `go.work` and `go.mod` inside `.lintcn/` so gopls provides full autocomplete, go-to-definition, and type checking on tsgolint APIs while writing rules.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
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.
package/README.md CHANGED
@@ -1,12 +1,140 @@
1
1
  # lintcn
2
2
 
3
- The [shadcn](https://ui.shadcn.com) for type-aware TypeScript lint rules.
3
+ The [shadcn](https://ui.shadcn.com) for type-aware TypeScript lint rules. Powered by [tsgolint](https://github.com/oxc-project/tsgolint).
4
4
 
5
- Browse rules, pick the ones you need, copy them into your project. You own the code.
5
+ Add rules by URL, own the source, customize freely. Rules are Go files that use the TypeScript type checker for deep analysis — things ESLint can't do.
6
6
 
7
- ## Coming soon
7
+ ## Install
8
8
 
9
9
  ```bash
10
- npx lintcn add no-floating-promises
11
- npx lintcn add no-unused-result
10
+ npm install -D lintcn
12
11
  ```
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ # Add a rule by URL
17
+ npx lintcn add https://github.com/user/repo/blob/main/rules/no_unhandled_error.go
18
+
19
+ # Lint your project
20
+ npx lintcn lint
21
+
22
+ # Lint with a specific tsconfig
23
+ npx lintcn lint --tsconfig tsconfig.build.json
24
+
25
+ # List installed rules
26
+ npx lintcn list
27
+
28
+ # Remove a rule
29
+ npx lintcn remove no-unhandled-error
30
+ ```
31
+
32
+ ## How it works
33
+
34
+ Rules live as `.go` files in `.lintcn/` at your project root. You own the source — edit, customize, delete.
35
+
36
+ ```
37
+ my-project/
38
+ ├── .lintcn/
39
+ │ ├── .gitignore ← ignores generated Go files
40
+ │ ├── no_unhandled_error.go ← your rule (committed)
41
+ │ └── no_unhandled_error_test.go ← its tests (committed)
42
+ ├── src/
43
+ │ ├── index.ts
44
+ │ └── ...
45
+ ├── tsconfig.json
46
+ └── package.json
47
+ ```
48
+
49
+ When you run `npx lintcn lint`, the CLI:
50
+
51
+ 1. Scans `.lintcn/*.go` for rule definitions
52
+ 2. Generates a Go workspace with all 50+ built-in tsgolint rules + your custom rules
53
+ 3. Compiles a custom binary (cached — rebuilds only when rules change)
54
+ 4. Runs the binary against your project
55
+
56
+ ## Writing a rule
57
+
58
+ Every rule is a Go file with `package lintcn` that exports a `rule.Rule` variable.
59
+
60
+ Here's a rule that errors when you discard the return value of a function that returns `Error | T` — enforcing the [errore](https://errore.org) pattern:
61
+
62
+ ```go
63
+ // lintcn:name no-unhandled-error
64
+ // lintcn:description Disallow discarding Error-typed return values
65
+
66
+ package lintcn
67
+
68
+ import (
69
+ "github.com/microsoft/typescript-go/shim/ast"
70
+ "github.com/microsoft/typescript-go/shim/checker"
71
+ "github.com/typescript-eslint/tsgolint/internal/rule"
72
+ "github.com/typescript-eslint/tsgolint/internal/utils"
73
+ )
74
+
75
+ var NoUnhandledErrorRule = rule.Rule{
76
+ Name: "no-unhandled-error",
77
+ Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
78
+ return rule.RuleListeners{
79
+ ast.KindExpressionStatement: func(node *ast.Node) {
80
+ expression := ast.SkipParentheses(node.AsExpressionStatement().Expression)
81
+
82
+ if ast.IsVoidExpression(expression) {
83
+ return // void = intentional discard
84
+ }
85
+
86
+ innerExpr := expression
87
+ if ast.IsAwaitExpression(innerExpr) {
88
+ innerExpr = ast.SkipParentheses(innerExpr.Expression())
89
+ }
90
+ if !ast.IsCallExpression(innerExpr) {
91
+ return
92
+ }
93
+
94
+ t := ctx.TypeChecker.GetTypeAtLocation(expression)
95
+
96
+ if utils.IsTypeFlagSet(t, checker.TypeFlagsVoid|checker.TypeFlagsUndefined|checker.TypeFlagsNever) {
97
+ return
98
+ }
99
+
100
+ for _, part := range utils.UnionTypeParts(t) {
101
+ if utils.IsErrorLike(ctx.Program, ctx.TypeChecker, part) {
102
+ ctx.ReportNode(node, rule.RuleMessage{
103
+ Id: "noUnhandledError",
104
+ Description: "Error-typed return value is not handled.",
105
+ })
106
+ return
107
+ }
108
+ }
109
+ },
110
+ }
111
+ },
112
+ }
113
+ ```
114
+
115
+ This catches code like:
116
+
117
+ ```typescript
118
+ // error — result discarded, Error not handled
119
+ getUser("id") // returns Error | User
120
+ await fetchData("/api") // returns Promise<Error | Data>
121
+
122
+ // ok — result is checked
123
+ const user = getUser("id")
124
+ if (user instanceof Error) return user
125
+
126
+ // ok — explicitly discarded
127
+ void getUser("id")
128
+ ```
129
+
130
+ ## Prerequisites
131
+
132
+ - **Node.js** — for the CLI
133
+ - **Go 1.26+** — for compiling rules (`go.dev/dl`)
134
+ - **Git** — for cloning tsgolint source on first build
135
+
136
+ Go is only needed for `lintcn lint` / `lintcn build`. Adding and listing rules works without Go.
137
+
138
+ ## License
139
+
140
+ MIT
@@ -0,0 +1,9 @@
1
+ export declare const DEFAULT_TSGOLINT_VERSION = "main";
2
+ export declare function getCacheDir(): string;
3
+ export declare function getTsgolintSourceDir(version: string): string;
4
+ export declare function getBinDir(): string;
5
+ export declare function getBinaryPath(contentHash: string): string;
6
+ export declare function getBuildDir(): string;
7
+ export declare function ensureTsgolintSource(version: string): Promise<string>;
8
+ export declare function cachedBinaryExists(contentHash: string): boolean;
9
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,wBAAwB,SAAS,CAAA;AAE9C,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;AAED,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA0D3E;AAED,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAQ/D"}
package/dist/cache.js ADDED
@@ -0,0 +1,84 @@
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
+ import fs from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import { execAsync } from "./exec.js";
9
+ // Default tsgolint version — pinned to a known-good commit
10
+ export const DEFAULT_TSGOLINT_VERSION = 'main';
11
+ export function getCacheDir() {
12
+ return path.join(os.homedir(), '.cache', 'lintcn');
13
+ }
14
+ export function getTsgolintSourceDir(version) {
15
+ return path.join(getCacheDir(), 'tsgolint', version);
16
+ }
17
+ export function getBinDir() {
18
+ return path.join(getCacheDir(), 'bin');
19
+ }
20
+ export function getBinaryPath(contentHash) {
21
+ return path.join(getBinDir(), contentHash);
22
+ }
23
+ export function getBuildDir() {
24
+ return path.join(getCacheDir(), 'build');
25
+ }
26
+ export async function ensureTsgolintSource(version) {
27
+ const sourceDir = getTsgolintSourceDir(version);
28
+ const readyMarker = path.join(sourceDir, '.lintcn-ready');
29
+ if (fs.existsSync(readyMarker)) {
30
+ return sourceDir;
31
+ }
32
+ console.log(`Cloning tsgolint@${version}...`);
33
+ fs.mkdirSync(sourceDir, { recursive: true });
34
+ // clone with depth 1 for speed
35
+ const cloneArgs = ['clone', '--depth', '1', '--recurse-submodules', '--shallow-submodules'];
36
+ if (version !== 'main') {
37
+ cloneArgs.push('--branch', version);
38
+ }
39
+ cloneArgs.push('https://github.com/oxc-project/tsgolint.git', sourceDir);
40
+ await execAsync('git', cloneArgs);
41
+ // apply patches if they exist
42
+ const patchesDir = path.join(sourceDir, 'patches');
43
+ if (fs.existsSync(patchesDir)) {
44
+ const patches = fs.readdirSync(patchesDir).filter((f) => {
45
+ return f.endsWith('.patch');
46
+ }).sort();
47
+ if (patches.length > 0) {
48
+ console.log(`Applying ${patches.length} patches...`);
49
+ const patchPaths = patches.map((p) => {
50
+ return path.join('..', 'patches', p);
51
+ });
52
+ await execAsync('git', ['am', '--3way', '--no-gpg-sign', ...patchPaths], {
53
+ cwd: path.join(sourceDir, 'typescript-go'),
54
+ });
55
+ }
56
+ }
57
+ // copy internal/collections from typescript-go (required by tsgolint, done by `just init`)
58
+ const collectionsDir = path.join(sourceDir, 'internal', 'collections');
59
+ const tsGoCollections = path.join(sourceDir, 'typescript-go', 'internal', 'collections');
60
+ if (!fs.existsSync(collectionsDir) && fs.existsSync(tsGoCollections)) {
61
+ fs.mkdirSync(collectionsDir, { recursive: true });
62
+ const files = fs.readdirSync(tsGoCollections).filter((f) => {
63
+ return f.endsWith('.go') && !f.endsWith('_test.go');
64
+ });
65
+ for (const file of files) {
66
+ fs.copyFileSync(path.join(tsGoCollections, file), path.join(collectionsDir, file));
67
+ }
68
+ console.log(`Copied ${files.length} collection files`);
69
+ }
70
+ // write ready marker
71
+ fs.writeFileSync(readyMarker, new Date().toISOString());
72
+ console.log('tsgolint source ready');
73
+ return sourceDir;
74
+ }
75
+ export function cachedBinaryExists(contentHash) {
76
+ const binPath = getBinaryPath(contentHash);
77
+ try {
78
+ fs.accessSync(binPath, fs.constants.X_OK);
79
+ return true;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
package/dist/cli.js CHANGED
@@ -1,4 +1,66 @@
1
1
  #!/usr/bin/env node
2
- console.log('lintcn - the shadcn for type-aware TypeScript lint rules');
3
- console.log('coming soon: npx lintcn add <rule-name>');
4
- export {};
2
+ // lintcn the shadcn for type-aware TypeScript lint rules.
3
+ // Add rules by URL, compile, and run them via tsgolint.
4
+ import { goke } from 'goke';
5
+ import { createRequire } from 'node:module';
6
+ import { addRule } from "./commands/add.js";
7
+ import { lint, buildBinary } from "./commands/lint.js";
8
+ import { listRules } from "./commands/list.js";
9
+ import { removeRule } from "./commands/remove.js";
10
+ const require = createRequire(import.meta.url);
11
+ const packageJson = require('../package.json');
12
+ const cli = goke('lintcn');
13
+ cli
14
+ .command('add <url>', 'Add a rule by URL. Fetches the .go file and copies it into .lintcn/')
15
+ .example('# Add a rule from GitHub')
16
+ .example('lintcn add https://github.com/user/repo/blob/main/rules/no_floating_promises.go')
17
+ .example('# Add from raw URL')
18
+ .example('lintcn add https://raw.githubusercontent.com/user/repo/main/rules/no_unused_result.go')
19
+ .action(async (url) => {
20
+ await addRule(url);
21
+ });
22
+ cli
23
+ .command('remove <name>', 'Remove an installed rule from .lintcn/')
24
+ .example('lintcn remove no-floating-promises')
25
+ .action((name) => {
26
+ removeRule(name);
27
+ });
28
+ cli
29
+ .command('list', 'List all installed rules')
30
+ .action(() => {
31
+ listRules();
32
+ });
33
+ cli
34
+ .command('lint', 'Build custom tsgolint binary and run it against the project')
35
+ .option('--rebuild', 'Force rebuild even if cached binary exists')
36
+ .option('--tsconfig <path>', 'Path to tsconfig.json')
37
+ .option('--list-files', 'List matched files')
38
+ .action(async (options) => {
39
+ const passthroughArgs = [];
40
+ if (options.tsconfig) {
41
+ passthroughArgs.push('--tsconfig', options.tsconfig);
42
+ }
43
+ if (options.listFiles) {
44
+ passthroughArgs.push('--list-files');
45
+ }
46
+ // pass through anything after --
47
+ const doubleDash = options['--'];
48
+ if (doubleDash && Array.isArray(doubleDash)) {
49
+ passthroughArgs.push(...doubleDash);
50
+ }
51
+ const exitCode = await lint({
52
+ rebuild: !!options.rebuild,
53
+ passthroughArgs,
54
+ });
55
+ process.exit(exitCode);
56
+ });
57
+ cli
58
+ .command('build', 'Build the custom tsgolint binary without running it')
59
+ .option('--rebuild', 'Force rebuild even if cached binary exists')
60
+ .action(async (options) => {
61
+ const binaryPath = await buildBinary({ rebuild: !!options.rebuild });
62
+ console.log(binaryPath);
63
+ });
64
+ cli.help();
65
+ cli.version(packageJson.version);
66
+ cli.parse();
@@ -0,0 +1,18 @@
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. */
10
+ export declare function generateEditorGoFiles(lintcnDir: string): void;
11
+ /** Generate build workspace in cache dir for compiling the custom binary */
12
+ export declare function generateBuildWorkspace({ buildDir, tsgolintDir, lintcnDir, rules, }: {
13
+ buildDir: string;
14
+ tsgolintDir: string;
15
+ lintcnDir: string;
16
+ rules: RuleMetadata[];
17
+ }): void;
18
+ //# sourceMappingURL=codegen.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codegen.d.ts","sourceRoot":"","sources":["../src/codegen.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAiGjD;;;;;;;oFAOoF;AACpF,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAiC7D;AAED,4EAA4E;AAC5E,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,CAgDP"}