stonyx 0.1.0 → 0.2.1

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.
@@ -18,14 +18,19 @@ jobs:
18
18
  - name: Checkout code
19
19
  uses: actions/checkout@v3
20
20
 
21
+ - name: Setup pnpm
22
+ uses: pnpm/action-setup@v4
23
+ with:
24
+ version: 9
25
+
21
26
  - name: Set up Node.js
22
27
  uses: actions/setup-node@v3
23
28
  with:
24
- node-version: 18
25
- cache: 'npm'
29
+ node-version: 22.18.0
30
+ cache: 'pnpm'
26
31
 
27
32
  - name: Install dependencies
28
- run: npm ci
33
+ run: pnpm install --frozen-lockfile
29
34
 
30
35
  - name: Run tests
31
- run: npm test
36
+ run: pnpm test
package/README.md CHANGED
@@ -1,52 +1,179 @@
1
- # stonyx-backend-application
2
- Main base vanilla application / host module
3
- The intention is that this application can be used as a "base" for the Stonyx framework
1
+ # Stonyx
4
2
 
5
- ## Running the application
3
+ **Stonyx** is a lightweight, modular framework for building modern Node.js applications. It provides a **plug-and-play architecture**, centralized color-coded logging, and seamless integration of asynchronous modules, making development faster, cleaner, and more maintainable.
4
+
5
+ ### Highlights
6
+
7
+ * ✅ 100% JavaScript
8
+ * ✅ Drop-in file system for most modules
9
+ * ✅ Don’t even hit the ground — just fly
10
+ * ✅ High performance
11
+
12
+ Stonyx acts as a **base application host**, allowing you to add official modules (`@stonyx/*`) or your own custom modules without boilerplate initialization.
13
+
14
+ ---
15
+
16
+ ## Quick Start
17
+
18
+ ### ESM Usage (Application Startup)
19
+
20
+ For standard applications, the **bootstrap file** ensures that the Stonyx framework and all submodules are fully loaded before your application code runs:
21
+
22
+ ```js
23
+ // index.js - your project's entry point
24
+ import Stonyx from './stonyx-bootstrap.cjs';
25
+ await Stonyx.ready; // wait until all modules are initialized
26
+
27
+ const { default: App } = await import('./app.js'); // your application
28
+ new App();
6
29
  ```
7
- node .
30
+
31
+ ### CommonJS Bootstrap Helper
32
+ Auto-generated and added to your project post installation
33
+
34
+ ```js
35
+ // stonyx-bootstrap.cjs
36
+ const Stonyx = require('stonyx').default;
37
+ const config = require('./config/environment.js').default;
38
+
39
+ new Stonyx(config, __dirname);
8
40
  ```
9
41
 
