localbadge 0.0.1 → 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 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,23 +1,88 @@
1
- # tsdown-starter
1
+ # localbadge
2
2
 
3
- A starter for creating a TypeScript package.
3
+ **You need a badge. Why does it need a service?**
4
4
 
5
- ## Development
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.
6
8
 
7
- - Install dependencies:
9
+ Your badge lives alongside your README. Displaying it requires no requests to
10
+ Codecov, Bundlephobia, or shields.io.
8
11
 
9
- ```bash
10
- npm install
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
+ ![coverage](.github/badges/coverage.svg)
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
+ })
11
54
  ```
12
55
 
13
- - Run the unit tests:
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.
14
60
 
15
- ```bash
16
- npm run test
61
+ ```sh
62
+ pnpm exec localbadge coverage --debug-config
63
+ pnpm exec localbadge coverage --dry-run
64
+ pnpm exec localbadge coverage --help
17
65
  ```
18
66
 
19
- - Build the library:
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'
20
74
 
21
- ```bash
22
- npm run build
75
+ const options = await resolveOptions()
76
+ await generateBadge(options)
23
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
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 CHANGED
@@ -1,33 +1,58 @@
1
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 Options = {
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 & {
3
9
  cwd?: string;
10
+ outputDir?: string;
4
11
  silentMode?: boolean;
5
12
  noColor?: boolean;
6
- outputDir?: string;
7
13
  fileName?: string;
8
- badge?: {
9
- label?: string;
10
- color?: string;
11
- labelColor?: string;
12
- style?: 'flat' | 'plastic' | 'flat-square' | 'for-the-badge' | 'social';
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>;
13
25
  };
14
26
  };
15
- type ResolvedOptions = DeepRequired<Options>;
16
- export declare const generateBadge: (options: ResolvedOptions) => Promise<void>;
17
27
  export declare const defaultOptions: {
18
28
  cwd: string;
19
- silentMode: false;
20
- noColor: false;
21
29
  outputDir: string;
22
- fileName: string;
23
- badge: {
24
- label: string;
25
- color: string;
26
- labelColor: string;
27
- style: "flat";
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
+ };
28
42
  };
29
43
  };
30
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
+ }>;
31
53
  export declare const resolveOptions: (overrides?: Options) => Promise<ResolvedOptions>;
54
+ export declare const generateBadge: (options: ResolvedOptions, { dryRun }?: {
55
+ dryRun?: boolean;
56
+ }) => Promise<void>;
32
57
  export declare const getCoveragePercent: (cwd?: string) => Promise<number>;
33
58
  export type { Options, ResolvedOptions };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { t as defaultOptions, n as defineConfig, i as generateBadge, a as getCoveragePercent, r as resolveOptions } from "./config-BSa1vlrn.js";
1
+ export { r as defaultOptions, i as defineConfig, t as generateBadge, n as getCoveragePercent, a as resolveOptions, o as resolveOptionsWithSources } from "./badge--GDl23Q5.js";
@@ -1,71 +1,114 @@
1
1
  #!/usr/bin/env node
2
- import { i as generateBadge, r as resolveOptions } from "./config-BSa1vlrn.js";
3
- import { parseArgs } from "node:util";
2
+ import { o as resolveOptionsWithSources, r as defaultOptions, t as generateBadge } from "./badge--GDl23Q5.js";
3
+ import { defineCommand, runMain } from "citty";
4
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.";
5
9
  // src/cli.ts
