dslinter 0.0.11 → 0.0.12
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 +12 -0
- package/README.md +3 -1
- package/bin/dslinter.mjs +1 -1
- package/package.json +1 -1
- package/scripts/ensure-dslint.mjs +58 -30
- package/scripts/github-release.mjs +109 -0
- package/scripts/print-missing-scanner.mjs +2 -0
- package/scripts/resolve-dslint-binary.mjs +8 -0
- package/src/resolve-dslint-binary.test.ts +29 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.0.12
|
|
4
|
+
|
|
5
|
+
[compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.11...v0.0.12)
|
|
6
|
+
|
|
7
|
+
### 🚀 Enhancements
|
|
8
|
+
|
|
9
|
+
- Enhance GitHub release asset handling and logging ([4a62b9f](https://github.com/jrmybtlr/DSLinter/commit/4a62b9f))
|
|
10
|
+
|
|
11
|
+
### ❤️ Contributors
|
|
12
|
+
|
|
13
|
+
- Jeremy Butler <jeremy.butler@laravel.com>
|
|
14
|
+
|
|
3
15
|
## v0.0.11
|
|
4
16
|
|
|
5
17
|
[compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.10...v0.0.11)
|
package/README.md
CHANGED
|
@@ -33,7 +33,9 @@ Environment variables:
|
|
|
33
33
|
|----------|---------|
|
|
34
34
|
| `DSLINT_SKIP_DOWNLOAD=1` | Skip postinstall download (air-gapped / you only use `PATH`). |
|
|
35
35
|
| `DSLINT_RELEASE_TAG` | Override release tag (default `v` + `dslinter` version from `package.json`). |
|
|
36
|
-
| `DSLINT_GITHUB_REPO` | Override `owner/repo` for downloads (default `jrmybtlr/DSLinter`). |
|
|
36
|
+
| `DSLINT_GITHUB_REPO` | Override `owner/repo` for downloads (default from `package.json` → `jrmybtlr/DSLinter`). |
|
|
37
|
+
| `DSLINT_VERBOSE=1` | Log which GitHub releases/assets were tried when downloading. |
|
|
38
|
+
| `GITHUB_TOKEN` / `GH_TOKEN` | Optional token for private repos or higher API rate limits. |
|
|
37
39
|
|
|
38
40
|
### How this differs from `oxlint`
|
|
39
41
|
|
package/bin/dslinter.mjs
CHANGED
|
@@ -43,7 +43,7 @@ async function resolveCommand() {
|
|
|
43
43
|
|
|
44
44
|
const vendored = vendorBinaryPath(packageRoot);
|
|
45
45
|
if (!existsSync(vendored)) {
|
|
46
|
-
await ensureDslintBinary(packageRoot, { quiet:
|
|
46
|
+
await ensureDslintBinary(packageRoot, { quiet: false });
|
|
47
47
|
}
|
|
48
48
|
if (existsSync(vendored)) {
|
|
49
49
|
return vendored;
|
package/package.json
CHANGED
|
@@ -6,9 +6,13 @@ import { readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
|
6
6
|
import { chmod, mkdir, stat } from "node:fs/promises";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
import {
|
|
10
|
+
fetchReleasesForVersion,
|
|
11
|
+
pickReleaseAsset,
|
|
12
|
+
} from "./github-release.mjs";
|
|
9
13
|
import {
|
|
10
14
|
githubRepoFromPackage,
|
|
11
|
-
|
|
15
|
+
releaseAssetCandidateNames,
|
|
12
16
|
vendorBinaryPath,
|
|
13
17
|
} from "./resolve-dslint-binary.mjs";
|
|
14
18
|
|
|
@@ -31,22 +35,12 @@ function readPackageVersion(packageRoot) {
|
|
|
31
35
|
return pkg.version;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
function releaseTag(version) {
|
|
35
|
-
const o = process.env.DSLINT_RELEASE_TAG?.trim();
|
|
36
|
-
if (o) return o.startsWith("v") ? o : `v${o}`;
|
|
37
|
-
return `v${version}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
38
|
function releaseRepo(packageRoot) {
|
|
41
39
|
const override = process.env.DSLINT_GITHUB_REPO?.trim();
|
|
42
40
|
if (override) return override;
|
|
43
41
|
return githubRepoFromPackage(packageRoot);
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
function assetUrl(packageRoot, tag, asset) {
|
|
47
|
-
return `https://github.com/${releaseRepo(packageRoot)}/releases/download/${tag}/${asset}`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
44
|
/**
|
|
51
45
|
* @param {string} packageRoot
|
|
52
46
|
* @param {{ quiet?: boolean }} [opts]
|
|
@@ -55,6 +49,7 @@ function assetUrl(packageRoot, tag, asset) {
|
|
|
55
49
|
export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts = {}) {
|
|
56
50
|
const { quiet = false } = opts;
|
|
57
51
|
const log = quiet ? () => {} : console.warn.bind(console);
|
|
52
|
+
const verbose = process.env.DSLINT_VERBOSE === "1";
|
|
58
53
|
|
|
59
54
|
if (process.env.DSLINT_SKIP_DOWNLOAD === "1") {
|
|
60
55
|
return pathExists(vendorBinaryPath(packageRoot));
|
|
@@ -63,8 +58,8 @@ export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts
|
|
|
63
58
|
const dest = vendorBinaryPath(packageRoot);
|
|
64
59
|
if (await pathExists(dest)) return true;
|
|
65
60
|
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
61
|
+
const candidates = releaseAssetCandidateNames();
|
|
62
|
+
if (candidates.length === 0) {
|
|
68
63
|
log(
|
|
69
64
|
`[dslinter] No prebuilt scanner for ${process.platform}-${process.arch}.`,
|
|
70
65
|
);
|
|
@@ -72,22 +67,55 @@ export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts
|
|
|
72
67
|
}
|
|
73
68
|
|
|
74
69
|
const version = readPackageVersion(packageRoot);
|
|
75
|
-
const
|
|
76
|
-
if (process.env.DSLINT_USE_LATEST_RELEASE !== "0") {
|
|
77
|
-
tagsToTry.push("latest");
|
|
78
|
-
}
|
|
79
|
-
|
|
70
|
+
const repo = releaseRepo(packageRoot);
|
|
80
71
|
const vendorDir = join(packageRoot, "vendor");
|
|
81
72
|
await mkdir(vendorDir, { recursive: true });
|
|
82
73
|
const tmp = `${dest}.part`;
|
|
83
74
|
|
|
84
|
-
|
|
85
|
-
|
|
75
|
+
let releases;
|
|
76
|
+
try {
|
|
77
|
+
releases = await fetchReleasesForVersion(repo, version);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
log(
|
|
80
|
+
`[dslinter] Could not query GitHub releases for ${repo}: ${err instanceof Error ? err.message : err}`,
|
|
81
|
+
);
|
|
82
|
+
if (process.env.GITHUB_TOKEN || process.env.GH_TOKEN) {
|
|
83
|
+
log(" (token was set but request still failed — check repo access)");
|
|
84
|
+
} else {
|
|
85
|
+
log(
|
|
86
|
+
" For private repos, set GITHUB_TOKEN or GH_TOKEN with read access to releases.",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (releases.length === 0) {
|
|
93
|
+
log(
|
|
94
|
+
`[dslinter] No GitHub release found on ${repo} for v${version}.\n` +
|
|
95
|
+
` Publish tag v${version} with workflow release-dslint-binaries.yml (assets: ${candidates.join(" or ")}).`,
|
|
96
|
+
);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const release of releases) {
|
|
101
|
+
const asset = pickReleaseAsset(release, candidates);
|
|
102
|
+
if (!asset) {
|
|
103
|
+
if (verbose) {
|
|
104
|
+
log(
|
|
105
|
+
`[dslinter] Release ${release.tag_name} has no asset in ${candidates.join(", ")} (has: ${release.assets.map((a) => a.name).join(", ") || "none"})`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (verbose) {
|
|
112
|
+
log(`[dslinter] Downloading ${asset.name} from ${release.tag_name}…`);
|
|
113
|
+
}
|
|
114
|
+
|
|
86
115
|
try {
|
|
87
|
-
const res = await fetch(
|
|
88
|
-
if (res.status === 404) continue;
|
|
116
|
+
const res = await fetch(asset.browser_download_url, { redirect: "follow" });
|
|
89
117
|
if (!res.ok) {
|
|
90
|
-
log(`[dslinter] Download failed (${res.status}): ${
|
|
118
|
+
log(`[dslinter] Download failed (${res.status}): ${asset.browser_download_url}`);
|
|
91
119
|
continue;
|
|
92
120
|
}
|
|
93
121
|
|
|
@@ -102,15 +130,15 @@ export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts
|
|
|
102
130
|
if (process.platform !== "win32") {
|
|
103
131
|
await chmod(dest, 0o755);
|
|
104
132
|
}
|
|
105
|
-
if (
|
|
133
|
+
if (release.tag_name !== `v${version}` && !quiet) {
|
|
106
134
|
log(
|
|
107
|
-
`[dslinter] Installed scanner from
|
|
135
|
+
`[dslinter] Installed scanner from release ${release.tag_name} (npm v${version}).`,
|
|
108
136
|
);
|
|
109
137
|
}
|
|
110
138
|
return true;
|
|
111
139
|
} catch (err) {
|
|
112
140
|
log(
|
|
113
|
-
`[dslinter] Could not download: ${err instanceof Error ? err.message : err}`,
|
|
141
|
+
`[dslinter] Could not download ${asset.name}: ${err instanceof Error ? err.message : err}`,
|
|
114
142
|
);
|
|
115
143
|
try {
|
|
116
144
|
unlinkSync(tmp);
|
|
@@ -121,8 +149,8 @@ export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts
|
|
|
121
149
|
}
|
|
122
150
|
|
|
123
151
|
log(
|
|
124
|
-
`[dslinter]
|
|
125
|
-
`
|
|
152
|
+
`[dslinter] Found releases on ${repo} but none include ${candidates.join(" or ")} for this platform.\n` +
|
|
153
|
+
` Upload platform binaries via .github/workflows/release-dslint-binaries.yml, or set DSLINT_BIN.`,
|
|
126
154
|
);
|
|
127
155
|
return false;
|
|
128
156
|
}
|
|
@@ -132,6 +160,6 @@ const isMain =
|
|
|
132
160
|
fileURLToPath(import.meta.url) === process.argv[1];
|
|
133
161
|
|
|
134
162
|
if (isMain) {
|
|
135
|
-
|
|
136
|
-
process.exit(
|
|
163
|
+
await ensureDslintBinary();
|
|
164
|
+
process.exit(0);
|
|
137
165
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve GitHub release assets via the REST API (works with exact names + legacy names).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const API = "https://api.github.com";
|
|
6
|
+
|
|
7
|
+
function apiHeaders() {
|
|
8
|
+
const headers = {
|
|
9
|
+
Accept: "application/vnd.github+json",
|
|
10
|
+
"User-Agent": "dslinter-npm",
|
|
11
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
12
|
+
};
|
|
13
|
+
const token =
|
|
14
|
+
process.env.GITHUB_TOKEN?.trim() || process.env.GH_TOKEN?.trim();
|
|
15
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
16
|
+
return headers;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} repo `owner/name`
|
|
21
|
+
* @param {string} path
|
|
22
|
+
*/
|
|
23
|
+
async function githubGet(repo, path) {
|
|
24
|
+
const res = await fetch(`${API}/repos/${repo}${path}`, {
|
|
25
|
+
headers: apiHeaders(),
|
|
26
|
+
redirect: "follow",
|
|
27
|
+
});
|
|
28
|
+
if (res.status === 404) return null;
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
const body = await res.text().catch(() => "");
|
|
31
|
+
throw new Error(`GitHub API ${res.status} for ${path}: ${body.slice(0, 200)}`);
|
|
32
|
+
}
|
|
33
|
+
return res.json();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} repo
|
|
38
|
+
* @param {string} tag e.g. `v0.0.11` or `latest`
|
|
39
|
+
*/
|
|
40
|
+
export async function fetchRelease(repo, tag) {
|
|
41
|
+
if (tag === "latest") {
|
|
42
|
+
return githubGet(repo, "/releases/latest");
|
|
43
|
+
}
|
|
44
|
+
const byTag = await githubGet(repo, `/releases/tags/${encodeURIComponent(tag)}`);
|
|
45
|
+
if (byTag) return byTag;
|
|
46
|
+
// Some releases use tag without `v` prefix.
|
|
47
|
+
if (tag.startsWith("v")) {
|
|
48
|
+
return githubGet(repo, `/releases/tags/${encodeURIComponent(tag.slice(1))}`);
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} repo
|
|
55
|
+
* @param {string} npmVersion e.g. `0.0.11`
|
|
56
|
+
*/
|
|
57
|
+
export async function fetchReleasesForVersion(repo, npmVersion) {
|
|
58
|
+
const out = [];
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
|
|
61
|
+
const push = (release) => {
|
|
62
|
+
if (!release?.assets?.length) return;
|
|
63
|
+
const key = release.id ?? release.tag_name;
|
|
64
|
+
if (seen.has(key)) return;
|
|
65
|
+
seen.add(key);
|
|
66
|
+
out.push(release);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
push(await fetchRelease(repo, `v${npmVersion}`));
|
|
70
|
+
push(await fetchRelease(repo, npmVersion));
|
|
71
|
+
push(await fetchRelease(repo, "latest"));
|
|
72
|
+
|
|
73
|
+
if (out.length > 0) return out;
|
|
74
|
+
|
|
75
|
+
const list = await githubGet(repo, "/releases?per_page=30");
|
|
76
|
+
if (!Array.isArray(list)) return out;
|
|
77
|
+
|
|
78
|
+
for (const release of list) {
|
|
79
|
+
const tag = String(release.tag_name ?? "");
|
|
80
|
+
if (tag === `v${npmVersion}` || tag === npmVersion) {
|
|
81
|
+
push(release);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (out.length > 0) return out;
|
|
85
|
+
|
|
86
|
+
// Newest release that has any scanner-looking asset.
|
|
87
|
+
for (const release of list) {
|
|
88
|
+
const names = release.assets?.map((a) => a.name) ?? [];
|
|
89
|
+
if (names.some((n) => n.startsWith("dslinter-") || n.startsWith("dslint-"))) {
|
|
90
|
+
push(release);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {string[]} candidateNames
|
|
100
|
+
* @param {{ assets: { name: string; browser_download_url: string }[] }} release
|
|
101
|
+
* @returns {{ name: string; browser_download_url: string } | null}
|
|
102
|
+
*/
|
|
103
|
+
export function pickReleaseAsset(release, candidateNames) {
|
|
104
|
+
for (const name of candidateNames) {
|
|
105
|
+
const asset = release.assets.find((a) => a.name === name);
|
|
106
|
+
if (asset?.browser_download_url) return asset;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
@@ -58,6 +58,14 @@ export function releaseAssetBaseName(proc = process) {
|
|
|
58
58
|
return null;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** Primary GitHub asset name plus legacy `dslint-*` names from older releases. */
|
|
62
|
+
export function releaseAssetCandidateNames(proc = process) {
|
|
63
|
+
const primary = releaseAssetBaseName(proc);
|
|
64
|
+
if (!primary) return [];
|
|
65
|
+
const legacy = primary.replace(/^dslinter/, "dslint");
|
|
66
|
+
return legacy === primary ? [primary] : [primary, legacy];
|
|
67
|
+
}
|
|
68
|
+
|
|
61
69
|
/**
|
|
62
70
|
* @param {string} packageRoot — directory containing package.json
|
|
63
71
|
* @param {NodeJS.Process} [proc]
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
pickReleaseAsset,
|
|
5
|
+
} from "../scripts/github-release.mjs";
|
|
3
6
|
import {
|
|
4
7
|
CLI_BINARY_NAME,
|
|
5
8
|
DEFAULT_GITHUB_REPO,
|
|
6
9
|
parseGitHubRepo,
|
|
7
10
|
releaseAssetBaseName,
|
|
11
|
+
releaseAssetCandidateNames,
|
|
8
12
|
vendorBinaryPath,
|
|
9
13
|
} from "../scripts/resolve-dslint-binary.mjs";
|
|
10
14
|
|
|
@@ -33,6 +37,31 @@ describe("parseGitHubRepo", () => {
|
|
|
33
37
|
});
|
|
34
38
|
});
|
|
35
39
|
|
|
40
|
+
describe("releaseAssetCandidateNames", () => {
|
|
41
|
+
it("includes legacy dslint asset name", () => {
|
|
42
|
+
expect(releaseAssetCandidateNames(proc("darwin", "arm64"))).toEqual([
|
|
43
|
+
"dslinter-aarch64-apple-darwin",
|
|
44
|
+
"dslint-aarch64-apple-darwin",
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("pickReleaseAsset", () => {
|
|
50
|
+
it("prefers primary name then legacy", () => {
|
|
51
|
+
const release = {
|
|
52
|
+
assets: [
|
|
53
|
+
{
|
|
54
|
+
name: "dslint-aarch64-apple-darwin",
|
|
55
|
+
browser_download_url: "https://example.com/legacy",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
expect(
|
|
60
|
+
pickReleaseAsset(release, releaseAssetCandidateNames(proc("darwin", "arm64"))),
|
|
61
|
+
).toMatchObject({ name: "dslint-aarch64-apple-darwin" });
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
36
65
|
describe("releaseAssetBaseName", () => {
|
|
37
66
|
it("maps darwin arm64", () => {
|
|
38
67
|
expect(releaseAssetBaseName(proc("darwin", "arm64"))).toBe(
|