@sandro-sikic/maker 1.0.5 → 1.0.7
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 +0 -2
- package/index.js +58 -8
- 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
|
@@ -16,10 +16,8 @@ export function run(
|
|
|
16
16
|
|
|
17
17
|
export function onExit(cb: () => void | Promise<void>): () => void;
|
|
18
18
|
|
|
19
|
-
// runtime-forwarded objects (exported as values in JS)
|
|
20
19
|
export const prompt: typeof import('@inquirer/prompts');
|
|
21
20
|
export const spinner: typeof import('ora');
|
|
22
21
|
|
|
23
|
-
// type forwarding for downstream consumers
|
|
24
22
|
export type { Ora } from 'ora';
|
|
25
23
|
export * from '@inquirer/prompts';
|
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,15 +122,27 @@ 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
|
-
exiting = ora('Gracefully shutting down...').start();
|
|
104
|
-
|
|
105
130
|
if (typeof cb !== 'function') {
|
|
106
131
|
throw new TypeError('onExit requires a callback function');
|
|
107
132
|
}
|
|
108
133
|
|
|
109
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
|
+
|
|
110
146
|
const handler = async () => {
|
|
111
147
|
if (called) return;
|
|
112
148
|
called = true;
|
|
@@ -115,8 +151,9 @@ function onExit(cb) {
|
|
|
115
151
|
} catch (err) {
|
|
116
152
|
console.error('onExit callback error:', err);
|
|
117
153
|
} finally {
|
|
118
|
-
|
|
119
|
-
|
|
154
|
+
if (exiting && typeof exiting.stop === 'function') {
|
|
155
|
+
exiting.stop();
|
|
156
|
+
}
|
|
120
157
|
process.exit(0);
|
|
121
158
|
}
|
|
122
159
|
};
|
|
@@ -127,10 +164,23 @@ function onExit(cb) {
|
|
|
127
164
|
return () => process.off('SIGINT', handler);
|
|
128
165
|
}
|
|
129
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
|
+
|
|
130
180
|
module.exports = {
|
|
131
181
|
init,
|
|
132
182
|
run,
|
|
133
183
|
onExit,
|
|
134
|
-
prompt
|
|
135
|
-
spinner
|
|
184
|
+
prompt,
|
|
185
|
+
spinner,
|
|
136
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.7",
|
|
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
|
}
|