6
- /**
7
- * добавить mode
8
- * создать субкоманду coverage, основная localbadge
9
- */
10
- const { values } = parseArgs({
11
- options: {
12
- "output-dir": { type: "string" },
13
- "file-name": { type: "string" },
14
- cwd: { type: "string" },
15
- silent: { type: "boolean" },
16
- "no-color": { type: "boolean" },
17
- label: { type: "string" },
18
- color: { type: "string" },
19
- "label-color": { type: "string" },
20
- style: { type: "string" },
21
- help: {
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": {
22
71
  type: "boolean",
23
- short: "h"
72
+ description: `Print resolved settings and their sources`
24
73
  },
25
- version: {
74
+ "dry-run": {
26
75
  type: "boolean",
27
- short: "v"
76
+ description: `Run coverage and generate a badge without writing it`
28
77
  }
29
78
  },
30
- allowPositionals: false,
31
- strict: true
32
- });
33
- if (values.help) {
34
- console.log(`
35
- Usage: coverage [options]
36
-
37
- Options:
38
- --output-dir <dir> Output directory (default: cwd)
39
- --file-name <name> Output file name without extension (default: coverage-badge)
40
- --cwd <dir> Project root (default: process.cwd())
41
- --silent Suppress output
42
- --no-color Disable colors
43
- --label <text> Badge label
44
- --color <color> Badge color
45
- --label-color <color> Badge label color
46
- --style <style> Badge style (flat|plastic|flat-square|for-the-badge|social)
47
- --mode <mode> Badge mode (modern|shields-io)
48
- `);
49
- process.exit(0);
50
- }
51
- const overrides = {
52
- cwd: values.cwd,
53
- silentMode: values.silent,
54
- noColor: values["no-color"],
55
- outputDir: values["output-dir"],
56
- fileName: values["file-name"],
57
- badge: {
58
- label: values.label,
59
- color: values.color,
60
- labelColor: values["label-color"],
61
- style: values.style
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
+ }
62
103
  }
63
- };
64
- try {
65
- const options = await resolveOptions(overrides);
66
- await generateBadge(options);
67
- } catch (error) {
68
- console.error(pc.red(`Error: ${error.message}`));
69
- process.exit(1);
70
- }
104
+ });
105
+ const main = defineCommand({
106
+ meta: {
107
+ name: `${name}`,
108
+ version: `${version}`,
109
+ description: `${description}`
110
+ },
111
+ subCommands: { coverage }
112
+ });
113
+ runMain(main);
71
114
  export {};
package/package.json CHANGED
@@ -1,20 +1,21 @@
1
1
  {
2
2
  "name": "localbadge",
3
3
  "type": "module",
4
- "version": "0.0.1",
5
- "description": "",
4
+ "version": "0.1.0",
5
+ "description": "Generate coverage badges in your repo. No external services.",
6
6
  "author": "Teplostanski Igor <hi@teplostanski.me> (https://teplostanski.me)",
7
7
  "license": "MIT",
8
- "homepage": "https://github.com/local-badges/coverage#readme",
8
+ "homepage": "https://github.com/teplostanski/localbadge#readme",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/local-badges/coverage.git"
11
+ "url": "git+https://github.com/teplostanski/localbadge.git"
12
12
  },
13
13
  "bugs": {
14
- "url": "https://github.com/local-badges/coverage/issues"
14
+ "url": "https://github.com/teplostanski/localbadge/issues"
15
15
  },
16
16
  "files": [
17
- "dist"
17
+ "dist",
18
+ "skills"
18
19
  ],
19
20
  "scripts": {
20
21
  "build": "tsdown",
@@ -28,22 +29,26 @@
28
29
  "lint": "biome check .",
29
30
  "lint:fix": "biome check --write .",
30
31
  "format": "biome format --write .",
31
- "prepare": "husky"
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"
32
35
  },
33
36
  "peerDependencies": {
34
- "vitest": "^5.1.0"
37
+ "@vitest/coverage-v8": ">=5.1.0",
38
+ "vitest": ">=5.1.0"
35
39
  },
36
40
  "devDependencies": {
37
- "@types/node": "^26.6.5",
38
- "husky": "^9.1.7",
39
- "tsdown": "^0.23.1",
40
- "typescript": "^7.0.2"
41
- },
42
- "dependencies": {
43
41
  "@biomejs/biome": "^2.6.7",
44
42
  "@teplostanski/biome-config": "^2.0.3",
43
+ "@types/node": "^26.6.6",
45
44
  "@vitest/coverage-v8": "^5.1.0",
46
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": {
47
52
  "badge-maker": "^6.0.0",
48
53
  "boxen": "^9.0.0",
49
54
  "citty": "^0.2.2",
@@ -51,7 +56,7 @@
51
56
  "picocolors": "^1.1.1",
52
57
  "unconfig": "^7.5.0"
53
58
  },
54
- "packageManager": "pnpm@12.18.0",
59
+ "packageManager": "pnpm@12.21.0",
55
60
  "bin": {
56
61
  "localbadge": "dist/localbadge.js"
57
62
  },
@@ -61,5 +66,9 @@
61
66
  "import": "./dist/index.js"
62
67
  },
63
68
  "./package.json": "./package.json"
69
+ },
70
+ "publishConfig": {
71
+ "access": "public",
72
+ "provenance": true
64
73
  }
