@stonyx/utils 0.2.3-alpha.1 → 0.2.3-alpha.3

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.
@@ -0,0 +1,73 @@
1
+ # Improvements
2
+
3
+ Known code issues and suggested fixes for `@stonyx/utils`.
4
+
5
+ ---
6
+
7
+ ## 1. Filename typo: `plurarize.js` should be `pluralize.js`
8
+
9
+ **Files affected:**
10
+ - `src/plurarize.js` (source)
11
+ - `test/unit/string/plurarize-test.js` (test)
12
+ - `src/string.js` (re-export references `./plurarize.js`)
13
+
14
+ **Details:**
15
+ The filename `plurarize.js` is missing the second "l" — it should be `pluralize.js`. The typo is propagated to the test file (`plurarize-test.js`) and the re-export in `src/string.js`:
16
+
17
+ ```js
18
+ // src/string.js, line 35
19
+ export { default as pluralize } from './plurarize.js';
20
+ ```
21
+
22
+ **Suggested fix:**
23
+ Rename `src/plurarize.js` to `src/pluralize.js`, rename `test/unit/string/plurarize-test.js` to `test/unit/string/pluralize-test.js`, and update the import path in `src/string.js`.
24
+
25
+ ---
26
+
27
+ ## 2. Self-referential package imports in `src/file.js`
28
+
29
+ **File:** `src/file.js`
30
+
31
+ **Details:**
32
+ `src/file.js` imports from its own package using the published package specifier:
33
+
34
+ ```js
35
+ // src/file.js, lines 1-3
36
+ import { getTimestamp } from '@stonyx/utils/date';
37
+ import { kebabCaseToCamelCase } from '@stonyx/utils/string';
38
+ import { objToJson } from '@stonyx/utils/object';
39
+ ```
40
+
41
+ These self-referential imports resolve correctly in local development (Node respects the `exports` map for the current package) but are fragile for a published package — they rely on Node's self-referencing behavior, which can break in certain bundler or monorepo resolution scenarios.
42
+
43
+ **Suggested fix:**
44
+ Use relative imports instead:
45
+
46
+ ```js
47
+ import { getTimestamp } from './date.js';
48
+ import { kebabCaseToCamelCase } from './string.js';
49
+ import { objToJson } from './object.js';
50
+ ```
51
+
52
+ ---
53
+
54
+ ## 3. `get()` uses `console.error` instead of throwing
55
+
56
+ **File:** `src/object.js`, lines 42-44
57
+
58
+ **Details:**
59
+ The `get()` function uses `console.error` and returns `undefined` for invalid arguments:
60
+
61
+ ```js
62
+ export function get(obj, path) {
63
+ if (arguments.length !== 2) return console.error('Get must be called with two arguments; an object and a property key.');
64
+ if (!obj) return console.error(`Cannot call get with '${path}' on an undefined object.`);
65
+ if (typeof path !== 'string') return console.error('The path provided to get must be a string.');
66
+ ...
67
+ }
68
+ ```
69
+
70
+ This is inconsistent with other functions in the same module — `mergeObject` throws `new Error('Cannot merge arrays.')` and `getOrSet` throws `new Error('First argument to getOrSet must be a Map.')`. The `console.error` approach silently returns `undefined` (the return value of `console.error`), making bugs harder to catch in calling code.
71
+
72
+ **Suggested fix:**
73
+ Replace `console.error` calls with `throw new Error(...)` to match the error-handling pattern used by `mergeObject` and `getOrSet`. This would also require updating the tests in `test/unit/object/get-test.js` to use `assert.throws` instead of spying on `console.error`.
@@ -0,0 +1,168 @@
1
+ # Project Structure
2
+
3
+ ## Overview
4
+
5
+ `@stonyx/utils` is a utilities module for the Stonyx Framework. It provides pure JavaScript helper functions for file system operations, object manipulation, string transformations, date handling, promises, and interactive CLI prompts.
6
+
7
+ - **Package name:** `@stonyx/utils`
8
+ - **Version:** `0.2.3-beta.1`
9
+ - **License:** Apache-2.0
10
+ - **Module system:** ES Modules (`"type": "module"`)
11
+ - **Node version:** v24.13.0 (`.nvmrc`)
12
+ - **Package manager:** pnpm
13
+ - **Repository:** https://github.com/abofs/stonyx-utils.git
14
+
15
+ ## Tech Stack
16
+
17
+ - **Runtime:** Node.js (ESM)
18
+ - **Test framework:** QUnit 2.x
19
+ - **Test mocking:** Sinon 21.x
20
+ - **CI/CD:** GitHub Actions (reusable workflows from `abofs/stonyx-workflows`)
21
+ - **Publishing:** npm (public, with provenance)
22
+
23
+ ## File Structure
24
+
25
+ ```
26
+ stonyx-utils/
27
+ .claude/ # Claude project memory
28
+ project-structure.md # This file
29
+ improvements.md # Known issues and improvement ideas
30
+ .github/
31
+ workflows/
32
+ ci.yml # CI on PRs to dev/main (reusable workflow)
33
+ publish.yml # NPM publish on push to main / manual dispatch
34
+ src/
35
+ date.js # Date utilities
36
+ file.js # File system utilities
37
+ object.js # Object/array utilities
38
+ plurarize.js # Pluralization engine (NOTE: filename typo)
39
+ promise.js # Promise utilities
40
+ prompt.js # CLI prompt utilities
41
+ string.js # String transformation utilities
42
+ test/
43
+ unit/
44
+ file-test.js # Tests for src/file.js
45
+ prompt-test.js # Tests for src/prompt.js
46
+ object/
47
+ get-test.js # Tests for object get()
48
+ getOrSet-test.js # Tests for object getOrSet()
49
+ object-test.js # Tests for mergeObject()
50
+ string/
51
+ plurarize-test.js # Tests for pluralize (NOTE: filename typo)
52
+ string-test.js # Tests for string conversion functions
53
+ .gitignore
54
+ .npmignore # Excludes test/ and .nvmrc from published package
55
+ .nvmrc # Node v24.13.0
56
+ LICENSE.md # Apache-2.0
57
+ README.md
58
+ package.json
59
+ pnpm-lock.yaml
60
+ ```
61
+
62
+ ## Package Exports
63
+
64
+ Defined in `package.json` under `"exports"`:
65
+
66
+ | Import path | File |
67
+ | ---------------------- | ---------------- |
68
+ | `@stonyx/utils/date` | `src/date.js` |
69
+ | `@stonyx/utils/object` | `src/object.js` |
70
+ | `@stonyx/utils/file` | `src/file.js` |
71
+ | `@stonyx/utils/promise`| `src/promise.js` |
72
+ | `@stonyx/utils/prompt` | `src/prompt.js` |
73
+ | `@stonyx/utils/string` | `src/string.js` |
74
+
75
+ ## Module Documentation
76
+
77
+ ### `src/date.js`
78
+
79
+ | Export | Signature | Description |
80
+ | ------ | --------- | ----------- |
81
+ | `getTimestamp` | `getTimestamp(dateObject?: Date): number` | Returns UNIX timestamp in seconds. If `dateObject` is provided, uses that date; otherwise uses `Date.now()`. |
82
+
83
+ ### `src/file.js`
84
+
85
+ Imports: `@stonyx/utils/date`, `@stonyx/utils/string`, `@stonyx/utils/object`, `fs`, `path`
86
+
87
+ | Export | Signature | Description |
88
+ | ------ | --------- | ----------- |
89
+ | `createFile` | `createFile(filePath, data, options?): Promise<void>` | Creates a file. `options.json` serializes data as JSON. Auto-creates parent directories. |
90
+ | `updateFile` | `updateFile(filePath, data, options?): Promise<void>` | Atomically updates an existing file via temp-file swap. `options.json` for JSON serialization. Throws if file does not exist. |
91
+ | `copyFile` | `copyFile(sourcePath, targetPath, options?): Promise<boolean>` | Copies a file. Returns `false` if target exists and `options.overwrite` is not `true`. |
92
+ | `readFile` | `readFile(filePath, options?): Promise<string\|object>` | Reads a file. `options.json` parses as JSON. `options.missingFileCallback(filePath)` called on ENOENT. |
93
+ | `deleteFile` | `deleteFile(filePath, options?): Promise<void>` | Deletes a file. `options.ignoreAccessFailure` silences missing-file errors. |
94
+ | `deleteDirectory` | `deleteDirectory(dir): Promise<void>` | Recursively deletes a directory (`rm -rf`). |
95
+ | `createDirectory` | `createDirectory(dir): Promise<void>` | Recursively creates a directory (`mkdir -p`). |
96
+ | `forEachFileImport` | `forEachFileImport(dir, callback, options?): Promise<void>` | Dynamically imports all `.js` files in a directory and invokes `callback(exports, { name, stats, path })`. Options: `fullExport`, `rawName`, `ignoreAccessFailure`, `recursive`, `recursiveNaming`, `namePrefix`. |
97
+ | `fileExists` | `fileExists(filePath): Promise<boolean>` | Returns `true` if file exists, `false` otherwise. |
98
+
99
+ ### `src/object.js`
100
+
101
+ | Export | Signature | Description |
102
+ | ------ | --------- | ----------- |
103
+ | `deepCopy` | `deepCopy(obj): any` | Deep clones via `JSON.parse(JSON.stringify())`. |
104
+ | `objToJson` | `objToJson(obj, format?): string` | Stringifies object with formatting (default: tab). |
105
+ | `makeArray` | `makeArray(obj): Array` | Wraps value in array if not already an array. |
106
+ | `mergeObject` | `mergeObject(obj1, obj2, options?): object` | Deep merges two objects. `options.ignoreNewKeys` skips keys not in `obj1`. Throws on array input. |
107
+ | `get` | `get(obj, path): any\|null` | Safely traverses dot-notation path. Returns `null` if any segment is `undefined`. Uses `console.error` for validation (does not throw). |
108
+ | `getOrSet` | `getOrSet(map, key, defaultValue): any` | Gets from a `Map`, or sets `defaultValue` (or calls it if function) when key is missing. Throws if not a `Map`. |
109
+
110
+ ### `src/plurarize.js`
111
+
112
+ | Export | Signature | Description |
113
+ | ------ | --------- | ----------- |
114
+ | `default` (pluralize) | `pluralize(word): string` | Returns plural form of an English noun. Handles irregular nouns, uncountable nouns, and rule-based suffixes (s/x/ch/sh, y, f/fe, o, z). Preserves casing. |
115
+
116
+ ### `src/promise.js`
117
+
118
+ | Export | Signature | Description |
119
+ | ------ | --------- | ----------- |
120
+ | `sleep` | `sleep(seconds): Promise<void>` | Async delay for the given number of seconds. |
121
+
122
+ ### `src/prompt.js`
123
+
124
+ | Export | Signature | Description |
125
+ | ------ | --------- | ----------- |
126
+ | `confirm` | `confirm(question, options?): Promise<boolean>` | Prompts user with `(y/N)` and returns `true` only if input is `"y"` (case-insensitive). Options: `{ input, output }` for custom streams. |
127
+ | `prompt` | `prompt(question, options?): Promise<string>` | Prompts user with a question and returns trimmed input. Options: `{ input, output }` for custom streams. |
128
+
129
+ ### `src/string.js`
130
+
131
+ Re-exports `pluralize` from `./plurarize.js`.
132
+
133
+ | Export | Signature | Description |
134
+ | ------ | --------- | ----------- |
135
+ | `kebabCaseToCamelCase` | `kebabCaseToCamelCase(str): string` | Converts `kebab-case` to `camelCase`. |
136
+ | `kebabCaseToPascalCase` | `kebabCaseToPascalCase(str): string` | Converts `kebab-case` to `PascalCase`. |
137
+ | `camelCaseToKebabCase` | `camelCaseToKebabCase(str): string` | Converts `camelCase` to `kebab-case`. |
138
+ | `generateRandomString` | `generateRandomString(length?): string` | Generates random alphanumeric string (default length: 8). |
139
+ | `pluralize` | (re-export) | Re-exported from `./plurarize.js`. |
140
+
141
+ ## Dependencies
142
+
143
+ ### Runtime
144
+
145
+ None (`"dependencies": {}`).
146
+
147
+ ### Dev
148
+
149
+ | Package | Version | Purpose |
150
+ | ------- | ------- | ------- |
151
+ | `qunit` | `^2.24.1` | Test framework |
152
+ | `sinon` | `^21.0.0` | Stubs/spies for tests |
153
+ | `fs` | `^0.0.1-security` | Placeholder (Node built-in) |
154
+
155
+ ## Test Patterns
156
+
157
+ - **Framework:** QUnit with nested `module()` blocks
158
+ - **Mocking:** Sinon spies/stubs (used for `console.error` spying in `get-test.js`, stub factories in `getOrSet-test.js`)
159
+ - **Stream mocking:** Custom `Readable`/`Writable` streams in `prompt-test.js`
160
+ - **File tests:** Create temp directory in `beforeEach`, clean up in `afterEach`
161
+ - **Run command:** `pnpm test` (which runs `qunit`)
162
+ - **Import style:** Tests import from package exports (e.g., `@stonyx/utils/object`)
163
+
164
+ ## CI/CD
165
+
166
+ - **CI workflow** (`ci.yml`): Runs on PRs to `dev` and `main` branches. Uses reusable workflow from `abofs/stonyx-workflows/.github/workflows/ci.yml@main`. Concurrency grouping cancels in-progress runs for the same branch.
167
+ - **Publish workflow** (`publish.yml`): Triggers on push to `main`, PR events, or manual dispatch. Supports `patch`/`minor`/`major` version bumps and custom version strings. Uses reusable workflow from `abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main`. Requires `contents: write`, `id-token: write`, and `pull-requests: write` permissions.
168
+ - **Prepublish hook:** `npm test` runs before publish via `prepublishOnly` script.
package/README.md CHANGED
@@ -25,10 +25,13 @@ Utilities module for the Stonyx Framework. Provides helpers for files, objects,
25
25
  | | `getOrSet` | Get or set value in a Map. |
