stonyx 0.2.3-beta.5 → 0.2.3-beta.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.
- package/.claude/index.md +96 -0
- package/docs/conventions/discord-conventions.md +62 -0
- package/docs/conventions/framework-modules.md +127 -0
- package/docs/conventions/index.md +24 -0
- package/docs/conventions/orm-conventions.md +158 -0
- package/docs/conventions/project-structure.md +105 -0
- package/docs/conventions/rest-conventions.md +96 -0
- package/docs/conventions/testing-conventions.md +56 -0
- package/package.json +2 -2
- package/src/cli/help.js +48 -29
- package/src/cli/new.js +237 -0
- package/src/cli.js +3 -1
package/.claude/index.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Stonyx CLI Guide
|
|
2
|
+
|
|
3
|
+
See [docs/index.md](../docs/index.md) for full documentation including conventions, modules, lifecycle, and API reference.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Stonyx is the core framework package. The CLI is provided by the `stonyx` binary defined in `package.json`.
|
|
8
|
+
|
|
9
|
+
In a project that depends on `stonyx`:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add -D stonyx
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The CLI is then available as `npx stonyx` or via pnpm scripts.
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
### `stonyx serve` (alias: `s`)
|
|
20
|
+
|
|
21
|
+
Bootstrap Stonyx and run the application.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
stonyx serve # loads app.js by default
|
|
25
|
+
stonyx serve --entry custom.js # custom entry point
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Behavior:
|
|
29
|
+
1. Loads `.env` file
|
|
30
|
+
2. Imports `config/environment.js`
|
|
31
|
+
3. Initializes Stonyx with all detected `@stonyx/*` modules
|
|
32
|
+
4. Runs startup lifecycle hooks
|
|
33
|
+
5. Imports and instantiates the entry point class
|
|
34
|
+
6. Registers SIGTERM/SIGINT handlers for graceful shutdown
|
|
35
|
+
|
|
36
|
+
### `stonyx test` (alias: `t`)
|
|
37
|
+
|
|
38
|
+
Bootstrap Stonyx in test mode and run QUnit tests.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
stonyx test # runs test/**/*-test.js
|
|
42
|
+
stonyx test test/unit/foo-test.js # specific file
|
|
43
|
+
stonyx test "test/integration/**/*-test.js" # glob pattern
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Sets `NODE_ENV=test` and auto-loads test setup which merges `test/config/environment.js` overrides.
|
|
47
|
+
|
|
48
|
+
### `stonyx help` (alias: `h`)
|
|
49
|
+
|
|
50
|
+
Show available commands including built-in and module-provided commands.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
stonyx help
|
|
54
|
+
stonyx --help
|
|
55
|
+
stonyx -h
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### `stonyx new <app-name>`
|
|
59
|
+
|
|
60
|
+
Scaffold a new Stonyx project with interactive module selection.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
stonyx new my-backend
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Prompts for:
|
|
67
|
+
- Package name
|
|
68
|
+
- Which `@stonyx/*` modules to include (REST server, WebSockets, ORM, cron, OAuth, events)
|
|
69
|
+
|
|
70
|
+
Creates:
|
|
71
|
+
- `package.json` with selected dependencies
|
|
72
|
+
- `app.js` entry point
|
|
73
|
+
- `config/environment.js` and `config/environment.example.js`
|
|
74
|
+
- Module-specific directories (`models/`, `requests/`, `crons/`, etc.)
|
|
75
|
+
- `test/` structure with `unit/`, `integration/`, `acceptance/`
|
|
76
|
+
- `.gitignore` and `.nvmrc`
|
|
77
|
+
- Runs `pnpm install`
|
|
78
|
+
|
|
79
|
+
## Module Command System
|
|
80
|
+
|
|
81
|
+
Stonyx modules can register CLI commands by exporting a `./commands` entry in their `package.json` exports map. The CLI auto-discovers these from installed `@stonyx/*` packages.
|
|
82
|
+
|
|
83
|
+
Module commands appear under "Module commands" in `stonyx help` output. They can optionally request Stonyx bootstrap before running (via `bootstrap: true`).
|
|
84
|
+
|
|
85
|
+
## Creating a Stonyx Project Manually
|
|
86
|
+
|
|
87
|
+
If not using `stonyx new`:
|
|
88
|
+
|
|
89
|
+
1. Create project directory
|
|
90
|
+
2. Add `.nvmrc` with current LTS Node version
|
|
91
|
+
3. `pnpm init` and set `"type": "module"`
|
|
92
|
+
4. `pnpm add -D stonyx` plus desired `@stonyx/*` modules
|
|
93
|
+
5. Create `config/environment.js` with module config
|
|
94
|
+
6. Create `app.js` entry point class
|
|
95
|
+
7. Create standard directories per selected modules
|
|
96
|
+
8. Add `@abofs/code-conventions` for linting
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Discord Conventions
|
|
2
|
+
|
|
3
|
+
Conventions for projects using `@stonyx/discord`.
|
|
4
|
+
|
|
5
|
+
## Command Files
|
|
6
|
+
|
|
7
|
+
- One class per file in the configured command directory (default: `discord-commands/`)
|
|
8
|
+
- Each file default-exports a class extending `Command` from `@stonyx/discord`
|
|
9
|
+
- Class must define a `data` property (a `SlashCommandBuilder` instance) and an `async execute(interaction)` method
|
|
10
|
+
- Class ordering: static properties → `data` → `execute()`
|
|
11
|
+
- Filenames use kebab-case; `forEachFileImport` converts to camelCase for internal registration
|
|
12
|
+
- Permission checks belong in the consuming application, not in the module
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import { Command } from '@stonyx/discord';
|
|
16
|
+
import { SlashCommandBuilder } from 'discord.js';
|
|
17
|
+
|
|
18
|
+
export default class PingCommand extends Command {
|
|
19
|
+
data = new SlashCommandBuilder()
|
|
20
|
+
.setName('ping')
|
|
21
|
+
.setDescription('Replies with Pong!');
|
|
22
|
+
|
|
23
|
+
async execute(interaction) {
|
|
24
|
+
await interaction.reply('Pong!');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Event Handler Files
|
|
30
|
+
|
|
31
|
+
- One class per file in the configured event directory (default: `discord-events/`)
|
|
32
|
+
- Each file default-exports a class extending `EventHandler` from `@stonyx/discord`
|
|
33
|
+
- Class must set `static event` to a valid Discord.js event name (e.g., `'messageCreate'`, `'voiceStateUpdate'`)
|
|
34
|
+
- Class must implement a `handle(...args)` method matching the Discord.js event signature
|
|
35
|
+
- Class ordering: static properties → `handle()`
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import { EventHandler } from '@stonyx/discord';
|
|
39
|
+
|
|
40
|
+
export default class WelcomeHandler extends EventHandler {
|
|
41
|
+
static event = 'guildMemberAdd';
|
|
42
|
+
|
|
43
|
+
handle(member) {
|
|
44
|
+
// Welcome new member
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
- Config namespace: `config.discord`
|
|
52
|
+
- Environment variables prefixed with `DISCORD_`
|
|
53
|
+
- Logging: `log.discord()` — configured via `logColor: '#7289da'` and `logMethod: 'discord'`
|
|
54
|
+
- Intent auto-derivation: the module computes required gateway intents from discovered event handlers; use `additionalIntents` in config for edge cases
|
|
55
|
+
|
|
56
|
+
## Module Keywords
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
"keywords": ["stonyx-async", "stonyx-module"]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`stonyx-async` indicates the module's `init()` is async and must be awaited by the framework.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Framework Modules
|
|
2
|
+
|
|
3
|
+
When to use which `@stonyx/*` module. Always check these before reaching for Node built-ins or npm packages.
|
|
4
|
+
|
|
5
|
+
## `stonyx/log`
|
|
6
|
+
|
|
7
|
+
All logging goes through `stonyx/log`. Never use `console.log`, `console.warn`, or `console.error`.
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import log from 'stonyx/log';
|
|
11
|
+
|
|
12
|
+
log.info('Server started');
|
|
13
|
+
log.error('Connection failed', error);
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Custom log types can be configured in `config/environment.js`:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
export default {
|
|
20
|
+
socket: { logColor: 'magenta' }
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## `@stonyx/utils`
|
|
25
|
+
|
|
26
|
+
Utility library organized by domain. Always check here before using Node built-ins or adding npm dependencies.
|
|
27
|
+
|
|
28
|
+
### File I/O (`@stonyx/utils/file`)
|
|
29
|
+
|
|
30
|
+
**`fs` is explicitly prohibited.** Use these instead:
|
|
31
|
+
|
|
32
|
+
- `createFile(filePath, data, options?)` — write a new file (creates parent dirs)
|
|
33
|
+
- `updateFile(filePath, data, options?)` — atomic update via swap file
|
|
34
|
+
- `copyFile(sourcePath, targetPath, options?)` — copy with optional overwrite
|
|
35
|
+
- `readFile(filePath, options?)` — read file, supports `{ json: true }` and `{ missingFileCallback }`
|
|
36
|
+
- `deleteFile(filePath, options?)` — delete file, supports `{ ignoreAccessFailure: true }`
|
|
37
|
+
- `createDirectory(dir)` — recursive mkdir
|
|
38
|
+
- `deleteDirectory(dir)` — recursive rm
|
|
39
|
+
- `forEachFileImport(dir, callback, options?)` — iterate and dynamically import all `.js` files in a directory
|
|
40
|
+
- `fileExists(filePath)` — check existence
|
|
41
|
+
|
|
42
|
+
### Object Manipulation (`@stonyx/utils/object`)
|
|
43
|
+
|
|
44
|
+
- `deepCopy(obj)` — deep clone via JSON
|
|
45
|
+
- `objToJson(obj, format?)` — stringify with formatting
|
|
46
|
+
- `makeArray(obj)` — wrap in array if not already
|
|
47
|
+
- `mergeObject(obj1, obj2, options?)` — deep merge objects, supports `{ ignoreNewKeys: true }`
|
|
48
|
+
- `get(obj, path)` — dot-path property access (e.g., `get(obj, 'a.b.c')`)
|
|
49
|
+
- `getOrSet(map, key, defaultValue)` — get from Map or set default (supports factory functions)
|
|
50
|
+
|
|
51
|
+
### String Transforms (`@stonyx/utils/string`)
|
|
52
|
+
|
|
53
|
+
- `kebabCaseToCamelCase(str)` — `'my-thing'` → `'myThing'`
|
|
54
|
+
- `kebabCaseToPascalCase(str)` — `'my-thing'` → `'MyThing'`
|
|
55
|
+
- `camelCaseToKebabCase(str)` — `'myThing'` → `'my-thing'`
|
|
56
|
+
- `generateRandomString(length?)` — alphanumeric random string (default 8 chars)
|
|
57
|
+
- `pluralize(str)` — basic pluralization
|
|
58
|
+
|
|
59
|
+
### Date / Timestamp (`@stonyx/utils/date`)
|
|
60
|
+
|
|
61
|
+
- `getTimestamp(dateObject?)` — Unix timestamp in seconds (current time if no arg)
|
|
62
|
+
|
|
63
|
+
### Promises (`@stonyx/utils/promise`)
|
|
64
|
+
|
|
65
|
+
- `sleep(seconds)` — async delay
|
|
66
|
+
|
|
67
|
+
### Interactive Prompts (`@stonyx/utils/prompt`)
|
|
68
|
+
|
|
69
|
+
- `confirm(question)` — yes/no prompt, returns boolean
|
|
70
|
+
- `prompt(question)` — free-text input, returns string
|
|
71
|
+
|
|
72
|
+
File issues on `stonyx-utils` for any gaps rather than adding workarounds or npm dependencies.
|
|
73
|
+
|
|
74
|
+
## `@stonyx/events`
|
|
75
|
+
|
|
76
|
+
All pub/sub event handling. Never create custom event emitters.
|
|
77
|
+
|
|
78
|
+
- `setup(eventNames)` — register valid event names
|
|
79
|
+
- `subscribe(event, callback)` — listen for events
|
|
80
|
+
- `once(event, callback)` — single-fire listener
|
|
81
|
+
- `unsubscribe(event, callback)` — remove listener
|
|
82
|
+
- `emit(event, ...args)` — fire event
|
|
83
|
+
- `clear(event)` / `reset()` — cleanup
|
|
84
|
+
|
|
85
|
+
## `@stonyx/cron`
|
|
86
|
+
|
|
87
|
+
All scheduled and interval tasks. Never use raw `setInterval` or `setTimeout` for recurring work.
|
|
88
|
+
|
|
89
|
+
- `register(key, callback, interval, runOnInit?)` — schedule a recurring job
|
|
90
|
+
- `unregister(key)` — cancel a job
|
|
91
|
+
|
|
92
|
+
Configurable via `config/environment.js`:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
export default {
|
|
96
|
+
cron: { log: true }
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## `@stonyx/sockets`
|
|
101
|
+
|
|
102
|
+
WebSocket handlers. Class ordering: static properties → `server()` method → `client()` method.
|
|
103
|
+
|
|
104
|
+
Exports: `SocketServer`, `SocketClient`, `Handler`, `Sockets`
|
|
105
|
+
|
|
106
|
+
## `@stonyx/discord`
|
|
107
|
+
|
|
108
|
+
Discord bot with command and event handler auto-discovery. Class ordering: static properties → `data` / `static event` → `execute()` / `handle()`.
|
|
109
|
+
|
|
110
|
+
Exports: `DiscordBot`, `Command`, `EventHandler`, `Discord`, `chunkMessage`
|
|
111
|
+
|
|
112
|
+
## `@stonyx/oauth`
|
|
113
|
+
|
|
114
|
+
OAuth providers. Class ordering: constructor with `super()` → async methods → transform methods.
|
|
115
|
+
|
|
116
|
+
Key methods: `getAuthorizationUrl()`, `handleCallback()`, `getSession()`, `logout()`
|
|
117
|
+
|
|
118
|
+
## `@abofs/code-conventions`
|
|
119
|
+
|
|
120
|
+
Shared lint and formatting config. Import and spread; never define local rules.
|
|
121
|
+
|
|
122
|
+
Exports:
|
|
123
|
+
- `@abofs/code-conventions/eslint` — ESLint config
|
|
124
|
+
- `@abofs/code-conventions/prettier` — Prettier config
|
|
125
|
+
- `@abofs/code-conventions/eslint-ember` — Ember-specific ESLint
|
|
126
|
+
- `@abofs/code-conventions/template-lint` — Template linting
|
|
127
|
+
- `@abofs/code-conventions/lint-staged` — Lint-staged config
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Stonyx Framework Conventions
|
|
2
|
+
|
|
3
|
+
Universal rules that apply to every Stonyx project. Section-specific conventions are linked below.
|
|
4
|
+
|
|
5
|
+
## Universal Rules
|
|
6
|
+
|
|
7
|
+
- **One class per file** — each file exports a single default class (or function for transforms)
|
|
8
|
+
- **Constants**: shared across files → dedicated constants file; single-use → top of the consuming file
|
|
9
|
+
- **No `console.log` / `.warn` / `.error`** — use `log` from `stonyx/log` for all logging
|
|
10
|
+
- **Check `@stonyx/utils` first** — before reaching for Node built-ins or npm packages, check if `@stonyx/utils` already provides what you need (file I/O, object manipulation, string transforms, date/timestamp, promises, prompts). File issues on stonyx-utils for gaps rather than working around them
|
|
11
|
+
- **`fs` is prohibited** — use file utilities from `@stonyx/utils/file` instead
|
|
12
|
+
- **Always LTS Node or higher** — version specified in `.nvmrc`
|
|
13
|
+
- **Always pnpm** — never npm or yarn
|
|
14
|
+
- **Lint config**: import from `@abofs/code-conventions`; never define local lint rules
|
|
15
|
+
- **ES Modules** — all projects use `"type": "module"` and ESM imports
|
|
16
|
+
|
|
17
|
+
## Section Conventions
|
|
18
|
+
|
|
19
|
+
- [Project Structure](./project-structure.md) — directory layout, file organization, config conventions
|
|
20
|
+
- [Framework Modules](./framework-modules.md) — when to use which `@stonyx/*` module
|
|
21
|
+
- [ORM Conventions](./orm-conventions.md) — models, serializers, access control, transforms, hooks
|
|
22
|
+
- [REST Conventions](./rest-conventions.md) — REST server request classes and handlers
|
|
23
|
+
- [Discord Conventions](./discord-conventions.md) — Discord bot commands and event handlers
|
|
24
|
+
- [Testing Conventions](./testing-conventions.md) — test organization, patterns, and tooling
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# ORM Conventions
|
|
2
|
+
|
|
3
|
+
## Models
|
|
4
|
+
|
|
5
|
+
Models define the data structure with attributes and relationships. One model per file in `models/`.
|
|
6
|
+
|
|
7
|
+
**Property ordering:** `attr()` → `belongsTo()` / `hasMany()` → computed getters
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { Model, attr, belongsTo, hasMany } from '@stonyx/orm';
|
|
11
|
+
import { ANIMALS } from '../constants.js';
|
|
12
|
+
|
|
13
|
+
export default class AnimalModel extends Model {
|
|
14
|
+
type = attr('animal');
|
|
15
|
+
age = attr('number');
|
|
16
|
+
size = attr('string');
|
|
17
|
+
|
|
18
|
+
owner = belongsTo('owner');
|
|
19
|
+
traits = hasMany('trait');
|
|
20
|
+
|
|
21
|
+
get tag() {
|
|
22
|
+
const { owner, size } = this;
|
|
23
|
+
|
|
24
|
+
if (!owner) {
|
|
25
|
+
return `Unowned ${size} ${ANIMALS[this.type]}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return `${owner.id}'s ${size} ${ANIMALS[this.type]}`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Property Flattening
|
|
34
|
+
|
|
35
|
+
Every property must resolve to a final data type — no passthrough objects. If an API returns a nested object, create a sub-model with `belongsTo` rather than storing the raw object.
|
|
36
|
+
|
|
37
|
+
### Nested Models
|
|
38
|
+
|
|
39
|
+
Use nested directories for `belongsTo` children:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
models/
|
|
43
|
+
character.js
|
|
44
|
+
character/
|
|
45
|
+
relationship.js
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Serializers
|
|
49
|
+
|
|
50
|
+
Serializers map raw external data to the model shape. One serializer per model in `serializers/`.
|
|
51
|
+
|
|
52
|
+
The serializer has a single `map` property. It is responsible for mapping only, not fetching.
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import { Serializer } from '@stonyx/orm';
|
|
56
|
+
|
|
57
|
+
const COLOR_TRAIT_MAP = {
|
|
58
|
+
'black': 2,
|
|
59
|
+
'white': 3,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default class AnimalSerializer extends Serializer {
|
|
63
|
+
map = {
|
|
64
|
+
age: 'details.age',
|
|
65
|
+
size: 'details.c',
|
|
66
|
+
color: 'details.x',
|
|
67
|
+
owner: 'details.location.owner',
|
|
68
|
+
|
|
69
|
+
// Array value: [sourcePath, customHandler]
|
|
70
|
+
traits: ['details', ({ x:color }) => {
|
|
71
|
+
const traits = [{ id: 1, type: 'habitat', value: 'farm', category: 'physical' }];
|
|
72
|
+
|
|
73
|
+
const id = COLOR_TRAIT_MAP[color];
|
|
74
|
+
if (id) traits.push({ id, type: 'color', value: color, category: 'appearance' });
|
|
75
|
+
|
|
76
|
+
return traits;
|
|
77
|
+
}]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Clients vs Serializers
|
|
83
|
+
|
|
84
|
+
- **Clients** fetch, decrypt, and provide raw data from external sources
|
|
85
|
+
- **Serializers** map that raw data to the model shape
|
|
86
|
+
|
|
87
|
+
Clients handle I/O; serializers handle structure. Never mix the two.
|
|
88
|
+
|
|
89
|
+
## Access Control
|
|
90
|
+
|
|
91
|
+
Access classes define which models they govern and implement an `access()` method. One access file per logical access boundary in `access/`.
|
|
92
|
+
|
|
93
|
+
**Structure:** `models` property → `access()` method
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
export default class GlobalAccess {
|
|
97
|
+
models = ['owner', 'animal', 'trait', 'category', 'phone-number'];
|
|
98
|
+
|
|
99
|
+
access(request) {
|
|
100
|
+
const { originalUrl: url } = request;
|
|
101
|
+
|
|
102
|
+
// Return false to deny
|
|
103
|
+
if (url.endsWith('/owners/angela')) return false;
|
|
104
|
+
|
|
105
|
+
// Return a filter function for conditional access
|
|
106
|
+
if (url.endsWith('/owners')) return record => record.id !== 'angela';
|
|
107
|
+
if (url.endsWith('/animals')) return record => record.owner !== 'angela';
|
|
108
|
+
|
|
109
|
+
// Return permission array for full access
|
|
110
|
+
return ['read', 'create', 'update', 'delete'];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The `models` property accepts an array of model names, or `'*'` to match all models.
|
|
116
|
+
|
|
117
|
+
`access()` return values:
|
|
118
|
+
- `false` — deny access
|
|
119
|
+
- `function` — filter function applied to response records
|
|
120
|
+
- `string[]` — allowed operations (e.g., `['read', 'create', 'update', 'delete']`)
|
|
121
|
+
|
|
122
|
+
## Transforms
|
|
123
|
+
|
|
124
|
+
Transforms are default-exported functions (not classes) that convert values. One transform per model in `transforms/`.
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
import { ANIMALS } from '../constants.js';
|
|
128
|
+
|
|
129
|
+
const codeEnumMap = {}
|
|
130
|
+
|
|
131
|
+
for (let i = 0; i < ANIMALS.length; i++) codeEnumMap[ANIMALS[i]] = i;
|
|
132
|
+
|
|
133
|
+
export default function(value) {
|
|
134
|
+
return codeEnumMap[value] || 0;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Hooks
|
|
139
|
+
|
|
140
|
+
Use `beforeHook` / `afterHook` from `@stonyx/orm/hooks`. Place hook files in `hooks/`.
|
|
141
|
+
|
|
142
|
+
## DB Schema
|
|
143
|
+
|
|
144
|
+
The database schema extends `Model` and uses `hasMany` for each collection:
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
import { Model, hasMany } from '@stonyx/orm';
|
|
148
|
+
|
|
149
|
+
export default class DBModel extends Model {
|
|
150
|
+
owners = hasMany('owner');
|
|
151
|
+
animals = hasMany('animal');
|
|
152
|
+
traits = hasMany('trait');
|
|
153
|
+
categories = hasMany('category');
|
|
154
|
+
phoneNumbers = hasMany('phone-number');
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Located at `config/db-schema.js`, referenced from `config/environment.js`.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Project Structure
|
|
2
|
+
|
|
3
|
+
## Entry Point
|
|
4
|
+
|
|
5
|
+
The application entry point is always `app.js` at the project root. This file exports a default class that initializes the application.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import log from 'stonyx/log';
|
|
9
|
+
|
|
10
|
+
export default class App {
|
|
11
|
+
constructor() {
|
|
12
|
+
if (App.instance) return App.instance;
|
|
13
|
+
App.instance = this;
|
|
14
|
+
|
|
15
|
+
this.ready = this.init();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async init() {
|
|
19
|
+
log.info('Initializing Application');
|
|
20
|
+
// Application setup here
|
|
21
|
+
log.info('Application has been initialized');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## File Layout
|
|
27
|
+
|
|
28
|
+
Flat file structure at root level — non-directory project files live at the project root.
|
|
29
|
+
|
|
30
|
+
### Standard Directories
|
|
31
|
+
|
|
32
|
+
| Directory | Purpose | When to create |
|
|
33
|
+
|-----------|---------|----------------|
|
|
34
|
+
| `config/` | Configuration files | Always |
|
|
35
|
+
| `models/` | ORM model definitions | When using `@stonyx/orm` |
|
|
36
|
+
| `serializers/` | Data serializers | When using `@stonyx/orm` |
|
|
37
|
+
| `access/` | Access control definitions | When using `@stonyx/orm` |
|
|
38
|
+
| `transforms/` | Value transforms | When using `@stonyx/orm` |
|
|
39
|
+
| `hooks/` | Lifecycle hooks | When using `@stonyx/orm` |
|
|
40
|
+
| `requests/` | REST request handlers | When using `@stonyx/rest-server` |
|
|
41
|
+
| `crons/` | Scheduled tasks | When using `@stonyx/cron` |
|
|
42
|
+
| `clients/` | External API clients | When fetching external data |
|
|
43
|
+
| `test/`| Tests | Always |
|
|
44
|
+
| `utils.js` or `utils/` | Project-specific reusable logic | As needed (never duplicate `@stonyx/utils`) |
|
|
45
|
+
|
|
46
|
+
### Nested Model Directories
|
|
47
|
+
|
|
48
|
+
Use nested directories under `models/` for `belongsTo` child models:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
models/
|
|
52
|
+
character.js
|
|
53
|
+
character/
|
|
54
|
+
relationship.js
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Config Conventions
|
|
58
|
+
|
|
59
|
+
### `config/environment.js`
|
|
60
|
+
|
|
61
|
+
Destructure env vars at the top, apply defaults with `??`, export a plain object:
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
const {
|
|
65
|
+
CORS_ORIGIN,
|
|
66
|
+
DB_FILE,
|
|
67
|
+
NODE_ENV,
|
|
68
|
+
REST_PORT,
|
|
69
|
+
} = process.env;
|
|
70
|
+
|
|
71
|
+
const environment = NODE_ENV ?? 'development';
|
|
72
|
+
|
|
73
|
+
export default {
|
|
74
|
+
orm: {
|
|
75
|
+
db: {
|
|
76
|
+
file: DB_FILE ?? 'db.json',
|
|
77
|
+
schema: './config/db-schema.js'
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
restServer: {
|
|
81
|
+
origin: CORS_ORIGIN ?? '*',
|
|
82
|
+
port: REST_PORT ?? 3000
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Do NOT re-declare module defaults.** Only include config values that differ from the module's built-in defaults.
|
|
88
|
+
|
|
89
|
+
### `config/db-schema.js`
|
|
90
|
+
|
|
91
|
+
Extends `Model` and uses `hasMany` to define each collection:
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
import { Model, hasMany } from '@stonyx/orm';
|
|
95
|
+
|
|
96
|
+
export default class DBModel extends Model {
|
|
97
|
+
owners = hasMany('owner');
|
|
98
|
+
animals = hasMany('animal');
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Reference Projects
|
|
103
|
+
|
|
104
|
+
- `smart-lock-backend/` — full example with REST, ORM, sockets, crons, hooks
|
|
105
|
+
- `nextgoal-backend/` — example with models, transforms, filters, bot commands
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# REST Conventions
|
|
2
|
+
|
|
3
|
+
## Request Classes
|
|
4
|
+
|
|
5
|
+
One request class per file in `requests/`. Each class extends `Request` from `@stonyx/rest-server`.
|
|
6
|
+
|
|
7
|
+
**Class ordering:** properties → `handlers` object → `auth()` hook → validation/middleware methods
|
|
8
|
+
|
|
9
|
+
### Public Request (no auth)
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { Request } from '@stonyx/rest-server';
|
|
13
|
+
|
|
14
|
+
export default class PublicRequest extends Request {
|
|
15
|
+
testProp = 'stonyx';
|
|
16
|
+
|
|
17
|
+
handlers = {
|
|
18
|
+
get: {
|
|
19
|
+
'/': (_request, _state) => {
|
|
20
|
+
return { data: 'foo' };
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
'/url-params/:x/:y/:z': ({ params }, _state) => {
|
|
24
|
+
return params;
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// Middleware chaining: [middlewareFn, handlerFn]
|
|
28
|
+
'/foo': [this.validationSuccessSample, (_request, state) => {
|
|
29
|
+
return { data: state };
|
|
30
|
+
}],
|
|
31
|
+
|
|
32
|
+
'/fail': [this.validationFailureSample, (_request, _state) => {
|
|
33
|
+
return { unreachable: 'response' };
|
|
34
|
+
}],
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
validationSuccessSample(_request, state) {
|
|
39
|
+
state.newProp = 'bar';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
validationFailureSample() {
|
|
43
|
+
return 504; // returning a status code rejects the request
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Private Request (with auth)
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
import { Request } from '@stonyx/rest-server';
|
|
52
|
+
|
|
53
|
+
export default class PrivateRequest extends Request {
|
|
54
|
+
handlers = {
|
|
55
|
+
get: {
|
|
56
|
+
'/success': (_request, _state) => {
|
|
57
|
+
return { data: 'foo' };
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
'/failure': (_request, _state) => {
|
|
61
|
+
return { data: 'foo' };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
auth(request, _state) {
|
|
67
|
+
if (request.path === '/failure') return 505;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Key Patterns
|
|
73
|
+
|
|
74
|
+
### `handlers` Object
|
|
75
|
+
|
|
76
|
+
Keyed by HTTP method (`get`, `post`, `put`, `delete`), each containing route-to-handler mappings.
|
|
77
|
+
|
|
78
|
+
Handler signature: `(request, state) => responseData`
|
|
79
|
+
|
|
80
|
+
### Middleware Chaining
|
|
81
|
+
|
|
82
|
+
Use `[middlewareFn, handlerFn]` arrays to chain middleware before a handler. The middleware function receives `(request, state)` and can:
|
|
83
|
+
- Mutate `state` to pass data to the handler
|
|
84
|
+
- Return a status code number to reject the request early
|
|
85
|
+
|
|
86
|
+
### `auth()` Hook
|
|
87
|
+
|
|
88
|
+
Runs before all handlers in the class. Return an error status code to reject the request. If `auth()` returns nothing (undefined), the request proceeds.
|
|
89
|
+
|
|
90
|
+
### Route Parameters
|
|
91
|
+
|
|
92
|
+
Express-style route params (`:param`) are available via `request.params`.
|
|
93
|
+
|
|
94
|
+
### Binding
|
|
95
|
+
|
|
96
|
+
Handler functions defined as arrow functions or class methods have access to `this` (the request class instance).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Testing Conventions
|
|
2
|
+
|
|
3
|
+
## Directory Structure
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
test/
|
|
7
|
+
unit/
|
|
8
|
+
integration/
|
|
9
|
+
acceptance/
|
|
10
|
+
config/
|
|
11
|
+
environment.js # test config overrides
|
|
12
|
+
sample/ # fixtures and mocks (alternative: test/mocks/)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## File Naming
|
|
16
|
+
|
|
17
|
+
All test files use the `*-test.js` suffix: `animal-test.js`, `public-request-test.js`.
|
|
18
|
+
|
|
19
|
+
## Test Framework
|
|
20
|
+
|
|
21
|
+
- **QUnit** — test framework
|
|
22
|
+
- **Sinon** — stubs, spies, and mocks
|
|
23
|
+
|
|
24
|
+
Run tests with `stonyx test` (alias: `stonyx t`), which:
|
|
25
|
+
1. Sets `NODE_ENV=test`
|
|
26
|
+
2. Loads test setup (bootstrap with test config overrides)
|
|
27
|
+
3. Runs QUnit against `test/**/*-test.js`
|
|
28
|
+
|
|
29
|
+
## Test Config Overrides
|
|
30
|
+
|
|
31
|
+
Place test-specific config at `test/config/environment.js`. Stonyx auto-merges these overrides when running in test mode.
|
|
32
|
+
|
|
33
|
+
## Sample / Fixture Files
|
|
34
|
+
|
|
35
|
+
Each stonyx module's `test/sample/` directory contains reference implementations. Mirror this structure in your project tests:
|
|
36
|
+
|
|
37
|
+
- `test/sample/models/` — model fixtures
|
|
38
|
+
- `test/sample/serializers/` — serializer fixtures
|
|
39
|
+
- `test/sample/access/` — access control fixtures
|
|
40
|
+
- `test/sample/transforms/` — transform fixtures
|
|
41
|
+
- `test/sample/requests/` — request handler fixtures
|
|
42
|
+
|
|
43
|
+
The file ordering in tests should mirror the sample files from each module's `test/sample/` directory.
|
|
44
|
+
|
|
45
|
+
## Running Tests
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Run all tests
|
|
49
|
+
stonyx test
|
|
50
|
+
|
|
51
|
+
# Run specific test file
|
|
52
|
+
stonyx test test/unit/animal-test.js
|
|
53
|
+
|
|
54
|
+
# Run specific glob
|
|
55
|
+
stonyx test "test/integration/**/*-test.js"
|
|
56
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stonyx",
|
|
3
|
-
"version": "0.2.3-beta.
|
|
3
|
+
"version": "0.2.3-beta.7",
|
|
4
4
|
"description": "Base stonyx framework module",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"node-chronicle": "^0.2.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@stonyx/utils": "0.2.3-beta.
|
|
36
|
+
"@stonyx/utils": "0.2.3-beta.5",
|
|
37
37
|
"qunit": "^2.24.1",
|
|
38
38
|
"sinon": "^21.0.0"
|
|
39
39
|
},
|
package/src/cli/help.js
CHANGED
|
@@ -1,29 +1,48 @@
|
|
|
1
|
-
export default async function help({ args, builtInCommands, loadModuleCommands } = {}) {
|
|
2
|
-
console.log('\nUsage: stonyx <command> [...args]\n');
|
|
3
|
-
console.log('Commands:\n');
|
|
4
|
-
|
|
5
|
-
if (builtInCommands) {
|
|
6
|
-
for (const [name, { description }] of Object.entries(builtInCommands)) {
|
|
7
|
-
console.log(` ${name.padEnd(20)} ${description}`);
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
if (loadModuleCommands) {
|
|
12
|
-
try {
|
|
13
|
-
const moduleCommands = await loadModuleCommands();
|
|
14
|
-
const entries = Object.entries(moduleCommands);
|
|
15
|
-
|
|
16
|
-
if (entries.length) {
|
|
17
|
-
console.log('\nModule commands:\n');
|
|
18
|
-
|
|
19
|
-
for (const [name, { description }] of entries) {
|
|
20
|
-
console.log(` ${name.padEnd(20)} ${description}`);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
} catch {
|
|
24
|
-
// Module commands not available (e.g., no project context)
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
console.log('\nAliases: s=serve, t=test, h=help\n');
|
|
29
|
-
|
|
1
|
+
export default async function help({ args, builtInCommands, loadModuleCommands } = {}) {
|
|
2
|
+
console.log('\nUsage: stonyx <command> [...args]\n');
|
|
3
|
+
console.log('Commands:\n');
|
|
4
|
+
|
|
5
|
+
if (builtInCommands) {
|
|
6
|
+
for (const [name, { description }] of Object.entries(builtInCommands)) {
|
|
7
|
+
console.log(` ${name.padEnd(20)} ${description}`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (loadModuleCommands) {
|
|
12
|
+
try {
|
|
13
|
+
const moduleCommands = await loadModuleCommands();
|
|
14
|
+
const entries = Object.entries(moduleCommands);
|
|
15
|
+
|
|
16
|
+
if (entries.length) {
|
|
17
|
+
console.log('\nModule commands:\n');
|
|
18
|
+
|
|
19
|
+
for (const [name, { description }] of entries) {
|
|
20
|
+
console.log(` ${name.padEnd(20)} ${description}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
// Module commands not available (e.g., no project context)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log('\nAliases: s=serve, t=test, n=new, h=help\n');
|
|
29
|
+
|
|
30
|
+
console.log('Project conventions:\n');
|
|
31
|
+
console.log(' Entry point: app.js');
|
|
32
|
+
console.log(' Config: config/environment.js');
|
|
33
|
+
console.log(' DB schema: config/db-schema.js');
|
|
34
|
+
console.log(' Models: models/');
|
|
35
|
+
console.log(' Serializers: serializers/');
|
|
36
|
+
console.log(' Access control: access/');
|
|
37
|
+
console.log(' Transforms: transforms/');
|
|
38
|
+
console.log(' Hooks: hooks/');
|
|
39
|
+
console.log(' REST requests: requests/');
|
|
40
|
+
console.log(' Cron jobs: crons/');
|
|
41
|
+
console.log(' Tests: test/{unit,integration,acceptance}/');
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log(' Logging: import log from \'stonyx/log\' (never console.log)');
|
|
44
|
+
console.log(' Utilities: import from \'@stonyx/utils/*\' (never raw fs)');
|
|
45
|
+
console.log(' Lint config: import from \'@abofs/code-conventions\'');
|
|
46
|
+
console.log(' Package manager: pnpm');
|
|
47
|
+
console.log('');
|
|
48
|
+
}
|
package/src/cli/new.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { confirm, prompt } from '@stonyx/utils/prompt';
|
|
2
|
+
import { createFile, createDirectory, copyFile, fileExists } from '@stonyx/utils/file';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const MODULE_OPTIONS = [
|
|
8
|
+
{
|
|
9
|
+
question: 'Will this project need a REST server?',
|
|
10
|
+
package: '@stonyx/rest-server',
|
|
11
|
+
dirs: ['requests']
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
question: 'Will this project need WebSockets?',
|
|
15
|
+
package: '@stonyx/sockets',
|
|
16
|
+
dirs: ['socket-handlers']
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
question: 'Will this project need data management?',
|
|
20
|
+
package: '@stonyx/orm',
|
|
21
|
+
dirs: ['models', 'serializers', 'access', 'transforms', 'hooks'],
|
|
22
|
+
files: { 'config/db-schema.js': generateDbSchema }
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
question: 'Will this project need scheduled tasks (cron)?',
|
|
26
|
+
package: '@stonyx/cron',
|
|
27
|
+
dirs: ['crons']
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
question: 'Will this project need OAuth?',
|
|
31
|
+
package: '@stonyx/oauth'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
question: 'Will this project need pub/sub events?',
|
|
35
|
+
package: '@stonyx/events'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
question: 'Will this project need a Discord bot?',
|
|
39
|
+
package: '@stonyx/discord',
|
|
40
|
+
dirs: ['discord-commands', 'discord-events']
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function generateDbSchema() {
|
|
45
|
+
return `import { Model, hasMany } from '@stonyx/orm';
|
|
46
|
+
|
|
47
|
+
export default class DBModel extends Model {
|
|
48
|
+
// Define your collections here
|
|
49
|
+
// examples = hasMany('example');
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function generatePackageJson(name, selectedModules) {
|
|
55
|
+
const devDependencies = { stonyx: 'latest' };
|
|
56
|
+
|
|
57
|
+
for (const mod of selectedModules) {
|
|
58
|
+
devDependencies[mod.package] = 'latest';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Sort dependencies alphabetically
|
|
62
|
+
const sorted = Object.fromEntries(
|
|
63
|
+
Object.entries(devDependencies).sort(([a], [b]) => a.localeCompare(b))
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return JSON.stringify({
|
|
67
|
+
name,
|
|
68
|
+
version: '0.1.0',
|
|
69
|
+
type: 'module',
|
|
70
|
+
private: true,
|
|
71
|
+
scripts: {
|
|
72
|
+
start: 'stonyx serve',
|
|
73
|
+
test: 'stonyx test'
|
|
74
|
+
},
|
|
75
|
+
devDependencies: sorted
|
|
76
|
+
}, null, 2) + '\n';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function generateAppJs() {
|
|
80
|
+
return `import log from 'stonyx/log';
|
|
81
|
+
|
|
82
|
+
export default class App {
|
|
83
|
+
constructor() {
|
|
84
|
+
if (App.instance) return App.instance;
|
|
85
|
+
App.instance = this;
|
|
86
|
+
|
|
87
|
+
this.ready = this.init();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async init() {
|
|
91
|
+
log.info('Initializing Application');
|
|
92
|
+
|
|
93
|
+
// Application setup here
|
|
94
|
+
|
|
95
|
+
log.info('Application has been initialized');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function generateEnvironmentJs() {
|
|
102
|
+
return `export default {
|
|
103
|
+
}
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function generateEnvironmentExampleJs() {
|
|
108
|
+
return `// Copy this file to environment.js and fill in your values
|
|
109
|
+
// All values should use environment variables with ?? fallback defaults
|
|
110
|
+
|
|
111
|
+
const {
|
|
112
|
+
NODE_ENV,
|
|
113
|
+
} = process.env;
|
|
114
|
+
|
|
115
|
+
const environment = NODE_ENV ?? 'development';
|
|
116
|
+
|
|
117
|
+
export default {
|
|
118
|
+
}
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function generateGitignore() {
|
|
123
|
+
return `node_modules/
|
|
124
|
+
.env
|
|
125
|
+
db.json
|
|
126
|
+
*.log
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function runPnpmInstall(projectDir) {
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const child = spawn('pnpm', ['install'], {
|
|
133
|
+
cwd: projectDir,
|
|
134
|
+
stdio: 'inherit'
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
child.on('close', code => {
|
|
138
|
+
if (code === 0) resolve();
|
|
139
|
+
else reject(new Error(`pnpm install exited with code ${code}`));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
child.on('error', reject);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export default async function newCommand({ args }) {
|
|
147
|
+
let appName = args[0];
|
|
148
|
+
|
|
149
|
+
if (!appName) {
|
|
150
|
+
appName = await prompt('Project name:');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!appName) {
|
|
154
|
+
console.error('Project name is required.');
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const projectDir = path.resolve(process.cwd(), appName);
|
|
159
|
+
|
|
160
|
+
if (await fileExists(projectDir)) {
|
|
161
|
+
console.error(`Directory "${appName}" already exists.`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log(`\nScaffolding new Stonyx project: ${appName}\n`);
|
|
166
|
+
|
|
167
|
+
// Prompt for module selection
|
|
168
|
+
const selectedModules = [];
|
|
169
|
+
|
|
170
|
+
for (const mod of MODULE_OPTIONS) {
|
|
171
|
+
if (await confirm(mod.question)) {
|
|
172
|
+
selectedModules.push(mod);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log('\nCreating project structure...\n');
|
|
177
|
+
|
|
178
|
+
// Create project directory
|
|
179
|
+
await createDirectory(projectDir);
|
|
180
|
+
|
|
181
|
+
// Copy .nvmrc from monorepo root
|
|
182
|
+
const monorepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
183
|
+
const nvmrcSource = path.join(monorepoRoot, '.nvmrc');
|
|
184
|
+
|
|
185
|
+
if (await fileExists(nvmrcSource)) {
|
|
186
|
+
await copyFile(nvmrcSource, path.join(projectDir, '.nvmrc'));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Generate core files
|
|
190
|
+
await createFile(path.join(projectDir, 'package.json'), generatePackageJson(appName, selectedModules));
|
|
191
|
+
await createFile(path.join(projectDir, 'app.js'), generateAppJs());
|
|
192
|
+
await createFile(path.join(projectDir, '.gitignore'), generateGitignore());
|
|
193
|
+
|
|
194
|
+
// Create config directory and files
|
|
195
|
+
await createFile(path.join(projectDir, 'config', 'environment.js'), generateEnvironmentJs());
|
|
196
|
+
await createFile(path.join(projectDir, 'config', 'environment.example.js'), generateEnvironmentExampleJs());
|
|
197
|
+
|
|
198
|
+
// Create module-specific directories and files
|
|
199
|
+
for (const mod of selectedModules) {
|
|
200
|
+
if (mod.dirs) {
|
|
201
|
+
for (const dir of mod.dirs) {
|
|
202
|
+
await createDirectory(path.join(projectDir, dir));
|
|
203
|
+
// Create .gitkeep so empty dirs are tracked
|
|
204
|
+
await createFile(path.join(projectDir, dir, '.gitkeep'), '');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (mod.files) {
|
|
209
|
+
for (const [filePath, generator] of Object.entries(mod.files)) {
|
|
210
|
+
await createFile(path.join(projectDir, filePath), generator());
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Create test structure
|
|
216
|
+
await createDirectory(path.join(projectDir, 'test', 'unit'));
|
|
217
|
+
await createFile(path.join(projectDir, 'test', 'unit', '.gitkeep'), '');
|
|
218
|
+
await createDirectory(path.join(projectDir, 'test', 'integration'));
|
|
219
|
+
await createFile(path.join(projectDir, 'test', 'integration', '.gitkeep'), '');
|
|
220
|
+
await createDirectory(path.join(projectDir, 'test', 'acceptance'));
|
|
221
|
+
await createFile(path.join(projectDir, 'test', 'acceptance', '.gitkeep'), '');
|
|
222
|
+
|
|
223
|
+
// Create test config
|
|
224
|
+
await createFile(path.join(projectDir, 'test', 'config', 'environment.js'), `export default {\n // Test-specific config overrides\n}\n`);
|
|
225
|
+
|
|
226
|
+
console.log('Installing dependencies...\n');
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
await runPnpmInstall(projectDir);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error('Failed to install dependencies. Run `pnpm install` manually in the project directory.');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log(`\n✓ Project "${appName}" created successfully!`);
|
|
235
|
+
console.log(`\n cd ${appName}`);
|
|
236
|
+
console.log(` stonyx serve\n`);
|
|
237
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
import serve from './cli/serve.js';
|
|
4
4
|
import test from './cli/test.js';
|
|
5
5
|
import help from './cli/help.js';
|
|
6
|
+
import newCommand from './cli/new.js';
|
|
6
7
|
import loadModuleCommands from './cli/load-commands.js';
|
|
7
8
|
|
|
8
9
|
try { process.loadEnvFile(); } catch { /* no .env file */ }
|
|
9
10
|
|
|
10
|
-
const aliases = { s: 'serve', t: 'test', h: 'help' };
|
|
11
|
+
const aliases = { s: 'serve', t: 'test', h: 'help', n: 'new' };
|
|
11
12
|
|
|
12
13
|
const builtInCommands = {
|
|
13
14
|
serve: { description: 'Bootstrap Stonyx and run the app', run: serve },
|
|
14
15
|
test: { description: 'Bootstrap Stonyx in test mode and run tests', run: test },
|
|
16
|
+
new: { description: 'Scaffold a new Stonyx project', run: newCommand },
|
|
15
17
|
help: { description: 'Show available commands', run: help }
|
|
16
18
|
};
|
|
17
19
|
|