dslinter 0.0.10 → 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,29 @@
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
+
15
+ ## v0.0.11
16
+
17
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.10...v0.0.11)
18
+
19
+ ### 🩹 Fixes
20
+
21
+ - Update repository references from DSLint to DSLinter ([2dbae8d](https://github.com/jrmybtlr/DSLinter/commit/2dbae8d))
22
+
23
+ ### ❤️ Contributors
24
+
25
+ - Jeremy Butler <jeremy.butler@laravel.com>
26
+
3
27
  ## v0.0.10
4
28
 
5
29
  [compare changes](https://github.com/jrmybtlr/DSLint/compare/v0.0.9...v0.0.10)
package/README.md CHANGED
@@ -25,7 +25,7 @@ The **`dslinter` binary** runs the **`dslint`** scanner with the same flags as t
25
25
 
26
26
  On **`npm install dslinter`**, a **`postinstall`** script tries to download a **prebuilt `dslint`** for your OS/arch from this repo’s **GitHub Releases**, using the **same tag as the npm version** (for example npm `dslinter@0.0.6` → release **`v0.0.6`** and assets like `dslint-x86_64-unknown-linux-gnu`). The binary is stored under `node_modules/dslinter/vendor/` and `dslinter` / `npx dslinter` prefer it over `PATH`.
27
27
 
28
- **Release workflow:** push git tag `v*` (after bumping the npm version) so [.github/workflows/release-dslint-binaries.yml](https://github.com/jrmybtlr/DSLint/blob/main/.github/workflows/release-dslint-binaries.yml) uploads the platform binaries, **then** publish `dslinter` to npm (or publish after the workflow finishes so installs resolve the assets).
28
+ **Release workflow:** push git tag `v*` (after bumping the npm version) so [.github/workflows/release-dslint-binaries.yml](https://github.com/jrmybtlr/DSLinter/blob/main/.github/workflows/release-dslint-binaries.yml) uploads the platform binaries, **then** publish `dslinter` to npm (or publish after the workflow finishes so installs resolve the assets).
29
29
 
30
30
  Environment variables:
31
31
 
@@ -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/DSLint`). |
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
 
@@ -45,7 +47,7 @@ Environment variables:
45
47
 
46
48
  The crates.io crate **`dslint`** (v0.0.x) is a **different project** (design-file linting). It is **not** this design-system scanner. Installing it will break `npx dslinter` if it ends up on your `PATH`.
47
49
 
48
- Use **`cargo install --git https://github.com/jrmybtlr/DSLint dslinter --locked`** or set **`DSLINT_BIN`** to a local `target/release/dslinter` build.
50
+ Use **`cargo install --git https://github.com/jrmybtlr/DSLinter dslinter --locked`** or set **`DSLINT_BIN`** to a local `target/release/dslinter` build.
49
51
 
50
52
  ### If there is no matching release asset yet
51
53
 
@@ -59,7 +61,7 @@ npx dslinter /path/to/repo --json -o dslint-report.json
59
61
  |--------------|-------------------------|
60
62
  | **npm + GitHub Releases** | Default: download when release `vX.Y.Z` includes your platform asset. |
61
63
  | **GitHub Releases** | Manual download of `dslinter-*` from the release; run directly or set `DSLINT_BIN`. |
62
- | **From source** | `cargo install --git https://github.com/jrmybtlr/DSLint dslinter --locked` (not `cargo install dslint`). |
64
+ | **From source** | `cargo install --git https://github.com/jrmybtlr/DSLinter dslinter --locked` (not `cargo install dslint`). |
63
65
 
64
66
  Typical usage:
65
67
 
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,18 +1,18 @@
1
1
  {
2
2
  "name": "dslinter",
3
- "version": "0.0.10",
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",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/jrmybtlr/DSLint.git",
9
+ "url": "https://github.com/jrmybtlr/DSLinter.git",
10
10
  "directory": "packages/dashboard"
11
11
  },
12
12
  "bugs": {
13
- "url": "https://github.com/jrmybtlr/DSLint/issues"
13
+ "url": "https://github.com/jrmybtlr/DSLinter/issues"
14
14
  },
15
- "homepage": "https://github.com/jrmybtlr/DSLint#readme",
15
+ "homepage": "https://github.com/jrmybtlr/DSLinter#readme",
16
16
  "keywords": [
17
17
  "dslinter",
18
18
  "design-system",
@@ -7,7 +7,12 @@ import { chmod, mkdir, stat } from "node:fs/promises";
7
7
  import { dirname, join } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import {
10
- releaseAssetBaseName,
10
+ fetchReleasesForVersion,
11
+ pickReleaseAsset,
12
+ } from "./github-release.mjs";
13
+ import {
14
+ githubRepoFromPackage,
15
+ releaseAssetCandidateNames,
11
16
  vendorBinaryPath,
12
17
  } from "./resolve-dslint-binary.mjs";
13
18
 
@@ -30,18 +35,10 @@ function readPackageVersion(packageRoot) {
30
35
  return pkg.version;
31
36
  }
32
37
 
33
- function releaseTag(version) {
34
- const o = process.env.DSLINT_RELEASE_TAG?.trim();
35
- if (o) return o.startsWith("v") ? o : `v${o}`;
36
- return `v${version}`;
37
- }
38
-
39
- function releaseRepo() {
40
- return process.env.DSLINT_GITHUB_REPO?.trim() || "jrmybtlr/DSLint";
41
- }
42
-
43
- function assetUrl(tag, asset) {
44
- return `https://github.com/${releaseRepo()}/releases/download/${tag}/${asset}`;
38
+ function releaseRepo(packageRoot) {
39
+ const override = process.env.DSLINT_GITHUB_REPO?.trim();
40
+ if (override) return override;
41
+ return githubRepoFromPackage(packageRoot);
45
42
  }
46
43
 
47
44
  /**
@@ -52,6 +49,7 @@ function assetUrl(tag, asset) {
52
49
  export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts = {}) {
53
50
  const { quiet = false } = opts;
54
51
  const log = quiet ? () => {} : console.warn.bind(console);
52
+ const verbose = process.env.DSLINT_VERBOSE === "1";
55
53
 
56
54
  if (process.env.DSLINT_SKIP_DOWNLOAD === "1") {
57
55
  return pathExists(vendorBinaryPath(packageRoot));
@@ -60,8 +58,8 @@ export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts
60
58
  const dest = vendorBinaryPath(packageRoot);
61
59
  if (await pathExists(dest)) return true;
62
60
 
63
- const asset = releaseAssetBaseName();
64
- if (!asset) {
61
+ const candidates = releaseAssetCandidateNames();
62
+ if (candidates.length === 0) {
65
63
  log(
66
64
  `[dslinter] No prebuilt scanner for ${process.platform}-${process.arch}.`,
67
65
  );
@@ -69,22 +67,55 @@ export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts
69
67
  }
70
68
 
71
69
  const version = readPackageVersion(packageRoot);
72
- const tagsToTry = [releaseTag(version)];
73
- if (process.env.DSLINT_USE_LATEST_RELEASE !== "0") {
74
- tagsToTry.push("latest");
75
- }
76
-
70
+ const repo = releaseRepo(packageRoot);
77
71
  const vendorDir = join(packageRoot, "vendor");
78
72
  await mkdir(vendorDir, { recursive: true });
79
73
  const tmp = `${dest}.part`;
80
74
 
81
- for (const tag of tagsToTry) {
82
- const url = assetUrl(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
+
83
115
  try {
84
- const res = await fetch(url, { redirect: "follow" });
85
- if (res.status === 404) continue;
116
+ const res = await fetch(asset.browser_download_url, { redirect: "follow" });
86
117
  if (!res.ok) {
87
- log(`[dslinter] Download failed (${res.status}): ${url}`);
118
+ log(`[dslinter] Download failed (${res.status}): ${asset.browser_download_url}`);
88
119
  continue;
89
120
  }
90
121
 
@@ -99,15 +130,15 @@ export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts
99
130
  if (process.platform !== "win32") {
100
131
  await chmod(dest, 0o755);
101
132
  }
102
- if (tag === "latest" && !quiet) {
133
+ if (release.tag_name !== `v${version}` && !quiet) {
103
134
  log(
104
- `[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}).`,
105
136
  );
106
137
  }
107
138
  return true;
108
139
  } catch (err) {
109
140
  log(
110
- `[dslinter] Could not download: ${err instanceof Error ? err.message : err}`,
141
+ `[dslinter] Could not download ${asset.name}: ${err instanceof Error ? err.message : err}`,
111
142
  );
112
143
  try {
113
144
  unlinkSync(tmp);
@@ -118,8 +149,8 @@ export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts
118
149
  }
119
150
 
120
151
  log(
121
- `[dslinter] No GitHub release with asset "${asset}" (tried ${tagsToTry.join(", ")}).\n` +
122
- ` 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.`,
123
154
  );
124
155
  return false;
125
156
  }
@@ -129,6 +160,6 @@ const isMain =
129
160
  fileURLToPath(import.meta.url) === process.argv[1];
130
161
 
131
162
  if (isMain) {
132
- const ok = await ensureDslintBinary();
133
- process.exit(ok ? 0 : 0);
163
+ await ensureDslintBinary();
164
+ process.exit(0);
134
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
+ }
@@ -16,10 +16,12 @@ To run the design-system scanner:
16
16
 
17
17
  1. Re-run after a GitHub release exists for v${version} (prebuilt download), or
18
18
  2. Build from this repo and point at it:
19
- cargo install --git https://github.com/jrmybtlr/DSLint dslinter --locked
19
+ cargo install --git https://github.com/jrmybtlr/DSLinter dslinter --locked
20
20
  export DSLINT_BIN="$(command -v dslinter)"
21
21
  npx dslinter ...
22
22
  3. Or set DSLINT_BIN to your local target/release/dslinter
23
23
 
24
- Releases: https://github.com/jrmybtlr/DSLint/releases
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
  `);
@@ -1,8 +1,38 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { join } from "node:path";
2
3
 
3
4
  /** CLI binary name (avoids collision with unrelated `dslint` on crates.io). */
4
5
  export const CLI_BINARY_NAME = "dslinter";
5
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
+
6
36
  /**
7
37
  * Maps the current OS/arch to the GitHub release asset basename (must match CI upload names).
8
38
  * @param {NodeJS.Process} [proc]
@@ -28,6 +58,14 @@ export function releaseAssetBaseName(proc = process) {
28
58
  return null;
29
59
  }
30
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
+
31
69
  /**
32
70
  * @param {string} packageRoot — directory containing package.json
33
71
  * @param {NodeJS.Process} [proc]
@@ -1,8 +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,
8
+ DEFAULT_GITHUB_REPO,
9
+ parseGitHubRepo,
5
10
  releaseAssetBaseName,
11
+ releaseAssetCandidateNames,
6
12
  vendorBinaryPath,
7
13
  } from "../scripts/resolve-dslint-binary.mjs";
8
14
 
@@ -10,6 +16,52 @@ function proc(platform: string, arch: string): NodeJS.Process {
10
16
  return { platform, arch } as NodeJS.Process;
11
17
  }
12
18
 
19
+ describe("parseGitHubRepo", () => {
20
+ it("parses https repository url", () => {
21
+ expect(
22
+ parseGitHubRepo("https://github.com/jrmybtlr/DSLinter.git"),
23
+ ).toBe("jrmybtlr/DSLinter");
24
+ });
25
+
26
+ it("parses repository object", () => {
27
+ expect(
28
+ parseGitHubRepo({
29
+ type: "git",
30
+ url: "git+https://github.com/jrmybtlr/DSLinter.git",
31
+ }),
32
+ ).toBe("jrmybtlr/DSLinter");
33
+ });
34
+
35
+ it("defaults constant points at DSLinter", () => {
36
+ expect(DEFAULT_GITHUB_REPO).toBe("jrmybtlr/DSLinter");
37
+ });
38
+ });
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
+
13
65
  describe("releaseAssetBaseName", () => {
14
66
  it("maps darwin arm64", () => {
15
67
  expect(releaseAssetBaseName(proc("darwin", "arm64"))).toBe(