lintcn 0.1.0 → 0.3.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,35 @@
1
+ ## 0.3.0
2
+
3
+ 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.
4
+
5
+ Before (0.2.0): `Found 2315 errors (linted 193 files with 45 rules)`
6
+ After (0.3.0): `Found 8 errors (linted 193 files with 1 rule)`
7
+
8
+ 2. **Run `lintcn lint` from any subdirectory** — uses `find-up` to walk parent directories for `.lintcn/`. You no longer need to be at the project root:
9
+ ```bash
10
+ cd packages/my-app
11
+ lintcn lint # finds .lintcn/ in parent
12
+ ```
13
+
14
+ 3. **No git required** — tsgolint source is now downloaded as a tarball from GitHub instead of cloned. Patches applied with `patch -p1`. Faster first setup, works without git installed.
15
+
16
+ 4. **Fixed stale binary cache** — added `CACHE_SCHEMA_VERSION` to the content hash. Upgrading lintcn now correctly invalidates cached binaries built by older versions.
17
+
18
+ 5. **Fixed partial download corruption** — if the tsgolint download fails midway, the partial directory is cleaned up so the next run starts fresh instead of failing repeatedly.
19
+
20
+ 6. **Fixed GitHub URLs with `/` in branch names** — `lintcn add` now correctly handles branch names like `feature/my-branch` in GitHub blob URLs.
21
+
22
+ ## 0.2.0
23
+
24
+ 1. **Pinned tsgolint version** — each lintcn release bundles a specific tsgolint version (`v0.9.2`). Builds are now reproducible: everyone on the same lintcn version compiles against the same tsgolint API. Previously used `main` branch which was non-deterministic.
25
+
26
+ 2. **`--tsgolint-version` flag** — override the pinned version for testing unreleased tsgolint:
27
+ ```bash
28
+ npx lintcn lint --tsgolint-version v0.10.0
29
+ ```
30
+
31
+ 3. **Version pinning docs** — README now explains why you should pin lintcn in `package.json` (no `^` or `~`) and how to update safely.
32
+
1
33
  ## 0.1.0
2
34
 
3
35
  1. **Initial release** — CLI for adding type-aware TypeScript lint rules as Go files to your project:
package/README.md CHANGED
@@ -127,6 +127,30 @@ if (user instanceof Error) return user
127
127
  void getUser("id")