65
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 `![coverage](.github/badges/coverage.svg)` 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.
@@ -1,118 +0,0 @@
1
- import pc from "picocolors";
2
- import { mkdir, writeFile } from "node:fs/promises";
3
- import path from "node:path";
4
- import { makeBadge } from "badge-maker";
5
- import boxen from "boxen";
6
- import { startVitest } from "vitest/node";
7
- import defu from "defu";
8
- import { loadConfig } from "unconfig";
9
- // src/coverage.ts
10
- const getCoveragePercent = async (cwd = process.cwd()) => {
11
- let linesPercent;
12
- await startVitest([], {
13
- root: cwd,
14
- run: true,
15
- reporters: ["dot", { onCoverage(coverage) {
16
- const pct = coverage.getCoverageSummary().lines.pct;
17
- if (typeof pct !== "number") throw new Error("Vitest coverage \"lines\": Unknown");
18
- linesPercent = pct;
19
- } }],
20
- silent: true,
21
- coverage: {
22
- enabled: true,
23
- clean: false,
24
- reporter: [],
25
- reportOnFailure: true
26
- }
27
- });
28
- if (linesPercent === void 0) throw new Error("Vitest did not provide coverage data");
29
- return linesPercent;
30
- };
31
- // src/badge.ts
32
- const escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
33
- const generateBadge = async (options) => {
34
- const pct = Math.floor(await getCoveragePercent(options.cwd));
35
- const svg = makeBadge({
36
- label: options.badge.label,
37
- message: `${pct}%`,
38
- color: options.badge.color,
39
- labelColor: options.badge.labelColor,
40
- style: options.badge.style
41
- });
42
- const dirParts = options.outputDir.split(/[\\/]+/).filter(Boolean);
43
- if (dirParts.includes("..")) throw new Error("\"outputDir\" must not contain \"..\"");
44
- const cwd = options.cwd || process.cwd();
45
- const outputDir = path.join(cwd, ...dirParts);
46
- const fileName = `${options.fileName}.svg`;
47
- await mkdir(outputDir, { recursive: true });
48
- await writeFile(path.join(outputDir, fileName), svg, "utf8");
49
- if (options.silentMode) return;
50
- const color = pc.createColors(!options.noColor);
51
- const markdownPath = [...dirParts, fileName].join("/");
52
- const safePath = escapeHtml(markdownPath);
53
- const safeLabel = escapeHtml(options.badge.label);
54
- const hint = `
55
- Coverage badge created at:
56
- ${color.cyan(color.underline(`${outputDir}/${fileName}`))}
57
-
58
- ${color.gray("Markdown:")}
59
- ${color.blue("!")}[${options.badge.label}]${color.cyan(`(${color.redBright(markdownPath)})`)}
60
-
61
- ${color.gray("HTML:")}
62
- <${color.blue("img")} ${color.red("src")}=${color.yellow(`"${safePath}"`)} ${color.red("alt")}=${color.yellow(`"${safeLabel}"`)}>
63
- `;
64
- console.log(boxen(hint, {
65
- borderStyle: "round",
66
- borderColor: "cyan",
67
- padding: {
68
- left: 2,
69
- right: 2
70
- }
71
- }));
72
- };
73
- // src/config.ts
74
- const defaultOptions = {
75
- cwd: "",
76
- silentMode: false,
77
- noColor: false,
78
- outputDir: "",
79
- fileName: "coverage-badge",
80
- badge: {
81
- label: "coverage",
82
- color: "green",
83
- labelColor: "gray",
84
- style: "flat"
85
- }
86
- };
87
- const defineConfig = (options) => {
88
- return options;
89
- };
90
- const resolveOptions = async (overrides = {}) => {
91
- const cwd = overrides.cwd || process.cwd();
92
- const { config } = await loadConfig({
93
- cwd,
94
- sources: [{
95
- files: ["localbadge.config"],
96
- extensions: [
97
- "ts",
98
- "mts",
99
- "cts",
100
- "js",
101
- "mjs",
102
- "cjs",
103
- "json"
104
- ]
105
- }, {
106
- files: [".localbadgerc"],
107
- extensions: [
108
- "json",
109
- "yaml",
110
- "yml",
111
- ""
112
- ]
113
- }],
114
- merge: true
115
- });
116
- return defu(overrides, config ?? {}, defaultOptions);
117
- };
118
- export { getCoveragePercent as a, generateBadge as i, defineConfig as n, resolveOptions as r, defaultOptions as t };