dslinter 0.0.16 → 0.0.27
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 +99 -0
- package/README.md +7 -36
- package/bin/dslinter.mjs +49 -44
- package/index.cjs +579 -0
- package/index.d.ts +3 -0
- package/package.json +30 -5
- package/src/components/ComponentPlaygroundPane.tsx +2 -1
- package/src/components/PlaygroundUsageCode.tsx +0 -1
- package/src/components/Section.tsx +4 -2
- package/src/components/playgroundUsageHighlight.ts +2 -21
- package/scripts/ensure-dslint.mjs +0 -187
- package/scripts/github-release.mjs +0 -219
- package/scripts/print-missing-scanner.mjs +0 -32
- package/scripts/resolve-dslint-binary.mjs +0 -80
- package/src/resolve-dslint-binary.test.ts +0 -127
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
1
3
|
export function Section({
|
|
2
4
|
id,
|
|
3
5
|
children,
|
|
@@ -6,10 +8,10 @@ export function Section({
|
|
|
6
8
|
actions,
|
|
7
9
|
}: {
|
|
8
10
|
id: string;
|
|
9
|
-
children:
|
|
11
|
+
children: ReactNode;
|
|
10
12
|
title: string;
|
|
11
13
|
description: string;
|
|
12
|
-
actions?:
|
|
14
|
+
actions?: ReactNode;
|
|
13
15
|
}) {
|
|
14
16
|
return (
|
|
15
17
|
<section id={id} className="scroll-mt-20">
|
|
@@ -6,15 +6,6 @@ import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
|
|
|
6
6
|
const THEME = "github-dark";
|
|
7
7
|
const LANG = "tsx";
|
|
8
8
|
|
|
9
|
-
/** When true, run Twoslash (CDN ATA + hover) before highlighting. */
|
|
10
|
-
export function usageSnippetNeedsTwoslash(source: string): boolean {
|
|
11
|
-
return (
|
|
12
|
-
/\^\?/.test(source) ||
|
|
13
|
-
/\/\/\s*@(?:errors|error|log|warn|filename|noErrors)/m.test(source) ||
|
|
14
|
-
/\/\/\s*---cut---/.test(source)
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
9
|
let highlighterPromise: Promise<HighlighterCore> | null = null;
|
|
19
10
|
|
|
20
11
|
function getHighlighter(): Promise<HighlighterCore> {
|
|
@@ -32,22 +23,12 @@ function assertNotAborted(signal: AbortSignal | undefined): void {
|
|
|
32
23
|
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
33
24
|
}
|
|
34
25
|
|
|
35
|
-
/**
|
|
36
|
-
* Renders usage source to Shiki HTML. Twoslash runs only when {@link usageSnippetNeedsTwoslash} is true
|
|
37
|
-
* (loaded in a separate async chunk so the default bundle stays smaller).
|
|
38
|
-
*/
|
|
26
|
+
/** Renders usage source to Shiki HTML. */
|
|
39
27
|
export async function renderPlaygroundUsageHtml(
|
|
40
28
|
source: string,
|
|
41
29
|
signal?: AbortSignal,
|
|
42
30
|
): Promise<string> {
|
|
43
31
|
const highlighter = await getHighlighter();
|
|
44
32
|
assertNotAborted(signal);
|
|
45
|
-
|
|
46
|
-
if (!usageSnippetNeedsTwoslash(source)) {
|
|
47
|
-
return highlighter.codeToHtml(source, { lang: LANG, theme: THEME });
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const { renderUsageWithTwoslash } = await import("./playgroundUsageTwoslash");
|
|
51
|
-
assertNotAborted(signal);
|
|
52
|
-
return renderUsageWithTwoslash(highlighter, source, signal);
|
|
33
|
+
return highlighter.codeToHtml(source, { lang: LANG, theme: THEME });
|
|
53
34
|
}
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Best-effort download of the prebuilt `dslinter` CLI for this platform.
|
|
3
|
-
*/
|
|
4
|
-
import { readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { chmod, mkdir, stat } from "node:fs/promises";
|
|
6
|
-
import { dirname, join } from "node:path";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
import {
|
|
9
|
-
fetchReleasesForVersion,
|
|
10
|
-
githubAuthToken,
|
|
11
|
-
pickReleaseAsset,
|
|
12
|
-
tryDirectAssetDownload,
|
|
13
|
-
} from "./github-release.mjs";
|
|
14
|
-
import {
|
|
15
|
-
githubRepoFromPackage,
|
|
16
|
-
releaseAssetCandidateNames,
|
|
17
|
-
vendorBinaryPath,
|
|
18
|
-
} from "./resolve-dslint-binary.mjs";
|
|
19
|
-
|
|
20
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
const defaultPackageRoot = join(__dirname, "..");
|
|
22
|
-
|
|
23
|
-
async function pathExists(p) {
|
|
24
|
-
try {
|
|
25
|
-
await stat(p);
|
|
26
|
-
return true;
|
|
27
|
-
} catch {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function readPackageVersion(packageRoot) {
|
|
33
|
-
const pkg = JSON.parse(
|
|
34
|
-
readFileSync(join(packageRoot, "package.json"), "utf8"),
|
|
35
|
-
);
|
|
36
|
-
return pkg.version;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function releaseRepo(packageRoot) {
|
|
40
|
-
const override = process.env.DSLINT_GITHUB_REPO?.trim();
|
|
41
|
-
if (override) return override;
|
|
42
|
-
return githubRepoFromPackage(packageRoot);
|
|
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
|
-
|
|
63
|
-
/**
|
|
64
|
-
* @param {string} packageRoot
|
|
65
|
-
* @param {{ quiet?: boolean }} [opts]
|
|
66
|
-
*/
|
|
67
|
-
export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts = {}) {
|
|
68
|
-
const { quiet = false } = opts;
|
|
69
|
-
const log = quiet ? () => {} : console.warn.bind(console);
|
|
70
|
-
|
|
71
|
-
if (process.env.DSLINT_SKIP_DOWNLOAD === "1") {
|
|
72
|
-
return pathExists(vendorBinaryPath(packageRoot));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const dest = vendorBinaryPath(packageRoot);
|
|
76
|
-
if (await pathExists(dest)) return true;
|
|
77
|
-
|
|
78
|
-
const candidates = releaseAssetCandidateNames();
|
|
79
|
-
if (candidates.length === 0) {
|
|
80
|
-
log(
|
|
81
|
-
`[dslinter] No prebuilt scanner for ${process.platform}-${process.arch}.`,
|
|
82
|
-
);
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const version = readPackageVersion(packageRoot);
|
|
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;
|
|
99
|
-
}
|
|
100
|
-
|
|
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
|
-
}
|
|
118
|
-
|
|
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;
|
|
132
|
-
|
|
133
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
134
|
-
await writeVendorBinary(dest, { buf });
|
|
135
|
-
if (release.tag_name !== `v${version}` && !quiet) {
|
|
136
|
-
log(
|
|
137
|
-
`[dslinter] Installed ${asset.name} from release ${release.tag_name} (npm v${version}).`,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
return true;
|
|
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;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
log(
|
|
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.`,
|
|
176
|
-
);
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const isMain =
|
|
181
|
-
process.argv[1] &&
|
|
182
|
-
fileURLToPath(import.meta.url) === process.argv[1];
|
|
183
|
-
|
|
184
|
-
if (isMain) {
|
|
185
|
-
await ensureDslintBinary();
|
|
186
|
-
process.exit(0);
|
|
187
|
-
}
|
|
@@ -1,219 +0,0 @@
|
|
|
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,32 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { githubRepoFromPackage } from "./resolve-dslint-binary.mjs";
|
|
5
|
-
|
|
6
|
-
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
-
const version = JSON.parse(
|
|
8
|
-
readFileSync(join(packageRoot, "package.json"), "utf8"),
|
|
9
|
-
).version;
|
|
10
|
-
const repo = githubRepoFromPackage(packageRoot);
|
|
11
|
-
|
|
12
|
-
process.stderr.write(`dslinter: scanner binary not available.
|
|
13
|
-
|
|
14
|
-
Do NOT run: cargo install dslint
|
|
15
|
-
That installs a different package on crates.io (design-file linter).
|
|
16
|
-
|
|
17
|
-
Install the design-system scanner from this repo instead:
|
|
18
|
-
|
|
19
|
-
cargo install --git https://github.com/${repo} dslinter --locked
|
|
20
|
-
export DSLINT_BIN="$(command -v dslinter)"
|
|
21
|
-
npx dslinter ...
|
|
22
|
-
|
|
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.
|
|
32
|
-
`);
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
/** CLI binary name (avoids collision with unrelated `dslint` on crates.io). */
|
|
5
|
-
export const CLI_BINARY_NAME = "dslinter";
|
|
6
|
-
|
|
7
|
-
/** Fallback when package.json has no parseable `repository` field. */
|
|
8
|
-
export const DEFAULT_GITHUB_REPO = "jrmybtlr/DSLinter";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @param {string | { type?: string; url?: string } | undefined} repository
|
|
12
|
-
* @returns {string | null} `owner/repo`
|
|
13
|
-
*/
|
|
14
|
-
export function parseGitHubRepo(repository) {
|
|
15
|
-
if (!repository) return null;
|
|
16
|
-
const url = typeof repository === "string" ? repository : repository.url;
|
|
17
|
-
if (!url) return null;
|
|
18
|
-
const m = String(url).match(/github\.com[/:]([^/]+)\/([^/.]+?)(?:\.git)?\/?$/i);
|
|
19
|
-
return m ? `${m[1]}/${m[2]}` : null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @param {string} packageRoot
|
|
24
|
-
*/
|
|
25
|
-
export function githubRepoFromPackage(packageRoot) {
|
|
26
|
-
try {
|
|
27
|
-
const pkg = JSON.parse(
|
|
28
|
-
readFileSync(join(packageRoot, "package.json"), "utf8"),
|
|
29
|
-
);
|
|
30
|
-
return parseGitHubRepo(pkg.repository) ?? DEFAULT_GITHUB_REPO;
|
|
31
|
-
} catch {
|
|
32
|
-
return DEFAULT_GITHUB_REPO;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Maps the current OS/arch to the GitHub release asset basename (must match CI upload names).
|
|
38
|
-
* @param {NodeJS.Process} [proc]
|
|
39
|
-
* @returns {string | null}
|
|
40
|
-
*/
|
|
41
|
-
export function releaseAssetBaseName(proc = process) {
|
|
42
|
-
const { platform, arch } = proc;
|
|
43
|
-
if (platform === "darwin" && arch === "arm64") {
|
|
44
|
-
return "dslinter-aarch64-apple-darwin";
|
|
45
|
-
}
|
|
46
|
-
if (platform === "darwin" && arch === "x64") {
|
|
47
|
-
return "dslinter-x86_64-apple-darwin";
|
|
48
|
-
}
|
|
49
|
-
if (platform === "linux" && arch === "x64") {
|
|
50
|
-
return "dslinter-x86_64-unknown-linux-gnu";
|
|
51
|
-
}
|
|
52
|
-
if (platform === "linux" && arch === "arm64") {
|
|
53
|
-
return "dslinter-aarch64-unknown-linux-gnu";
|
|
54
|
-
}
|
|
55
|
-
if (platform === "win32" && arch === "x64") {
|
|
56
|
-
return "dslinter-x86_64-pc-windows-msvc.exe";
|
|
57
|
-
}
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
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
|
-
|
|
69
|
-
/**
|
|
70
|
-
* @param {string} packageRoot — directory containing package.json
|
|
71
|
-
* @param {NodeJS.Process} [proc]
|
|
72
|
-
*/
|
|
73
|
-
export function vendorBinaryPath(packageRoot, proc = process) {
|
|
74
|
-
const name =
|
|
75
|
-
proc.platform === "win32" ? `${CLI_BINARY_NAME}.exe` : CLI_BINARY_NAME;
|
|
76
|
-
return join(packageRoot, "vendor", name);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Our scanner prints this in `dslinter --version` help output (clap about line). */
|
|
80
|
-
export const SCANNER_VERSION_MARKER = "design system linting";
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import {
|
|
4
|
-
directAssetUrl,
|
|
5
|
-
pickReleaseAsset,
|
|
6
|
-
releaseTagsToTry,
|
|
7
|
-
} from "../scripts/github-release.mjs";
|
|
8
|
-
import {
|
|
9
|
-
CLI_BINARY_NAME,
|
|
10
|
-
DEFAULT_GITHUB_REPO,
|
|
11
|
-
parseGitHubRepo,
|
|
12
|
-
releaseAssetBaseName,
|
|
13
|
-
releaseAssetCandidateNames,
|
|
14
|
-
vendorBinaryPath,
|
|
15
|
-
} from "../scripts/resolve-dslint-binary.mjs";
|
|
16
|
-
|
|
17
|
-
function proc(platform: string, arch: string): NodeJS.Process {
|
|
18
|
-
return { platform, arch } as NodeJS.Process;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe("parseGitHubRepo", () => {
|
|
22
|
-
it("parses https repository url", () => {
|
|
23
|
-
expect(
|
|
24
|
-
parseGitHubRepo("https://github.com/jrmybtlr/DSLinter.git"),
|
|
25
|
-
).toBe("jrmybtlr/DSLinter");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("parses repository object", () => {
|
|
29
|
-
expect(
|
|
30
|
-
parseGitHubRepo({
|
|
31
|
-
type: "git",
|
|
32
|
-
url: "git+https://github.com/jrmybtlr/DSLinter.git",
|
|
33
|
-
}),
|
|
34
|
-
).toBe("jrmybtlr/DSLinter");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("defaults constant points at DSLinter", () => {
|
|
38
|
-
expect(DEFAULT_GITHUB_REPO).toBe("jrmybtlr/DSLinter");
|
|
39
|
-
});
|
|
40
|
-
});
|
|
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
|
-
|
|
91
|
-
describe("releaseAssetBaseName", () => {
|
|
92
|
-
it("maps darwin arm64", () => {
|
|
93
|
-
expect(releaseAssetBaseName(proc("darwin", "arm64"))).toBe(
|
|
94
|
-
"dslinter-aarch64-apple-darwin",
|
|
95
|
-
);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("maps linux x64", () => {
|
|
99
|
-
expect(releaseAssetBaseName(proc("linux", "x64"))).toBe(
|
|
100
|
-
"dslinter-x86_64-unknown-linux-gnu",
|
|
101
|
-
);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("maps win32 x64", () => {
|
|
105
|
-
expect(releaseAssetBaseName(proc("win32", "x64"))).toBe(
|
|
106
|
-
"dslinter-x86_64-pc-windows-msvc.exe",
|
|
107
|
-
);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("returns null for unknown", () => {
|
|
111
|
-
expect(releaseAssetBaseName(proc("freebsd", "x64"))).toBeNull();
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
describe("vendorBinaryPath", () => {
|
|
116
|
-
it("uses dslinter.exe on Windows", () => {
|
|
117
|
-
expect(vendorBinaryPath(join("/", "pkg"), proc("win32", "x64"))).toBe(
|
|
118
|
-
join("/", "pkg", "vendor", `${CLI_BINARY_NAME}.exe`),
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("uses dslinter on Unix", () => {
|
|
123
|
-
expect(vendorBinaryPath(join("/", "pkg"), proc("linux", "x64"))).toBe(
|
|
124
|
-
join("/", "pkg", "vendor", CLI_BINARY_NAME),
|
|
125
|
-
);
|
|
126
|
-
});
|
|
127
|
-
});
|