stonyx 0.2.3-alpha.7 → 0.2.3-alpha.71

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.
Files changed (69) hide show
  1. package/README.md +30 -7
  2. package/config/{environment.js → environment copy.ts } +16 -12
  3. package/dist/cli/help.d.ts +8 -0
  4. package/dist/cli/help.js +43 -0
  5. package/dist/cli/load-commands.d.ts +10 -0
  6. package/dist/cli/load-commands.js +49 -0
  7. package/dist/cli/new.d.ts +72 -0
  8. package/dist/cli/new.js +422 -0
  9. package/dist/cli/serve.d.ts +6 -0
  10. package/dist/cli/serve.js +49 -0
  11. package/dist/cli/test-setup.d.ts +1 -0
  12. package/dist/cli/test-setup.js +30 -0
  13. package/dist/cli/test.d.ts +4 -0
  14. package/dist/cli/test.js +23 -0
  15. package/dist/cli.d.ts +2 -0
  16. package/dist/cli.js +53 -0
  17. package/dist/exports/config.d.ts +2 -0
  18. package/dist/exports/config.js +2 -0
  19. package/dist/exports/log.d.ts +2 -0
  20. package/{src → dist}/exports/log.js +1 -2
  21. package/dist/exports/test-helpers.d.ts +5 -0
  22. package/dist/exports/test-helpers.js +7 -0
  23. package/dist/lifecycle.d.ts +7 -0
  24. package/dist/lifecycle.js +18 -0
  25. package/dist/main.d.ts +22 -0
  26. package/dist/main.js +102 -0
  27. package/dist/modules.d.ts +5 -0
  28. package/dist/modules.js +186 -0
  29. package/dist/util/duplicate-core.d.ts +186 -0
  30. package/dist/util/duplicate-core.js +377 -0
  31. package/dist/util/import-config.d.ts +52 -0
  32. package/dist/util/import-config.js +256 -0
  33. package/dist/util/resolve-entry-point.d.ts +1 -0
  34. package/dist/util/resolve-entry-point.js +14 -0
  35. package/dist/util/resolve-module-name.d.ts +14 -0
  36. package/dist/util/resolve-module-name.js +33 -0
  37. package/package.json +41 -12
  38. package/scripts/postinstall.js +24 -2
  39. package/.claude/index.md +0 -96
  40. package/.github/workflows/ci.yml +0 -16
  41. package/.github/workflows/publish.yml +0 -51
  42. package/docs/api.md +0 -89
  43. package/docs/cli.md +0 -78
  44. package/docs/configuration.md +0 -72
  45. package/docs/conventions/discord-conventions.md +0 -62
  46. package/docs/conventions/framework-modules.md +0 -127
  47. package/docs/conventions/index.md +0 -24
  48. package/docs/conventions/orm-conventions.md +0 -190
  49. package/docs/conventions/project-structure.md +0 -105
  50. package/docs/conventions/rest-conventions.md +0 -96
  51. package/docs/conventions/testing-conventions.md +0 -56
  52. package/docs/developing-modules.md +0 -108
  53. package/docs/index.md +0 -15
  54. package/docs/lifecycle.md +0 -47
  55. package/docs/logging.md +0 -52
  56. package/docs/modules.md +0 -78
  57. package/docs/testing.md +0 -64
  58. package/src/cli/help.js +0 -48
  59. package/src/cli/load-commands.js +0 -56
  60. package/src/cli/new.js +0 -237
  61. package/src/cli/serve.js +0 -38
  62. package/src/cli/test-setup.js +0 -8
  63. package/src/cli/test.js +0 -27
  64. package/src/cli.js +0 -63
  65. package/src/exports/config.js +0 -3
  66. package/src/exports/test-helpers.js +0 -8
  67. package/src/lifecycle.js +0 -17
  68. package/src/main.js +0 -101
  69. package/src/modules.js +0 -112
