dslinter 0.0.12 → 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 +16 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/scripts/ensure-dslint.mjs +88 -66
- package/scripts/github-release.mjs +140 -30
- package/scripts/print-missing-scanner.mjs +16 -11
- package/src/resolve-dslint-binary.test.ts +26 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
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
|
+
|
|
3
19
|
## v0.0.12
|
|
4
20
|
|
|
5
21
|
[compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.11...v0.0.12)
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Environment variables:
|
|
|
35
35
|
| `DSLINT_RELEASE_TAG` | Override release tag (default `v` + `dslinter` version from `package.json`). |
|
|
36
36
|
| `DSLINT_GITHUB_REPO` | Override `owner/repo` for downloads (default from `package.json` → `jrmybtlr/DSLinter`). |
|
|
37
37
|
| `DSLINT_VERBOSE=1` | Log which GitHub releases/assets were tried when downloading. |
|
|
38
|
-
| `GITHUB_TOKEN` / `GH_TOKEN` |
|
|
38
|
+
| `GITHUB_TOKEN` / `GH_TOKEN` | **Required for private repos** (`jrmybtlr/DSLinter`). Token needs read access to releases. |
|
|
39
39
|
|
|
40
40
|
### How this differs from `oxlint`
|
|
41
41
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
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";
|
|
@@ -8,7 +7,9 @@ import { dirname, join } from "node:path";
|
|
|
8
7
|
import { fileURLToPath } from "node:url";
|
|
9
8
|
import {
|
|
10
9
|
fetchReleasesForVersion,
|
|
10
|
+
githubAuthToken,
|
|
11
11
|
pickReleaseAsset,
|
|
12
|
+
tryDirectAssetDownload,
|
|
12
13
|
} from "./github-release.mjs";
|
|
13
14
|
import {
|
|
14
15
|
githubRepoFromPackage,
|
|
@@ -41,15 +42,31 @@ function releaseRepo(packageRoot) {
|
|
|
41
42
|
return githubRepoFromPackage(packageRoot);
|
|
42
43
|
}
|
|
43
44
|
|
|
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
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
44
63
|
/**
|
|
45
64
|
* @param {string} packageRoot
|
|
46
65
|
* @param {{ quiet?: boolean }} [opts]
|
|
47
|
-
* @returns {Promise<boolean>} true if vendored binary exists after this call
|
|
48
66
|
*/
|
|
49
67
|
export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts = {}) {
|
|
50
68
|
const { quiet = false } = opts;
|
|
51
69
|
const log = quiet ? () => {} : console.warn.bind(console);
|
|
52
|
-
const verbose = process.env.DSLINT_VERBOSE === "1";
|
|
53
70
|
|
|
54
71
|
if (process.env.DSLINT_SKIP_DOWNLOAD === "1") {
|
|
55
72
|
return pathExists(vendorBinaryPath(packageRoot));
|
|
@@ -68,89 +85,94 @@ export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts
|
|
|
68
85
|
|
|
69
86
|
const version = readPackageVersion(packageRoot);
|
|
70
87
|
const repo = releaseRepo(packageRoot);
|
|
71
|
-
|
|
72
|
-
await mkdir(vendorDir, { recursive: true });
|
|
73
|
-
const tmp = `${dest}.part`;
|
|
88
|
+
await mkdir(join(packageRoot, "vendor"), { recursive: true });
|
|
74
89
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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 {
|
|
90
|
+
const direct = await tryDirectAssetDownload(repo, version, candidates, log);
|
|
91
|
+
if (direct) {
|
|
92
|
+
await writeVendorBinary(dest, direct);
|
|
93
|
+
if (direct.tag !== `v${version}` && !quiet) {
|
|
85
94
|
log(
|
|
86
|
-
|
|
95
|
+
`[dslinter] Installed ${direct.name} from release ${direct.tag} (npm v${version}).`,
|
|
87
96
|
);
|
|
88
97
|
}
|
|
89
|
-
return
|
|
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
|
+
return true;
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
+
);
|
|
109
117
|
}
|
|
110
118
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
for (const release of releases) {
|
|
120
|
+
const asset = pickReleaseAsset(release, candidates);
|
|
121
|
+
if (!asset) continue;
|
|
114
122
|
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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;
|
|
121
132
|
|
|
122
133
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
if (await pathExists(dest)) unlinkSync(dest);
|
|
126
|
-
} catch {
|
|
127
|
-
/* ignore */
|
|
128
|
-
}
|
|
129
|
-
renameSync(tmp, dest);
|
|
130
|
-
if (process.platform !== "win32") {
|
|
131
|
-
await chmod(dest, 0o755);
|
|
132
|
-
}
|
|
134
|
+
await writeVendorBinary(dest, { buf });
|
|
133
135
|
if (release.tag_name !== `v${version}` && !quiet) {
|
|
134
136
|
log(
|
|
135
|
-
`[dslinter] Installed
|
|
137
|
+
`[dslinter] Installed ${asset.name} from release ${release.tag_name} (npm v${version}).`,
|
|
136
138
|
);
|
|
137
139
|
}
|
|
138
140
|
return true;
|
|
139
|
-
} catch (err) {
|
|
140
|
-
log(
|
|
141
|
-
`[dslinter] Could not download ${asset.name}: ${err instanceof Error ? err.message : err}`,
|
|
142
|
-
);
|
|
143
|
-
try {
|
|
144
|
-
unlinkSync(tmp);
|
|
145
|
-
} catch {
|
|
146
|
-
/* ignore */
|
|
147
|
-
}
|
|
148
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;
|
|
149
171
|
}
|
|
150
172
|
|
|
151
173
|
log(
|
|
152
|
-
`[dslinter]
|
|
153
|
-
`
|
|
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.`,
|
|
154
176
|
);
|
|
155
177
|
return false;
|
|
156
178
|
}
|
|
@@ -1,79 +1,150 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Resolve GitHub release assets
|
|
2
|
+
* Resolve and download GitHub release assets (API + direct URLs; supports private repos with a token).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const API = "https://api.github.com";
|
|
6
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
|
+
|
|
7
18
|
function apiHeaders() {
|
|
8
|
-
|
|
19
|
+
return {
|
|
9
20
|
Accept: "application/vnd.github+json",
|
|
10
21
|
"User-Agent": "dslinter-npm",
|
|
11
22
|
"X-GitHub-Api-Version": "2022-11-28",
|
|
23
|
+
...downloadAuthHeaders(),
|
|
12
24
|
};
|
|
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
25
|
}
|
|
18
26
|
|
|
19
27
|
/**
|
|
20
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
|
|
21
53
|
* @param {string} path
|
|
54
|
+
* @returns {Promise<{ data: unknown } | { notFound: true } | { error: Error }>}
|
|
22
55
|
*/
|
|
23
56
|
async function githubGet(repo, path) {
|
|
24
57
|
const res = await fetch(`${API}/repos/${repo}${path}`, {
|
|
25
58
|
headers: apiHeaders(),
|
|
26
59
|
redirect: "follow",
|
|
27
60
|
});
|
|
28
|
-
if (res.status === 404) return
|
|
61
|
+
if (res.status === 404) return { notFound: true };
|
|
29
62
|
if (!res.ok) {
|
|
30
63
|
const body = await res.text().catch(() => "");
|
|
31
|
-
|
|
64
|
+
return {
|
|
65
|
+
error: new Error(`GitHub API ${res.status} for ${path}: ${body.slice(0, 200)}`),
|
|
66
|
+
};
|
|
32
67
|
}
|
|
33
|
-
return res.json();
|
|
68
|
+
return { data: await res.json() };
|
|
34
69
|
}
|
|
35
70
|
|
|
36
71
|
/**
|
|
37
72
|
* @param {string} repo
|
|
38
|
-
* @param {string} tag e.g. `v0.0.
|
|
73
|
+
* @param {string} tag e.g. `v0.0.12` or `latest`
|
|
39
74
|
*/
|
|
40
75
|
export async function fetchRelease(repo, tag) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
);
|
|
49
84
|
}
|
|
85
|
+
if ("data" in result) return result.data;
|
|
50
86
|
return null;
|
|
51
87
|
}
|
|
52
88
|
|
|
53
89
|
/**
|
|
54
90
|
* @param {string} repo
|
|
55
|
-
* @param {string} npmVersion
|
|
91
|
+
* @param {string} npmVersion
|
|
56
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
|
+
|
|
57
99
|
export async function fetchReleasesForVersion(repo, npmVersion) {
|
|
58
100
|
const out = [];
|
|
59
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
|
+
};
|
|
60
114
|
|
|
61
115
|
const push = (release) => {
|
|
62
|
-
if (!release
|
|
116
|
+
if (!hasScannerAssets(release)) return;
|
|
63
117
|
const key = release.id ?? release.tag_name;
|
|
64
118
|
if (seen.has(key)) return;
|
|
65
119
|
seen.add(key);
|
|
66
120
|
out.push(release);
|
|
67
121
|
};
|
|
68
122
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
push(
|
|
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);
|
|
72
133
|
|
|
73
|
-
if (out.length > 0)
|
|
134
|
+
if (out.length > 0) {
|
|
135
|
+
return { releases: out, apiDenied: false, tagExistsWithoutBinaries };
|
|
136
|
+
}
|
|
74
137
|
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
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 };
|
|
77
148
|
|
|
78
149
|
for (const release of list) {
|
|
79
150
|
const tag = String(release.tag_name ?? "");
|
|
@@ -81,9 +152,10 @@ export async function fetchReleasesForVersion(repo, npmVersion) {
|
|
|
81
152
|
push(release);
|
|
82
153
|
}
|
|
83
154
|
}
|
|
84
|
-
if (out.length > 0)
|
|
155
|
+
if (out.length > 0) {
|
|
156
|
+
return { releases: out, apiDenied: false, tagExistsWithoutBinaries };
|
|
157
|
+
}
|
|
85
158
|
|
|
86
|
-
// Newest release that has any scanner-looking asset.
|
|
87
159
|
for (const release of list) {
|
|
88
160
|
const names = release.assets?.map((a) => a.name) ?? [];
|
|
89
161
|
if (names.some((n) => n.startsWith("dslinter-") || n.startsWith("dslint-"))) {
|
|
@@ -92,13 +164,12 @@ export async function fetchReleasesForVersion(repo, npmVersion) {
|
|
|
92
164
|
}
|
|
93
165
|
}
|
|
94
166
|
|
|
95
|
-
return out;
|
|
167
|
+
return { releases: out, apiDenied: false, tagExistsWithoutBinaries };
|
|
96
168
|
}
|
|
97
169
|
|
|
98
170
|
/**
|
|
99
171
|
* @param {string[]} candidateNames
|
|
100
172
|
* @param {{ assets: { name: string; browser_download_url: string }[] }} release
|
|
101
|
-
* @returns {{ name: string; browser_download_url: string } | null}
|
|
102
173
|
*/
|
|
103
174
|
export function pickReleaseAsset(release, candidateNames) {
|
|
104
175
|
for (const name of candidateNames) {
|
|
@@ -107,3 +178,42 @@ export function pickReleaseAsset(release, candidateNames) {
|
|
|
107
178
|
}
|
|
108
179
|
return null;
|
|
109
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,27 +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
25
|
|
|
26
|
-
|
|
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.
|
|
27
32
|
`);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
import {
|
|
4
|
+
directAssetUrl,
|
|
4
5
|
pickReleaseAsset,
|
|
6
|
+
releaseTagsToTry,
|
|
5
7
|
} from "../scripts/github-release.mjs";
|
|
6
8
|
import {
|
|
7
9
|
CLI_BINARY_NAME,
|
|
@@ -37,6 +39,30 @@ describe("parseGitHubRepo", () => {
|
|
|
37
39
|
});
|
|
38
40
|
});
|
|
39
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
|
+
|
|
40
66
|
describe("releaseAssetCandidateNames", () => {
|
|
41
67
|
it("includes legacy dslint asset name", () => {
|
|
42
68
|
expect(releaseAssetCandidateNames(proc("darwin", "arm64"))).toEqual([
|