@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.
@@ -31,6 +31,9 @@ jobs:
31
31
  - name: Install dependencies
32
32
  run: npm ci
33
33
 
34
+ - name: Run tests
35
+ run: npm test
36
+
34
37
  - name: Check if version already published
35
38
  id: check
36
39
  run: |
@@ -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
+ });
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ presets: [
3
+ [
4
+ '@babel/preset-env',
5
+ {
6
+ targets: { node: 'current' },
7
+ },
8
+ ],
9
+ ],
10
+ };
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: typeof import('@inquirer/prompts');
21
- export const spinner: typeof import('ora');
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
- const ora = require('ora');
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
- // ensure we exit after callback finishes (0 = success)
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: inquirer?.default || inquirer,
132
- spinner: ora?.default || ora,
184
+ prompt,
185
+ spinner,
133
186
  };
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ testEnvironment: 'node',
3
+ transform: {
4
+ '^.+\\.js$': 'babel-jest',
5
+ },
6
+ // transform @inquirer/prompts (ESM) but ignore other node_modules
7
+ transformIgnorePatterns: ['/node_modules/(?!(?:@inquirer/prompts)/)'],
8
+ };
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@sandro-sikic/maker",
3
- "version": "1.0.4",
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
  }