@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.
@@ -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
@@ -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
- 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,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
- // ensure we exit after callback finishes (0 = success)
119
- exiting.stop();
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: inquirer?.default || inquirer,
135
- spinner: ora?.default || ora,
184
+ prompt,
185
+ spinner,
136
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.5",
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
  }