@sandro-sikic/maker 1.0.4 → 1.0.6
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/.github/workflows/publish.yml +3 -0
- package/__tests__/index.test.js +119 -0
- package/babel.config.js +10 -0
- package/index.d.ts +5 -2
- package/index.js +58 -5
- package/jest.config.cjs +8 -0
- package/package.json +9 -2
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const maker = require('../index');
|
|
2
|
+
|
|
3
|
+
describe('maker module', () => {
|
|
4
|
+
describe('init()', () => {
|
|
5
|
+
let origStdinIsTTY;
|
|
6
|
+
let origStdoutIsTTY;
|
|
7
|
+
let exitSpy;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
origStdinIsTTY = process.stdin.isTTY;
|
|
11
|
+
origStdoutIsTTY = process.stdout.isTTY;
|
|
12
|
+
exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
process.stdin.isTTY = origStdinIsTTY;
|
|
17
|
+
process.stdout.isTTY = origStdoutIsTTY;
|
|
18
|
+
exitSpy.mockRestore();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('exits with code 1 when not running in a TTY', () => {
|
|
22
|
+
process.stdin.isTTY = false;
|
|
23
|
+
process.stdout.isTTY = false;
|
|
24
|
+
maker.init();
|
|
25
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('does not exit when running in a TTY', () => {
|
|
29
|
+
process.stdin.isTTY = true;
|
|
30
|
+
process.stdout.isTTY = true;
|
|
31
|
+
maker.init();
|
|
32
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('run()', () => {
|
|
37
|
+
jest.setTimeout(10000);
|
|
38
|
+
|
|
39
|
+
test('resolves stdout and exit code on success', async () => {
|
|
40
|
+
const res = await maker.run('echo hello-from-run');
|
|
41
|
+
expect(res.code).toBe(0);
|
|
42
|
+
expect(res.isError).toBe(false);
|
|
43
|
+
expect(res.stdout).toMatch(/hello-from-run/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('reports non-zero exit code', async () => {
|
|
47
|
+
const res = await maker.run('node -e "process.exit(3)"');
|
|
48
|
+
expect(res.code).toBe(3);
|
|
49
|
+
expect(res.isError).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('onExit()', () => {
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
// ensure no stray listeners between tests
|
|
56
|
+
process.removeAllListeners('SIGINT');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('throws if callback is not a function', () => {
|
|
60
|
+
expect(() => maker.onExit(null)).toThrow(TypeError);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('runs callback on SIGINT and exits after awaiting it', async () => {
|
|
64
|
+
const cb = jest.fn(async () => {
|
|
65
|
+
// simulate async cleanup
|
|
66
|
+
await Promise.resolve();
|
|
67
|
+
});
|
|
68
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
|
69
|
+
|
|
70
|
+
const dispose = maker.onExit(cb);
|
|
71
|
+
|
|
72
|
+
process.emit('SIGINT');
|
|
73
|
+
// wait for async handler to complete
|
|
74
|
+
await new Promise((r) => setImmediate(r));
|
|
75
|
+
|
|
76
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
77
|
+
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
78
|
+
|
|
79
|
+
dispose();
|
|
80
|
+
exitSpy.mockRestore();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('disposer removes the SIGINT listener', async () => {
|
|
84
|
+
const cb = jest.fn();
|
|
85
|
+
const dispose = maker.onExit(cb);
|
|
86
|
+
dispose();
|
|
87
|
+
|
|
88
|
+
process.emit('SIGINT');
|
|
89
|
+
// allow any async microtasks to run
|
|
90
|
+
await new Promise((r) => setImmediate(r));
|
|
91
|
+
expect(cb).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('callback is invoked only once for repeated SIGINTs', async () => {
|
|
95
|
+
const cb = jest.fn();
|
|
96
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
|
97
|
+
const dispose = maker.onExit(cb);
|
|
98
|
+
|
|
99
|
+
process.emit('SIGINT');
|
|
100
|
+
process.emit('SIGINT');
|
|
101
|
+
await new Promise((r) => setImmediate(r));
|
|
102
|
+
|
|
103
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
104
|
+
expect(exitSpy).toHaveBeenCalledTimes(1);
|
|
105
|
+
|
|
106
|
+
dispose();
|
|
107
|
+
exitSpy.mockRestore();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('public API', () => {
|
|
112
|
+
test('exports only prompt and spinner for runtime helpers', () => {
|
|
113
|
+
expect(typeof maker.prompt).toBe('function');
|
|
114
|
+
expect(typeof maker.spinner).toBe('function');
|
|
115
|
+
expect(maker.getPrompt).toBeUndefined();
|
|
116
|
+
expect(maker.getSpinner).toBeUndefined();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
package/babel.config.js
ADDED
package/index.d.ts
CHANGED
|
@@ -17,8 +17,11 @@ export function run(
|
|
|
17
17
|
export function onExit(cb: () => void | Promise<void>): () => void;
|
|
18
18
|
|
|
19
19
|
// runtime-forwarded objects (exported as values in JS)
|
|
20
|
-
export const prompt:
|
|
21
|
-
export const spinner:
|
|
20
|
+
export const prompt: <T = any>(...args: any[]) => Promise<T | null>;
|
|
21
|
+
export const spinner: (
|
|
22
|
+
text?: string,
|
|
23
|
+
options?: any,
|
|
24
|
+
) => Promise<import('ora').Ora | null>;
|
|
22
25
|
|
|
23
26
|
// type forwarding for downstream consumers
|
|
24
27
|
export type { Ora } from 'ora';
|
package/index.js
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
|
-
const inquirer = require('@inquirer/prompts');
|
|
2
1
|
const { spawn } = require('child_process');
|
|
3
2
|
const path = require('path');
|
|
4
|
-
|
|
3
|
+
|
|
4
|
+
// small helper to lazily load (CJS-first, ESM dynamic import fallback)
|
|
5
|
+
function makeLazyLoader(moduleId) {
|
|
6
|
+
let _cached = null;
|
|
7
|
+
return async function load() {
|
|
8
|
+
if (_cached) return _cached;
|
|
9
|
+
try {
|
|
10
|
+
/* eslint-disable global-require, import/no-dynamic-require */
|
|
11
|
+
const mod = require(moduleId);
|
|
12
|
+
_cached = mod && (mod.default || mod);
|
|
13
|
+
return _cached;
|
|
14
|
+
} catch (err) {
|
|
15
|
+
try {
|
|
16
|
+
const ns = await import(moduleId);
|
|
17
|
+
_cached = ns && (ns.default || ns);
|
|
18
|
+
return _cached;
|
|
19
|
+
} catch (e) {
|
|
20
|
+
// unable to load in this environment — return null so callers can continue
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const loadPrompts = makeLazyLoader('@inquirer/prompts');
|
|
28
|
+
const loadOra = makeLazyLoader('ora');
|
|
5
29
|
|
|
6
30
|
function init() {
|
|
7
31
|
// Ensure we're running in an interactive terminal
|
|
@@ -98,6 +122,9 @@ function run(command, opts = {}) {
|
|
|
98
122
|
* Register a callback to run when the process receives SIGINT (Ctrl+C).
|
|
99
123
|
* - Accepts a function (may be async).
|
|
100
124
|
* - Returns a disposer function that removes the listener.
|
|
125
|
+
*
|
|
126
|
+
* Note: spinner is started asynchronously (ora is lazy-loaded); onExit itself
|
|
127
|
+
* remains synchronous so callers can register the handler immediately.
|
|
101
128
|
*/
|
|
102
129
|
function onExit(cb) {
|
|
103
130
|
if (typeof cb !== 'function') {
|
|
@@ -105,6 +132,17 @@ function onExit(cb) {
|
|
|
105
132
|
}
|
|
106
133
|
|
|
107
134
|
let called = false;
|
|
135
|
+
let exiting = null;
|
|
136
|
+
|
|
137
|
+
// start spinner asynchronously (best-effort; ignore load errors)
|
|
138
|
+
loadOra().then((o) => {
|
|
139
|
+
try {
|
|
140
|
+
exiting = o('Gracefully shutting down...').start();
|
|
141
|
+
} catch (e) {
|
|
142
|
+
/* ignore */
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
108
146
|
const handler = async () => {
|
|
109
147
|
if (called) return;
|
|
110
148
|
called = true;
|
|
@@ -113,7 +151,9 @@ function onExit(cb) {
|
|
|
113
151
|
} catch (err) {
|
|
114
152
|
console.error('onExit callback error:', err);
|
|
115
153
|
} finally {
|
|
116
|
-
|
|
154
|
+
if (exiting && typeof exiting.stop === 'function') {
|
|
155
|
+
exiting.stop();
|
|
156
|
+
}
|
|
117
157
|
process.exit(0);
|
|
118
158
|
}
|
|
119
159
|
};
|
|
@@ -124,10 +164,23 @@ function onExit(cb) {
|
|
|
124
164
|
return () => process.off('SIGINT', handler);
|
|
125
165
|
}
|
|
126
166
|
|
|
167
|
+
// `prompt` is an async wrapper around the ESM-only `@inquirer/prompts` package.
|
|
168
|
+
// `prompt(...args)` forwards the call to the underlying module and returns its result.
|
|
169
|
+
async function prompt(...args) {
|
|
170
|
+
const p = await loadPrompts();
|
|
171
|
+
if (!p) return null; // prompts unavailable in this environment
|
|
172
|
+
return p(...args);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function spinner(...args) {
|
|
176
|
+
const s = await loadOra();
|
|
177
|
+
return s(...args);
|
|
178
|
+
}
|
|
179
|
+
|
|
127
180
|
module.exports = {
|
|
128
181
|
init,
|
|
129
182
|
run,
|
|
130
183
|
onExit,
|
|
131
|
-
prompt
|
|
132
|
-
spinner
|
|
184
|
+
prompt,
|
|
185
|
+
spinner,
|
|
133
186
|
};
|
package/jest.config.cjs
ADDED
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sandro-sikic/maker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "A library to create dev cli",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "jest"
|
|
9
|
+
},
|
|
7
10
|
"author": "sandro-sikic",
|
|
8
11
|
"repository": {
|
|
9
12
|
"type": "git",
|
|
@@ -17,6 +20,10 @@
|
|
|
17
20
|
"devDependencies": {
|
|
18
21
|
"archiver": "^5.3.1",
|
|
19
22
|
"pkg": "^5.8.1",
|
|
20
|
-
"rimraf": "^5.0.1"
|
|
23
|
+
"rimraf": "^5.0.1",
|
|
24
|
+
"jest": "^29.7.0",
|
|
25
|
+
"babel-jest": "^29.7.0",
|
|
26
|
+
"@babel/core": "^7.23.0",
|
|
27
|
+
"@babel/preset-env": "^7.23.0"
|
|
21
28
|
}
|
|
22
29
|
}
|