10
- ## Color Coded Logging via Chronicle
42
+ ---
43
+
44
+ ## Core Features
45
+
46
+ ### 1. **Modular Architecture**
47
+
48
+ * Automatically detects modules in `devDependencies` prefixed with `@stonyx/`.
49
+ * Supports async initialization with `stonyx-async` modules.
50
+ * Safe module sequencing for submodule development with `waitForModule()`.
51
+
52
+ ### 2. **Color-Coded Logging**
53
+
54
+ * Centralized logging via [Chronicle](https://github.com/abofs/chronicle).
55
+ * Module-specific logs configurable in `environment.js`:
11
56
 
12
57
  ```js
13
- import log from 'stonyx/log';
58
+ restServer: { logColor: 'yellow', logMethod: 'api', logTimestamp: true }
14
59
  ```
15
60
 
16
- Stonyx utilizes our own chronicle library for logging: https://github.com/abofs/chronicle.
17
- For all Stonyx modules, color coding can be dynamically configured by providing a `logColor` setting in the `environent.js` file.
61
+ * Create custom logs for any class with minimal configuration.
62
+
63
+ ### 3. **Singleton Design**
64
+
65
+ * Only one instance of Stonyx exists per project.
66
+ * Ensures consistent access to modules, logs, and configuration.
67
+
68
+ ### 4. **Plug-and-Play Module Loading**
69
+
70
+ * Modules with `stonyx-module` keyword in `package.json` are auto-initialized.
71
+ * Modules with `stonyx-async` keyword are awaited automatically before usage.
72
+
73
+ ---
18
74
 
19
- ### Example:
75
+ ## Official Modules
76
+
77
+ ### **[@stonyx/cron](https://github.com/abofs/stonyx-cron)**
78
+
79
+ Lightweight asynchronous job scheduling utility.
20
80
 
21
81
  ```js
22
- // environment.js
23
- //...
24
- db: {
25
- logColor: 'white',
26
- //...
27
- }
82
+ import Cron from '@stonyx/cron';
83
+
84
+ const cron = new Cron();
85
+ cron.register('exampleJob', async () => console.log('Job executed!'), 5, true);
86
+ ```
87
+
88
+ * Efficient scheduling using a min-heap.
89
+ * Optional logging via `config.cron`.
90
+
91
+ ---
92
+
93
+ ### **[@stonyx/rest-server](https://github.com/abofs/stonyx-rest-server)**
94
+
95
+ Dynamic REST server module with auto-route registration.
96
+
97
+ ```js
98
+ import Stonyx from 'stonyx';
99
+ import config from './config/environment.js';
100
+
101
+ new Stonyx(config);
28
102
  ```
29
- The above setting will expose the `log.db()` method, which will output in white.
30
- Alternatively you can provide a `logMethod` setting if you would rather use a different alias that
31
- does not match the class name, and `logTimestamp` as a true if you wish your log to include timestamp.
32
- `stonyx/log` fully exposes the `chronicle` object. See Chronicle documentation for more information.
103
+
104
+ * Zero configuration for routes: drop request classes into the `requests` directory.
105
+ * Automatic path generation, JSON parsing, and CORS handling.
106
+ * Supports per-route authentication hooks.
107
+
108
+ ---
109
+
110
+ ### **[@stonyx/orm](https://github.com/abofs/stonyx-orm)**
111
+
112
+ Lightweight ORM with model definitions, relationships, serializers, and optional REST integration.
33
113
 
34
114
  ```js
35
- // environment.js
36
- //...
37
- restServer: {
38
- logColor: 'yellow',
39
- logMethod: 'api'
40
- //...
115
+ import Stonyx from 'stonyx';
116
+ import config from './config/environment.js';
117
+
118
+ new Stonyx(config);
119
+
120
+ // Define models
121
+ import { Model, attr, hasMany, belongsTo } from '@stonyx/orm';
122
+
123
+ class Owner extends Model {
124
+ id = attr('string');
125
+ pets = hasMany('animal');
41
126
  }
42
127
  ```
43
128
 
44
- The above example will expose `log.api()`, which will output in yellow.
129
+ * Auto-loads models, serializers, transforms, and access classes.
130
+ * Optional JSON file persistence with auto-save intervals.
131
+ * Integrates with `@stonyx/rest-server` for automatic route setup.
132
+
133
+ ---
134
+
135
+ ## Configuration
136
+
137
+ All modules are configurable via `config/environment.js`:
138
+
139
+ ```js
140
+ export default {
141
+ restServer: { logColor: 'yellow', port: 3000 },
142
+ orm: { logColor: 'white', db: { file: './db.json', autosave: true } },
143
+ cron: { log: true }
144
+ };
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Running the Application
150
+
151
+ ```bash
152
+ node . # Start the main app
153
+ npm start # Run using npm script
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Developing Submodules
159
+
160
+ For developers building new Stonyx modules or experimenting with async modules:
161
+
162
+ ```js
163
+ import Stonyx, { waitForModule } from 'stonyx';
164
+ import config from './config/environment.js';
165
+
166
+ const app = new Stonyx(config, __dirname);
167
+
168
+ // Wait for specific async module readiness
169
+ await waitForModule('restServer');
170
+ ```
171
+
172
+ * Use `waitForModule()` **only for submodule development**, testing, or when you need to ensure a specific module is fully initialized before continuing.
173
+ * Official modules automatically initialize during normal application startup, so end-users do **not** need to call `waitForModule()`.
45
174
 
46
- ## Module Development
175
+ ---
47
176
 
48
- `modules.js` is responsible for dynamically loading any async stonyx modules.
49
- Conventionally, all official Stonyx modules must be prefixed with `@stonyx/`, and
50
- contain the `stonyx-module` keyword in its respective `package.json` file.
177
+ ## License
51
178
 
52
- Async modules require the `stonyx-async` keyword in order to be automatically loaded.
179
+ Apache 2.0 — do what you want, just keep attribution.
@@ -0,0 +1,13 @@
1
+ // project configuration, override-able by listed environment variables
2
+ const {
3
+ DEBUG,
4
+ NODE_ENV,
5
+ } = process.env;
6
+
7
+ const isTest = typeof QUnit !== 'undefined';
8
+ const environment = isTest ? 'test' : (NODE_ENV ?? 'development');
9
+
10
+ export default {
11
+ environment,
12
+ debug: DEBUG ?? environment === 'development',
13
+ }
package/package.json CHANGED
@@ -1,19 +1,20 @@
1
1
  {
2
2
  "name": "stonyx",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Base stonyx framework module",
5
5
  "main": "main.js",
6
6
  "type": "module",
7
7
  "exports": {
8
8
  ".": "./src/main.js",
9
9
  "./config": "./src/exports/config.js",
10
- "./log": "./src/exports/log.js"
10
+ "./log": "./src/exports/log.js",
11
+ "./test-helpers": "./src/exports/test-helpers.js"
11
12
  },
12
13
  "scripts": {
13
14
  "start": "node .",
14
15
  "test": "qunit",
15
16
  "error": "echo \"Error: no error handling\" && exit 1",
16
- "postinstall": "echo \"TODO: Postinstall\""
17
+ "postinstall": "node scripts/postinstall.js"
17
18
  },
18
19
  "repository": {
19
20
  "type": "git",
@@ -31,6 +32,8 @@
31
32
  "node-chronicle": "^0.2.0"
32
33
  },
33
34
  "devDependencies": {
34
- "@stonyx/utils": "^0.1.0"
35
+ "@stonyx/utils": "^0.2.0",
36
+ "qunit": "^2.24.1",
37
+ "sinon": "^21.0.0"
35
38
  }
36
39
  }
@@ -0,0 +1,16 @@
1
+ import { copyFile, createDirectory } from '@stonyx/utils/file';
2
+
3
+ const projectDir = process.env.INIT_CWD;
4
+ const configDir = `${projectDir}/config`;
5
+ const bootstrapFile = 'stonyx-bootstrap.cjs';
6
+ const envFile = 'environment.js';
7
+
8
+ createDirectory(configDir);
9
+
10
+ copyFile(`./config/environment copy.js`, `${configDir}/${envFile}`).then(result => {
11
+ if (result) console.log(`Stonyx: ${envFile} has been successfully created. Please see README.md for more information.`);
12
+ });
13
+
14
+ copyFile(`./src/bootstrap.cjs`, `${projectDir}/${bootstrapFile}`).then(result => {
15
+ if (result) console.log(`Stonyx: ${bootstrapFile} has been successfully created. Please see README.md for more information.`);
16
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * commonJS Bootstrap loading - Stonyx must be loaded first, prior to the rest of the application
3
+ */
4
+ const { default:Stonyx } = require('stonyx');
5
+ const { default:config } = require('./config/environment.js');
6
+
7
+ new Stonyx(config, __dirname);
8
+
9
+ module.exports = Stonyx;
@@ -0,0 +1,8 @@
1
+ import Stonyx from 'stonyx';
2
+
3
+ export function setupIntegrationTests(hooks) {
4
+ hooks.before(async function() {
5
+ await Stonyx.ready;
6
+ console.log('Stonyx ready');
7
+ });
8
+ }
package/src/main.js CHANGED
@@ -16,9 +16,11 @@
16
16
 
17
17
  import Chronicle from 'node-chronicle';
18
18
  import loadModules from './modules.js';
19
+ import { kebabCaseToCamelCase } from '@stonyx/utils/string';
19
20
 
20
21
  export default class Stonyx {
21
22
  static initialized = false
23
+ static modulePromises = {};
22
24
 
23
25
  constructor(config, rootPath) {
24
26
  if (Stonyx.instance) return Stonyx.instance;
@@ -31,6 +33,13 @@ export default class Stonyx {
31
33
  if (!config) throw new Error('Stonyx requires full environment configuration on startup');
32
34
  if (!rootPath) throw new Error('Stonyx requires root project\'s path on startup');
33
35
 
36
+ // Transform config from stonyx-modules running as a standalone
37
+ if (rootPath.includes('stonyx-')) {
38
+ const moduleName = kebabCaseToCamelCase(rootPath.split('/').pop().replace('stonyx-', ''));
39
+ config = { [moduleName]: config, ...(config.modules || {}) };
40
+ delete config.modules;
41
+ }
42
+
34
43
  this.config = config;
35
44
  this.chronicle = new Chronicle({ additionalLogs: { title: 'green' }});
36
45
 
@@ -77,3 +86,5 @@ export default class Stonyx {
77
86
  return Stonyx.instance.config;
78
87
  }
79
88
  }
89
+
90
+ export { waitForModule } from './modules.js';
package/src/modules.js CHANGED
@@ -1,19 +1,62 @@
1
1
  /**
2
2
  * Validates and initializes stonyx modules
3
+ * TODO: Refactor into a ModuleLoader class (good coding exercise)
3
4
  */
4
-
5
5
  import { readFile } from '@stonyx/utils/file';
6
+ import { kebabCaseToCamelCase } from '@stonyx/utils/string';
7
+ import { mergeObject } from '@stonyx/utils/object';
8
+
9
+ const modulePromises = {};
10
+
11
+ // Configure module-specific logging
12
+ function configureLog(chronicle, module, config) {
13
+ const { logColor, logMethod, logTimestamp } = config;
14
+ if (!logColor) return;
15
+
16
+ chronicle.defineType(logMethod || module, logColor, { logTimestamp: !!logTimestamp });
17
+ }
18
+
19
+ function initializeModule(moduleName, moduleClass, modules, initPromises) {
20
+ const moduleInstance = new moduleClass();
21
+
22
+ modules.push(moduleInstance);
6
23
 
7
- export default async (config, rootPath, chronicle) => {
24
+ if (!moduleInstance.init) return;
25
+
26
+ initPromises.push((async () => {
27
+ await moduleInstance.init();
28
+ modulePromises[moduleName]?.resolve();
29
+ })());
30
+ }
31
+
32
+ export default async function(config, rootPath, chronicle) {
8
33
  const modules = [];
9
34
  const initPromises = [];
10
35
  const rootPackage = await readFile(`${rootPath}/package.json`, { json: true });
11
- const { devDependencies:dependencies } = rootPackage;
36
+ const { devDependencies:dependencies, name:projectName } = rootPackage;
37
+
38
+ // Expose rootPath to public configuration
12
39
  config.rootPath = rootPath;
13
40
 
14
- for (const moduleName of Object.keys(dependencies)) {
15
- if (!moduleName.startsWith('@stonyx/')) continue; // All official stonyx modules should be start with "@stonyx/"
16
-
41
+ const moduleDependencies = Object.keys(dependencies).filter(moduleName => moduleName.startsWith('@stonyx/'));
42
+
43
+ // Setup module promises prior to initialization
44
+ for (const moduleName of moduleDependencies) {
45
+ const promise = {};
46
+ modulePromises[moduleName] = promise;
47
+ promise.ready = new Promise(resolve => promise.resolve = resolve);
48
+ }
49
+
50
+ // Standalone module configuration
51
+ if (rootPackage.keywords?.includes('stonyx-module')) {
52
+ configureLog(chronicle, projectName, config);
53
+
54
+ const { main: entryPoint } = rootPackage;
55
+ const { default: moduleClass } = await import(`${rootPath}/${entryPoint}`);
56
+ initializeModule(rootPackage.name, moduleClass, modules, initPromises);
57
+ }
58
+
59
+ for (const moduleName of moduleDependencies) {
17
60
  const modulePackage = await readFile(`${rootPath}/node_modules/${moduleName}/package.json`, { json: true, missingFileCallback: () => {
18
61
  console.warn(`Warning: Could not locate stonyx module: "${moduleName}". Module was not loaded`);
19
62
  }});
@@ -26,36 +69,44 @@ export default async (config, rootPath, chronicle) => {
26
69
  console.warn(`Warning: Stonyx modules must contain the "stonyx-module" keyword. Module was not loaded`);
27
70
  continue;
28
71
  }
29
-
30
- if (!keywords.includes('stonyx-async')) continue;
72
+
73
+ if (!keywords.includes('stonyx-async')) {
74
+ modulePromises[moduleName].resolve();
75
+ continue;
76
+ }
31
77
 
32
78
  try {
33
-
34
79
  // Load & Configure Async Modules
35
80
  const { default: moduleConfig } = await import(`${rootPath}/node_modules/${moduleName}/config/environment.js`);
36
81
 
37
- const module = moduleName.split('/').pop();
82
+ const module = kebabCaseToCamelCase(moduleName.split('/').pop());
38
83
  const userConfig = config[module] || {};
39
- const finalConfig = { ...moduleConfig, ...userConfig };
84
+ const finalConfig = mergeObject(moduleConfig, userConfig);
40
85
  config[module] = finalConfig;
41
86
 
42
87
  // Configure module-specific logging
43
- const { logColor, logMethod, logTimestamp } = finalConfig;
44
- if (logColor) chronicle.defineType(logMethod || module, logColor, { logTimestamp: !!logTimestamp });
45
-
88
+ configureLog(chronicle, module, finalConfig);
89
+
46
90
  const { main: entryPoint } = modulePackage;
47
91
  const { default: moduleClass } = await import(`${rootPath}/node_modules/${moduleName}/${entryPoint}`);
48
- const moduleInstance = new moduleClass();
49
-
50
- modules.push(moduleInstance);
51
- if (moduleInstance.init) initPromises.push(moduleInstance.init());
52
-
92
+ initializeModule(moduleName, moduleClass, modules, initPromises);
53
93
  } catch (error) {
54
94
  console.error(error);
55
- throw new Error(`Stonyx modules with async loading must have a config/environment.js file with default configurations.`);
95
+ throw new Error(`Stonyx modules with async loading must have a config/environment.js file with default configurations. Module "${moduleName}" failed to load.`);
56
96
  }
57
97
  }
58
98
 
59
99
  // Wait until all modules are initialized
60
100
  await Promise.all(initPromises);
101
+
102
+ return modules;
103
+ }
104
+
105
+ export async function waitForModule(module) {
106
+ module = `@stonyx/${module}`;
107
+ const modulePromise = modulePromises[module];
108
+
109
+ if (!modulePromise) throw new Error(`Could wait for module: ${module}. Module was not registered in project dependencies`);
110
+
111
+ await modulePromises[module].ready;
61
112
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * commonJS Bootstrap loading - Stonyx must be loaded first, prior to the rest of the application
3
+ */
4
+ const { default:Stonyx } = require('stonyx');
5
+ const { default:config } = require('./config/environment.js');
6
+
7
+ new Stonyx(config, __dirname);
8
+
9
+ module.exports = Stonyx;
package/.nvmrc DELETED
@@ -1 +0,0 @@
1
- v22.18.0