@@ -0,0 +1,256 @@
1
+ import { existsSync, realpathSync } from 'fs';
2
+ import { fileURLToPath, pathToFileURL } from 'url';
3
+ /**
4
+ * Extensions `importConfig` will load, in preference order.
5
+ *
6
+ * `.ts` first. `stonyx@0.2.3-beta.62` shipped exactly this list; some release in
7
+ * `(beta.62, beta.82]` reduced it to a hard-coded `.js`, which is the regression
8
+ * abofs/stonyx#105 reports. Three places in this repo disagreed with that
9
+ * reduction and none were changed with it:
10
+ * - `docs/configuration.md:7,24` documents `.ts` as preferred and describes
11
+ * this exact algorithm verbatim;
12
+ * - `scripts/postinstall.js` writes `config/environment.ts` whenever the
13
+ * consumer has a `tsconfig.json` — the framework wrote the file its own
14
+ * loader then refused to read, which is how every TypeScript consumer (the
15
+ * shape `docs/cli.md:29` calls the default) failed to boot at all;
16
+ * - `src/util/resolve-entry-point.ts` — the sibling resolver, for `app.{ts,js}`
17
+ * — still prefers `.ts` today.
18
+ */
19
+ const LOADABLE_EXTENSIONS = ['ts', 'js'];
20
+ /**
21
+ * Extensions a consumer plausibly writes a config as, that this loader does not
22
+ * RESOLVE. Their only purpose is to be DETECTED: see `CONFIG_NOT_LOADABLE_PREFIX`.
23
+ *
24
+ * Named for what is actually true of them. `.mjs`, `.cjs` and `.json` are all
25
+ * perfectly readable by Node — `import()` handles every one of them today.
26
+ * Nothing here is unreadable; they are simply not in `LOADABLE_EXTENSIONS`, so
27
+ * this loader never forms a path with them, and a consumer who writes one gets
28
+ * "config present but not loadable" instead of a silent "not found". The old
29
+ * name, UNREADABLE_EXTENSIONS, asserted a property of Node that is false, and
30
+ * invited a future reader to "fix" it by adding them to the resolver.
31
+ */
32
+ const UNRESOLVED_EXTENSIONS = ['mts', 'cts', 'tsx', 'jsx', 'mjs', 'cjs', 'json'];
33
+ /**
34
+ * "There is no config here." Callers are allowed to treat this as benign —
35
+ * `src/main.ts` does, for the optional `test/config/environment.*` override.
36
+ *
37
+ * `main.ts` matches on this exact string, so it is exported rather than
38
+ * duplicated: the swallow and the thrower must not be able to drift apart.
39
+ */
40
+ export const CONFIG_NOT_FOUND_PREFIX = 'Config not found:';
41
+ /**
42
+ * "There is a config here and I declined to load it." Deliberately NOT prefixed
43
+ * with `CONFIG_NOT_FOUND_PREFIX`, because `main.ts`'s `NODE_ENV=test` catch
44
+ * swallows that prefix and must not swallow this.
45
+ *
46
+ * This is install-and-boot invariant I2 — "no silent
47
+ * decline". Before #105 the two states were literally indistinguishable:
48
+ * measured against `stonyx@0.2.3-beta.95`, a consumer with `config/environment.ts`
49
+ * and the same consumer after `mv config/environment.js config/environment.xyz`
50
+ * both produced the byte-identical `Config not found: .../config/environment.js`.
51
+ */
52
+ export const CONFIG_NOT_LOADABLE_PREFIX = 'Config present but not loadable:';
53
+ /**
54
+ * Node error codes that mean "I can see this file and I refuse to load it",
55
+ * as distinct from "your config threw" or "your config imports something
56
+ * missing". Only these are re-framed; everything else propagates untouched so a
57
+ * genuine syntax error in a consumer's config still reads as one.
58
+ *
59
+ * `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING` is the load-bearing member:
60
+ * Node refuses to type-strip anything under `node_modules`, so a `@stonyx/*`
61
+ * module that ships `config/environment.ts` crashes every consumer. That rule
62
+ * is unchanged by this fix and is why `docs/conventions/framework-modules.md`
63
+ * still requires modules to ship `.js` — but the failure is now named.
64
+ */
65
+ const FILE_TYPE_REFUSAL_CODES = new Set([
66
+ 'ERR_UNKNOWN_FILE_EXTENSION',
67
+ 'ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING',
68
+ ]);
69
+ /**
70
+ * The absolute POSIX path a Node refusal names, in the two shapes it names it.
71
+ *
72
+ * Both refusal codes name the file they refused, but differently — measured on
73
+ * node v24.13.0 at this head:
74
+ *
75
+ * ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING
76
+ * Stripping types is currently unsupported for files under node_modules,
77
+ * for "/private/tmp/x/node_modules/dep/thing.ts"
78
+ * ERR_UNKNOWN_FILE_EXTENSION
79
+ * Unknown file extension ".ts" for /private/tmp/x/environment.ts
80
+ *
81
+ * In BOTH shapes the path is the last thing in the message, and both patterns
82
+ * are GREEDY, so each runs to the message tail rather than being bounded by a
83
+ * character class on the path body: the quoted one to the message-final quote,
84
+ * the bare one to the last `.ext` in the message. That is the whole mechanism —
85
+ * measured, an explicit `\s*$` anchor on either one is an equivalent mutant,
86
+ * so it is not there. The previous single pattern instead excluded space, `(`,
87
+ * `)`, `'` and `"` from the body,
88
+ * which meant a config under `~/My Project/` either failed extraction outright
89
+ * or backtracked onto an earlier `.` and yielded a truncated non-path.
90
+ * Measured end-to-end through `dist/` before this change, two consumers one
91
+ * character apart, each with a good `.js` config that nested-imports a `.ts`
92
+ * under `node_modules`:
93
+ *
94
+ * .../plain/config/environment.js -> raw ERR_UNSUPPORTED_...
95
+ * naming node_modules/dep/thing.ts
96
+ * .../My Project/config/environment.js -> "Config present but not loadable:
97
+ * .../My Project/config/environment.js"
98
+ *
99
+ * The second names a `.js` that loaded and ran perfectly. Anchoring on the
100
+ * tail extracts both, and the quoted form tolerates quotes inside the path
101
+ * because it runs to the message-final quote rather than to the first one.
102
+ *
103
+ * `(?:file:\/\/)?` is not defensive padding. Node names the refused file as a
104
+ * `file://` URL whenever the package scope containing it declares a `type`,
105
+ * and as a bare path when it does not -- one function
106
+ * (`stripTypeScriptModuleTypes`) reached from two call sites,
107
+ * `internal/modules/esm/translators:654,663` with the URL and
108
+ * `internal/modules/esm/get_format:148` with `fileURLToPath(url)`. Scope
109
+ * lookup stops at `node_modules`, so for `src/modules.ts:103` the deciding
110
+ * file is the MODULE's `package.json` -- and every `@stonyx/*` module declares
111
+ * `type: 'module'`, as does the app `src/cli/new.ts:81` scaffolds. The URL
112
+ * shape is the default case. See `test/unit/import-config-test.ts` for the
113
+ * measured matrix.
114
+ *
115
+ * Still POSIX-only, and still not exhaustive — a Windows path is not extracted,
116
+ * and neither is a message with trailing text after the path. That is now SAFE
117
+ * rather than merely degraded: see `refusalIsAboutTheConfig`.
118
+ */
119
+ const QUOTED_REFUSED_PATH = /["']((?:file:\/\/)?\/.+)["']/;
120
+ const BARE_REFUSED_PATH = /(?:^|\s)((?:file:\/\/)?\/.+\.[A-Za-z0-9]+)/;
121
+ /** Resolves symlinks so `/tmp/...` and `/private/tmp/...` compare equal. */
122
+ function realPath(path) {
123
+ try {
124
+ return realpathSync(path);
125
+ }
126
+ catch {
127
+ return path;
128
+ }
129
+ }
130
+ /**
131
+ * The path a refusal names, or `null` when no path can be extracted from it.
132
+ *
133
+ * `fileURLToPath` is contained for the same reason `realPath` is: this runs
134
+ * INSIDE `importConfig`'s catch handler, so anything it throws replaces the
135
+ * refusal being handled and takes its `cause` with it — the exact loss
136
+ * invariant I2 exists to prevent. Regex-matchable inputs that throw exist and
137
+ * were measured at this head: `file:///a%2Fb.ts` -> `ERR_INVALID_FILE_URL_PATH`,
138
+ * `file:///%.ts` and `file:///a%zz.ts` -> bare `URIError: URI malformed`.
139
+ *
140
+ * Node's own emissions never produce them — `pathToFileURL` percent-encodes a
141
+ * literal `%` and never emits a bare `%2F` for a POSIX filename — but the
142
+ * message is only USUALLY Node's. `refusalIsAboutTheConfig` is exported public
143
+ * API with no input validation, and a `register()` / `--import` loader hook
144
+ * (this suite itself runs under `tsx`) can throw `ERR_UNKNOWN_FILE_EXTENSION`
145
+ * with any message it likes. Driven end-to-end through `dist/` with such a
146
+ * hook before this containment, the consumer got `ERR_INVALID_FILE_URL_PATH:
147
+ * File URL path must not include encoded / characters` with `cause` undefined,
148
+ * and the refusal it was actually handling was gone.
149
+ *
150
+ * An unparseable URL returns `null`, which the predicate reads as "cannot
151
+ * tell" -> `false` -> the original error propagates untouched. Same fail
152
+ * direction as every other unknown in here.
153
+ *
154
+ * Quoted shape first: it is the more specific of the two. Measured at this
155
+ * head the order is an EQUIVALENT mutant — swapping it leaves the suite at
156
+ * 130/0, because neither measured message shape matches both patterns — so the
157
+ * order is a statement of intent, not a load-bearing branch. Recorded here so
158
+ * the next mutation run does not re-chase it.
159
+ */
160
+ function refusedPathFrom(message) {
161
+ const refused = QUOTED_REFUSED_PATH.exec(message)?.[1] ?? BARE_REFUSED_PATH.exec(message)?.[1];
162
+ if (!refused)
163
+ return null;
164
+ if (!refused.startsWith('file://'))
165
+ return refused;
166
+ try {
167
+ return fileURLToPath(refused);
168
+ }
169
+ catch {
170
+ return null;
171
+ }
172
+ }
173
+ /**
174
+ * Did this runtime refuse OUR config file, or something our config imported?
175
+ *
176
+ * `FILE_TYPE_REFUSAL_CODES.has(code)` alone cannot tell. A config that loaded
177
+ * and ran perfectly well, but whose own nested `import()` hit a `.ts` under
178
+ * `node_modules`, throws the SAME code — and was reported as "this Node runtime
179
+ * refused to load your config", naming a file that is fine. That misdirection
180
+ * points the consumer at the one file that is not the problem, and the likeliest
181
+ * instance of it is the exact `.ts`-under-node_modules case this loader's own
182
+ * docs tell people to look for.
183
+ *
184
+ * FAIL DIRECTION — this is the property, not an implementation detail.
185
+ * `true` means "re-frame this as a refusal of the config", which is exactly the
186
+ * F-3 misdirection above. So when no path can be extracted this returns
187
+ * `false`, and the caller rethrows Node's own error untouched, `cause` and all.
188
+ * The consumer then reads what actually happened instead of a friendly sentence
189
+ * about the wrong file. This is what makes every future gap in the two patterns
190
+ * harmless: an unparseable message costs the I2 re-framing, not correctness.
191
+ *
192
+ * It previously returned `true` here, on the reasoning that keeping the loud
193
+ * re-frame mattered more than getting the filename right. Measured, that is
194
+ * backwards: the raw error is loud too — `main.ts`'s `NODE_ENV=test` swallow
195
+ * only absorbs `CONFIG_NOT_FOUND_PREFIX`, which a Node refusal never starts
196
+ * with — so failing this way loses nothing and stops naming the wrong file.
197
+ *
198
+ * Exported ONLY so its branches can be reached directly. Driving it through
199
+ * `importConfig` reaches the two path-comparison outcomes and nothing else.
200
+ * A predicate inside the guard that makes failures loud does not get to have
201
+ * branches no test can reach.
202
+ */
203
+ export function refusalIsAboutTheConfig(message, configPath) {
204
+ const refusedPath = refusedPathFrom(message);
205
+ if (refusedPath === null)
206
+ return false;
207
+ return refusedPath === configPath || realPath(refusedPath) === realPath(configPath);
208
+ }
209
+ export async function importConfig(basePath) {
210
+ const matches = LOADABLE_EXTENSIONS.filter(ext => existsSync(`${basePath}.${ext}`));
211
+ if (matches.length === 0) {
212
+ const declined = UNRESOLVED_EXTENSIONS.filter(ext => existsSync(`${basePath}.${ext}`));
213
+ if (declined.length > 0) {
214
+ throw new Error(`${CONFIG_NOT_LOADABLE_PREFIX} ${basePath}.${declined.join(` and ${basePath}.`)} ` +
215
+ `exists, but Stonyx loads ${basePath}.ts or ${basePath}.js only. ` +
216
+ `Rename or compile it — this is not a missing config.`);
217
+ }
218
+ throw new Error(`${CONFIG_NOT_FOUND_PREFIX} ${basePath}.{ts,js}`);
219
+ }
220
+ if (matches.length > 1) {
221
+ console.warn(`Warning: both ${basePath}.ts and ${basePath}.js exist. Using .ts — delete the .js to silence this warning (it is likely a stale compiled artifact or postinstall stub).`);
222
+ }
223
+ const path = `${basePath}.${matches[0]}`;
224
+ try {
225
+ const mod = await import(pathToFileURL(path).href);
226
+ return mod.default;
227
+ }
228
+ catch (error) {
229
+ const code = error?.code;
230
+ if (code && FILE_TYPE_REFUSAL_CODES.has(code) && refusalIsAboutTheConfig(error.message, path)) {
231
+ // Both files exist AND the `.ts` was refused. This is the one state where
232
+ // the both-present warning printed a few lines above is actively wrong
233
+ // advice: it says "delete the .js", and here the `.js` is the only file
234
+ // this runtime can read. Say so explicitly rather than leaving the
235
+ // consumer to reconcile two messages from the same call.
236
+ //
237
+ // It is also a deliberate behaviour change. `stonyx@0.2.3-beta.95`
238
+ // resolved `${basePath}.js` unconditionally, so this exact state LOADED
239
+ // and booted; measured against that loader, same fixture, plain node:
240
+ // it returned the `.js` default export with no warning. Preferring `.ts`
241
+ // is the point of #105, and the population most likely to hold both
242
+ // files is the one that worked around #105 by renaming to `.js` while
243
+ // the postinstall kept writing the `.ts` stub. So this message has to
244
+ // carry the "this used to work" fact, not just the remedy.
245
+ const sibling = matches.length > 1
246
+ ? ` ${basePath}.js also exists and this runtime CAN read it, but .ts wins the preference order` +
247
+ ` so the .js is never reached. Disregard the "delete the .js" warning above — in THIS state the` +
248
+ ` fix is the opposite one: remove or compile ${basePath}.ts.` +
249
+ ` (stonyx@0.2.3-beta.95 loaded the .js here; preferring .ts is a deliberate change — abofs/stonyx#105.)`
250
+ : '';
251
+ throw new Error(`${CONFIG_NOT_LOADABLE_PREFIX} ${path} exists, but this Node runtime refused to load it ` +
252
+ `(${code}): ${error.message}.${sibling}`, { cause: error });
253
+ }
254
+ throw error;
255
+ }
256
+ }
@@ -0,0 +1 @@
1
+ export declare function resolveEntryPoint(basePath: string): string;
@@ -0,0 +1,14 @@
1
+ import { existsSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ const EXTENSIONS = ['ts', 'js'];
4
+ export function resolveEntryPoint(basePath) {
5
+ const matches = EXTENSIONS.filter(ext => existsSync(resolve(`${basePath}.${ext}`)));
6
+ if (matches.length === 0) {
7
+ throw new Error(`Entry point not found: ${basePath}.{ts,js}`);
8
+ }
9
+ if (matches.length > 1) {
10
+ console.warn(`Warning: both ${basePath}.ts and ${basePath}.js exist. Using .ts — delete the .js to silence this warning (it is likely a stale compiled artifact or postinstall stub).`);
11
+ }
12
+ const ext = matches[0];
13
+ return resolve(`${basePath}.${ext}`);
14
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Resolve a stonyx standalone module's camelCase name from the consumer's
3
+ * `package.json#name`. Used by the standalone-config transform to wrap a flat
4
+ * config under the module's key when a stonyx module runs its own tests.
5
+ *
6
+ * Recognises:
7
+ * - `@stonyx/<kebab>` → `<camel>`
8
+ * - `stonyx-<kebab>` → `<camel>` (unscoped fallback)
9
+ * - Anything else → `null` (caller should skip the transform)
10
+ *
11
+ * Missing or malformed `package.json` returns `null` silently — matches the
12
+ * fail-safe semantics of the previous `rootPath.includes('stonyx-')` heuristic.
13
+ */
14
+ export declare function resolveModuleName(rootPath: string): string | null;
@@ -0,0 +1,33 @@
1
+ import { readFileSync } from 'fs';
2
+ import { kebabCaseToCamelCase } from '@stonyx/utils/string';
3
+ /**
4
+ * Resolve a stonyx standalone module's camelCase name from the consumer's
5
+ * `package.json#name`. Used by the standalone-config transform to wrap a flat
6
+ * config under the module's key when a stonyx module runs its own tests.
7
+ *
8
+ * Recognises:
9
+ * - `@stonyx/<kebab>` → `<camel>`
10
+ * - `stonyx-<kebab>` → `<camel>` (unscoped fallback)
11
+ * - Anything else → `null` (caller should skip the transform)
12
+ *
13
+ * Missing or malformed `package.json` returns `null` silently — matches the
14
+ * fail-safe semantics of the previous `rootPath.includes('stonyx-')` heuristic.
15
+ */
16
+ export function resolveModuleName(rootPath) {
17
+ let pkg;
18
+ try {
19
+ const raw = readFileSync(`${rootPath}/package.json`, 'utf8');
20
+ pkg = JSON.parse(raw);
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ const name = typeof pkg.name === 'string' ? pkg.name : '';
26
+ if (name.startsWith('@stonyx/')) {
27
+ return kebabCaseToCamelCase(name.slice('@stonyx/'.length));
28
+ }
29
+ if (name.startsWith('stonyx-')) {
30
+ return kebabCaseToCamelCase(name.slice('stonyx-'.length));
31
+ }
32
+ return null;
33
+ }
package/package.json CHANGED
@@ -1,23 +1,45 @@
1
1
  {
2
2
  "name": "stonyx",
3
- "version": "0.2.3-alpha.7",
3
+ "version": "0.2.3-alpha.71",
4
4
  "description": "Base stonyx framework module",
5
- "main": "main.js",
5
+ "main": "dist/main.js",
6
+ "types": "dist/main.d.ts",
6
7
  "type": "module",
7
8
  "bin": {
8
- "stonyx": "./src/cli.js"
9
+ "stonyx": "./dist/cli.js"
9
10
  },
10
11
  "exports": {
11
- ".": "./src/main.js",
12
- "./config": "./src/exports/config.js",
13
- "./log": "./src/exports/log.js",
14
- "./test-helpers": "./src/exports/test-helpers.js",
15
- "./lifecycle": "./src/lifecycle.js"
12
+ ".": {
13
+ "types": "./dist/main.d.ts",
14
+ "default": "./dist/main.js"
15
+ },
16
+ "./config": {
17
+ "types": "./dist/exports/config.d.ts",
18
+ "default": "./dist/exports/config.js"
19
+ },
20
+ "./log": {
21
+ "types": "./dist/exports/log.d.ts",
22
+ "default": "./dist/exports/log.js"
23
+ },
24
+ "./test-helpers": {
25
+ "types": "./dist/exports/test-helpers.d.ts",
26
+ "default": "./dist/exports/test-helpers.js"
27
+ },
28
+ "./lifecycle": {
29
+ "types": "./dist/lifecycle.d.ts",
30
+ "default": "./dist/lifecycle.js"
31
+ }
16
32
  },
17
33
  "repository": {
18
34
  "type": "git",
19
35
  "url": "git+https://github.com/abofs/stonyx.git"
20
36
  },
37
+ "files": [
38
+ "dist",
39
+ "config",
40
+ "scripts",
41
+ "README.md"
42
+ ],
21
43
  "publishConfig": {
22
44
  "provenance": true
23
45
  },
@@ -30,16 +52,23 @@
30
52
  "url": "https://github.com/abofs/stonyx/issues"
31
53
  },
32
54
  "dependencies": {
33
- "node-chronicle": "^0.2.0"
55
+ "@stonyx/utils": "0.2.3-beta.27",
56
+ "@stonyx/logs": "1.0.1-beta.21"
34
57
  },
35
58
  "devDependencies": {
36
- "@stonyx/utils": "0.2.3-beta.7",
59
+ "@types/node": "^25.5.2",
60
+ "@types/qunit": "^2.19.13",
61
+ "@types/sinon": "^21.0.1",
37
62
  "qunit": "^2.24.1",
38
- "sinon": "^21.0.0"
63
+ "sinon": "^21.0.0",
64
+ "tsx": "^4.21.0",
65
+ "typescript": "^5.8.3"
39
66
  },
40
67
  "scripts": {
68
+ "build": "tsc",
69
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
41
70
  "start": "node .",
42
- "test": "qunit",
71
+ "test": "pnpm build && node --import tsx node_modules/qunit/bin/qunit.js 'test/**/*-test.ts'",
43
72
  "error": "echo \"Error: no error handling\" && exit 1",
44
73
  "postinstall": "node scripts/postinstall.js"
45
74
  }
@@ -2,14 +2,36 @@
2
2
  const isGlobal = !process.env.INIT_CWD || process.env.npm_config_global === 'true';
3
3
  if (isGlobal) process.exit(0);
4
4
 
5
+ const { existsSync } = await import('node:fs');
5
6
  const { copyFile, createDirectory } = await import('@stonyx/utils/file');
6
7
 
7
8
  const projectDir = process.env.INIT_CWD;
8
9
  const configDir = `${projectDir}/config`;
9
- const envFile = 'environment.js';
10
+
11
+ // Detect consumer project type: presence of `tsconfig.json` in the consumer
12
+ // root signals a TypeScript project (see abofs/stonyx#62). TS consumers can't
13
+ // use the `.js` stub, so we ship both templates and copy the matching one.
14
+ const isTypeScriptConsumer = existsSync(`${projectDir}/tsconfig.json`);
15
+ const extension = isTypeScriptConsumer ? 'ts' : 'js';
16
+ const envFile = `environment.${extension}`;
17
+ const templateFile = `environment copy.${extension}`;
18
+
19
+ // Skip if the consumer ships its own `config/environment copy.{ts,js}` template —
20
+ // they manage their own bootstrap (see abofs/stonyx#54, extended in abofs/stonyx#62
21
+ // to honor either extension).
22
+ if (
23
+ existsSync(`${configDir}/environment copy.ts`) ||
24
+ existsSync(`${configDir}/environment copy.js`)
25
+ ) process.exit(0);
26
+
27
+ // Don't overwrite an existing consumer config (either extension).
28
+ if (
29
+ existsSync(`${configDir}/environment.ts`) ||
30
+ existsSync(`${configDir}/environment.js`)
31
+ ) process.exit(0);
10
32
 
11
33
  createDirectory(configDir);
12
34
 
13
- copyFile(`./config/environment copy.js`, `${configDir}/${envFile}`).then(result => {
35
+ copyFile(`./config/${templateFile}`, `${configDir}/${envFile}`).then(result => {
14
36
  if (result) console.log(`Stonyx: ${envFile} has been successfully created. Please see README.md for more information.`);
15
37
  });
package/.claude/index.md DELETED
@@ -1,96 +0,0 @@
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
@@ -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,51 +0,0 @@
1
- name: Publish to NPM
2
-
3
- on:
4
- repository_dispatch:
5
- types: [cascade-publish]
6
- workflow_dispatch:
7
- inputs:
8
- version-type:
9
- description: 'Version type'
10
- required: true
11
- type: choice
12
- options:
13
- - patch
14
- - minor
15
- - major
16
- custom-version:
17
- description: 'Custom version (optional, overrides version-type)'
18
- required: false
19
- type: string
20
- pull_request:
21
- types: [opened, synchronize, reopened]
22
- branches: [main]
23
- push:
24
- branches: [main]
25
-
26
- concurrency:
27
- group: ${{ github.event_name == 'repository_dispatch' && 'cascade-update' || format('publish-{0}', github.ref) }}
28
- cancel-in-progress: false
29
-
30
- permissions:
31
- contents: write
32
- id-token: write
33
- pull-requests: write
34
-
35
- jobs:
36
- publish:
37
- if: "!contains(github.event.head_commit.message, '[skip ci]')"
38
- uses: abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main
39
- with:
40
- version-type: ${{ github.event.inputs.version-type }}
41
- custom-version: ${{ github.event.inputs.custom-version }}
42
- cascade-source: ${{ github.event.client_payload.source_package || '' }}
43
- secrets: inherit
44
-
45
- cascade:
46
- needs: publish
47
- uses: abofs/stonyx-workflows/.github/workflows/cascade.yml@main
48
- with:
49
- package-name: ${{ needs.publish.outputs.package-name }}
50
- published-version: ${{ needs.publish.outputs.published-version }}
51
- secrets: inherit