lintcn 0.2.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,24 @@
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
+
1
22
  ## 0.2.0
2
23
 
3
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.
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,wBAAwB,WAAW,CAAA;AAEhD,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,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":"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,16 +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
13
  // Pinned tsgolint version — updated with each lintcn release.
10
14
  // This ensures reproducible builds: every user on the same lintcn version
11
15
  // compiles rules against the same tsgolint API. Changing this is a conscious
12
16
  // decision — tsgolint API changes can break user rules.
13
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';
14
22
  export function getCacheDir() {
15
23
  return path.join(os.homedir(), '.cache', 'lintcn');
16
24
  }
@@ -26,54 +34,87 @@ export function getBinaryPath(contentHash) {
26
34
  export function getBuildDir() {
27
35
  return path.join(getCacheDir(), 'build');
28
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
+ }
29
67
  export async function ensureTsgolintSource(version) {
30
68
  const sourceDir = getTsgolintSourceDir(version);
31
69
  const readyMarker = path.join(sourceDir, '.lintcn-ready');
32
70
  if (fs.existsSync(readyMarker)) {
33
71
  return sourceDir;
34
72
  }
35
- console.log(`Cloning tsgolint@${version}...`);
36
- fs.mkdirSync(sourceDir, { recursive: true });
37
- // clone with depth 1 for speed — --branch works with tags and branches
38
- const cloneArgs = [
39
- 'clone', '--depth', '1',
40
- '--branch', version,
41
- '--recurse-submodules', '--shallow-submodules',
42
- 'https://github.com/oxc-project/tsgolint.git', sourceDir,
43
- ];
44
- await execAsync('git', cloneArgs);
45
- // apply patches if they exist
46
- const patchesDir = path.join(sourceDir, 'patches');
47
- if (fs.existsSync(patchesDir)) {
48
- const patches = fs.readdirSync(patchesDir).filter((f) => {
49
- return f.endsWith('.patch');
50
- }).sort();
51
- if (patches.length > 0) {
52
- console.log(`Applying ${patches.length} patches...`);
53
- const patchPaths = patches.map((p) => {
54
- return path.join('..', 'patches', p);
55
- });
56
- await execAsync('git', ['am', '--3way', '--no-gpg-sign', ...patchPaths], {
57
- cwd: path.join(sourceDir, 'typescript-go'),
73
+ // clean up any partial previous attempt so we start fresh
74
+ if (fs.existsSync(sourceDir)) {
75
+ fs.rmSync(sourceDir, { recursive: true });
76
+ }
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');
58
102
  });
103
+ for (const file of files) {
104
+ fs.copyFileSync(path.join(tsGoCollections, file), path.join(collectionsDir, file));
105
+ }
59
106
  }
107
+ // write ready marker
108
+ fs.writeFileSync(readyMarker, new Date().toISOString());
109
+ console.log('tsgolint source ready');
60
110
  }
61
- // copy internal/collections from typescript-go (required by tsgolint, done by `just init`)
62
- const collectionsDir = path.join(sourceDir, 'internal', 'collections');
63
- const tsGoCollections = path.join(sourceDir, 'typescript-go', 'internal', 'collections');
64
- if (!fs.existsSync(collectionsDir) && fs.existsSync(tsGoCollections)) {
65
- fs.mkdirSync(collectionsDir, { recursive: true });
66
- const files = fs.readdirSync(tsGoCollections).filter((f) => {
67
- return f.endsWith('.go') && !f.endsWith('_test.go');
68
- });
69
- for (const file of files) {
70
- 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 });
71
115
  }
72
- console.log(`Copied ${files.length} collection files`);
116
+ throw err;
73
117
  }
74
- // write ready marker
75
- fs.writeFileSync(readyMarker, new Date().toISOString());
76
- console.log('tsgolint source ready');
77
118
  return sourceDir;
78
119
  }
79
120
  export function cachedBinaryExists(contentHash) {
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"}