dslinter 0.0.11 → 0.0.16
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 +28 -0
- package/README.md +3 -1
- package/bin/dslinter.mjs +1 -1
- package/package.json +1 -1
- package/scripts/ensure-dslint.mjs +103 -53
- package/scripts/github-release.mjs +219 -0
- package/scripts/print-missing-scanner.mjs +17 -10
- package/scripts/resolve-dslint-binary.mjs +8 -0
- package/src/resolve-dslint-binary.test.ts +55 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.0.16
|
|
4
|
+
|
|
5
|
+
[compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.15...v0.0.16)
|
|
6
|
+
|
|
7
|
+
## v0.0.15
|
|
8
|
+
|
|
9
|
+
[compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.12...v0.0.15)
|
|
10
|
+
|
|
11
|
+
### 🚀 Enhancements
|
|
12
|
+
|
|
13
|
+
- Update release workflow and versioning ([5e28f64](https://github.com/jrmybtlr/DSLinter/commit/5e28f64))
|
|
14
|
+
|
|
15
|
+
### ❤️ Contributors
|
|
16
|
+
|
|
17
|
+
- Jeremy Butler <jeremy.butler@laravel.com>
|
|
18
|
+
|
|
19
|
+
## v0.0.12
|
|
20
|
+
|
|
21
|
+
[compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.11...v0.0.12)
|
|
22
|
+
|
|
23
|
+
### 🚀 Enhancements
|
|
24
|
+
|
|
25
|
+
- Enhance GitHub release asset handling and logging ([4a62b9f](https://github.com/jrmybtlr/DSLinter/commit/4a62b9f))
|
|
26
|
+
|
|
27
|
+
### ❤️ Contributors
|
|
28
|
+
|
|
29
|
+
- Jeremy Butler <jeremy.butler@laravel.com>
|
|
30
|
+
|
|
3
31
|
## v0.0.11
|
|
4
32
|
|
|
5
33
|
[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` | **Required for private repos** (`jrmybtlr/DSLinter`). Token needs read access to releases. |
|
|
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
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Best-effort download of the prebuilt `dslinter` CLI for this platform.
|
|
3
|
-
* Used by postinstall and on first `dslinter` / `npx dslinter` invocation.
|
|
4
3
|
*/
|
|
5
4
|
import { readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
5
|
import { chmod, mkdir, stat } from "node:fs/promises";
|
|
7
6
|
import { dirname, join } from "node:path";
|
|
8
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import {
|
|
9
|
+
fetchReleasesForVersion,
|
|
10
|
+
githubAuthToken,
|
|
11
|
+
pickReleaseAsset,
|
|
12
|
+
tryDirectAssetDownload,
|
|
13
|
+
} from "./github-release.mjs";
|
|
9
14
|
import {
|
|
10
15
|
githubRepoFromPackage,
|
|
11
|
-
|
|
16
|
+
releaseAssetCandidateNames,
|
|
12
17
|
vendorBinaryPath,
|
|
13
18
|
} from "./resolve-dslint-binary.mjs";
|
|
14
19
|
|
|
@@ -31,26 +36,33 @@ function readPackageVersion(packageRoot) {
|
|
|
31
36
|
return pkg.version;
|
|
32
37
|
}
|
|
33
38
|
|
|
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
39
|
function releaseRepo(packageRoot) {
|
|
41
40
|
const override = process.env.DSLINT_GITHUB_REPO?.trim();
|
|
42
41
|
if (override) return override;
|
|
43
42
|
return githubRepoFromPackage(packageRoot);
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} dest
|
|
47
|
+
* @param {{ buf: Buffer }} payload
|
|
48
|
+
*/
|
|
49
|
+
async function writeVendorBinary(dest, { buf }) {
|
|
50
|
+
const tmp = `${dest}.part`;
|
|
51
|
+
writeFileSync(tmp, buf);
|
|
52
|
+
try {
|
|
53
|
+
if (await pathExists(dest)) unlinkSync(dest);
|
|
54
|
+
} catch {
|
|
55
|
+
/* ignore */
|
|
56
|
+
}
|
|
57
|
+
renameSync(tmp, dest);
|
|
58
|
+
if (process.platform !== "win32") {
|
|
59
|
+
await chmod(dest, 0o755);
|
|
60
|
+
}
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
/**
|
|
51
64
|
* @param {string} packageRoot
|
|
52
65
|
* @param {{ quiet?: boolean }} [opts]
|
|
53
|
-
* @returns {Promise<boolean>} true if vendored binary exists after this call
|
|
54
66
|
*/
|
|
55
67
|
export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts = {}) {
|
|
56
68
|
const { quiet = false } = opts;
|
|
@@ -63,8 +75,8 @@ export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts
|
|
|
63
75
|
const dest = vendorBinaryPath(packageRoot);
|
|
64
76
|
if (await pathExists(dest)) return true;
|
|
65
77
|
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
78
|
+
const candidates = releaseAssetCandidateNames();
|
|
79
|
+
if (candidates.length === 0) {
|
|
68
80
|
log(
|
|
69
81
|
`[dslinter] No prebuilt scanner for ${process.platform}-${process.arch}.`,
|
|
70
82
|
);
|
|
@@ -72,57 +84,95 @@ export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts
|
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
const version = readPackageVersion(packageRoot);
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
const repo = releaseRepo(packageRoot);
|
|
88
|
+
await mkdir(join(packageRoot, "vendor"), { recursive: true });
|
|
89
|
+
|
|
90
|
+
const direct = await tryDirectAssetDownload(repo, version, candidates, log);
|
|
91
|
+
if (direct) {
|
|
92
|
+
await writeVendorBinary(dest, direct);
|
|
93
|
+
if (direct.tag !== `v${version}` && !quiet) {
|
|
94
|
+
log(
|
|
95
|
+
`[dslinter] Installed ${direct.name} from release ${direct.tag} (npm v${version}).`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
78
99
|
}
|
|
79
100
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
let apiDenied = false;
|
|
102
|
+
let tagExistsWithoutBinaries = null;
|
|
103
|
+
try {
|
|
104
|
+
const {
|
|
105
|
+
releases,
|
|
106
|
+
apiDenied: denied,
|
|
107
|
+
apiError,
|
|
108
|
+
tagExistsWithoutBinaries: emptyTag,
|
|
109
|
+
} = await fetchReleasesForVersion(repo, version);
|
|
110
|
+
apiDenied = denied;
|
|
111
|
+
tagExistsWithoutBinaries = emptyTag ?? null;
|
|
112
|
+
|
|
113
|
+
if (apiError) {
|
|
114
|
+
log(
|
|
115
|
+
`[dslinter] GitHub API error: ${apiError.message}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
83
118
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
119
|
+
for (const release of releases) {
|
|
120
|
+
const asset = pickReleaseAsset(release, candidates);
|
|
121
|
+
if (!asset) continue;
|
|
122
|
+
|
|
123
|
+
const headers = {};
|
|
124
|
+
const token = githubAuthToken();
|
|
125
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
126
|
+
|
|
127
|
+
const res = await fetch(asset.browser_download_url, {
|
|
128
|
+
headers,
|
|
129
|
+
redirect: "follow",
|
|
130
|
+
});
|
|
131
|
+
if (!res.ok) continue;
|
|
93
132
|
|
|
94
133
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (await pathExists(dest)) unlinkSync(dest);
|
|
98
|
-
} catch {
|
|
99
|
-
/* ignore */
|
|
100
|
-
}
|
|
101
|
-
renameSync(tmp, dest);
|
|
102
|
-
if (process.platform !== "win32") {
|
|
103
|
-
await chmod(dest, 0o755);
|
|
104
|
-
}
|
|
105
|
-
if (tag === "latest" && !quiet) {
|
|
134
|
+
await writeVendorBinary(dest, { buf });
|
|
135
|
+
if (release.tag_name !== `v${version}` && !quiet) {
|
|
106
136
|
log(
|
|
107
|
-
`[dslinter] Installed
|
|
137
|
+
`[dslinter] Installed ${asset.name} from release ${release.tag_name} (npm v${version}).`,
|
|
108
138
|
);
|
|
109
139
|
}
|
|
110
140
|
return true;
|
|
111
|
-
} catch (err) {
|
|
112
|
-
log(
|
|
113
|
-
`[dslinter] Could not download: ${err instanceof Error ? err.message : err}`,
|
|
114
|
-
);
|
|
115
|
-
try {
|
|
116
|
-
unlinkSync(tmp);
|
|
117
|
-
} catch {
|
|
118
|
-
/* ignore */
|
|
119
|
-
}
|
|
120
141
|
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
log(
|
|
144
|
+
`[dslinter] GitHub API: ${err instanceof Error ? err.message : err}`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const tag = `v${version}`;
|
|
149
|
+
const releasePage = `https://github.com/${repo}/releases/tag/${tag}`;
|
|
150
|
+
|
|
151
|
+
if (tagExistsWithoutBinaries) {
|
|
152
|
+
log(
|
|
153
|
+
`[dslinter] GitHub release ${tagExistsWithoutBinaries} exists but has no scanner binaries attached.\n` +
|
|
154
|
+
` Run the "Release dslinter binaries" workflow on ${repo} (Actions → workflow_dispatch, tag ${tagExistsWithoutBinaries}),\n` +
|
|
155
|
+
` or push a new tag so CI uploads assets like ${candidates[0]}.\n` +
|
|
156
|
+
` ${releasePage}`,
|
|
157
|
+
);
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (apiDenied || !githubAuthToken()) {
|
|
162
|
+
log(
|
|
163
|
+
`[dslinter] Cannot reach ${repo} releases without authentication (GitHub returned 404).\n` +
|
|
164
|
+
` If you can open ${releasePage} in a browser, the repo is likely private.\n` +
|
|
165
|
+
` Create a fine-grained or classic token with repo read access, then:\n` +
|
|
166
|
+
` export GITHUB_TOKEN=ghp_...\n` +
|
|
167
|
+
` npx dslinter\n` +
|
|
168
|
+
` Or install from source: cargo install --git https://github.com/${repo} dslinter --locked`,
|
|
169
|
+
);
|
|
170
|
+
return false;
|
|
121
171
|
}
|
|
122
172
|
|
|
123
173
|
log(
|
|
124
|
-
`[dslinter] No
|
|
125
|
-
`
|
|
174
|
+
`[dslinter] No release with asset ${candidates.join(" or ")} on ${repo} for ${tag}.\n` +
|
|
175
|
+
` Create ${releasePage} and run release-dslint-binaries.yml, or set DSLINT_BIN.`,
|
|
126
176
|
);
|
|
127
177
|
return false;
|
|
128
178
|
}
|
|
@@ -132,6 +182,6 @@ const isMain =
|
|
|
132
182
|
fileURLToPath(import.meta.url) === process.argv[1];
|
|
133
183
|
|
|
134
184
|
if (isMain) {
|
|
135
|
-
|
|
136
|
-
process.exit(
|
|
185
|
+
await ensureDslintBinary();
|
|
186
|
+
process.exit(0);
|
|
137
187
|
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve and download GitHub release assets (API + direct URLs; supports private repos with a token).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const API = "https://api.github.com";
|
|
6
|
+
|
|
7
|
+
export function githubAuthToken() {
|
|
8
|
+
return (
|
|
9
|
+
process.env.GITHUB_TOKEN?.trim() || process.env.GH_TOKEN?.trim() || null
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function downloadAuthHeaders() {
|
|
14
|
+
const token = githubAuthToken();
|
|
15
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function apiHeaders() {
|
|
19
|
+
return {
|
|
20
|
+
Accept: "application/vnd.github+json",
|
|
21
|
+
"User-Agent": "dslinter-npm",
|
|
22
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
23
|
+
...downloadAuthHeaders(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} repo `owner/name`
|
|
29
|
+
* @param {string} tag `v0.0.12` or `latest`
|
|
30
|
+
* @param {string} assetName
|
|
31
|
+
*/
|
|
32
|
+
export function directAssetUrl(repo, tag, assetName) {
|
|
33
|
+
const file = encodeURIComponent(assetName);
|
|
34
|
+
if (tag === "latest") {
|
|
35
|
+
return `https://github.com/${repo}/releases/latest/download/${file}`;
|
|
36
|
+
}
|
|
37
|
+
return `https://github.com/${repo}/releases/download/${encodeURIComponent(tag)}/${file}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} npmVersion
|
|
42
|
+
*/
|
|
43
|
+
export function releaseTagsToTry(npmVersion) {
|
|
44
|
+
const override = process.env.DSLINT_RELEASE_TAG?.trim();
|
|
45
|
+
if (override) {
|
|
46
|
+
return [override.startsWith("v") ? override : `v${override}`];
|
|
47
|
+
}
|
|
48
|
+
return [`v${npmVersion}`, npmVersion, "latest"];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} repo
|
|
53
|
+
* @param {string} path
|
|
54
|
+
* @returns {Promise<{ data: unknown } | { notFound: true } | { error: Error }>}
|
|
55
|
+
*/
|
|
56
|
+
async function githubGet(repo, path) {
|
|
57
|
+
const res = await fetch(`${API}/repos/${repo}${path}`, {
|
|
58
|
+
headers: apiHeaders(),
|
|
59
|
+
redirect: "follow",
|
|
60
|
+
});
|
|
61
|
+
if (res.status === 404) return { notFound: true };
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
const body = await res.text().catch(() => "");
|
|
64
|
+
return {
|
|
65
|
+
error: new Error(`GitHub API ${res.status} for ${path}: ${body.slice(0, 200)}`),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { data: await res.json() };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} repo
|
|
73
|
+
* @param {string} tag e.g. `v0.0.12` or `latest`
|
|
74
|
+
*/
|
|
75
|
+
export async function fetchRelease(repo, tag) {
|
|
76
|
+
const path =
|
|
77
|
+
tag === "latest" ? "/releases/latest" : `/releases/tags/${encodeURIComponent(tag)}`;
|
|
78
|
+
let result = await githubGet(repo, path);
|
|
79
|
+
if (result.notFound && tag.startsWith("v")) {
|
|
80
|
+
result = await githubGet(
|
|
81
|
+
repo,
|
|
82
|
+
`/releases/tags/${encodeURIComponent(tag.slice(1))}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if ("data" in result) return result.data;
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} repo
|
|
91
|
+
* @param {string} npmVersion
|
|
92
|
+
*/
|
|
93
|
+
/** @param {unknown} release */
|
|
94
|
+
function hasScannerAssets(release) {
|
|
95
|
+
const names = release?.assets?.map((a) => a.name) ?? [];
|
|
96
|
+
return names.some((n) => n.startsWith("dslinter-") || n.startsWith("dslint-"));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function fetchReleasesForVersion(repo, npmVersion) {
|
|
100
|
+
const out = [];
|
|
101
|
+
const seen = new Set();
|
|
102
|
+
/** @type {string | null} */
|
|
103
|
+
let tagExistsWithoutBinaries = null;
|
|
104
|
+
|
|
105
|
+
const consider = (release) => {
|
|
106
|
+
if (!release?.tag_name) return;
|
|
107
|
+
const tag = String(release.tag_name);
|
|
108
|
+
const matches =
|
|
109
|
+
tag === `v${npmVersion}` || tag === npmVersion;
|
|
110
|
+
if (matches && !hasScannerAssets(release)) {
|
|
111
|
+
tagExistsWithoutBinaries = tag;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const push = (release) => {
|
|
116
|
+
if (!hasScannerAssets(release)) return;
|
|
117
|
+
const key = release.id ?? release.tag_name;
|
|
118
|
+
if (seen.has(key)) return;
|
|
119
|
+
seen.add(key);
|
|
120
|
+
out.push(release);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const vRelease = await fetchRelease(repo, `v${npmVersion}`);
|
|
124
|
+
consider(vRelease);
|
|
125
|
+
push(vRelease);
|
|
126
|
+
|
|
127
|
+
const bareRelease = await fetchRelease(repo, npmVersion);
|
|
128
|
+
consider(bareRelease);
|
|
129
|
+
push(bareRelease);
|
|
130
|
+
|
|
131
|
+
const latestRelease = await fetchRelease(repo, "latest");
|
|
132
|
+
push(latestRelease);
|
|
133
|
+
|
|
134
|
+
if (out.length > 0) {
|
|
135
|
+
return { releases: out, apiDenied: false, tagExistsWithoutBinaries };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const listResult = await githubGet(repo, "/releases?per_page=30");
|
|
139
|
+
if ("error" in listResult) {
|
|
140
|
+
return { releases: out, apiDenied: false, apiError: listResult.error };
|
|
141
|
+
}
|
|
142
|
+
if (listResult.notFound) {
|
|
143
|
+
return { releases: out, apiDenied: !githubAuthToken() };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const list = listResult.data;
|
|
147
|
+
if (!Array.isArray(list)) return { releases: out, apiDenied: false };
|
|
148
|
+
|
|
149
|
+
for (const release of list) {
|
|
150
|
+
const tag = String(release.tag_name ?? "");
|
|
151
|
+
if (tag === `v${npmVersion}` || tag === npmVersion) {
|
|
152
|
+
push(release);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (out.length > 0) {
|
|
156
|
+
return { releases: out, apiDenied: false, tagExistsWithoutBinaries };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const release of list) {
|
|
160
|
+
const names = release.assets?.map((a) => a.name) ?? [];
|
|
161
|
+
if (names.some((n) => n.startsWith("dslinter-") || n.startsWith("dslint-"))) {
|
|
162
|
+
push(release);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { releases: out, apiDenied: false, tagExistsWithoutBinaries };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {string[]} candidateNames
|
|
172
|
+
* @param {{ assets: { name: string; browser_download_url: string }[] }} release
|
|
173
|
+
*/
|
|
174
|
+
export function pickReleaseAsset(release, candidateNames) {
|
|
175
|
+
for (const name of candidateNames) {
|
|
176
|
+
const asset = release.assets.find((a) => a.name === name);
|
|
177
|
+
if (asset?.browser_download_url) return asset;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Download via public/private release URLs (no API metadata required).
|
|
184
|
+
* @param {(msg: string) => void} log
|
|
185
|
+
*/
|
|
186
|
+
export async function tryDirectAssetDownload(repo, npmVersion, candidateNames, log) {
|
|
187
|
+
const verbose = process.env.DSLINT_VERBOSE === "1";
|
|
188
|
+
const headers = downloadAuthHeaders();
|
|
189
|
+
const tags = releaseTagsToTry(npmVersion);
|
|
190
|
+
|
|
191
|
+
for (const tag of tags) {
|
|
192
|
+
for (const name of candidateNames) {
|
|
193
|
+
const url = directAssetUrl(repo, tag, name);
|
|
194
|
+
if (verbose) log(`[dslinter] GET ${url}`);
|
|
195
|
+
try {
|
|
196
|
+
const res = await fetch(url, { headers, redirect: "follow" });
|
|
197
|
+
if (res.status === 404) continue;
|
|
198
|
+
if (!res.ok) {
|
|
199
|
+
if (verbose) log(`[dslinter] ${res.status} ${url}`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
203
|
+
if (contentType.includes("text/html")) continue;
|
|
204
|
+
|
|
205
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
206
|
+
if (buf.length < 512) continue;
|
|
207
|
+
|
|
208
|
+
return { buf, tag, name, url };
|
|
209
|
+
} catch (err) {
|
|
210
|
+
if (verbose) {
|
|
211
|
+
log(
|
|
212
|
+
`[dslinter] ${url}: ${err instanceof Error ? err.message : err}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { githubRepoFromPackage } from "./resolve-dslint-binary.mjs";
|
|
4
5
|
|
|
5
6
|
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
7
|
const version = JSON.parse(
|
|
7
8
|
readFileSync(join(packageRoot, "package.json"), "utf8"),
|
|
8
9
|
).version;
|
|
10
|
+
const repo = githubRepoFromPackage(packageRoot);
|
|
9
11
|
|
|
10
12
|
process.stderr.write(`dslinter: scanner binary not available.
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
Do NOT run: cargo install dslint
|
|
15
|
+
That installs a different package on crates.io (design-file linter).
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
Install the design-system scanner from this repo instead:
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export DSLINT_BIN="$(command -v dslinter)"
|
|
21
|
-
npx dslinter ...
|
|
22
|
-
3. Or set DSLINT_BIN to your local target/release/dslinter
|
|
19
|
+
cargo install --git https://github.com/${repo} dslinter --locked
|
|
20
|
+
export DSLINT_BIN="$(command -v dslinter)"
|
|
21
|
+
npx dslinter ...
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
If releases exist at https://github.com/${repo}/releases/tag/v${version} but
|
|
24
|
+
download failed, the repo may be private — set a GitHub token then retry:
|
|
25
|
+
|
|
26
|
+
export GITHUB_TOKEN=ghp_... # needs read access to ${repo}
|
|
27
|
+
npx dslinter
|
|
28
|
+
|
|
29
|
+
Or point at a local build: DSLINT_BIN=/path/to/dslinter
|
|
30
|
+
|
|
31
|
+
Tip: DSLINT_VERBOSE=1 shows each download URL tried.
|
|
25
32
|
`);
|
|
@@ -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,16 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
directAssetUrl,
|
|
5
|
+
pickReleaseAsset,
|
|
6
|
+
releaseTagsToTry,
|
|
7
|
+
} from "../scripts/github-release.mjs";
|
|
3
8
|
import {
|
|
4
9
|
CLI_BINARY_NAME,
|
|
5
10
|
DEFAULT_GITHUB_REPO,
|
|
6
11
|
parseGitHubRepo,
|
|
7
12
|
releaseAssetBaseName,
|
|
13
|
+
releaseAssetCandidateNames,
|
|
8
14
|
vendorBinaryPath,
|
|
9
15
|
} from "../scripts/resolve-dslint-binary.mjs";
|
|
10
16
|
|
|
@@ -33,6 +39,55 @@ describe("parseGitHubRepo", () => {
|
|
|
33
39
|
});
|
|
34
40
|
});
|
|
35
41
|
|
|
42
|
+
describe("directAssetUrl", () => {
|
|
43
|
+
it("uses releases/latest/download for latest tag", () => {
|
|
44
|
+
expect(
|
|
45
|
+
directAssetUrl("jrmybtlr/DSLinter", "latest", "dslinter-aarch64-apple-darwin"),
|
|
46
|
+
).toBe(
|
|
47
|
+
"https://github.com/jrmybtlr/DSLinter/releases/latest/download/dslinter-aarch64-apple-darwin",
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("uses releases/download for version tags", () => {
|
|
52
|
+
expect(
|
|
53
|
+
directAssetUrl("jrmybtlr/DSLinter", "v0.0.12", "dslinter-aarch64-apple-darwin"),
|
|
54
|
+
).toBe(
|
|
55
|
+
"https://github.com/jrmybtlr/DSLinter/releases/download/v0.0.12/dslinter-aarch64-apple-darwin",
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("releaseTagsToTry", () => {
|
|
61
|
+
it("tries v-prefixed tag first", () => {
|
|
62
|
+
expect(releaseTagsToTry("0.0.12")).toEqual(["v0.0.12", "0.0.12", "latest"]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("releaseAssetCandidateNames", () => {
|
|
67
|
+
it("includes legacy dslint asset name", () => {
|
|
68
|
+
expect(releaseAssetCandidateNames(proc("darwin", "arm64"))).toEqual([
|
|
69
|
+
"dslinter-aarch64-apple-darwin",
|
|
70
|
+
"dslint-aarch64-apple-darwin",
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("pickReleaseAsset", () => {
|
|
76
|
+
it("prefers primary name then legacy", () => {
|
|
77
|
+
const release = {
|
|
78
|
+
assets: [
|
|
79
|
+
{
|
|
80
|
+
name: "dslint-aarch64-apple-darwin",
|
|
81
|
+
browser_download_url: "https://example.com/legacy",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
expect(
|
|
86
|
+
pickReleaseAsset(release, releaseAssetCandidateNames(proc("darwin", "arm64"))),
|
|
87
|
+
).toMatchObject({ name: "dslint-aarch64-apple-darwin" });
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
36
91
|
describe("releaseAssetBaseName", () => {
|
|
37
92
|
it("maps darwin arm64", () => {
|
|
38
93
|
expect(releaseAssetBaseName(proc("darwin", "arm64"))).toBe(
|