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.
- package/README.md +6 -3
- package/config/{environment.js → environment copy.ts } +16 -12
- package/dist/cli/help.d.ts +8 -0
- package/dist/cli/help.js +43 -0
- package/dist/cli/load-commands.d.ts +10 -0
- package/dist/cli/load-commands.js +49 -0
- package/dist/cli/new.d.ts +72 -0
- package/dist/cli/new.js +426 -0
- package/dist/cli/serve.d.ts +6 -0
- package/dist/cli/serve.js +49 -0
- package/dist/cli/test-setup.d.ts +1 -0
- package/dist/cli/test-setup.js +30 -0
- package/dist/cli/test.d.ts +4 -0
- package/dist/cli/test.js +23 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +53 -0
- package/dist/exports/config.d.ts +2 -0
- package/dist/exports/config.js +2 -0
- package/dist/exports/log.d.ts +2 -0
- package/{src → dist}/exports/log.js +1 -2
- package/dist/exports/test-helpers.d.ts +5 -0
- package/dist/exports/test-helpers.js +7 -0
- package/dist/lifecycle.d.ts +7 -0
- package/dist/lifecycle.js +18 -0
- package/dist/main.d.ts +22 -0
- package/dist/main.js +102 -0
- package/dist/modules.d.ts +30 -0
- package/dist/modules.js +363 -0
- package/dist/util/duplicate-core.d.ts +186 -0
- package/dist/util/duplicate-core.js +413 -0
- package/dist/util/import-config.d.ts +52 -0
- package/dist/util/import-config.js +256 -0
- package/dist/util/resolve-entry-point.d.ts +1 -0
- package/dist/util/resolve-entry-point.js +14 -0
- package/dist/util/resolve-module-name.d.ts +14 -0
- package/dist/util/resolve-module-name.js +33 -0
- package/package.json +41 -12
- package/scripts/postinstall.js +24 -2
- package/.claude/index.md +0 -96
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -51
- package/docs/api.md +0 -89
- package/docs/cli.md +0 -88
- package/docs/configuration.md +0 -72
- package/docs/conventions/discord-conventions.md +0 -62
- package/docs/conventions/framework-modules.md +0 -127
- package/docs/conventions/index.md +0 -24
- package/docs/conventions/orm-conventions.md +0 -158
- package/docs/conventions/project-structure.md +0 -105
- package/docs/conventions/rest-conventions.md +0 -96
- package/docs/conventions/testing-conventions.md +0 -56
- package/docs/developing-modules.md +0 -108
- package/docs/index.md +0 -15
- package/docs/lifecycle.md +0 -47
- package/docs/logging.md +0 -52
- package/docs/modules.md +0 -82
- package/docs/testing.md +0 -64
- package/src/cli/help.js +0 -48
- package/src/cli/load-commands.js +0 -56
- package/src/cli/new.js +0 -237
- package/src/cli/serve.js +0 -38
- package/src/cli/test-setup.js +0 -8
- package/src/cli/test.js +0 -27
- package/src/cli.js +0 -63
- package/src/exports/config.js +0 -3
- package/src/exports/test-helpers.js +0 -8
- package/src/lifecycle.js +0 -17
- package/src/main.js +0 -101
- 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
|
+
});
|
package/dist/cli/test.js
ADDED
|
@@ -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
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,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>;
|