wukong-progress 0.1.1

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) [2025] [杨琼]
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 ADDED
@@ -0,0 +1,219 @@
1
+ # wukong-progress
2
+
3
+ 🎨 A Node.js / ESM style CLI progress bar library that supports:
4
+
5
+ - Single / multiple progress bars
6
+ - Group / Stage / prefix
7
+ - Concurrent task API (wrap async functions)
8
+ - Automatic terminal width adaptation
9
+ - Optional colored output (chalk, auto fallback)
10
+ - JSON fallback (for non-TTY environments)
11
+ - Full ESM + Node.js 18+ compatible
12
+ - Works on Windows / Linux / macOS
13
+
14
+ ---
15
+ ## English | [简体中文](./README.zh-CN.md)
16
+ ---
17
+ ## 🚀 Installation
18
+
19
+ ```bash
20
+ yarn add wukong-progress chalk
21
+ # or
22
+ npm install wukong-progress chalk
23
+ ```
24
+
25
+ ---
26
+
27
+ ## ⚡️ Basic Usage
28
+
29
+ ### Single Progress Bar
30
+
31
+ ```js
32
+ import chalk from "chalk";
33
+ import { createMultiBar } from "wukong-progress";
34
+
35
+ const mb = createMultiBar();
36
+ const bar = mb.create(100, {
37
+ prefix: chalk.cyan("Build"),
38
+ format: "Build [:bar] :percent :current/:total",
39
+ });
40
+
41
+ async function run() {
42
+ for (let i = 0; i <= 100; i++) {
43
+ await new Promise((r) => setTimeout(r, 20));
44
+ bar.tick();
45
+ }
46
+ mb.stop();
47
+ console.log(chalk.green("\nDone!\n"));
48
+ }
49
+
50
+ run();
51
+ ```
52
+
53
+ ### Multiple Progress Bars
54
+
55
+ ```js
56
+ import chalk from "chalk";
57
+ import { createMultiBar } from "wukong-progress";
58
+
59
+ const mb = createMultiBar();
60
+ const build = mb.create(100, {
61
+ prefix: chalk.blue("Build"),
62
+ format: "Build [:bar] :percent",
63
+ });
64
+ const test = mb.create(50, {
65
+ prefix: chalk.magenta("Test"),
66
+ format: "Test [:bar] :percent",
67
+ });
68
+
69
+ async function run() {
70
+ for (let i = 0; i <= 100; i++) {
71
+ await new Promise((r) => setTimeout(r, 15));
72
+ if (i <= 50) test.tick();
73
+ build.tick();
74
+ }
75
+ mb.stop();
76
+ console.log(chalk.green("\nAll tasks done!\n"));
77
+ }
78
+
79
+ run();
80
+ ```
81
+
82
+ ---
83
+
84
+ ### Group / Stage / prefix
85
+
86
+ ```js
87
+ import chalk from "chalk";
88
+ import { createMultiBar } from "wukong-progress";
89
+
90
+ const mb = createMultiBar();
91
+ const buildGroup = mb.group("Build Group");
92
+ buildGroup.create(50, {
93
+ prefix: chalk.blue("Compile"),
94
+ format: "Compile [:bar] :percent",
95
+ });
96
+ buildGroup.create(30, {
97
+ prefix: chalk.cyan("Bundle"),
98
+ format: "Bundle [:bar] :percent",
99
+ });
100
+
101
+ const testGroup = mb.group("Test Group");
102
+ testGroup.create(20, {
103
+ prefix: chalk.magenta("Unit"),
104
+ format: "Unit [:bar] :percent",
105
+ });
106
+ testGroup.create(10, {
107
+ prefix: chalk.yellow("E2E"),
108
+ format: "E2E [:bar] :percent",
109
+ });
110
+
111
+ async function run() {
112
+ const allTasks = [...buildGroup.bars, ...testGroup.bars];
113
+
114
+ for (let i = 0; i < 50; i++) {
115
+ await new Promise((r) => setTimeout(r, 20));
116
+ allTasks.forEach((bar) => {
117
+ if (!bar.state.complete) bar.tick();
118
+ });
119
+ }
120
+
121
+ mb.stop();
122
+ console.log(chalk.green("\nGroups completed!\n"));
123
+ }
124
+
125
+ run();
126
+ ```
127
+
128
+ ---
129
+
130
+ ### JSON Fallback (non-TTY / CI)
131
+
132
+ ```js
133
+ import { Writable } from "node:stream";
134
+ import { createMultiBar } from "wukong-progress";
135
+
136
+ let out = "";
137
+ const stream = new Writable({
138
+ write(chunk, _, cb) {
139
+ out += chunk;
140
+ cb();
141
+ },
142
+ });
143
+
144
+ const mb = createMultiBar({ stream, json: true });
145
+ const bar = mb.create(5, { prefix: "JSON" });
146
+
147
+ async function run() {
148
+ for (let i = 0; i <= 5; i++) {
149
+ await new Promise((r) => setTimeout(r, 20));
150
+ bar.tick();
151
+ }
152
+ mb.stop();
153
+
154
+ console.log("JSON fallback output:");
155
+ console.log(out);
156
+ }
157
+
158
+ run();
159
+ ```
160
+
161
+ ---
162
+
163
+ ## 🎨 Colored Output (optional)
164
+
165
+ - Use `chalk` to color prefixes and bar output
166
+ - Auto fallback to plain text if chalk is not installed
167
+ - Example:
168
+
169
+ ```js
170
+ prefix: chalk.green('Build'),
171
+ format: chalk.green('Build [:bar] :percent')
172
+ ```
173
+
174
+ ---
175
+
176
+ ## 📂 Examples Folder
177
+
178
+ ```bash
179
+ node examples/index.mjs
180
+ ```
181
+
182
+ - Interactive selection of examples:
183
+
184
+ - Single Bar
185
+ - Multi Bar
186
+ - Group / Stage
187
+ - JSON Fallback
188
+
189
+ - Fully compatible with Windows / Linux / macOS
190
+ - All examples use async/await + chalk colored output
191
+
192
+ ---
193
+
194
+ ## ⚡️ Testing
195
+
196
+ ```bash
197
+ # Node.js native test
198
+ yarn test:node
199
+
200
+ # Vitest snapshot test
201
+ yarn test:vitest
202
+
203
+ # Run all tests
204
+ yarn test
205
+ ```
206
+
207
+ - ✅ Node:test for progress logic
208
+ - ✅ Vitest snapshot for rendering stability
209
+ - ✅ Supports mock TTY / ANSI strip / JSON fallback
210
+
211
+ ---
212
+
213
+ ## 💻 Use Cases
214
+
215
+ - CLI tools
216
+ - Automation scripts
217
+ - GitHub Actions / CI
218
+ - Concurrent task visualization
219
+ - Progress visualization + JSON logging for analytics
@@ -0,0 +1,217 @@
1
+
2
+ # wukong-progress
3
+
4
+ 🎨 Node.js / ESM 风格的 CLI 进度条库,支持:
5
+
6
+ - 单条 / 多条进度条
7
+ - Group / Stage / prefix
8
+ - 并发任务 API(wrap async fn)
9
+ - 自动适配终端宽度
10
+ - 彩色渲染(chalk,可选,自动降级)
11
+ - JSON fallback(非 TTY)
12
+ - 完全 ESM + Node.js 18+ 兼容
13
+ - 可在 Windows / Linux / macOS 使用
14
+
15
+ ---
16
+ ## 中文 | [English](./README.md)
17
+ ---
18
+
19
+ ## 🚀 安装
20
+
21
+ ```bash
22
+ yarn add wukong-progress chalk
23
+ # or
24
+ npm install wukong-progress chalk
25
+ ```
26
+
27
+ ---
28
+
29
+ ## ⚡️ 基本用法
30
+
31
+ ### 单条进度条
32
+
33
+ ```js
34
+ import chalk from 'chalk'
35
+ import { createMultiBar } from 'wukong-progress'
36
+
37
+ const mb = createMultiBar()
38
+ const bar = mb.create(100, { prefix: chalk.cyan('Build'), format: 'Build [:bar] :percent :current/:total' })
39
+
40
+ async function run() {
41
+ for (let i = 0; i <= 100; i++) {
42
+ await new Promise(r => setTimeout(r, 20))
43
+ bar.tick()
44
+ }
45
+ mb.stop()
46
+ console.log(chalk.green('\nDone!\n'))
47
+ }
48
+
49
+ run()
50
+ ```
51
+
52
+ ### 多条进度条
53
+
54
+ ```js
55
+ import chalk from 'chalk'
56
+ import { createMultiBar } from 'wukong-progress'
57
+
58
+ const mb = createMultiBar()
59
+ const build = mb.create(100, { prefix: chalk.blue('Build'), format: 'Build [:bar] :percent' })
60
+ const test = mb.create(50, { prefix: chalk.magenta('Test'), format: 'Test [:bar] :percent' })
61
+
62
+ async function run() {
63
+ for (let i = 0; i <= 100; i++) {
64
+ await new Promise(r => setTimeout(r, 15))
65
+ if (i <= 50) test.tick()
66
+ build.tick()
67
+ }
68
+ mb.stop()
69
+ console.log(chalk.green('\nAll tasks done!\n'))
70
+ }
71
+
72
+ run()
73
+ ```
74
+
75
+ ---
76
+
77
+ ### Group / Stage / prefix
78
+
79
+ ```js
80
+ import chalk from 'chalk'
81
+ import { createMultiBar } from 'wukong-progress'
82
+
83
+ const mb = createMultiBar()
84
+ const buildGroup = mb.group('Build Group')
85
+ buildGroup.create(50, { prefix: chalk.blue('Compile'), format: 'Compile [:bar] :percent' })
86
+ buildGroup.create(30, { prefix: chalk.cyan('Bundle'), format: 'Bundle [:bar] :percent' })
87
+
88
+ const testGroup = mb.group('Test Group')
89
+ testGroup.create(20, { prefix: chalk.magenta('Unit'), format: 'Unit [:bar] :percent' })
90
+ testGroup.create(10, { prefix: chalk.yellow('E2E'), format: 'E2E [:bar] :percent' })
91
+
92
+ async function run() {
93
+ const allTasks = [...buildGroup.bars, ...testGroup.bars]
94
+
95
+ for (let i = 0; i < 50; i++) {
96
+ await new Promise(r => setTimeout(r, 20))
97
+ allTasks.forEach(bar => {
98
+ if (!bar.state.complete) bar.tick()
99
+ })
100
+ }
101
+
102
+ mb.stop()
103
+ console.log(chalk.green('\nGroups completed!\n'))
104
+ }
105
+
106
+ run()
107
+ ```
108
+
109
+ ---
110
+
111
+ ### JSON fallback(非 TTY 或 CI)
112
+
113
+ ```js
114
+ import { Writable } from 'node:stream'
115
+ import { createMultiBar } from 'wukong-progress'
116
+
117
+ let out = ''
118
+ const stream = new Writable({
119
+ write(chunk, _, cb) {
120
+ out += chunk
121
+ cb()
122
+ }
123
+ })
124
+
125
+ const mb = createMultiBar({ stream, json: true })
126
+ const bar = mb.create(5, { prefix: 'JSON' })
127
+
128
+ async function run() {
129
+ for (let i = 0; i <= 5; i++) {
130
+ await new Promise(r => setTimeout(r, 20))
131
+ bar.tick()
132
+ }
133
+ mb.stop()
134
+
135
+ console.log('JSON fallback output:')
136
+ console.log(out)
137
+ }
138
+
139
+ run()
140
+ ```
141
+
142
+ ---
143
+
144
+ ## 🎨 彩色渲染(可选)
145
+
146
+ - 使用 `chalk` 可以给 prefix、bar 和提示上色
147
+
148
+ - 不依赖彩色也能降级到普通文本
149
+
150
+ - 示例:
151
+
152
+
153
+ ```js
154
+ prefix: chalk.green('Build'),
155
+ format: chalk.green('Build [:bar] :percent')
156
+ ```
157
+
158
+ ---
159
+
160
+ ## 📂 Examples 文件夹
161
+
162
+ ```bash
163
+ node examples/index.mjs
164
+ ```
165
+
166
+ - 交互式选择运行示例:
167
+
168
+ - Single Bar
169
+
170
+ - Multi Bar
171
+
172
+ - Group / Stage
173
+
174
+ - JSON Fallback
175
+
176
+ - Windows / Linux / macOS 全平台兼容
177
+
178
+ - 所有示例都用 async/await + chalk 彩色渲染
179
+
180
+
181
+ ---
182
+
183
+ ## ⚡️ 测试
184
+
185
+ ```bash
186
+ # Node.js 原生测试
187
+ yarn test:node
188
+
189
+ # Vitest snapshot 测试
190
+ yarn test:vitest
191
+
192
+ # 全部测试
193
+ yarn test
194
+ ```
195
+
196
+ - ✅ Node:test 测试进度条逻辑
197
+
198
+ - ✅ Vitest snapshot 测试渲染稳定性
199
+
200
+ - ✅ 支持 mock TTY / ANSI strip / JSON fallback
201
+
202
+
203
+ ---
204
+
205
+ ## 💻 适用场景
206
+
207
+ - CLI 工具
208
+
209
+ - 自动化脚本
210
+
211
+ - GitHub Actions / CI
212
+
213
+ - 多任务并发显示
214
+
215
+ - 可视化进度 / JSON 输出结合日志分析
216
+
217
+
package/dist/index.mjs ADDED
@@ -0,0 +1,159 @@
1
+ // src/color.mjs
2
+ var chalk = null;
3
+ try {
4
+ const mod = await import("chalk");
5
+ chalk = mod.default;
6
+ } catch {
7
+ }
8
+ var color = {
9
+ bar: (s) => chalk ? chalk.cyan(s) : s,
10
+ dim: (s) => chalk ? chalk.dim(s) : s,
11
+ percent: (s) => chalk ? chalk.green(s) : s
12
+ };
13
+
14
+ // src/utils.mjs
15
+ var clamp = (n, min, max) => Math.min(Math.max(n, min), max);
16
+ var formatTime = (sec) => {
17
+ if (!isFinite(sec)) return "--";
18
+ if (sec < 60) return `${sec.toFixed(1)}s`;
19
+ const m = Math.floor(sec / 60);
20
+ const s = Math.floor(sec % 60);
21
+ return `${m}m${s}s`;
22
+ };
23
+ var termWidth = (stream) => stream.columns || 80;
24
+
25
+ // src/bar.mjs
26
+ function createBar(ctx, opts) {
27
+ const state = {
28
+ total: opts.total,
29
+ current: 0,
30
+ start: Date.now(),
31
+ format: opts.format,
32
+ minWidth: opts.minWidth ?? 10,
33
+ prefix: opts.prefix ?? "",
34
+ hideOnComplete: opts.hideOnComplete ?? false,
35
+ complete: false
36
+ };
37
+ function update(n) {
38
+ state.current = clamp(n, 0, state.total);
39
+ if (state.current >= state.total) {
40
+ state.complete = true;
41
+ }
42
+ ctx.render();
43
+ }
44
+ function tick(n = 1) {
45
+ update(state.current + n);
46
+ }
47
+ function render(width) {
48
+ const elapsed = (Date.now() - state.start) / 1e3;
49
+ const percent = state.current / state.total;
50
+ const staticLen = state.format.replace(":bar", "").replace(":percent", "100%").replace(":current", state.total).replace(":total", state.total).replace(":eta", "00s").length;
51
+ const barWidth = Math.max(
52
+ state.minWidth,
53
+ width - staticLen - state.prefix.length - 4
54
+ );
55
+ const filled = Math.round(barWidth * percent);
56
+ const bar = color.bar("\u2588".repeat(filled)) + color.dim("\u2591".repeat(barWidth - filled));
57
+ const rate = state.current / elapsed;
58
+ const eta = rate ? (state.total - state.current) / rate : Infinity;
59
+ return (state.prefix ? `${state.prefix} ` : "") + state.format.replace(":bar", bar).replace(":percent", color.percent(`${Math.round(percent * 100)}%`)).replace(":current", state.current).replace(":total", state.total).replace(":elapsed", formatTime(elapsed)).replace(":eta", formatTime(eta));
60
+ }
61
+ return {
62
+ state,
63
+ tick,
64
+ update,
65
+ render
66
+ };
67
+ }
68
+
69
+ // src/group.mjs
70
+ function createGroup(ctx, name) {
71
+ const bars = [];
72
+ function create(total, opts = {}) {
73
+ const bar = ctx._createBar({
74
+ ...opts,
75
+ prefix: opts.prefix ?? name,
76
+ total
77
+ });
78
+ bars.push(bar);
79
+ return bar;
80
+ }
81
+ return { create, bars };
82
+ }
83
+
84
+ // src/json.mjs
85
+ function renderJSON(stream, bars) {
86
+ const payload = {
87
+ type: "progress",
88
+ bars: bars.map((b) => ({
89
+ name: b.state.prefix,
90
+ current: b.state.current,
91
+ total: b.state.total,
92
+ percent: Math.round(
93
+ b.state.current / b.state.total * 100
94
+ ),
95
+ complete: b.state.complete
96
+ }))
97
+ };
98
+ stream.write(`${JSON.stringify(payload)}
99
+ `);
100
+ }
101
+
102
+ // src/multibar.mjs
103
+ function createMultiBar({
104
+ stream = process.stderr,
105
+ clearOnComplete = false,
106
+ json = false
107
+ } = {}) {
108
+ const bars = [];
109
+ let lines = 0;
110
+ let active = true;
111
+ const isJSON = json || !stream.isTTY;
112
+ let ctx = null;
113
+ function group(name) {
114
+ return createGroup(ctx, name);
115
+ }
116
+ function render() {
117
+ if (!active) return;
118
+ if (isJSON) {
119
+ renderJSON(stream, bars);
120
+ return;
121
+ }
122
+ if (lines > 0) {
123
+ stream.write(`\x1B[${lines}A`);
124
+ }
125
+ const width = termWidth(stream);
126
+ const visible = bars.filter(
127
+ (b) => !(b.state.complete && b.state.hideOnComplete)
128
+ );
129
+ const output = visible.map((b) => b.render(width));
130
+ stream.write(`${output.join("\n")}
131
+ `);
132
+ lines = output.length;
133
+ }
134
+ function stop() {
135
+ active = false;
136
+ if (!isJSON && clearOnComplete) {
137
+ stream.write(`\x1B[${lines}A\x1B[J`);
138
+ }
139
+ }
140
+ function _createBar(opts) {
141
+ const bar = createBar(ctx, opts);
142
+ bars.push(bar);
143
+ render();
144
+ return bar;
145
+ }
146
+ ctx = {
147
+ render,
148
+ _createBar
149
+ };
150
+ return {
151
+ create: (total, opts = {}) => _createBar({ ...opts, total }),
152
+ group,
153
+ stop
154
+ };
155
+ }
156
+ export {
157
+ createMultiBar
158
+ };
159
+ //# sourceMappingURL=index.mjs.map
package/package.json ADDED
@@ -0,0 +1,104 @@
1
+ {
2
+ "name": "wukong-progress",
3
+ "version": "0.1.1",
4
+ "description": "Lightweight ESM CLI progress bar for Node.js with multiple bars, groups, stages and JSON fallback",
5
+ "keywords": [
6
+ "cli",
7
+ "progress",
8
+ "progress-bar",
9
+ "esm",
10
+ "node",
11
+ "terminal",
12
+ "multibar",
13
+ "async",
14
+ "concurrent",
15
+ "cli-tool",
16
+ "json-fallback",
17
+ "wukong"
18
+ ],
19
+ "homepage": "https://tomatobybike.github.io/wukong-progress/",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/tomatobybike/wukong-progress"
23
+ },
24
+ "license": "MIT",
25
+ "author": "Tom <tomatobybike@gmail.com>",
26
+ "type": "module",
27
+ "exports": {
28
+ ".": "./src/index.mjs"
29
+ },
30
+ "main": "dist/index.mjs",
31
+ "module": "dist/index.mjs",
32
+ "bin": {
33
+ "wukong-progress": "dist/index.mjs"
34
+ },
35
+ "files": [
36
+ "bin/",
37
+ "src/",
38
+ "README.md",
39
+ "README.zh.md"
40
+ ],
41
+ "scripts": {
42
+ "build": "node scripts/build.js",
43
+ "check:npm:login": "node scripts/check-npm-login.mjs",
44
+ "example": "node examples/index.mjs",
45
+ "format": "prettier --write \"src/**/*.{js,mjs}\"",
46
+ "lint": "eslint src --ext .js,.mjs src",
47
+ "lint:fix": "eslint src --ext .js,.mjs --fix",
48
+ "pack": "npm pack",
49
+ "prepare": "husky install",
50
+ "prepublishOnly": "yarn lint && yarn build",
51
+ "prettierall": "npx prettier --write 'src/**/*.{js,jsx}'",
52
+ "push:tags": "git push origin main --follow-tags",
53
+ "prerelease": "yarn lint",
54
+ "release": "yarn check:npm:login && yarn release:patch && npm publish && yarn push:tags",
55
+ "release:major": "yarn lint && standard-version --release-as major",
56
+ "release:minor": "yarn lint && standard-version --release-as minor",
57
+ "release:patch": "yarn lint && standard-version --release-as patch",
58
+ "release:rollback": "git reset --hard HEAD~1 && git tag -d $(git tag --points-at HEAD~1)",
59
+ "report": "wukong-progress report ./profile.json -o my-report.html --open",
60
+ "sort": "sort-package-json",
61
+ "test": "npm run test:node && npm run test:vitest",
62
+ "test:node": "node --test test/node/render.node.test.mjs",
63
+ "test:vitest": "vitest run"
64
+ },
65
+ "husky": {
66
+ "hooks": {
67
+ "pre-commit": "lint-staged"
68
+ }
69
+ },
70
+ "lint-staged": {
71
+ "package.json": [
72
+ "sort-package-json"
73
+ ],
74
+ "src/**/*.js": [
75
+ "prettier --write",
76
+ "eslint --fix"
77
+ ]
78
+ },
79
+ "dependencies": {
80
+ "chalk": "^5.3.0"
81
+ },
82
+ "devDependencies": {
83
+ "@trivago/prettier-plugin-sort-imports": "6.0.0",
84
+ "eslint": "8.57.1",
85
+ "eslint-config-airbnb-base": "15.0.0",
86
+ "eslint-config-prettier": "10.1.8",
87
+ "eslint-import-resolver-alias": "1.1.2",
88
+ "eslint-plugin-import": "2.32.0",
89
+ "eslint-plugin-prettier": "5.5.4",
90
+ "eslint-plugin-simple-import-sort": "12.1.1",
91
+ "husky": "9.1.7",
92
+ "lint-staged": "16.2.7",
93
+ "prettier": "3.7.4",
94
+ "prettier-plugin-packagejson": "2.5.20",
95
+ "sort-package-json": "3.6.0",
96
+ "standard-version": "9.5.0",
97
+ "strip-ansi": "^7.1.2",
98
+ "vitest": "^1.3.0"
99
+ },
100
+ "packageManager": "yarn@1.22.22",
101
+ "engines": {
102
+ "node": ">=18"
103
+ }
104
+ }
package/src/bar.mjs ADDED
@@ -0,0 +1,67 @@
1
+ import { color } from './color.mjs'
2
+ import { clamp, formatTime } from './utils.mjs'
3
+
4
+ export function createBar(ctx, opts) {
5
+ const state = {
6
+ total: opts.total,
7
+ current: 0,
8
+ start: Date.now(),
9
+ format: opts.format,
10
+ minWidth: opts.minWidth ?? 10,
11
+ prefix: opts.prefix ?? '',
12
+ hideOnComplete: opts.hideOnComplete ?? false,
13
+ complete: false
14
+ }
15
+ function update(n) {
16
+ state.current = clamp(n, 0, state.total)
17
+ if (state.current >= state.total) {
18
+ state.complete = true
19
+ }
20
+ ctx.render()
21
+ }
22
+ function tick(n = 1) {
23
+ update(state.current + n)
24
+ }
25
+
26
+ function render(width) {
27
+ const elapsed = (Date.now() - state.start) / 1000
28
+ const percent = state.current / state.total
29
+
30
+ const staticLen = state.format
31
+ .replace(':bar', '')
32
+ .replace(':percent', '100%')
33
+ .replace(':current', state.total)
34
+ .replace(':total', state.total)
35
+ .replace(':eta', '00s').length
36
+
37
+ const barWidth = Math.max(
38
+ state.minWidth,
39
+ width - staticLen - state.prefix.length - 4
40
+ )
41
+
42
+ const filled = Math.round(barWidth * percent)
43
+ const bar =
44
+ color.bar('█'.repeat(filled)) + color.dim('░'.repeat(barWidth - filled))
45
+
46
+ const rate = state.current / elapsed
47
+ const eta = rate ? (state.total - state.current) / rate : Infinity
48
+
49
+ return (
50
+ (state.prefix ? `${state.prefix} ` : '') +
51
+ state.format
52
+ .replace(':bar', bar)
53
+ .replace(':percent', color.percent(`${Math.round(percent * 100)}%`))
54
+ .replace(':current', state.current)
55
+ .replace(':total', state.total)
56
+ .replace(':elapsed', formatTime(elapsed))
57
+ .replace(':eta', formatTime(eta))
58
+ )
59
+ }
60
+
61
+ return {
62
+ state,
63
+ tick,
64
+ update,
65
+ render
66
+ }
67
+ }
package/src/color.mjs ADDED
@@ -0,0 +1,12 @@
1
+ let chalk = null
2
+
3
+ try {
4
+ const mod = await import('chalk')
5
+ chalk = mod.default
6
+ } catch { /* empty */ }
7
+
8
+ export const color = {
9
+ bar: s => (chalk ? chalk.cyan(s) : s),
10
+ dim: s => (chalk ? chalk.dim(s) : s),
11
+ percent: s => (chalk ? chalk.green(s) : s),
12
+ }
package/src/group.mjs ADDED
@@ -0,0 +1,15 @@
1
+ export function createGroup(ctx, name) {
2
+ const bars = []
3
+
4
+ function create(total, opts = {}) {
5
+ const bar = ctx._createBar({
6
+ ...opts,
7
+ prefix: opts.prefix ?? name,
8
+ total,
9
+ })
10
+ bars.push(bar)
11
+ return bar
12
+ }
13
+
14
+ return { create, bars }
15
+ }
package/src/index.mjs ADDED
@@ -0,0 +1 @@
1
+ export { createMultiBar } from './multibar.mjs'
package/src/json.mjs ADDED
@@ -0,0 +1,16 @@
1
+ export function renderJSON(stream, bars) {
2
+ const payload = {
3
+ type: 'progress',
4
+ bars: bars.map(b => ({
5
+ name: b.state.prefix,
6
+ current: b.state.current,
7
+ total: b.state.total,
8
+ percent: Math.round(
9
+ (b.state.current / b.state.total) * 100
10
+ ),
11
+ complete: b.state.complete,
12
+ })),
13
+ }
14
+
15
+ stream.write(`${JSON.stringify(payload) }\n`)
16
+ }
@@ -0,0 +1,69 @@
1
+ import { createBar } from './bar.mjs'
2
+ import { createGroup } from './group.mjs'
3
+ import { renderJSON } from './json.mjs'
4
+ import { termWidth } from './utils.mjs'
5
+
6
+ export function createMultiBar({
7
+ stream = process.stderr,
8
+ clearOnComplete = false,
9
+ json = false
10
+ } = {}) {
11
+ const bars = []
12
+ let lines = 0
13
+ let active = true
14
+
15
+ const isJSON = json || !stream.isTTY
16
+
17
+ let ctx = null
18
+ function group(name) {
19
+ return createGroup(ctx, name)
20
+ }
21
+
22
+ function render() {
23
+ if (!active) return
24
+
25
+ if (isJSON) {
26
+ renderJSON(stream, bars)
27
+ return
28
+ }
29
+
30
+ if (lines > 0) {
31
+ stream.write(`\x1b[${lines}A`)
32
+ }
33
+
34
+ const width = termWidth(stream)
35
+ const visible = bars.filter(
36
+ (b) => !(b.state.complete && b.state.hideOnComplete)
37
+ )
38
+
39
+ const output = visible.map((b) => b.render(width))
40
+
41
+ stream.write(`${output.join('\n')}\n`)
42
+ lines = output.length
43
+ }
44
+
45
+ function stop() {
46
+ active = false
47
+ if (!isJSON && clearOnComplete) {
48
+ stream.write(`\x1b[${lines}A\x1b[J`)
49
+ }
50
+ }
51
+
52
+ function _createBar(opts) {
53
+ const bar = createBar(ctx, opts)
54
+ bars.push(bar)
55
+ render()
56
+ return bar
57
+ }
58
+
59
+ ctx = {
60
+ render,
61
+ _createBar
62
+ }
63
+
64
+ return {
65
+ create: (total, opts = {}) => _createBar({ ...opts, total }),
66
+ group,
67
+ stop
68
+ }
69
+ }
package/src/utils.mjs ADDED
@@ -0,0 +1,14 @@
1
+ export const clamp = (n, min, max) =>
2
+ Math.min(Math.max(n, min), max)
3
+
4
+ export const formatTime = sec => {
5
+ // eslint-disable-next-line no-restricted-globals
6
+ if (!isFinite(sec)) return '--'
7
+ if (sec < 60) return `${sec.toFixed(1)}s`
8
+ const m = Math.floor(sec / 60)
9
+ const s = Math.floor(sec % 60)
10
+ return `${m}m${s}s`
11
+ }
12
+
13
+ export const termWidth = stream =>
14
+ stream.columns || 80