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 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: true });
46
+ await ensureDslintBinary(packageRoot, { quiet: false });
47
47
  }
48
48
  if (existsSync(vendored)) {
49
49
  return vendored;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dslinter",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "DSLinter dashboard UI: playground shell, token wall, and governance panels (consumes dslint-report.json).",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -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
- releaseAssetBaseName,
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 asset = releaseAssetBaseName();
67
- if (!asset) {
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 tagsToTry = [releaseTag(version)];
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
- for (const tag of tagsToTry) {
85
- const url = assetUrl(packageRoot, tag, asset);
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(url, { redirect: "follow" });
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}): ${url}`);
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 (tag === "latest" && !quiet) {
133
+ if (release.tag_name !== `v${version}` && !quiet) {
106
134
  log(
107
- `[dslinter] Installed scanner from latest GitHub release (no asset for npm v${version}).`,
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] No GitHub release with asset "${asset}" (tried ${tagsToTry.join(", ")}).\n` +
125
- ` Publish tag ${releaseTag(version)} with workflow release-dslint-binaries.yml, or set DSLINT_BIN to a local build.`,
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
- const ok = await ensureDslintBinary();
136
- process.exit(ok ? 0 : 0);
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
+ }
@@ -22,4 +22,6 @@ To run the design-system scanner:
22
22
  3. Or set DSLINT_BIN to your local target/release/dslinter
23
23
 
24
24
  Releases: https://github.com/jrmybtlr/DSLinter/releases
25
+
26
+ Tip: re-run with DSLINT_VERBOSE=1 to see which GitHub releases/assets were tried.
25
27
  `);
@@ -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(