@stonyx/utils 0.2.3-alpha.3 → 0.2.3-alpha.31
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/README.md +16 -2
- package/dist/date.d.ts +1 -0
- package/dist/date.js +4 -0
- package/dist/file.d.ts +79 -0
- package/dist/file.js +234 -0
- package/dist/fuzzy-match.d.ts +26 -0
- package/dist/fuzzy-match.js +80 -0
- package/dist/object.d.ts +14 -0
- package/dist/object.js +60 -0
- package/dist/plurarize.d.ts +1 -0
- package/dist/plurarize.js +87 -0
- package/dist/promise.d.ts +1 -0
- package/dist/promise.js +5 -0
- package/dist/prompt.d.ts +8 -0
- package/dist/prompt.js +33 -0
- package/dist/string.d.ts +5 -0
- package/dist/string.js +32 -0
- package/package.json +41 -10
- package/.claude/improvements.md +0 -73
- package/.claude/project-structure.md +0 -168
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -35
- package/src/date.js +0 -5
- package/src/file.js +0 -145
- package/src/object.js +0 -61
- package/src/plurarize.js +0 -103
- package/src/promise.js +0 -5
- package/src/prompt.js +0 -29
- package/src/string.js +0 -35
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createInterface } from 'readline';
|
|
2
|
+
export function confirm(question, { input, output } = {}) {
|
|
3
|
+
if (!input && !process.stdin.isTTY) {
|
|
4
|
+
return Promise.reject(new Error('Interactive confirm() requires a TTY on stdin. ' +
|
|
5
|
+
'For headless/container deployments, use the autoMigrate config option instead.'));
|
|
6
|
+
}
|
|
7
|
+
const rl = createInterface({
|
|
8
|
+
input: input ?? process.stdin,
|
|
9
|
+
output: output ?? process.stdout,
|
|
10
|
+
});
|
|
11
|
+
return new Promise(resolve => {
|
|
12
|
+
rl.question(`${question} (y/N) `, (answer) => {
|
|
13
|
+
rl.close();
|
|
14
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function prompt(question, { input, output } = {}) {
|
|
19
|
+
if (!input && !process.stdin.isTTY) {
|
|
20
|
+
return Promise.reject(new Error('Interactive prompt() requires a TTY on stdin. ' +
|
|
21
|
+
'For headless/container deployments, configure non-interactive alternatives instead.'));
|
|
22
|
+
}
|
|
23
|
+
const rl = createInterface({
|
|
24
|
+
input: input ?? process.stdin,
|
|
25
|
+
output: output ?? process.stdout,
|
|
26
|
+
});
|
|
27
|
+
return new Promise(resolve => {
|
|
28
|
+
rl.question(`${question} `, (answer) => {
|
|
29
|
+
rl.close();
|
|
30
|
+
resolve(answer.trim());
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
package/dist/string.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function kebabCaseToCamelCase(str: string): string;
|
|
2
|
+
export declare function kebabCaseToPascalCase(str: string): string;
|
|
3
|
+
export declare function camelCaseToKebabCase(str: string): string;
|
|
4
|
+
export declare function generateRandomString(length?: number): string;
|
|
5
|
+
export { default as pluralize } from './plurarize.js';
|
package/dist/string.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function kebabToCase(str, pascal = false) {
|
|
2
|
+
let out = '';
|
|
3
|
+
let upperNext = pascal; // PascalCase starts uppercase
|
|
4
|
+
for (let i = 0; i < str.length; i++) {
|
|
5
|
+
const ch = str.charAt(i);
|
|
6
|
+
if (ch === '-') {
|
|
7
|
+
upperNext = true;
|
|
8
|
+
}
|
|
9
|
+
else if (upperNext) {
|
|
10
|
+
out += ch.toUpperCase();
|
|
11
|
+
upperNext = false;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
out += ch;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
export function kebabCaseToCamelCase(str) {
|
|
20
|
+
return kebabToCase(str, false);
|
|
21
|
+
}
|
|
22
|
+
export function kebabCaseToPascalCase(str) {
|
|
23
|
+
return kebabToCase(str, true);
|
|
24
|
+
}
|
|
25
|
+
export function camelCaseToKebabCase(str) {
|
|
26
|
+
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
export function generateRandomString(length = 8) {
|
|
29
|
+
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
30
|
+
return Array(length).fill('').map(() => characters.charAt(Math.floor(Math.random() * characters.length))).join('');
|
|
31
|
+
}
|
|
32
|
+
export { default as pluralize } from './plurarize.js';
|
package/package.json
CHANGED
|
@@ -3,20 +3,46 @@
|
|
|
3
3
|
"keywords": [
|
|
4
4
|
"stonyx-module"
|
|
5
5
|
],
|
|
6
|
-
"version": "0.2.3-alpha.
|
|
6
|
+
"version": "0.2.3-alpha.31",
|
|
7
7
|
"description": "Utils module for Stonyx Framework",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "https://github.com/abofs/stonyx-utils.git"
|
|
11
11
|
},
|
|
12
12
|
"type": "module",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
13
17
|
"exports": {
|
|
14
|
-
"./date":
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"./
|
|
19
|
-
|
|
18
|
+
"./date": {
|
|
19
|
+
"types": "./dist/date.d.ts",
|
|
20
|
+
"default": "./dist/date.js"
|
|
21
|
+
},
|
|
22
|
+
"./object": {
|
|
23
|
+
"types": "./dist/object.d.ts",
|
|
24
|
+
"default": "./dist/object.js"
|
|
25
|
+
},
|
|
26
|
+
"./file": {
|
|
27
|
+
"types": "./dist/file.d.ts",
|
|
28
|
+
"default": "./dist/file.js"
|
|
29
|
+
},
|
|
30
|
+
"./promise": {
|
|
31
|
+
"types": "./dist/promise.d.ts",
|
|
32
|
+
"default": "./dist/promise.js"
|
|
33
|
+
},
|
|
34
|
+
"./prompt": {
|
|
35
|
+
"types": "./dist/prompt.d.ts",
|
|
36
|
+
"default": "./dist/prompt.js"
|
|
37
|
+
},
|
|
38
|
+
"./string": {
|
|
39
|
+
"types": "./dist/string.d.ts",
|
|
40
|
+
"default": "./dist/string.js"
|
|
41
|
+
},
|
|
42
|
+
"./fuzzy-match": {
|
|
43
|
+
"types": "./dist/fuzzy-match.d.ts",
|
|
44
|
+
"default": "./dist/fuzzy-match.js"
|
|
45
|
+
}
|
|
20
46
|
},
|
|
21
47
|
"publishConfig": {
|
|
22
48
|
"access": "public",
|
|
@@ -28,12 +54,17 @@
|
|
|
28
54
|
"Stone Costa <stone.costa@synamicd.com>"
|
|
29
55
|
],
|
|
30
56
|
"devDependencies": {
|
|
57
|
+
"@types/node": "^25.5.2",
|
|
58
|
+
"@types/qunit": "^2.19.13",
|
|
59
|
+
"@types/sinon": "^21.0.1",
|
|
31
60
|
"fs": "^0.0.1-security",
|
|
32
61
|
"qunit": "^2.24.1",
|
|
33
|
-
"sinon": "^21.0.0"
|
|
62
|
+
"sinon": "^21.0.0",
|
|
63
|
+
"typescript": "^5.8.3"
|
|
34
64
|
},
|
|
35
|
-
"dependencies": {},
|
|
36
65
|
"scripts": {
|
|
37
|
-
"
|
|
66
|
+
"build": "tsc",
|
|
67
|
+
"build:test": "tsc -p tsconfig.test.json",
|
|
68
|
+
"test": "pnpm build && pnpm build:test && qunit 'dist-test/test/**/*.js'"
|
|
38
69
|
}
|
|
39
70
|
}
|
package/.claude/improvements.md
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
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`.
|
|
@@ -1,168 +0,0 @@
|
|
|
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/.github/workflows/ci.yml
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
branches: [dev, main]
|
|
6
|
-
|
|
7
|
-
concurrency:
|
|
8
|
-
group: ci-${{ github.head_ref || github.ref }}
|
|
9
|
-
cancel-in-progress: true
|
|
10
|
-
|
|
11
|
-
permissions:
|
|
12
|
-
contents: read
|
|
13
|
-
|
|
14
|
-
jobs:
|
|
15
|
-
test:
|
|
16
|
-
uses: abofs/stonyx-workflows/.github/workflows/ci.yml@main
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
name: Publish to NPM
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_dispatch:
|
|
5
|
-
inputs:
|
|
6
|
-
version-type:
|
|
7
|
-
description: 'Version type'
|
|
8
|
-
required: true
|
|
9
|
-
type: choice
|
|
10
|
-
options:
|
|
11
|
-
- patch
|
|
12
|
-
- minor
|
|
13
|
-
- major
|
|
14
|
-
custom-version:
|
|
15
|
-
description: 'Custom version (optional, overrides version-type)'
|
|
16
|
-
required: false
|
|
17
|
-
type: string
|
|
18
|
-
pull_request:
|
|
19
|
-
types: [opened, synchronize, reopened]
|
|
20
|
-
branches: [main, dev]
|
|
21
|
-
push:
|
|
22
|
-
branches: [main]
|
|
23
|
-
|
|
24
|
-
permissions:
|
|
25
|
-
contents: write
|
|
26
|
-
id-token: write
|
|
27
|
-
pull-requests: write
|
|
28
|
-
|
|
29
|
-
jobs:
|
|
30
|
-
publish:
|
|
31
|
-
uses: abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main
|
|
32
|
-
with:
|
|
33
|
-
version-type: ${{ github.event.inputs.version-type }}
|
|
34
|
-
custom-version: ${{ github.event.inputs.custom-version }}
|
|
35
|
-
secrets: inherit
|
package/src/date.js
DELETED
package/src/file.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { getTimestamp } from '@stonyx/utils/date';
|
|
2
|
-
import { kebabCaseToCamelCase } from '@stonyx/utils/string';
|
|
3
|
-
import { objToJson } from '@stonyx/utils/object';
|
|
4
|
-
import { promises as fsp } from 'fs';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
|
|
7
|
-
export async function createFile(filePath, data, options={}) {
|
|
8
|
-
try {
|
|
9
|
-
filePath = path.resolve(filePath);
|
|
10
|
-
|
|
11
|
-
await createDirectory(path.dirname(filePath));
|
|
12
|
-
await fsp.writeFile(filePath, options.json ? objToJson(data) : data, 'utf8');
|
|
13
|
-
} catch (error) {
|
|
14
|
-
throw new Error(error);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function updateFile(filePath, data, options={}) {
|
|
19
|
-
try {
|
|
20
|
-
await fsp.access(filePath);
|
|
21
|
-
|
|
22
|
-
const swapFile = `${filePath}.temp-${getTimestamp()}`;
|
|
23
|
-
await fsp.writeFile(swapFile, options.json ? objToJson(data) : data);
|
|
24
|
-
await fsp.rename(swapFile, filePath);
|
|
25
|
-
} catch (error) {
|
|
26
|
-
|
|
27
|
-
throw new Error(error);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function copyFile(sourcePath, targetPath, options={}) {
|
|
32
|
-
try {
|
|
33
|
-
sourcePath = path.resolve(sourcePath);
|
|
34
|
-
targetPath = path.resolve(targetPath);
|
|
35
|
-
await fsp.access(sourcePath);
|
|
36
|
-
} catch (error) {
|
|
37
|
-
throw new Error(error);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
await fsp.access(targetPath);
|
|
42
|
-
if (!options.overwrite) return false;
|
|
43
|
-
} catch {}
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
await fsp.copyFile(sourcePath, targetPath);
|
|
47
|
-
} catch (error) {
|
|
48
|
-
throw new Error(error);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function readFile(filePath, options={}) {
|
|
55
|
-
try {
|
|
56
|
-
filePath = path.resolve(filePath);
|
|
57
|
-
|
|
58
|
-
await fsp.access(filePath);
|
|
59
|
-
const fileData = await fsp.readFile(filePath, 'utf8');
|
|
60
|
-
|
|
61
|
-
return options.json ? JSON.parse(fileData) : fileData;
|
|
62
|
-
} catch (error) {
|
|
63
|
-
const { missingFileCallback } = options;
|
|
64
|
-
|
|
65
|
-
if (error.code === 'ENOENT' && missingFileCallback) {
|
|
66
|
-
return missingFileCallback(filePath);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
throw new Error(error);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export async function deleteFile(filePath, options) {
|
|
74
|
-
try {
|
|
75
|
-
filePath = path.resolve(filePath);
|
|
76
|
-
|
|
77
|
-
await fsp.access(filePath);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
if (options?.ignoreAccessFailure) return;
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
await fsp.unlink(filePath);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export async function deleteDirectory(dir) {
|
|
87
|
-
await fsp.rm(dir, { recursive: true, force: true });
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export async function createDirectory(dir) {
|
|
91
|
-
await fsp.mkdir(dir, { recursive: true });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export async function forEachFileImport(dir, callback, options={}) {
|
|
95
|
-
if (typeof callback !== 'function') throw new Error('Callback must be valid function');
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
await fsp.access(dir);
|
|
99
|
-
} catch (error) {
|
|
100
|
-
if (!options.ignoreAccessFailure) throw new Error(`Unable to access directory: ${dir}`);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const files = await fsp.readdir(dir);
|
|
105
|
-
|
|
106
|
-
for (const file of files) {
|
|
107
|
-
const filePath = path.join(dir, file);
|
|
108
|
-
const stats = await fsp.stat(filePath);
|
|
109
|
-
|
|
110
|
-
if (options.recursive && stats.isDirectory()) {
|
|
111
|
-
const newOptions = { ...options };
|
|
112
|
-
|
|
113
|
-
if (options.recursiveNaming) {
|
|
114
|
-
const pathPrefix = options.rawName ? file : `${kebabCaseToCamelCase(file)}`;
|
|
115
|
-
newOptions.namePrefix = options.namePrefix ? `${options.namePrefix}${pathPrefix}/` : `${pathPrefix}/`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
await forEachFileImport(filePath, callback, newOptions);
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (!stats.isFile() || !file.endsWith('.js')) continue;
|
|
123
|
-
|
|
124
|
-
const prefix = process.platform === 'win32' ? 'file://' : '';
|
|
125
|
-
const rawName = file.replace('.js', '');
|
|
126
|
-
let name = options.rawName ? rawName : kebabCaseToCamelCase(rawName);
|
|
127
|
-
|
|
128
|
-
if (options.namePrefix) name = `${options.namePrefix}${name}`;
|
|
129
|
-
|
|
130
|
-
const exported = await import(prefix + path.resolve(filePath));
|
|
131
|
-
const output = !options.fullExport ? exported.default : exported;
|
|
132
|
-
|
|
133
|
-
callback(output, { name, stats, path: filePath });
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export async function fileExists(filePath) {
|
|
138
|
-
try {
|
|
139
|
-
filePath = path.resolve(filePath);
|
|
140
|
-
await fsp.access(filePath);
|
|
141
|
-
return true;
|
|
142
|
-
} catch (error) {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
}
|
package/src/object.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
export function deepCopy(obj) {
|
|
2
|
-
return JSON.parse(JSON.stringify(obj));
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export function objToJson(obj, format='\t') {
|
|
6
|
-
return JSON.stringify(obj, null, format);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function makeArray(obj) {
|
|
10
|
-
return Array.isArray(obj) ? obj : [obj];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function cloneShallow(value) {
|
|
14
|
-
if (Array.isArray(value)) return value.slice();
|
|
15
|
-
if (value && typeof value === 'object') return { ...value };
|
|
16
|
-
return value;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function mergeObject(obj1, obj2, options={}) {
|
|
20
|
-
if (Array.isArray(obj1) || Array.isArray(obj2)) throw new Error('Cannot merge arrays.');
|
|
21
|
-
|
|
22
|
-
if (obj1 === null || typeof obj1 !== 'object') return cloneShallow(obj2);
|
|
23
|
-
if (obj2 === null || typeof obj2 !== 'object') return cloneShallow(obj1);
|
|
24
|
-
|
|
25
|
-
const result = {};
|
|
26
|
-
|
|
27
|
-
for (const key of Object.keys(obj1)) result[key] = cloneShallow(obj1[key]);
|
|
28
|
-
for (const key of Object.keys(obj2)) {
|
|
29
|
-
if (options.ignoreNewKeys && !(key in obj1)) continue;
|
|
30
|
-
|
|
31
|
-
const val1 = obj1[key];
|
|
32
|
-
const val2 = obj2[key];
|
|
33
|
-
const shouldMerge = val1 && val2 && typeof val1 === 'object' && typeof val2 === 'object' && !Array.isArray(val1) && !Array.isArray(val2);
|
|
34
|
-
result[key] = shouldMerge ? mergeObject(val1, val2, options) : cloneShallow(val2);
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return result;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function get(obj, path) {
|
|
42
|
-
if (arguments.length !== 2) return console.error('Get must be called with two arguments; an object and a property key.');
|
|
43
|
-
if (!obj) return console.error(`Cannot call get with '${path}' on an undefined object.`);
|
|
44
|
-
if (typeof path !== 'string') return console.error('The path provided to get must be a string.');
|
|
45
|
-
|
|
46
|
-
for (const key of path.split('.')) {
|
|
47
|
-
if (obj[key] === undefined) return null;
|
|
48
|
-
|
|
49
|
-
obj = obj[key];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return obj;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function getOrSet(map, key, defaultValue) {
|
|
56
|
-
if (!(map instanceof Map)) throw new Error('First argument to getOrSet must be a Map.');
|
|
57
|
-
|
|
58
|
-
if (!map.has(key)) map.set(key, typeof defaultValue === "function" ? defaultValue() : defaultValue);
|
|
59
|
-
|
|
60
|
-
return map.get(key);
|
|
61
|
-
}
|