express-file-cluster 0.1.4 → 0.1.5
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/package.json +4 -1
- package/src/auth/index.ts +0 -71
- package/src/cli/commands/build.ts +0 -43
- package/src/cli/commands/diagnostics.ts +0 -124
- package/src/cli/commands/generate.ts +0 -91
- package/src/cli/commands/run.ts +0 -28
- package/src/cli/commands/start.ts +0 -93
- package/src/cli/index.ts +0 -22
- package/src/cluster/index.ts +0 -31
- package/src/compose.ts +0 -26
- package/src/db/index.ts +0 -31
- package/src/db/model.ts +0 -95
- package/src/db/mongo.ts +0 -22
- package/src/errors.ts +0 -10
- package/src/ignite.test.ts +0 -196
- package/src/index.ts +0 -191
- package/src/router/mount.test.ts +0 -75
- package/src/router/mount.ts +0 -49
- package/src/router/scan.test.ts +0 -23
- package/src/router/scan.ts +0 -55
- package/src/tasks/bullmq-backend.ts +0 -76
- package/src/tasks/index.ts +0 -51
- package/src/tasks/scanner.test.ts +0 -58
- package/src/tasks/scanner.ts +0 -30
- package/src/tasks/thread-runner.ts +0 -40
- package/src/types.ts +0 -77
- package/tsconfig.json +0 -9
- package/tsup.config.ts +0 -26
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "express-file-cluster",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Enterprise-grade, zero-boilerplate backend framework with file-based routing and multi-core clustering",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
7
10
|
"main": "./dist/index.cjs",
|
|
8
11
|
"module": "./dist/index.js",
|
|
9
12
|
"types": "./dist/index.d.ts",
|
package/src/auth/index.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import type { RequestHandler, Response } from 'express';
|
|
2
|
-
import jwt from 'jsonwebtoken';
|
|
3
|
-
import type { AuthStrategy } from '../types.js';
|
|
4
|
-
|
|
5
|
-
interface AuthConfig {
|
|
6
|
-
secret: string;
|
|
7
|
-
strategy: AuthStrategy;
|
|
8
|
-
expiresIn: string;
|
|
9
|
-
cookieDomain?: string | undefined;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
let _config: AuthConfig | null = null;
|
|
13
|
-
|
|
14
|
-
export function configureAuth(config: AuthConfig): void {
|
|
15
|
-
_config = config;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function getConfig(): AuthConfig {
|
|
19
|
-
if (!_config) throw new Error('[EFC] Auth not configured — pass jwtSecret to ignite()');
|
|
20
|
-
return _config;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function issueToken(res: Response, payload: Record<string, unknown>): void {
|
|
24
|
-
const { secret, expiresIn, cookieDomain } = getConfig();
|
|
25
|
-
// expiresIn is a plain string (e.g. '7d'); cast satisfies jwt's branded StringValue
|
|
26
|
-
const token = jwt.sign(payload, secret, { expiresIn: expiresIn as jwt.SignOptions['expiresIn'] & string });
|
|
27
|
-
res.cookie('efc_token', token, {
|
|
28
|
-
httpOnly: true,
|
|
29
|
-
secure: process.env['NODE_ENV'] === 'production',
|
|
30
|
-
sameSite: 'strict',
|
|
31
|
-
...(cookieDomain !== undefined && { domain: cookieDomain }),
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function revokeToken(res: Response): void {
|
|
36
|
-
res.clearCookie('efc_token');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function signToken(payload: Record<string, unknown>): string {
|
|
40
|
-
const { secret, expiresIn } = getConfig();
|
|
41
|
-
return jwt.sign(payload, secret, { expiresIn: expiresIn as jwt.SignOptions['expiresIn'] & string });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export const requireAuth: RequestHandler = (req, res, next) => {
|
|
45
|
-
const { secret, strategy } = getConfig();
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
let token: string | undefined;
|
|
49
|
-
|
|
50
|
-
if (strategy === 'http-only') {
|
|
51
|
-
const cookies = (req as typeof req & { cookies: Record<string, string> }).cookies;
|
|
52
|
-
token = cookies['efc_token'];
|
|
53
|
-
} else {
|
|
54
|
-
const auth = req.headers['authorization'];
|
|
55
|
-
if (typeof auth === 'string' && auth.startsWith('Bearer ')) {
|
|
56
|
-
token = auth.slice(7);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (!token) {
|
|
61
|
-
res.status(401).json({ error: 'Unauthorized' });
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const decoded = jwt.verify(token, secret);
|
|
66
|
-
(req as typeof req & { user: unknown }).user = decoded;
|
|
67
|
-
next();
|
|
68
|
-
} catch {
|
|
69
|
-
res.status(401).json({ error: 'Unauthorized' });
|
|
70
|
-
}
|
|
71
|
-
};
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
|
|
5
|
-
export function buildCommand(): Command {
|
|
6
|
-
const cmd = new Command('build');
|
|
7
|
-
|
|
8
|
-
cmd
|
|
9
|
-
.argument('<mode>', 'prod')
|
|
10
|
-
.description('Build the application for production')
|
|
11
|
-
.action((mode: string) => {
|
|
12
|
-
if (mode !== 'prod') {
|
|
13
|
-
console.error(chalk.red(`Unknown build mode: ${mode}. Use 'prod'.`));
|
|
14
|
-
process.exit(1);
|
|
15
|
-
}
|
|
16
|
-
buildProd();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
return cmd;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function buildProd(): void {
|
|
23
|
-
console.log(chalk.cyan('[EFC] Building for production…'));
|
|
24
|
-
|
|
25
|
-
// Type-check first
|
|
26
|
-
const tsc = spawn('npx', ['tsc', '--noEmit'], { stdio: 'inherit' });
|
|
27
|
-
|
|
28
|
-
tsc.on('exit', (code) => {
|
|
29
|
-
if (code !== 0) {
|
|
30
|
-
console.error(chalk.red('[EFC] TypeScript errors found. Fix them before building.'));
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const tsup = spawn('npx', ['tsup'], { stdio: 'inherit' });
|
|
35
|
-
tsup.on('exit', (tsupCode) => {
|
|
36
|
-
if (tsupCode === 0) {
|
|
37
|
-
console.log(chalk.green('[EFC] Build complete → dist/'));
|
|
38
|
-
} else {
|
|
39
|
-
process.exit(tsupCode ?? 1);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { scanDir } from '../../router/scan.js';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import chalk from 'chalk';
|
|
6
|
-
|
|
7
|
-
export function diagnosticsCommands(): Command[] {
|
|
8
|
-
return [routesCommand(), tasksCommand(), doctorCommand()];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function routesCommand(): Command {
|
|
12
|
-
return new Command('routes')
|
|
13
|
-
.description('Print the resolved route table')
|
|
14
|
-
.action(() => {
|
|
15
|
-
const apiDir = resolveApiDir();
|
|
16
|
-
if (!apiDir) {
|
|
17
|
-
console.error(chalk.red('[EFC] Could not find apiDir (expected src/api)'));
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const routes = scanDir(apiDir);
|
|
22
|
-
if (routes.length === 0) {
|
|
23
|
-
console.log(chalk.yellow('No routes found.'));
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
console.log(chalk.bold('\n Route Table\n'));
|
|
28
|
-
console.log(chalk.dim(' ' + '─'.repeat(60)));
|
|
29
|
-
for (const route of routes) {
|
|
30
|
-
const rel = path.relative(process.cwd(), route.filePath);
|
|
31
|
-
console.log(` ${chalk.cyan(route.urlPath.padEnd(35))} ${chalk.dim(rel)}`);
|
|
32
|
-
}
|
|
33
|
-
console.log(chalk.dim(' ' + '─'.repeat(60)));
|
|
34
|
-
console.log(chalk.dim(`\n ${routes.length} route(s) found\n`));
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function tasksCommand(): Command {
|
|
39
|
-
return new Command('tasks')
|
|
40
|
-
.description('List registered background tasks')
|
|
41
|
-
.action(() => {
|
|
42
|
-
const tasksDir = resolveTasksDir();
|
|
43
|
-
if (!tasksDir || !fs.existsSync(tasksDir)) {
|
|
44
|
-
console.log(chalk.yellow('No tasks directory found.'));
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const files = fs.readdirSync(tasksDir).filter((f) => /\.(ts|js)$/.test(f));
|
|
49
|
-
if (files.length === 0) {
|
|
50
|
-
console.log(chalk.yellow('No tasks found.'));
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
console.log(chalk.bold('\n Background Tasks\n'));
|
|
55
|
-
for (const file of files) {
|
|
56
|
-
console.log(` ${chalk.cyan(path.basename(file, path.extname(file)))}`);
|
|
57
|
-
}
|
|
58
|
-
console.log();
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function doctorCommand(): Command {
|
|
63
|
-
return new Command('doctor')
|
|
64
|
-
.description('Validate config, env vars, and project setup')
|
|
65
|
-
.action(() => {
|
|
66
|
-
const cwd = process.cwd();
|
|
67
|
-
const checks: { label: string; ok: boolean; hint?: string }[] = [
|
|
68
|
-
{
|
|
69
|
-
label: 'package.json exists',
|
|
70
|
-
ok: fs.existsSync(path.join(cwd, 'package.json')),
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
label: 'tsconfig.json exists',
|
|
74
|
-
ok: fs.existsSync(path.join(cwd, 'tsconfig.json')),
|
|
75
|
-
hint: 'Run `tsc --init` to create one',
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
label: 'src/api directory exists',
|
|
79
|
-
ok: fs.existsSync(path.join(cwd, 'src', 'api')),
|
|
80
|
-
hint: 'Create src/api/ and add route files',
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
label: 'DATABASE_URL set',
|
|
84
|
-
ok: Boolean(process.env['DATABASE_URL']),
|
|
85
|
-
hint: 'Add DATABASE_URL to .env',
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
label: 'JWT_SECRET set',
|
|
89
|
-
ok: Boolean(process.env['JWT_SECRET']),
|
|
90
|
-
hint: 'Add JWT_SECRET to .env (generate: openssl rand -hex 64)',
|
|
91
|
-
},
|
|
92
|
-
];
|
|
93
|
-
|
|
94
|
-
console.log(chalk.bold('\n EFC Doctor\n'));
|
|
95
|
-
let allOk = true;
|
|
96
|
-
for (const check of checks) {
|
|
97
|
-
const icon = check.ok ? chalk.green('✓') : chalk.red('✗');
|
|
98
|
-
console.log(` ${icon} ${check.label}`);
|
|
99
|
-
if (!check.ok) {
|
|
100
|
-
allOk = false;
|
|
101
|
-
if (check.hint) console.log(chalk.dim(` → ${check.hint}`));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
console.log();
|
|
105
|
-
if (allOk) {
|
|
106
|
-
console.log(chalk.green(' All checks passed!\n'));
|
|
107
|
-
} else {
|
|
108
|
-
console.log(chalk.yellow(' Some checks failed. Fix the issues above.\n'));
|
|
109
|
-
process.exit(1);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function resolveApiDir(): string | null {
|
|
115
|
-
const cwd = process.cwd();
|
|
116
|
-
const candidates = [path.join(cwd, 'src', 'api'), path.join(cwd, 'api')];
|
|
117
|
-
return candidates.find((d) => fs.existsSync(d)) ?? null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function resolveTasksDir(): string | null {
|
|
121
|
-
const cwd = process.cwd();
|
|
122
|
-
const candidates = [path.join(cwd, 'src', 'tasks'), path.join(cwd, 'tasks')];
|
|
123
|
-
return candidates.find((d) => fs.existsSync(d)) ?? null;
|
|
124
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
|
|
6
|
-
export function generateCommand(): Command {
|
|
7
|
-
const cmd = new Command('generate').alias('g').description('Scaffold EFC modules');
|
|
8
|
-
|
|
9
|
-
cmd
|
|
10
|
-
.command('route <routePath>')
|
|
11
|
-
.description('Scaffold a route module, e.g. users/[id]')
|
|
12
|
-
.action((routePath: string) => generateRoute(routePath));
|
|
13
|
-
|
|
14
|
-
cmd
|
|
15
|
-
.command('task <name>')
|
|
16
|
-
.description('Scaffold a background task module')
|
|
17
|
-
.action((name: string) => generateTask(name));
|
|
18
|
-
|
|
19
|
-
cmd
|
|
20
|
-
.command('middleware <name>')
|
|
21
|
-
.description('Scaffold a middleware module')
|
|
22
|
-
.action((name: string) => generateMiddleware(name));
|
|
23
|
-
|
|
24
|
-
return cmd;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function writeFile(filePath: string, content: string): void {
|
|
28
|
-
const dir = path.dirname(filePath);
|
|
29
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
30
|
-
if (fs.existsSync(filePath)) {
|
|
31
|
-
console.error(chalk.red(`File already exists: ${filePath}`));
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
35
|
-
console.log(chalk.green(` created ${path.relative(process.cwd(), filePath)}`));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function generateRoute(routePath: string): void {
|
|
39
|
-
const cwd = process.cwd();
|
|
40
|
-
const filePath = path.join(cwd, 'src', 'api', `${routePath}.ts`);
|
|
41
|
-
|
|
42
|
-
const content = `import type { Request, Response } from 'express';
|
|
43
|
-
|
|
44
|
-
export const GET = async (req: Request, res: Response) => {
|
|
45
|
-
res.json({ message: 'OK' });
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export const POST = async (req: Request, res: Response) => {
|
|
49
|
-
res.status(201).json({ message: 'Created' });
|
|
50
|
-
};
|
|
51
|
-
`;
|
|
52
|
-
|
|
53
|
-
writeFile(filePath, content);
|
|
54
|
-
console.log(chalk.cyan(`[EFC] Route scaffolded`));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function generateTask(name: string): void {
|
|
58
|
-
const cwd = process.cwd();
|
|
59
|
-
const filePath = path.join(cwd, 'src', 'tasks', `${name}.ts`);
|
|
60
|
-
|
|
61
|
-
const content = `import { defineTask } from 'express-file-cluster/tasks';
|
|
62
|
-
|
|
63
|
-
interface ${name}Payload {
|
|
64
|
-
// TODO: define payload fields
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export default defineTask<${name}Payload>(async (payload) => {
|
|
68
|
-
// TODO: implement task logic
|
|
69
|
-
console.log('[Task:${name}]', payload);
|
|
70
|
-
});
|
|
71
|
-
`;
|
|
72
|
-
|
|
73
|
-
writeFile(filePath, content);
|
|
74
|
-
console.log(chalk.cyan(`[EFC] Task scaffolded`));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function generateMiddleware(name: string): void {
|
|
78
|
-
const cwd = process.cwd();
|
|
79
|
-
const filePath = path.join(cwd, 'src', 'middlewares', `${name}.ts`);
|
|
80
|
-
|
|
81
|
-
const content = `import type { Request, Response, NextFunction } from 'express';
|
|
82
|
-
|
|
83
|
-
export function ${name}(req: Request, res: Response, next: NextFunction): void {
|
|
84
|
-
// TODO: implement middleware logic
|
|
85
|
-
next();
|
|
86
|
-
}
|
|
87
|
-
`;
|
|
88
|
-
|
|
89
|
-
writeFile(filePath, content);
|
|
90
|
-
console.log(chalk.cyan(`[EFC] Middleware scaffolded`));
|
|
91
|
-
}
|
package/src/cli/commands/run.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
|
|
5
|
-
export function runCommand(): Command {
|
|
6
|
-
const cmd = new Command('run');
|
|
7
|
-
|
|
8
|
-
cmd
|
|
9
|
-
.argument('<runner>', 'tests')
|
|
10
|
-
.description('Run EFC sub-commands (tests)')
|
|
11
|
-
.allowUnknownOption()
|
|
12
|
-
.action((runner: string) => {
|
|
13
|
-
if (runner === 'tests') {
|
|
14
|
-
runTests(cmd.args.slice(1));
|
|
15
|
-
} else {
|
|
16
|
-
console.error(chalk.red(`Unknown runner: ${runner}. Use 'tests'.`));
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
return cmd;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function runTests(extraArgs: string[]): void {
|
|
25
|
-
console.log(chalk.cyan('[EFC] Running tests via Vitest…'));
|
|
26
|
-
const child = spawn('npx', ['vitest', 'run', ...extraArgs], { stdio: 'inherit' });
|
|
27
|
-
child.on('exit', (code) => process.exit(code ?? 0));
|
|
28
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import chalk from 'chalk';
|
|
6
|
-
|
|
7
|
-
export function startCommand(): Command {
|
|
8
|
-
const cmd = new Command('start');
|
|
9
|
-
|
|
10
|
-
cmd
|
|
11
|
-
.argument('<mode>', 'dev | prod')
|
|
12
|
-
.description('Start the EFC server')
|
|
13
|
-
.action((mode: string) => {
|
|
14
|
-
if (mode === 'dev') {
|
|
15
|
-
startDev();
|
|
16
|
-
} else if (mode === 'prod') {
|
|
17
|
-
startProd();
|
|
18
|
-
} else {
|
|
19
|
-
console.error(chalk.red(`Unknown mode: ${mode}. Use 'dev' or 'prod'.`));
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
return cmd;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function parseDotenv(cwd: string): Record<string, string> {
|
|
28
|
-
const envFile = path.join(cwd, '.env');
|
|
29
|
-
if (!fs.existsSync(envFile)) return {};
|
|
30
|
-
const vars: Record<string, string> = {};
|
|
31
|
-
for (const line of fs.readFileSync(envFile, 'utf8').split('\n')) {
|
|
32
|
-
const trimmed = line.trim();
|
|
33
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
34
|
-
const eq = trimmed.indexOf('=');
|
|
35
|
-
if (eq === -1) continue;
|
|
36
|
-
const key = trimmed.slice(0, eq).trim();
|
|
37
|
-
const raw = trimmed.slice(eq + 1).trim();
|
|
38
|
-
vars[key] = raw.replace(/^(['"])(.*)\1$/, '$2');
|
|
39
|
-
}
|
|
40
|
-
return vars;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function startDev(): void {
|
|
44
|
-
const entry = resolveEntry();
|
|
45
|
-
if (!entry) {
|
|
46
|
-
console.error(chalk.red('[EFC] Could not find entry point. Expected src/index.ts or index.ts'));
|
|
47
|
-
process.exit(1);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
console.log(chalk.cyan('[EFC] Starting development server…'));
|
|
51
|
-
console.log(chalk.dim(` Entry: ${entry}`));
|
|
52
|
-
|
|
53
|
-
const cwd = process.cwd();
|
|
54
|
-
const localTsx = path.join(cwd, 'node_modules', '.bin', 'tsx');
|
|
55
|
-
const tsx = fs.existsSync(localTsx) ? localTsx : 'tsx';
|
|
56
|
-
|
|
57
|
-
// .env values are base; existing process.env takes precedence (same as dotenv default)
|
|
58
|
-
const env: NodeJS.ProcessEnv = { ...parseDotenv(cwd), ...process.env, NODE_ENV: 'development' };
|
|
59
|
-
|
|
60
|
-
const child = spawn(tsx, ['watch', entry], { stdio: 'inherit', env });
|
|
61
|
-
child.on('exit', (code) => process.exit(code ?? 0));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function startProd(): void {
|
|
65
|
-
const cwd = process.cwd();
|
|
66
|
-
const distEntry = path.join(cwd, 'dist', 'index.js');
|
|
67
|
-
|
|
68
|
-
if (!fs.existsSync(distEntry)) {
|
|
69
|
-
console.error(chalk.red('[EFC] dist/index.js not found. Run `efc build prod` first.'));
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
console.log(chalk.cyan('[EFC] Starting production server…'));
|
|
74
|
-
|
|
75
|
-
// Production env comes exclusively from process.env — no .env file loading.
|
|
76
|
-
// Platforms (Docker, Kubernetes, Railway, Heroku, etc.) inject vars directly.
|
|
77
|
-
const child = spawn('node', [distEntry], {
|
|
78
|
-
stdio: 'inherit',
|
|
79
|
-
env: { ...process.env, NODE_ENV: 'production' },
|
|
80
|
-
});
|
|
81
|
-
child.on('exit', (code) => process.exit(code ?? 0));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function resolveEntry(): string | null {
|
|
85
|
-
const cwd = process.cwd();
|
|
86
|
-
const candidates = [
|
|
87
|
-
path.join(cwd, 'src', 'index.ts'),
|
|
88
|
-
path.join(cwd, 'index.ts'),
|
|
89
|
-
path.join(cwd, 'src', 'index.js'),
|
|
90
|
-
path.join(cwd, 'index.js'),
|
|
91
|
-
];
|
|
92
|
-
return candidates.find((f) => fs.existsSync(f)) ?? null;
|
|
93
|
-
}
|
package/src/cli/index.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
import { startCommand } from './commands/start.js';
|
|
4
|
-
import { buildCommand } from './commands/build.js';
|
|
5
|
-
import { runCommand } from './commands/run.js';
|
|
6
|
-
import { generateCommand } from './commands/generate.js';
|
|
7
|
-
import { diagnosticsCommands } from './commands/diagnostics.js';
|
|
8
|
-
|
|
9
|
-
const program = new Command('efc')
|
|
10
|
-
.description('Express File Cluster CLI')
|
|
11
|
-
.version('0.1.0');
|
|
12
|
-
|
|
13
|
-
program.addCommand(startCommand());
|
|
14
|
-
program.addCommand(buildCommand());
|
|
15
|
-
program.addCommand(runCommand());
|
|
16
|
-
program.addCommand(generateCommand());
|
|
17
|
-
|
|
18
|
-
for (const cmd of diagnosticsCommands()) {
|
|
19
|
-
program.addCommand(cmd);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
program.parse(process.argv);
|
package/src/cluster/index.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import cluster from 'node:cluster';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
|
|
4
|
-
interface ClusterOptions {
|
|
5
|
-
workers?: number;
|
|
6
|
-
onWorkerReady?: ((id: number) => void) | undefined;
|
|
7
|
-
onWorkerCrash?: ((id: number, code: number) => void) | undefined;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function runMaster(options: ClusterOptions = {}): void {
|
|
11
|
-
const count = options.workers ?? os.cpus().length;
|
|
12
|
-
|
|
13
|
-
console.log(`[EFC] Primary ${process.pid} starting ${count} workers`);
|
|
14
|
-
|
|
15
|
-
for (let i = 0; i < count; i++) {
|
|
16
|
-
cluster.fork();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
cluster.on('online', (worker) => {
|
|
20
|
-
options.onWorkerReady?.(worker.id);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
cluster.on('exit', (worker, code, signal) => {
|
|
24
|
-
const exitCode = code ?? (signal ? -1 : 0);
|
|
25
|
-
console.warn(`[EFC] Worker ${worker.id} exited (code=${exitCode}), respawning…`);
|
|
26
|
-
options.onWorkerCrash?.(worker.id, exitCode);
|
|
27
|
-
cluster.fork();
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export { cluster };
|
package/src/compose.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { RequestHandler } from 'express';
|
|
2
|
-
|
|
3
|
-
export function compose(...handlers: RequestHandler[]): RequestHandler {
|
|
4
|
-
return (req, res, next) => {
|
|
5
|
-
let index = 0;
|
|
6
|
-
|
|
7
|
-
function dispatch(i: number): void {
|
|
8
|
-
if (i >= handlers.length) {
|
|
9
|
-
next();
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
const handler = handlers[i];
|
|
13
|
-
if (!handler) {
|
|
14
|
-
next();
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
try {
|
|
18
|
-
Promise.resolve(handler(req, res, () => dispatch(i + 1))).catch(next);
|
|
19
|
-
} catch (err) {
|
|
20
|
-
next(err);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
dispatch(index);
|
|
25
|
-
};
|
|
26
|
-
}
|
package/src/db/index.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// Thread-local DB client populated during Pre-Flight.
|
|
2
|
-
// The Proxy ensures callers import `db` once and never null-check it —
|
|
3
|
-
// it throws with a helpful message if accessed before Pre-Flight completes.
|
|
4
|
-
|
|
5
|
-
type AnyClient = Record<string, unknown>;
|
|
6
|
-
|
|
7
|
-
let _client: AnyClient | null = null;
|
|
8
|
-
|
|
9
|
-
export function setDbClient(client: AnyClient): void {
|
|
10
|
-
_client = client;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function getDbClient(): AnyClient {
|
|
14
|
-
if (!_client) {
|
|
15
|
-
throw new Error('[EFC] db not ready — accessed before Pre-Flight completed');
|
|
16
|
-
}
|
|
17
|
-
return _client;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const db: AnyClient = new Proxy({} as AnyClient, {
|
|
21
|
-
get(_target, prop: string) {
|
|
22
|
-
return getDbClient()[prop];
|
|
23
|
-
},
|
|
24
|
-
set(_target, prop: string, value: unknown) {
|
|
25
|
-
getDbClient()[prop] = value;
|
|
26
|
-
return true;
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
export { connectMongo } from './mongo.js';
|
|
31
|
-
export { defineModel } from './model.js';
|
package/src/db/model.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import type { ModelSchema, ModelCRUD } from '../types.js';
|
|
2
|
-
|
|
3
|
-
type AnyRecord = Record<string, unknown> & { id: string };
|
|
4
|
-
|
|
5
|
-
function normalise(doc: Record<string, unknown>): AnyRecord {
|
|
6
|
-
const id = doc['_id'] ? String(doc['_id']) : '';
|
|
7
|
-
return { ...doc, id } as AnyRecord;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
async function getModel(name: string, schemaObj: Record<string, unknown>): Promise<import('mongoose').Model<Record<string, unknown>>> {
|
|
11
|
-
let mg: typeof import('mongoose');
|
|
12
|
-
try {
|
|
13
|
-
mg = await import('mongoose');
|
|
14
|
-
} catch {
|
|
15
|
-
throw new Error('[EFC] mongoose is not installed. Run: npm install mongoose');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (mg.models[name]) return mg.models[name] as import('mongoose').Model<Record<string, unknown>>;
|
|
19
|
-
|
|
20
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
-
const schema = new mg.Schema(schemaObj as any, { timestamps: true });
|
|
22
|
-
return mg.model<Record<string, unknown>>(name, schema);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function buildMongooseSchema(schema: ModelSchema): Record<string, unknown> {
|
|
26
|
-
const out: Record<string, unknown> = {};
|
|
27
|
-
for (const [key, def] of Object.entries(schema)) {
|
|
28
|
-
const entry: Record<string, unknown> = {};
|
|
29
|
-
switch (def.type) {
|
|
30
|
-
case 'string': entry['type'] = String; break;
|
|
31
|
-
case 'number': entry['type'] = Number; break;
|
|
32
|
-
case 'boolean': entry['type'] = Boolean; break;
|
|
33
|
-
case 'date': entry['type'] = Date; break;
|
|
34
|
-
case 'object': entry['type'] = Object; break;
|
|
35
|
-
case 'array': entry['type'] = Array; break;
|
|
36
|
-
}
|
|
37
|
-
if (def.required !== undefined) entry['required'] = def.required;
|
|
38
|
-
if (def.unique !== undefined) entry['unique'] = def.unique;
|
|
39
|
-
if (def.default !== undefined) entry['default'] = def.default;
|
|
40
|
-
out[key] = entry;
|
|
41
|
-
}
|
|
42
|
-
return out;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function defineModel<T extends Record<string, unknown>>(
|
|
46
|
-
name: string,
|
|
47
|
-
schema: ModelSchema,
|
|
48
|
-
): ModelCRUD<T> {
|
|
49
|
-
const mongooseSchema = buildMongooseSchema(schema);
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
async find(filter = {}) {
|
|
53
|
-
const M = await getModel(name, mongooseSchema);
|
|
54
|
-
const docs = await M.find(filter as Record<string, unknown>).lean();
|
|
55
|
-
return (docs as Record<string, unknown>[]).map(normalise) as (T & { id: string })[];
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
async findById(id) {
|
|
59
|
-
const M = await getModel(name, mongooseSchema);
|
|
60
|
-
const doc = await M.findById(id).lean();
|
|
61
|
-
if (!doc) return null;
|
|
62
|
-
return normalise(doc as Record<string, unknown>) as T & { id: string };
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
async findOne(filter) {
|
|
66
|
-
const M = await getModel(name, mongooseSchema);
|
|
67
|
-
const doc = await M.findOne(filter as Record<string, unknown>).lean();
|
|
68
|
-
if (!doc) return null;
|
|
69
|
-
return normalise(doc as Record<string, unknown>) as T & { id: string };
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
async create(data) {
|
|
73
|
-
const M = await getModel(name, mongooseSchema);
|
|
74
|
-
const doc = await M.create(data);
|
|
75
|
-
return normalise(doc.toObject() as Record<string, unknown>) as T & { id: string };
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
async update(id, data) {
|
|
79
|
-
const M = await getModel(name, mongooseSchema);
|
|
80
|
-
const doc = await M.findByIdAndUpdate(id, data, { new: true }).lean();
|
|
81
|
-
if (!doc) return null;
|
|
82
|
-
return normalise(doc as Record<string, unknown>) as T & { id: string };
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
async delete(id) {
|
|
86
|
-
const M = await getModel(name, mongooseSchema);
|
|
87
|
-
await M.findByIdAndDelete(id);
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
async count(filter = {}) {
|
|
91
|
-
const M = await getModel(name, mongooseSchema);
|
|
92
|
-
return M.countDocuments(filter as Record<string, unknown>);
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
}
|