localbadge 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +87 -2
- package/dist/badge--GDl23Q5.js +184 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +1 -0
- package/dist/localbadge.d.ts +1 -0
- package/dist/localbadge.js +114 -0
- package/package.json +72 -4
- package/skills/localbadge-ci/SKILL.md +31 -0
- package/skills/localbadge-coverage/SKILL.md +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present Teplostanski Igor (https://github.com/teplostanski)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,88 @@
|
|
|
1
|
-
#
|
|
1
|
+
# localbadge
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**You need a badge. Why does it need a service?**
|
|
4
|
+
|
|
5
|
+
Generate SVG badges right in your repository, locally or in GitHub Actions.
|
|
6
|
+
No sign-ups, external service tokens, or API rate limits.
|
|
7
|
+
Customize colors, labels, and styles in your project.
|
|
8
|
+
|
|
9
|
+
Your badge lives alongside your README. Displaying it requires no requests to
|
|
10
|
+
Codecov, Bundlephobia, or shields.io.
|
|
11
|
+
|
|
12
|
+
Currently supports **coverage** for Vitest through a CLI and a programmatic API.
|
|
13
|
+
**bundle-size** with a CLI and API is planned.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
Add `localbadge` to a project using Vitest and `@vitest/coverage-v8`.
|
|
18
|
+
Vitest and the coverage provider must have matching versions, at least 5.1.0.
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
pnpm add -D localbadge
|
|
22
|
+
pnpm exec localbadge coverage --output-dir .github/badges --file-name coverage
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The command runs your tests, calculates line coverage, and saves an SVG.
|
|
26
|
+
Add it to your README:
|
|
27
|
+
|
|
28
|
+
```md
|
|
29
|
+

|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Run the same command in GitHub Actions. To update the badge in your README,
|
|
33
|
+
commit the generated SVG to your repository.
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
Create `localbadge.config.ts`:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { defineConfig } from 'localbadge'
|
|
41
|
+
|
|
42
|
+
export default defineConfig({
|
|
43
|
+
outputDir: '.github/badges',
|
|
44
|
+
coverage: {
|
|
45
|
+
fileName: 'coverage',
|
|
46
|
+
badge: {
|
|
47
|
+
label: 'coverage',
|
|
48
|
+
color: 'brightgreen',
|
|
49
|
+
labelColor: 'black',
|
|
50
|
+
style: 'flat',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Duplicate settings inherit from the nearest ancestor. The deeper an explicit
|
|
57
|
+
value is defined, the higher its priority. For example:
|
|
58
|
+
`coverage.badge.color` > `coverage.color` > `color` > built-in default.
|
|
59
|
+
At the same depth, CLI arguments override configuration values.
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
pnpm exec localbadge coverage --debug-config
|
|
63
|
+
pnpm exec localbadge coverage --dry-run
|
|
64
|
+
pnpm exec localbadge coverage --help
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`--debug-config` shows resolved values and their sources.
|
|
68
|
+
`--dry-run` calculates coverage and generates the SVG without writing the badge.
|
|
69
|
+
|
|
70
|
+
## Programmatic API
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { generateBadge, resolveOptions } from 'localbadge'
|
|
74
|
+
|
|
75
|
+
const options = await resolveOptions()
|
|
76
|
+
await generateBadge(options)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Only need the number? Use `getCoveragePercent()` from `localbadge`.
|
|
80
|
+
|
|
81
|
+
## Agent skills
|
|
82
|
+
|
|
83
|
+
The npm package includes skills for configuring coverage badges and integrating
|
|
84
|
+
badge generation into CI under `skills/`. Both work from the installed package
|
|
85
|
+
without downloading additional instructions.
|
|
86
|
+
|
|
87
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for CI checks, trusted publishing setup,
|
|
88
|
+
and the dependency update schedule.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import defu from "defu";
|
|
3
|
+
import { loadConfig } from "unconfig";
|
|
4
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { makeBadge } from "badge-maker";
|
|
7
|
+
import boxen from "boxen";
|
|
8
|
+
import { startVitest } from "vitest/node";
|
|
9
|
+
// src/config.ts
|
|
10
|
+
const defaultOptions = {
|
|
11
|
+
cwd: "",
|
|
12
|
+
outputDir: "",
|
|
13
|
+
coverage: {
|
|
14
|
+
cwd: "",
|
|
15
|
+
silentMode: false,
|
|
16
|
+
noColor: false,
|
|
17
|
+
outputDir: "",
|
|
18
|
+
fileName: "coverage-badge",
|
|
19
|
+
badge: {
|
|
20
|
+
label: "coverage",
|
|
21
|
+
color: "green",
|
|
22
|
+
labelColor: "gray",
|
|
23
|
+
style: "flat"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const defineConfig = (options) => {
|
|
28
|
+
return options;
|
|
29
|
+
};
|
|
30
|
+
const resolveOptionsWithSources = async (overrides = {}) => {
|
|
31
|
+
const files = /* @__PURE__ */ new Map();
|
|
32
|
+
const cwd = overrides.cwd || process.cwd();
|
|
33
|
+
const { config, sources } = await loadConfig({
|
|
34
|
+
cwd,
|
|
35
|
+
sources: [{
|
|
36
|
+
files: ["localbadge.config"],
|
|
37
|
+
extensions: [
|
|
38
|
+
"ts",
|
|
39
|
+
"mts",
|
|
40
|
+
"cts",
|
|
41
|
+
"js",
|
|
42
|
+
"mjs",
|
|
43
|
+
"cjs",
|
|
44
|
+
"json"
|
|
45
|
+
],
|
|
46
|
+
rewrite: (values, filepath) => {
|
|
47
|
+
const options = values;
|
|
48
|
+
files.set(filepath, options);
|
|
49
|
+
return options;
|
|
50
|
+
}
|
|
51
|
+
}],
|
|
52
|
+
merge: true
|
|
53
|
+
});
|
|
54
|
+
const runtimeDefaults = {
|
|
55
|
+
...defaultOptions,
|
|
56
|
+
cwd: process.cwd()
|
|
57
|
+
};
|
|
58
|
+
const explicit = defu(overrides, config ?? {});
|
|
59
|
+
const shape = defu(explicit, runtimeDefaults);
|
|
60
|
+
const layers = [
|
|
61
|
+
{
|
|
62
|
+
source: "CLI",
|
|
63
|
+
values: overrides
|
|
64
|
+
},
|
|
65
|
+
...sources.map((source) => ({
|
|
66
|
+
source,
|
|
67
|
+
values: files.get(source) ?? {}
|
|
68
|
+
})),
|
|
69
|
+
{
|
|
70
|
+
source: "default",
|
|
71
|
+
values: runtimeDefaults
|
|
72
|
+
}
|
|
73
|
+
];
|
|
74
|
+
const entries = [];
|
|
75
|
+
const read = (values, keys) => {
|
|
76
|
+
let value = values;
|
|
77
|
+
for (const key of keys) {
|
|
78
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
79
|
+
value = value[key];
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
};
|
|
83
|
+
const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
84
|
+
const visit = (values, keys = []) => {
|
|
85
|
+
const result = {};
|
|
86
|
+
for (const [key, value] of Object.entries(values)) {
|
|
87
|
+
const path = [...keys, key];
|
|
88
|
+
if (isRecord(value)) result[key] = visit(value, path);
|
|
89
|
+
else {
|
|
90
|
+
let selected = value;
|
|
91
|
+
let source = "default";
|
|
92
|
+
for (let depth = keys.length; depth >= 0; depth--) {
|
|
93
|
+
const candidate = [...keys.slice(0, depth), key];
|
|
94
|
+
const inherited = read(explicit, candidate);
|
|
95
|
+
if (inherited == null || isRecord(inherited)) continue;
|
|
96
|
+
selected = inherited;
|
|
97
|
+
source = layers.find(({ values }) => read(values, candidate) != null)?.source ?? "default";
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
result[key] = selected;
|
|
101
|
+
entries.push({
|
|
102
|
+
setting: path.join("."),
|
|
103
|
+
value: selected,
|
|
104
|
+
source
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
options: visit(shape),
|
|
112
|
+
entries
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
const resolveOptions = async (overrides = {}) => {
|
|
116
|
+
return (await resolveOptionsWithSources(overrides)).options;
|
|
117
|
+
};
|
|
118
|
+
// src/coverage/coverage.ts
|
|
119
|
+
const getCoveragePercent = async (cwd = process.cwd()) => {
|
|
120
|
+
let linesPercent;
|
|
121
|
+
await startVitest([], {
|
|
122
|
+
root: cwd,
|
|
123
|
+
run: true,
|
|
124
|
+
reporters: ["dot", { onCoverage(coverage) {
|
|
125
|
+
const pct = coverage.getCoverageSummary().lines.pct;
|
|
126
|
+
if (typeof pct !== "number") throw new Error("Vitest coverage \"lines\": Unknown");
|
|
127
|
+
linesPercent = pct;
|
|
128
|
+
} }],
|
|
129
|
+
silent: true,
|
|
130
|
+
coverage: {
|
|
131
|
+
enabled: true,
|
|
132
|
+
clean: false,
|
|
133
|
+
reporter: [],
|
|
134
|
+
reportOnFailure: true
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
if (linesPercent === void 0) throw new Error("Vitest did not provide coverage data");
|
|
138
|
+
return linesPercent;
|
|
139
|
+
};
|
|
140
|
+
// src/coverage/badge.ts
|
|
141
|
+
const escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
142
|
+
const generateBadge = async (options, { dryRun = false } = {}) => {
|
|
143
|
+
const cwd = options.coverage.cwd || options.cwd || process.cwd();
|
|
144
|
+
const pct = Math.floor(await getCoveragePercent(cwd));
|
|
145
|
+
const svg = makeBadge({
|
|
146
|
+
label: options.coverage.badge.label,
|
|
147
|
+
message: `${pct}%`,
|
|
148
|
+
color: options.coverage.badge.color,
|
|
149
|
+
labelColor: options.coverage.badge.labelColor,
|
|
150
|
+
style: options.coverage.badge.style
|
|
151
|
+
});
|
|
152
|
+
const dirParts = (options.coverage.outputDir || options.outputDir).split(/[\\/]+/).filter(Boolean);
|
|
153
|
+
if (dirParts.includes("..")) throw new Error("\"outputDir\" must not contain \"..\"");
|
|
154
|
+
const outputDir = path.join(cwd, ...dirParts);
|
|
155
|
+
const fileName = `${options.coverage.fileName}.svg`;
|
|
156
|
+
if (!dryRun) {
|
|
157
|
+
await mkdir(outputDir, { recursive: true });
|
|
158
|
+
await writeFile(path.join(outputDir, fileName), svg, "utf8");
|
|
159
|
+
}
|
|
160
|
+
if (options.coverage.silentMode) return;
|
|
161
|
+
const color = pc.createColors(!options.coverage.noColor);
|
|
162
|
+
const markdownPath = [...dirParts, fileName].join("/");
|
|
163
|
+
const safePath = escapeHtml(markdownPath);
|
|
164
|
+
const safeLabel = escapeHtml(options.coverage.badge.label);
|
|
165
|
+
const hint = `
|
|
166
|
+
${dryRun ? `Dry run: coverage ${pct}%. Badge would be written to:` : "Coverage badge created at:"}
|
|
167
|
+
${color.cyan(color.underline(`${outputDir}/${fileName}`))}
|
|
168
|
+
|
|
169
|
+
${color.gray("Markdown:")}
|
|
170
|
+
${color.blue("!")}[${options.coverage.badge.label}]${color.cyan(`(${color.redBright(markdownPath)})`)}
|
|
171
|
+
|
|
172
|
+
${color.gray("HTML:")}
|
|
173
|
+
<${color.blue("img")} ${color.red("src")}=${color.yellow(`"${safePath}"`)} ${color.red("alt")}=${color.yellow(`"${safeLabel}"`)}>
|
|
174
|
+
`;
|
|
175
|
+
console.log(boxen(hint, {
|
|
176
|
+
borderStyle: "round",
|
|
177
|
+
borderColor: "cyan",
|
|
178
|
+
padding: {
|
|
179
|
+
left: 2,
|
|
180
|
+
right: 2
|
|
181
|
+
}
|
|
182
|
+
}));
|
|
183
|
+
};
|
|
184
|
+
export { resolveOptions as a, defineConfig as i, getCoveragePercent as n, resolveOptionsWithSources as o, defaultOptions as r, generateBadge as t };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
type DeepRequired<T> = T extends object ? { [K in keyof T]-?: NonNullable<T[K]> extends object ? DeepRequired<NonNullable<T[K]>> : T[K]; } : T;
|
|
2
|
+
type BadgeOptions = {
|
|
3
|
+
label?: string;
|
|
4
|
+
color?: string;
|
|
5
|
+
labelColor?: string;
|
|
6
|
+
style?: 'flat' | 'plastic' | 'flat-square' | 'for-the-badge' | 'social';
|
|
7
|
+
};
|
|
8
|
+
type SharedOptions = BadgeOptions & {
|
|
9
|
+
cwd?: string;
|
|
10
|
+
outputDir?: string;
|
|
11
|
+
silentMode?: boolean;
|
|
12
|
+
noColor?: boolean;
|
|
13
|
+
fileName?: string;
|
|
14
|
+
};
|
|
15
|
+
type Options = SharedOptions & {
|
|
16
|
+
coverage?: SharedOptions & {
|
|
17
|
+
badge?: BadgeOptions;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
type ResolvedOptions = {
|
|
21
|
+
cwd: string;
|
|
22
|
+
outputDir: string;
|
|
23
|
+
coverage: DeepRequired<Omit<SharedOptions, keyof BadgeOptions>> & {
|
|
24
|
+
badge: DeepRequired<BadgeOptions>;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export declare const defaultOptions: {
|
|
28
|
+
cwd: string;
|
|
29
|
+
outputDir: string;
|
|
30
|
+
coverage: {
|
|
31
|
+
cwd: string;
|
|
32
|
+
silentMode: false;
|
|
33
|
+
noColor: false;
|
|
34
|
+
outputDir: string;
|
|
35
|
+
fileName: string;
|
|
36
|
+
badge: {
|
|
37
|
+
label: string;
|
|
38
|
+
color: string;
|
|
39
|
+
labelColor: string;
|
|
40
|
+
style: "flat";
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export declare const defineConfig: (options: Options) => Options;
|
|
45
|
+
export declare const resolveOptionsWithSources: (overrides?: Options) => Promise<{
|
|
46
|
+
options: ResolvedOptions;
|
|
47
|
+
entries: {
|
|
48
|
+
setting: string;
|
|
49
|
+
value: unknown;
|
|
50
|
+
source: string;
|
|
51
|
+
}[];
|
|
52
|
+
}>;
|
|
53
|
+
export declare const resolveOptions: (overrides?: Options) => Promise<ResolvedOptions>;
|
|
54
|
+
export declare const generateBadge: (options: ResolvedOptions, { dryRun }?: {
|
|
55
|
+
dryRun?: boolean;
|
|
56
|
+
}) => Promise<void>;
|
|
57
|
+
export declare const getCoveragePercent: (cwd?: string) => Promise<number>;
|
|
58
|
+
export type { Options, ResolvedOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { r as defaultOptions, i as defineConfig, t as generateBadge, n as getCoveragePercent, a as resolveOptions, o as resolveOptionsWithSources } from "./badge--GDl23Q5.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { o as resolveOptionsWithSources, r as defaultOptions, t as generateBadge } from "./badge--GDl23Q5.js";
|
|
3
|
+
import { defineCommand, runMain } from "citty";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
// package.json
|
|
6
|
+
var name = "localbadge";
|
|
7
|
+
var version = "0.1.0";
|
|
8
|
+
var description = "Generate coverage badges in your repo. No external services.";
|
|
9
|
+
// src/cli.ts
|
|
10
|
+
const coverage = defineCommand({
|
|
11
|
+
meta: {
|
|
12
|
+
name: "coverage",
|
|
13
|
+
description: "coverage command"
|
|
14
|
+
},
|
|
15
|
+
args: {
|
|
16
|
+
"output-dir": {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: `Output directory ${pc.dim(`(Default: cwd)`)}`,
|
|
19
|
+
valueHint: "dir"
|
|
20
|
+
},
|
|
21
|
+
"file-name": {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: `Output file name without extension ${pc.dim(`(Default: ${defaultOptions.coverage.fileName})`)}`,
|
|
24
|
+
valueHint: "name"
|
|
25
|
+
},
|
|
26
|
+
cwd: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: `Project root ${pc.dim(`(Default: process.cwd())`)}`,
|
|
29
|
+
valueHint: "dir"
|
|
30
|
+
},
|
|
31
|
+
silent: {
|
|
32
|
+
type: "boolean",
|
|
33
|
+
description: `Suppress output ${pc.dim(`(Default: ${defaultOptions.coverage.silentMode})`)}`
|
|
34
|
+
},
|
|
35
|
+
"no-color": {
|
|
36
|
+
type: "boolean",
|
|
37
|
+
description: `Disable cli output colors ${pc.dim(`(Default: ${defaultOptions.coverage.noColor})`)}`
|
|
38
|
+
},
|
|
39
|
+
label: {
|
|
40
|
+
type: "string",
|
|
41
|
+
description: `Badge label ${pc.dim(`(Default: ${defaultOptions.coverage.badge.label})`)}`,
|
|
42
|
+
valueHint: "text"
|
|
43
|
+
},
|
|
44
|
+
color: {
|
|
45
|
+
type: "string",
|
|
46
|
+
description: `Badge color ${pc.dim(`(Default: ${defaultOptions.coverage.badge.color})`)}`
|
|
47
|
+
},
|
|
48
|
+
"label-color": {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: `Badge label color ${pc.dim(`(Default: ${defaultOptions.coverage.badge.labelColor})`)}`,
|
|
51
|
+
valueHint: "color"
|
|
52
|
+
},
|
|
53
|
+
style: {
|
|
54
|
+
type: "enum",
|
|
55
|
+
description: `Badge style ${pc.dim(`(Default: ${defaultOptions.coverage.badge.style})`)}`,
|
|
56
|
+
options: [
|
|
57
|
+
"flat",
|
|
58
|
+
"plastic",
|
|
59
|
+
"flat-square",
|
|
60
|
+
"for-the-badge",
|
|
61
|
+
"social"
|
|
62
|
+
],
|
|
63
|
+
valueHint: "style"
|
|
64
|
+
},
|
|
65
|
+
mode: {
|
|
66
|
+
type: "enum",
|
|
67
|
+
description: `Badge style mode ${pc.dim(`(Default: shields-io)`)}`,
|
|
68
|
+
options: ["shields-io"]
|
|
69
|
+
},
|
|
70
|
+
"debug-config": {
|
|
71
|
+
type: "boolean",
|
|
72
|
+
description: `Print resolved settings and their sources`
|
|
73
|
+
},
|
|
74
|
+
"dry-run": {
|
|
75
|
+
type: "boolean",
|
|
76
|
+
description: `Run coverage and generate a badge without writing it`
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
run: async ({ args }) => {
|
|
80
|
+
const overrides = {
|
|
81
|
+
cwd: args.cwd,
|
|
82
|
+
coverage: {
|
|
83
|
+
silentMode: args.silent,
|
|
84
|
+
noColor: args["no-color"],
|
|
85
|
+
outputDir: args["output-dir"],
|
|
86
|
+
fileName: args["file-name"],
|
|
87
|
+
badge: {
|
|
88
|
+
label: args.label,
|
|
89
|
+
color: args.color,
|
|
90
|
+
labelColor: args["label-color"],
|
|
91
|
+
style: args.style
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
const { options, entries } = await resolveOptionsWithSources(overrides);
|
|
97
|
+
if (args["debug-config"]) console.table(entries);
|
|
98
|
+
await generateBadge(options, { dryRun: args["dry-run"] });
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error(pc.red(`Error: ${error.message}`));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
const main = defineCommand({
|
|
106
|
+
meta: {
|
|
107
|
+
name: `${name}`,
|
|
108
|
+
version: `${version}`,
|
|
109
|
+
description: `${description}`
|
|
110
|
+
},
|
|
111
|
+
subCommands: { coverage }
|
|
112
|
+
});
|
|
113
|
+
runMain(main);
|
|
114
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,74 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "localbadge",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Generate coverage badges in your repo. No external services.",
|
|
6
|
+
"author": "Teplostanski Igor <hi@teplostanski.me> (https://teplostanski.me)",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/teplostanski/localbadge#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/teplostanski/localbadge.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/teplostanski/localbadge/issues"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"skills"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsdown",
|
|
22
|
+
"dev": "tsdown --watch",
|
|
23
|
+
"test": "vitest",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"prepublishOnly": "pnpm run build",
|
|
26
|
+
"prepack": "pnpm typecheck && pnpm build",
|
|
27
|
+
"check": "pnpm lint && pnpm typecheck",
|
|
28
|
+
"check:fix": "pnpm lint:fix && pnpm typecheck",
|
|
29
|
+
"lint": "biome check .",
|
|
30
|
+
"lint:fix": "biome check --write .",
|
|
31
|
+
"format": "biome format --write .",
|
|
32
|
+
"prepare": "husky",
|
|
33
|
+
"smoke": "node scripts/package-smoke.mjs",
|
|
34
|
+
"check:ci": "pnpm lint && pnpm typecheck && pnpm exec vitest run && pnpm build && pnpm smoke"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@vitest/coverage-v8": ">=5.1.0",
|
|
38
|
+
"vitest": ">=5.1.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@biomejs/biome": "^2.6.7",
|
|
42
|
+
"@teplostanski/biome-config": "^2.0.3",
|
|
43
|
+
"@types/node": "^26.6.6",
|
|
44
|
+
"@vitest/coverage-v8": "^5.1.0",
|
|
45
|
+
"@vitest/istanbul-lib-coverage": "^2.1.0",
|
|
46
|
+
"husky": "^9.1.7",
|
|
47
|
+
"tsdown": "^0.23.7",
|
|
48
|
+
"typescript": "^7.0.2",
|
|
49
|
+
"vitest": "^5.1.0"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"badge-maker": "^6.0.0",
|
|
53
|
+
"boxen": "^9.0.0",
|
|
54
|
+
"citty": "^0.2.2",
|
|
55
|
+
"defu": "^6.1.7",
|
|
56
|
+
"picocolors": "^1.1.1",
|
|
57
|
+
"unconfig": "^7.5.0"
|
|
58
|
+
},
|
|
59
|
+
"packageManager": "pnpm@12.21.0",
|
|
60
|
+
"bin": {
|
|
61
|
+
"localbadge": "dist/localbadge.js"
|
|
62
|
+
},
|
|
63
|
+
"exports": {
|
|
64
|
+
".": {
|
|
65
|
+
"types": "./dist/index.d.ts",
|
|
66
|
+
"import": "./dist/index.js"
|
|
67
|
+
},
|
|
68
|
+
"./package.json": "./package.json"
|
|
69
|
+
},
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"access": "public",
|
|
72
|
+
"provenance": true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: localbadge-ci
|
|
3
|
+
description: Integrate localbadge coverage badge generation into GitHub Actions or other CI. Use when replacing hosted coverage badge services or deciding how to persist a generated SVG; use localbadge-coverage for styling and API configuration.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Coverage badges in CI
|
|
7
|
+
|
|
8
|
+
Inspect the project's workflow, package manager and Vitest configuration before
|
|
9
|
+
editing. Use its existing installation and Node setup. Vitest and
|
|
10
|
+
@vitest/coverage-v8 must have matching versions, at least 5.1.0.
|
|
11
|
+
|
|
12
|
+
After dependency installation, run:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm exec localbadge coverage --output-dir .github/badges --file-name coverage
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This command already runs the tests with coverage. Avoid adding another identical
|
|
19
|
+
test run solely to produce the badge. It does not upload results to a service.
|
|
20
|
+
A dry run executes tests but does not persist a badge.
|
|
21
|
+
|
|
22
|
+
A README link to `.github/badges/coverage.svg` reads the file from the repository,
|
|
23
|
+
not from the runner. A generated runner file or downloadable Actions artifact
|
|
24
|
+
does not update that README badge. Agree on how the SVG will be persisted before
|
|
25
|
+
adding write credentials or automatic commits. Do not create commits or PRs unless
|
|
26
|
+
the user has authorized them. Keep untrusted pull-request runs read-only.
|
|
27
|
+
|
|
28
|
+
For an initial verification, use `--dry-run --debug-config` to inspect the selected
|
|
29
|
+
settings and test the calculation. To generate an artifact, omit `--dry-run` and
|
|
30
|
+
use the project's artifact upload step with the actual output path. Report whether
|
|
31
|
+
the result is only an artifact or has been made available to the README.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: localbadge-coverage
|
|
3
|
+
description: Generate and customize repository-local Vitest coverage badges with localbadge through its CLI or TypeScript API. Use when adding a coverage SVG to a README or configuring localbadge; use localbadge-ci for release and CI integration.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Local coverage badges
|
|
7
|
+
|
|
8
|
+
Read the installed localbadge package version and the project's Vitest setup.
|
|
9
|
+
Vitest and @vitest/coverage-v8 must have identical versions, at least 5.1.0.
|
|
10
|
+
Reuse the project's package manager and tests. Coverage measures lines and the
|
|
11
|
+
badge rounds the result down to a whole percentage.
|
|
12
|
+
|
|
13
|
+
Run `localbadge coverage --output-dir .github/badges --file-name coverage` through
|
|
14
|
+
the package manager. Add `` to the README.
|
|
15
|
+
No coverage service credentials or uploads are needed.
|
|
16
|
+
|
|
17
|
+
For persistent settings, create `localbadge.config.ts`:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { defineConfig } from 'localbadge'
|
|
21
|
+
|
|
22
|
+
export default defineConfig({
|
|
23
|
+
outputDir: '.github/badges',
|
|
24
|
+
coverage: {
|
|
25
|
+
fileName: 'coverage',
|
|
26
|
+
badge: { label: 'coverage', color: 'brightgreen', style: 'flat' },
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Explicit duplicate values inherit from their nearest ancestor. For example,
|
|
32
|
+
coverage.badge.color overrides coverage.color, which overrides root color.
|
|
33
|
+
Depth wins across sources; CLI wins at the same depth. Defaults apply last.
|
|
34
|
+
The root cwd controls config discovery; coverage.cwd controls tests and output.
|
|
35
|
+
|
|
36
|
+
Use `--debug-config` to inspect values and their sources. `--dry-run` still runs
|
|
37
|
+
tests and creates the SVG in memory, but does not create the badge directory or
|
|
38
|
+
write the SVG. It does not suppress side effects inside tests.
|
|
39
|
+
|
|
40
|
+
For TypeScript integration:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { generateBadge, getCoveragePercent, resolveOptions } from 'localbadge'
|
|
44
|
+
|
|
45
|
+
const options = await resolveOptions()
|
|
46
|
+
await generateBadge(options)
|
|
47
|
+
// Alternatively, skip writing the badge:
|
|
48
|
+
await generateBadge(options, { dryRun: true })
|
|
49
|
+
// Or retrieve only the numeric line coverage:
|
|
50
|
+
const percentage = await getCoveragePercent(options.coverage.cwd || options.cwd)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Do not advertise bundle-size as implemented. It is planned.
|