128
128
  ```
129
129
 
130
+ ## Version pinning
131
+
132
+ **Pin lintcn in your `package.json`** — do not use `^` or `~`:
133
+
134
+ ```json
135
+ {
136
+ "devDependencies": {
137
+ "lintcn": "0.1.0"
138
+ }
139
+ }
140
+ ```
141
+
142
+ Each lintcn release bundles a specific tsgolint version. Updating lintcn can change the underlying tsgolint API, which may cause your rules to no longer compile. Always update consciously:
143
+
144
+ 1. Check the [changelog](./CHANGELOG.md) for tsgolint version changes
145
+ 2. Run `npx lintcn build` after updating to verify your rules still compile
146
+ 3. Fix any compilation errors before committing
147
+
148
+ You can test against an unreleased tsgolint version without updating lintcn:
149
+
150
+ ```bash
151
+ npx lintcn lint --tsgolint-version v0.10.0
152
+ ```
153
+
130
154
  ## Prerequisites
131
155
 
132
156
  - **Node.js** — for the CLI
package/dist/cache.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const DEFAULT_TSGOLINT_VERSION = "main";
1
+ export declare const DEFAULT_TSGOLINT_VERSION = "v0.9.2";
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":"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"}
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"}
package/dist/cache.js CHANGED
@@ -1,13 +1,24 @@
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
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.
4
+ //
5
+ // Cache layout:
6
+ // ~/.cache/lintcn/tsgolint/<version>/ — extracted source (read-only)
7
+ // ~/.cache/lintcn/bin/<content-hash> — compiled binaries
5
8
  import fs from 'node:fs';
6
9
  import os from 'node:os';
7
10
  import path from 'node:path';
11
+ import { pipeline } from 'node:stream/promises';
8
12
  import { execAsync } from "./exec.js";
9
- // Default tsgolint version — pinned to a known-good commit
10
- export const DEFAULT_TSGOLINT_VERSION = 'main';
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.
20
+ // Must be updated when DEFAULT_TSGOLINT_VERSION changes.
21
+ const TYPESCRIPT_GO_COMMIT = '2437fa43e85103d2a18e8e41e1a2a994d0708ccf';
11
22
  export function getCacheDir() {
12
23
  return path.join(os.homedir(), '.cache', 'lintcn');
13
24
  }
@@ -23,53 +34,87 @@ export function getBinaryPath(contentHash) {
23
34
  export function getBuildDir() {
24
35
  return path.join(getCacheDir(), 'build');
25
36
  }
37
+ /** Download a tarball from URL and extract it to targetDir.
38
+ * GitHub tarballs have a top-level directory like `repo-ref/`,
39
+ * so we strip the first path component during extraction. */
40
+ async function downloadAndExtract(url, targetDir) {
41
+ const response = await fetch(url);
42
+ if (!response.ok || !response.body) {
43
+ throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
44
+ }
45
+ fs.mkdirSync(targetDir, { recursive: true });
46
+ // pipe through gunzip, then extract with tar (strip top-level directory)
47
+ const tmpTarGz = path.join(os.tmpdir(), `lintcn-${Date.now()}.tar.gz`);
48
+ const fileStream = fs.createWriteStream(tmpTarGz);
49
+ // @ts-ignore ReadableStream vs NodeJS.ReadableStream mismatch
50
+ await pipeline(response.body, fileStream);
51
+ await execAsync('tar', ['xzf', tmpTarGz, '--strip-components=1', '-C', targetDir]);
52
+ fs.rmSync(tmpTarGz, { force: true });
53
+ }
54
+ /** Apply git-format patches using `patch -p1` (no git required).
55
+ * Patches are standard unified diff format, `patch` ignores the git metadata. */
56
+ async function applyPatches(patchesDir, targetDir) {
57
+ const patches = fs.readdirSync(patchesDir)
58
+ .filter((f) => { return f.endsWith('.patch'); })
59
+ .sort();
60
+ for (const patchFile of patches) {
61
+ const patchPath = path.join(patchesDir, patchFile);
62
+ // --batch silences interactive prompts, -f forces application
63
+ await execAsync('patch', ['-p1', '--batch', '-i', patchPath], { cwd: targetDir });
64
+ }
65
+ return patches.length;
66
+ }
26
67
  export async function ensureTsgolintSource(version) {
27
68
  const sourceDir = getTsgolintSourceDir(version);
28
69
  const readyMarker = path.join(sourceDir, '.lintcn-ready');
29
70
  if (fs.existsSync(readyMarker)) {
30
71
  return sourceDir;
31
72
  }
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);
73
+ // clean up any partial previous attempt so we start fresh
74
+ if (fs.existsSync(sourceDir)) {
75
+ fs.rmSync(sourceDir, { recursive: true });
38
76
  }
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'),
77
+ 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`;
81
+ await downloadAndExtract(tsgolintUrl, sourceDir);
82
+ // download typescript-go source tarball into tsgolint/typescript-go/
83
+ const tsGoDir = path.join(sourceDir, 'typescript-go');
84
+ console.log('Downloading typescript-go...');
85
+ const tsGoUrl = `https://github.com/microsoft/typescript-go/archive/${TYPESCRIPT_GO_COMMIT}.tar.gz`;
86
+ await downloadAndExtract(tsGoUrl, tsGoDir);
87
+ // apply patches to typescript-go
88
+ const patchesDir = path.join(sourceDir, 'patches');
89
+ if (fs.existsSync(patchesDir)) {
90
+ const count = await applyPatches(patchesDir, tsGoDir);
91
+ if (count > 0) {
92
+ console.log(`Applied ${count} patches`);
93
+ }
94
+ }
95
+ // copy internal/collections from typescript-go (required by tsgolint, done by `just init`)
96
+ const collectionsDir = path.join(sourceDir, 'internal', 'collections');
97
+ const tsGoCollections = path.join(tsGoDir, 'internal', 'collections');
98
+ if (fs.existsSync(tsGoCollections)) {
99
+ fs.mkdirSync(collectionsDir, { recursive: true });
100
+ const files = fs.readdirSync(tsGoCollections).filter((f) => {
101
+ return f.endsWith('.go') && !f.endsWith('_test.go');
54
102
  });
103
+ for (const file of files) {
104
+ fs.copyFileSync(path.join(tsGoCollections, file), path.join(collectionsDir, file));
105
+ }
55
106
  }