26
26
  | **String** | `kebabCaseToCamelCase` | Convert kebab-case to camelCase. |
27
27
  | | `kebabCaseToPascalCase` | Convert kebab-case to PascalCase. |
28
+ | | `camelCaseToKebabCase` | Convert camelCase to kebab-case. |
28
29
  | | `generateRandomString` | Generate a random alphanumeric string. |
29
30
  | | `pluralize` | Return plural form of English nouns. |
30
31
  | **Date** | `getTimestamp` | Return current UNIX timestamp in seconds. |
31
32
  | **Promise** | `sleep` | Async delay for a given number of seconds. |
33
+ | **Prompt** | `confirm` | Prompt user for y/N confirmation. |
34
+ | | `prompt` | Prompt user for free-text input. |
32
35
 
33
36
  ---
34
37
 
@@ -41,6 +44,7 @@ Utilities module for the Stonyx Framework. Provides helpers for files, objects,
41
44
  * [String Utils](#string-utils)
42
45
  * [Date Utils](#date-utils)
43
46
  * [Promise Utils](#promise-utils)
47
+ * [Prompt Utils](#prompt-utils)
44
48
  * [License](#license)
45
49
 
46
50
  ---
@@ -107,6 +111,9 @@ Dynamically imports all `.js` files in a directory and calls `callback(exports,
107
111
  | `fullExport` | Boolean | false | If true, callback receives all exports, not just default. |
108
112
  | `rawName` | Boolean | false | If true, the file name is not converted to camelCase. |
109
113
  | `ignoreAccessFailure` | Boolean | false | If true, directory access errors are ignored. |
114
+ | `recursive` | Boolean | false | If true, recurse into subdirectories. |
115
+ | `recursiveNaming` | Boolean | false | If true, prefix imported names with their directory path. |
116
+ | `namePrefix` | String | `""` | Manual prefix prepended to each imported name. |
110
117
 
111
118
  Example:
112
119
 
@@ -221,6 +228,35 @@ await sleep(2); // waits 2 seconds
221
228
 
222
229
  ---
223
230
 
231
+ ## Prompt Utils
232
+
233
+ Interactive CLI prompt helpers built on Node's `readline`.
234
+
235
+ ### Functions
236
+
237
+ #### `confirm(question, options={})`
238
+
239
+ Prompts the user with `(y/N)` and resolves to `true` only if the answer is `"y"` (case-insensitive).
240
+
241
+ * `options.input` — Readable stream (default: `process.stdin`).
242
+ * `options.output` — Writable stream (default: `process.stdout`).
243
+
244
+ #### `prompt(question, options={})`
245
+
246
+ Prompts the user with a question and resolves to the trimmed input string.
247
+
248
+ * `options.input` — Readable stream (default: `process.stdin`).
249
+ * `options.output` — Writable stream (default: `process.stdout`).
250
+
251
+ ```js
252
+ import { confirm, prompt } from '@stonyx/utils/prompt';
253
+
254
+ const name = await prompt('What is your name?');
255
+ const ok = await confirm('Proceed?');
256
+ ```
257
+
258
+ ---
259
+
224
260
  ## License
225
261
 
226
262
  Apache — do what you want, just keep attribution.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "keywords": [
4
4
  "stonyx-module"
5
5
  ],
6
- "version": "0.2.3-alpha.1",
6
+ "version": "0.2.3-alpha.3",
7
7
  "description": "Utils module for Stonyx Framework",
8
8
  "repository": {
9
9
  "type": "git",
@@ -15,6 +15,7 @@
15
15
  "./object": "./src/object.js",
16
16
  "./file": "./src/file.js",
17
17
  "./promise": "./src/promise.js",
18
+ "./prompt": "./src/prompt.js",
18
19
  "./string": "./src/string.js"
19
20
  },
20
21
  "publishConfig": {
package/src/prompt.js ADDED
@@ -0,0 +1,29 @@
1
+ import { createInterface } from 'readline';
2
+
3
+ export function confirm(question, { input, output } = {}) {
4
+ const rl = createInterface({
5
+ input: input ?? process.stdin,
6
+ output: output ?? process.stdout,
7
+ });
8
+
9
+ return new Promise(resolve => {
10
+ rl.question(`${question} (y/N) `, answer => {
11
+ rl.close();
12
+ resolve(answer.trim().toLowerCase() === 'y');
13
+ });
14
+ });
15
+ }
16
+
17
+ export function prompt(question, { input, output } = {}) {
18
+ const rl = createInterface({
19
+ input: input ?? process.stdin,
20
+ output: output ?? process.stdout,
21
+ });
22
+
23
+ return new Promise(resolve => {
24
+ rl.question(`${question} `, answer => {
25
+ rl.close();
26
+ resolve(answer.trim());
27
+ });
28
+ });
29
+ }