stonyx 0.2.3-alpha.6 → 0.2.3-alpha.60

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 +6 -3
  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 +426 -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 +30 -0
  28. package/dist/modules.js +363 -0
  29. package/dist/util/duplicate-core.d.ts +186 -0
  30. package/dist/util/duplicate-core.js +413 -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 -88
  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 -158
  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 -82
  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,49 @@
1
+ import { resolve } from 'path';
2
+ import { pathToFileURL } from 'url';
3
+ import { runStartupHooks, runShutdownHooks } from '../lifecycle.js';
4
+ import { importConfig } from '../util/import-config.js';
5
+ import { resolveEntryPoint } from '../util/resolve-entry-point.js';
6
+ export function createShutdownHandler(modules) {
7
+ let shuttingDown = false;
8
+ return async () => {
9
+ if (shuttingDown)
10
+ return;
11
+ shuttingDown = true;
12
+ await runShutdownHooks(modules);
13
+ process.exit(0);
14
+ };
15
+ }
16
+ export function maybeRegisterAppShutdown(modules, appInstance, originalShutdown) {
17
+ if (appInstance &&
18
+ typeof appInstance.shutdown === 'function') {
19
+ const appShutdown = createShutdownHandler([
20
+ ...modules,
21
+ appInstance
22
+ ]);
23
+ process.removeListener('SIGTERM', originalShutdown);
24
+ process.removeListener('SIGINT', originalShutdown);
25
+ process.on('SIGTERM', appShutdown);
26
+ process.on('SIGINT', appShutdown);
27
+ }
28
+ }
29
+ export default async function serve({ args }) {
30
+ const cwd = process.cwd();
31
+ const entryFlag = args.indexOf('--entry');
32
+ const entryArg = (entryFlag !== -1 && entryFlag + 1 < args.length) ? args[entryFlag + 1] : null;
33
+ const entryPoint = entryArg !== null ? resolve(cwd, entryArg) : resolveEntryPoint(`${cwd}/app`);
34
+ const config = await importConfig(`${cwd}/config/environment`);
35
+ const { default: Stonyx } = await import('../main.js');
36
+ new Stonyx(config, cwd);
37
+ await Stonyx.ready;
38
+ const { modules } = Stonyx.instance;
39
+ await runStartupHooks(modules);
40
+ const shutdown = createShutdownHandler(modules);
41
+ process.on('SIGTERM', shutdown);
42
+ process.on('SIGINT', shutdown);
43
+ const entryModule = await import(pathToFileURL(entryPoint).href);
44
+ let appInstance;
45
+ if (entryModule.default) {
46
+ appInstance = new entryModule.default();
47
+ }
48
+ maybeRegisterAppShutdown(modules, appInstance, shutdown);
49
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import { createRequire } from 'module';
2
+ import { importConfig } from '../util/import-config.js';
3
+ const cwd = process.cwd();
4
+ const { default: Stonyx } = await import('stonyx');
5
+ const config = await importConfig(`${cwd}/config/environment`);
6
+ new Stonyx(config, cwd);
7
+ // Drain the event loop once qunit finishes. Modules loaded by Stonyx
8
+ // (e.g. @stonyx/rest-server) may keep listeners/servers open that would
9
+ // otherwise block the process from exiting, causing CI to hit timeouts
10
+ // even though all tests passed.
11
+ //
12
+ // qunit's CLI bin does `delete require.cache[qunitPath]` and re-requires
13
+ // qunit before running, so any qunit instance we grab synchronously from
14
+ // this --import setup is a DIFFERENT object than the one qunit's CLI
15
+ // actually uses. We defer the hook registration with setImmediate so it
16
+ // runs AFTER the qunit bin has populated require.cache with its own
17
+ // instance; grabbing qunit via CJS require at that point returns the
18
+ // instance the tests are running on.
19
+ //
20
+ // See: https://github.com/abofs/stonyx/issues/55
21
+ const require = createRequire(import.meta.url);
22
+ setImmediate(() => {
23
+ const qunitPath = require.resolve('qunit', {
24
+ paths: [`${cwd}/node_modules`, cwd]
25
+ });
26
+ const QUnit = require(qunitPath);
27
+ QUnit.on('runEnd', () => {
28
+ setImmediate(() => process.exit(process.exitCode ?? 0));
29
+ });
30
+ });
@@ -0,0 +1,4 @@
1
+ export declare const DEFAULT_TEST_GLOB = "test/**/*-test.{js,ts}";
2
+ export default function test({ args }: {
3
+ args: string[];
4
+ }): Promise<void>;
@@ -0,0 +1,23 @@
1
+ import { spawn } from 'child_process';
2
+ import { fileURLToPath, pathToFileURL } from 'url';
3
+ import path from 'path';
4
+ export const DEFAULT_TEST_GLOB = 'test/**/*-test.{js,ts}';
5
+ export default async function test({ args }) {
6
+ const cwd = process.cwd();
7
+ const setupFile = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'test-setup.js');
8
+ const setupFileUrl = pathToFileURL(setupFile).href;
9
+ const qunitBin = path.resolve(cwd, 'node_modules/qunit/bin/qunit.js');
10
+ const testArgs = args.length > 0 ? args : [DEFAULT_TEST_GLOB];
11
+ const child = spawn(process.execPath, [
12
+ '--import', setupFileUrl,
13
+ qunitBin,
14
+ ...testArgs
15
+ ], {
16
+ cwd,
17
+ stdio: 'inherit',
18
+ env: { ...process.env, NODE_ENV: 'test' }
19
+ });
20
+ child.on('close', (code) => {
21
+ process.exit(code ?? 1);
22
+ });
23
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ import serve from './cli/serve.js';
3
+ import test from './cli/test.js';
4
+ import help from './cli/help.js';
5
+ import newCommand from './cli/new.js';
6
+ import loadModuleCommands from './cli/load-commands.js';
7
+ import { importConfig } from './util/import-config.js';
8
+ try {
9
+ process.loadEnvFile();
10
+ }
11
+ catch { /* no .env file */ }
12
+ const aliases = { s: 'serve', t: 'test', h: 'help', n: 'new' };
13
+ const builtInCommands = {
14
+ serve: { description: 'Bootstrap Stonyx and run the app', run: serve },
15
+ test: { description: 'Bootstrap Stonyx in test mode and run tests', run: test },
16
+ new: { description: 'Scaffold a new Stonyx project', run: newCommand },
17
+ help: { description: 'Show available commands', run: help }
18
+ };
19
+ const args = process.argv.slice(2);
20
+ const commandName = aliases[args[0]] || args[0];
21
+ const commandArgs = args.slice(1);
22
+ async function main() {
23
+ if (!commandName || commandName === 'help') {
24
+ await help({ args: commandArgs, builtInCommands, loadModuleCommands });
25
+ return;
26
+ }
27
+ const builtIn = builtInCommands[commandName];
28
+ if (builtIn) {
29
+ await builtIn.run({ args: commandArgs });
30
+ return;
31
+ }
32
+ // Search module commands
33
+ const moduleCommands = await loadModuleCommands();
34
+ const moduleCommand = moduleCommands[commandName];
35
+ if (moduleCommand) {
36
+ const cwd = process.cwd();
37
+ if (moduleCommand.bootstrap) {
38
+ const config = await importConfig(`${cwd}/config/environment`);
39
+ const { default: Stonyx } = await import('./main.js');
40
+ new Stonyx(config, cwd);
41
+ await Stonyx.ready;
42
+ }
43
+ await moduleCommand.run({ args: commandArgs, cwd });
44
+ return;
45
+ }
46
+ console.error(`Unknown command: ${args[0]}\n`);
47
+ await help({ args: [], builtInCommands, loadModuleCommands });
48
+ process.exit(1);
49
+ }
50
+ main().catch(error => {
51
+ console.error(error);
52
+ process.exit(1);
53
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("stonyx").StoynxConfig;
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ import Stonyx from 'stonyx';
2
+ export default Stonyx.config;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("@stonyx/logs").default;
2
+ export default _default;
@@ -1,3 +1,2 @@
1
1
  import Stonyx from 'stonyx';
2
-
3
- export default Stonyx.log;
2
+ export default Stonyx.log;
@@ -0,0 +1,5 @@
1
+ interface TestHooks {
2
+ before(fn: () => Promise<void>): void;
3
+ }
4
+ export declare function setupIntegrationTests(hooks: TestHooks): void;
5
+ export {};
@@ -0,0 +1,7 @@
1
+ import Stonyx from 'stonyx';
2
+ export function setupIntegrationTests(hooks) {
3
+ hooks.before(async function () {
4
+ await Stonyx.ready;
5
+ console.log('Stonyx ready');
6
+ });
7
+ }
@@ -0,0 +1,7 @@
1
+ export interface StoynxModule {
2
+ init?(): Promise<void>;
3
+ startup?(): Promise<void>;
4
+ shutdown?(): Promise<void>;
5
+ }
6
+ export declare function runStartupHooks(modules: StoynxModule[]): Promise<void>;
7
+ export declare function runShutdownHooks(modules: StoynxModule[]): Promise<void>;
@@ -0,0 +1,18 @@
1
+ export async function runStartupHooks(modules) {
2
+ for (const module of modules) {
3
+ if (typeof module.startup === 'function')
4
+ await module.startup();
5
+ }
6
+ }
7
+ export async function runShutdownHooks(modules) {
8
+ for (const module of [...modules].reverse()) {
9
+ if (typeof module.shutdown === 'function') {
10
+ try {
11
+ await module.shutdown();
12
+ }
13
+ catch (error) {
14
+ console.error('Error during module shutdown:', error);
15
+ }
16
+ }
17
+ }
18
+ }
package/dist/main.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import Chronicle from '@stonyx/logs';
2
+ import type { StoynxModule } from './lifecycle.js';
3
+ import type { StoynxConfig } from './modules.js';
4
+ export default class Stonyx {
5
+ static initialized: boolean;
6
+ static modulePromises: Record<string, unknown>;
7
+ static instance: Stonyx;
8
+ static ready: Promise<void>;
9
+ config: StoynxConfig;
10
+ chronicle: Chronicle;
11
+ modules: StoynxModule[];
12
+ constructor(config: StoynxConfig, rootPath: string);
13
+ start(config: StoynxConfig, rootPath: string): Promise<void>;
14
+ /**
15
+ * Allows users to define their own log for any extra class via environment config
16
+ */
17
+ configureUserLogs(): void;
18
+ static get log(): Chronicle;
19
+ static get config(): StoynxConfig;
20
+ }
21
+ export { waitForModule } from './modules.js';
22
+ export type { StoynxConfig } from './modules.js';
package/dist/main.js ADDED
@@ -0,0 +1,102 @@
1
+ /*
2
+ * Copyright 2025 Stone Costa
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import Chronicle from '@stonyx/logs';
17
+ import loadModules from './modules.js';
18
+ import { mergeObject } from '@stonyx/utils/object';
19
+ import { importConfig, CONFIG_NOT_FOUND_PREFIX } from './util/import-config.js';
20
+ import { resolveModuleName } from './util/resolve-module-name.js';
21
+ export default class Stonyx {
22
+ static initialized = false;
23
+ static modulePromises = {};
24
+ static instance;
25
+ static ready;
26
+ config;
27
+ chronicle;
28
+ modules;
29
+ constructor(config, rootPath) {
30
+ if (Stonyx.instance)
31
+ return Stonyx.instance;
32
+ Stonyx.instance = this;
33
+ Stonyx.ready = this.start(config, rootPath);
34
+ }
35
+ async start(config, rootPath) {
36
+ if (!config)
37
+ throw new Error('Stonyx requires full environment configuration on startup');
38
+ if (!rootPath)
39
+ throw new Error('Stonyx requires root project\'s path on startup');
40
+ // Transform config from stonyx-modules running as a standalone.
41
+ // Signal is the consumer's `package.json#name` (see resolveModuleName);
42
+ // this replaces the brittle `rootPath.includes('stonyx-')` heuristic so
43
+ // non-canonical rootPaths (worktrees, forks, renames) still work.
44
+ const moduleName = resolveModuleName(rootPath);
45
+ if (moduleName) {
46
+ const flatConfig = config;
47
+ const siblingModules = flatConfig.modules || {};
48
+ config = { [moduleName]: config, ...siblingModules };
49
+ delete config.modules;
50
+ }
51
+ this.config = config;
52
+ this.chronicle = new Chronicle({ additionalLogs: { title: 'green' } });
53
+ Stonyx.initialized = true;
54
+ // Auto-merge test environment overrides (after initialized flag, before modules load)
55
+ // Uses in-place mutation to preserve existing references (e.g. stonyx/config export cache)
56
+ if (process.env.NODE_ENV === 'test') {
57
+ try {
58
+ const testOverrides = await importConfig(`${rootPath}/test/config/environment`);
59
+ const merged = mergeObject(config, testOverrides);
60
+ Object.assign(config, merged);
61
+ }
62
+ catch (err) {
63
+ // A test override that is genuinely ABSENT is non-fatal. Everything else
64
+ // re-throws — including `CONFIG_NOT_LOADABLE_PREFIX`, which is the whole
65
+ // point of that prefix existing (abofs/stonyx#105, invariant I2). A
66
+ // config the consumer wrote and this loader declined must never reach
67
+ // the caller as "you have no test overrides".
68
+ if (!(err instanceof Error) || !err.message.startsWith(CONFIG_NOT_FOUND_PREFIX))
69
+ throw err;
70
+ }
71
+ }
72
+ this.modules = await loadModules(config, rootPath, this.chronicle);
73
+ this.configureUserLogs();
74
+ }
75
+ /**
76
+ * Allows users to define their own log for any extra class via environment config
77
+ */
78
+ configureUserLogs() {
79
+ const { chronicle } = this;
80
+ for (const [className, config] of Object.entries(this.config)) {
81
+ if (!config || typeof config !== 'object')
82
+ continue;
83
+ if (className in chronicle)
84
+ continue;
85
+ const { logColor, logMethod, logTimestamp } = config;
86
+ if (!logColor)
87
+ continue;
88
+ chronicle.defineType(logMethod || className, logColor, { logTimestamp: !!logTimestamp });
89
+ }
90
+ }
91
+ static get log() {
92
+ if (!Stonyx.initialized)
93
+ throw new Error('Stonyx has not been initialized yet');
94
+ return Stonyx.instance.chronicle;
95
+ }
96
+ static get config() {
97
+ if (!Stonyx.initialized)
98
+ throw new Error('Stonyx has not been initialized yet');
99
+ return Stonyx.instance.config;
100
+ }
101
+ }
102
+ export { waitForModule } from './modules.js';
@@ -0,0 +1,30 @@
1
+ import type { StoynxModule } from './lifecycle.js';
2
+ import type Chronicle from '@stonyx/logs';
3
+ export type StoynxConfig = Record<string, Record<string, unknown> | unknown>;
4
+ export default function loadModules(config: StoynxConfig, rootPath: string, chronicle: Chronicle): Promise<StoynxModule[]>;
5
+ /**
6
+ * Blocks until `@stonyx/<moduleName>` has finished initializing.
7
+ *
8
+ * TWO failure sentences, because one sentence stopped being able to carry both
9
+ * facts. Until PR #120 fix round 1 `modulePromises` held every DECLARED
10
+ * `@stonyx/*` name, so "absent from the map" and "absent from the manifest"
11
+ * were the same fact and the single inherited sentence was true. Registration
12
+ * is now scoped to the DISCOVERED set — which is what removes the boot hang,
13
+ * see the registration loop in `loadModules` — so absence from the map has two
14
+ * causes, and the inherited sentence is FALSE for one of them: it tells the
15
+ * operator the package is not in their dependencies while it is sitting in
16
+ * their manifest.
17
+ *
18
+ * Not hypothetical, and not a shape this repo has to imagine: `@stonyx/logs`
19
+ * ships without the `stonyx-module` keyword and is an ordinary `dependencies`
20
+ * entry of THIS package.json. An app in the same shape calling
21
+ * `waitForModule('logs')` would be sent to grep a manifest that already
22
+ * contains the answer. Naming the wrong cause is the defect abofs/stonyx#108
23
+ * was filed over, and it is not made acceptable by being one line long.
24
+ *
25
+ * The declared branch does not say WHICH of the two causes applies, because
26
+ * this function does not know. Discovery does, and it already `console.warn`s
27
+ * the specific reason at load time, so the message points at that warning
28
+ * rather than guessing between them.
29
+ */
30
+ export declare function waitForModule(moduleName: string): Promise<void>;