107
+ // write ready marker
108
+ fs.writeFileSync(readyMarker, new Date().toISOString());
109
+ console.log('tsgolint source ready');
56
110
  }
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));
111
+ catch (err) {
112
+ // clean up partial download so next run starts fresh
113
+ if (fs.existsSync(sourceDir)) {
114
+ fs.rmSync(sourceDir, { recursive: true });
67
115
  }
68
- console.log(`Copied ${files.length} collection files`);
116
+ throw err;
69
117
  }
70
- // write ready marker
71
- fs.writeFileSync(readyMarker, new Date().toISOString());
72
- console.log('tsgolint source ready');
73
118
  return sourceDir;
74
119
  }
75
120
  export function cachedBinaryExists(contentHash) {
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import { addRule } from "./commands/add.js";
7
7
  import { lint, buildBinary } from "./commands/lint.js";
8
8
  import { listRules } from "./commands/list.js";
9
9
  import { removeRule } from "./commands/remove.js";
10
+ import { DEFAULT_TSGOLINT_VERSION } from "./cache.js";
10
11
  const require = createRequire(import.meta.url);
11
12
  const packageJson = require('../package.json');
12
13
  const cli = goke('lintcn');
@@ -35,7 +36,9 @@ cli
35
36
  .option('--rebuild', 'Force rebuild even if cached binary exists')
36
37
  .option('--tsconfig <path>', 'Path to tsconfig.json')
37
38
  .option('--list-files', 'List matched files')
39
+ .option('--tsgolint-version [version]', 'Override the pinned tsgolint version (tag or commit). For testing unreleased tsgolint versions.')
38
40
  .action(async (options) => {
41
+ const tsgolintVersion = options.tsgolintVersion || DEFAULT_TSGOLINT_VERSION;
39
42
  const passthroughArgs = [];
40
43
  if (options.tsconfig) {
41
44
  passthroughArgs.push('--tsconfig', options.tsconfig);
@@ -50,6 +53,7 @@ cli
50
53
  }
51
54
  const exitCode = await lint({
52
55
  rebuild: !!options.rebuild,
56
+ tsgolintVersion,
53
57
  passthroughArgs,
54
58
  });
55
59
  process.exit(exitCode);
@@ -57,8 +61,10 @@ cli
57
61
  cli
58
62
  .command('build', 'Build the custom tsgolint binary without running it')
59
63
  .option('--rebuild', 'Force rebuild even if cached binary exists')
64
+ .option('--tsgolint-version [version]', 'Override the pinned tsgolint version (tag or commit). For testing unreleased tsgolint versions.')
60
65
  .action(async (options) => {
61
- const binaryPath = await buildBinary({ rebuild: !!options.rebuild });
66
+ const tsgolintVersion = options.tsgolintVersion || DEFAULT_TSGOLINT_VERSION;
67
+ const binaryPath = await buildBinary({ rebuild: !!options.rebuild, tsgolintVersion });
62
68
  console.log(binaryPath);
63
69
  });
64
70
  cli.help();
package/dist/codegen.d.ts CHANGED
@@ -8,11 +8,17 @@ import type { RuleMetadata } from './discover.ts';
8
8
  * tsgolint's own go.work (which does this) is ignored by the outer workspace.
9
9
  * - go.mod should be minimal (no requires) — the workspace resolves everything. */
10
10
  export declare function generateEditorGoFiles(lintcnDir: string): void;
11
- /** Generate build workspace in cache dir for compiling the custom binary */
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. */
12
15
  export declare function generateBuildWorkspace({ buildDir, tsgolintDir, lintcnDir, rules, }: {
13
16
  buildDir: string;
14
17
  tsgolintDir: string;
15
18
  lintcnDir: string;
16
19
  rules: RuleMetadata[];
17
20
  }): 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;
18
24
  //# 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;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"}
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"}