express-file-cluster 0.1.3 → 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.
@@ -1,90 +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
- const env: NodeJS.ProcessEnv = { ...parseDotenv(cwd), ...process.env, NODE_ENV: 'production' };
76
-
77
- const child = spawn('node', [distEntry], { stdio: 'inherit', env });
78
- child.on('exit', (code) => process.exit(code ?? 0));
79
- }
80
-
81
- function resolveEntry(): string | null {
82
- const cwd = process.cwd();
83
- const candidates = [
84
- path.join(cwd, 'src', 'index.ts'),
85
- path.join(cwd, 'index.ts'),
86
- path.join(cwd, 'src', 'index.js'),
87
- path.join(cwd, 'index.js'),
88
- ];
89
- return candidates.find((f) => fs.existsSync(f)) ?? null;
90
- }
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);
@@ -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
- }
package/src/db/mongo.ts DELETED
@@ -1,22 +0,0 @@
1
- // Wraps mongoose connection for Pre-Flight. Throws clearly if mongoose isn't installed.
2
-
3
- let mongoose: typeof import('mongoose');
4
-
5
- async function loadMongoose(): Promise<typeof import('mongoose')> {
6
- if (mongoose) return mongoose;
7
- try {
8
- mongoose = await import('mongoose');
9
- return mongoose;
10
- } catch {
11
- throw new Error(
12
- '[EFC] mongoose is not installed. Run: npm install mongoose',
13
- );
14
- }
15
- }
16
-
17
- export async function connectMongo(url: string): Promise<import('mongoose').Connection> {
18
- const mg = await loadMongoose();
19
- await mg.connect(url);
20
- console.log('[EFC] MongoDB connected');
21
- return mg.connection;
22
- }
package/src/errors.ts DELETED
@@ -1,10 +0,0 @@
1
- export class HttpError extends Error {
2
- readonly statusCode: number;
3
-
4
- constructor(statusCode: number, message: string) {
5
- super(message);
6
- this.name = 'HttpError';
7
- this.statusCode = statusCode;
8
- Error.captureStackTrace(this, HttpError);
9
- }
10
- }
@@ -1,193 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import request from 'supertest';
3
- import fs from 'node:fs';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
- import type { Server } from 'node:http';
7
- import { ignite } from './index.js';
8
-
9
- // helpers ----------------------------------------------------------------
10
-
11
- function tmpDir() {
12
- return fs.mkdtempSync(path.join(os.tmpdir(), 'efc-ignite-'));
13
- }
14
-
15
- function writeRoute(dir: string, name: string, code: string) {
16
- const file = path.join(dir, name);
17
- fs.writeFileSync(file, code);
18
- return file;
19
- }
20
-
21
- function closeServer(server: Server): Promise<void> {
22
- return new Promise((resolve, reject) => server.close((e) => (e ? reject(e) : resolve())));
23
- }
24
-
25
- // -----------------------------------------------------------------------
26
-
27
- describe('ignite() — server integration', () => {
28
- let apiDir: string;
29
- let server: Server | undefined;
30
-
31
- beforeEach(() => {
32
- apiDir = tmpDir();
33
- // ensure no DATABASE_URL so connectMongo is never called
34
- delete process.env['DATABASE_URL'];
35
- delete process.env['CORS_ORIGINS'];
36
- });
37
-
38
- afterEach(async () => {
39
- if (server) { await closeServer(server); server = undefined; }
40
- fs.rmSync(apiDir, { recursive: true, force: true });
41
- });
42
-
43
- // --- route serving ----------------------------------------------------
44
-
45
- it('serves a GET route', async () => {
46
- writeRoute(apiDir, 'health.js', `export const GET = async (_req, res) => res.json({ ok: true });`);
47
- server = await ignite({ apiDir, cluster: false });
48
- const res = await request(server).get('/health');
49
- expect(res.status).toBe(200);
50
- expect(res.body).toEqual({ ok: true });
51
- });
52
-
53
- it('serves a POST route', async () => {
54
- writeRoute(apiDir, 'echo.js', `export const POST = async (req, res) => res.status(201).json(req.body);`);
55
- server = await ignite({ apiDir, cluster: false });
56
- const res = await request(server).post('/echo').send({ msg: 'hello' });
57
- expect(res.status).toBe(201);
58
- expect(res.body).toEqual({ msg: 'hello' });
59
- });
60
-
61
- it('resolves dynamic route params', async () => {
62
- fs.mkdirSync(path.join(apiDir, 'users'));
63
- writeRoute(
64
- path.join(apiDir, 'users'), '[id].js',
65
- `export const GET = async (req, res) => res.json({ id: req.params.id });`,
66
- );
67
- server = await ignite({ apiDir, cluster: false });
68
- const res = await request(server).get('/users/42');
69
- expect(res.status).toBe(200);
70
- expect(res.body).toEqual({ id: '42' });
71
- });
72
-
73
- it('returns 405 for an unregistered method', async () => {
74
- writeRoute(apiDir, 'items.js', `export const GET = async (_req, res) => res.json([]);`);
75
- server = await ignite({ apiDir, cluster: false });
76
- const res = await request(server).post('/items');
77
- expect(res.status).toBe(405);
78
- });
79
-
80
- // --- body parsing -----------------------------------------------------
81
-
82
- it('parses JSON bodies automatically', async () => {
83
- writeRoute(apiDir, 'body.js', `export const POST = async (req, res) => res.json(req.body);`);
84
- server = await ignite({ apiDir, cluster: false });
85
- const res = await request(server)
86
- .post('/body')
87
- .set('Content-Type', 'application/json')
88
- .send(JSON.stringify({ x: 1 }));
89
- expect(res.body).toEqual({ x: 1 });
90
- });
91
-
92
- it('parses URL-encoded bodies automatically', async () => {
93
- writeRoute(apiDir, 'form.js', `export const POST = async (req, res) => res.json(req.body);`);
94
- server = await ignite({ apiDir, cluster: false });
95
- const res = await request(server)
96
- .post('/form')
97
- .set('Content-Type', 'application/x-www-form-urlencoded')
98
- .send('name=efc&version=1');
99
- expect(res.body).toEqual({ name: 'efc', version: '1' });
100
- });
101
-
102
- // --- CORS -------------------------------------------------------------
103
-
104
- it('allows all origins by default', async () => {
105
- writeRoute(apiDir, 'ping.js', `export const GET = async (_req, res) => res.json({});`);
106
- server = await ignite({ apiDir, cluster: false });
107
- const res = await request(server).get('/ping').set('Origin', 'http://unknown.example');
108
- expect(res.headers['access-control-allow-origin']).toBe('http://unknown.example');
109
- });
110
-
111
- it('restricts origins when CORS_ORIGINS is set', async () => {
112
- process.env['CORS_ORIGINS'] = 'http://allowed.example';
113
- writeRoute(apiDir, 'ping.js', `export const GET = async (_req, res) => res.json({});`);
114
- server = await ignite({ apiDir, cluster: false });
115
-
116
- const allowed = await request(server).get('/ping').set('Origin', 'http://allowed.example');
117
- expect(allowed.headers['access-control-allow-origin']).toBe('http://allowed.example');
118
-
119
- const blocked = await request(server).get('/ping').set('Origin', 'http://evil.example');
120
- expect(blocked.headers['access-control-allow-origin']).toBeUndefined();
121
- });
122
-
123
- it('supports multiple origins in CORS_ORIGINS', async () => {
124
- process.env['CORS_ORIGINS'] = 'http://a.example, http://b.example';
125
- writeRoute(apiDir, 'ping.js', `export const GET = async (_req, res) => res.json({});`);
126
- server = await ignite({ apiDir, cluster: false });
127
-
128
- const a = await request(server).get('/ping').set('Origin', 'http://a.example');
129
- expect(a.headers['access-control-allow-origin']).toBe('http://a.example');
130
-
131
- const b = await request(server).get('/ping').set('Origin', 'http://b.example');
132
- expect(b.headers['access-control-allow-origin']).toBe('http://b.example');
133
- });
134
-
135
- it('disables CORS when cors: false', async () => {
136
- writeRoute(apiDir, 'ping.js', `export const GET = async (_req, res) => res.json({});`);
137
- server = await ignite({ apiDir, cluster: false, cors: false });
138
- const res = await request(server).get('/ping').set('Origin', 'http://anywhere.example');
139
- expect(res.headers['access-control-allow-origin']).toBeUndefined();
140
- });
141
-
142
- // --- port & env auto-reading ------------------------------------------
143
-
144
- it('reads PORT from env when port is not passed', async () => {
145
- process.env['PORT'] = '0'; // 0 = OS assigns a free port
146
- writeRoute(apiDir, 'hi.js', `export const GET = async (_req, res) => res.json({ hi: true });`);
147
- server = await ignite({ apiDir, cluster: false });
148
- const addr = server.address() as { port: number };
149
- expect(addr.port).toBeGreaterThan(0);
150
- delete process.env['PORT'];
151
- });
152
-
153
- it('falls back to port 3000 when PORT env is absent', async () => {
154
- delete process.env['PORT'];
155
- writeRoute(apiDir, 'hi.js', `export const GET = async (_req, res) => res.json({});`);
156
- server = await ignite({ apiDir, cluster: false, port: 0 }); // 0 avoids binding 3000 in tests
157
- const addr = server.address() as { port: number };
158
- expect(addr.port).toBeGreaterThan(0);
159
- });
160
-
161
- it('ignores NaN port and falls back to env PORT', async () => {
162
- process.env['PORT'] = '0';
163
- writeRoute(apiDir, 'hi.js', `export const GET = async (_req, res) => res.json({});`);
164
- server = await ignite({ apiDir, cluster: false, port: Number(undefined) }); // NaN
165
- const addr = server.address() as { port: number };
166
- expect(addr.port).toBeGreaterThan(0);
167
- delete process.env['PORT'];
168
- });
169
-
170
- // --- error handling ---------------------------------------------------
171
-
172
- it('returns 404 for unknown routes', async () => {
173
- writeRoute(apiDir, 'exists.js', `export const GET = async (_req, res) => res.json({});`);
174
- server = await ignite({ apiDir, cluster: false });
175
- const res = await request(server).get('/does-not-exist');
176
- expect(res.status).toBe(404);
177
- });
178
-
179
- it('returns structured JSON for HttpError', async () => {
180
- // Attach statusCode directly to avoid cross-module instanceof issues in tests
181
- writeRoute(
182
- apiDir, 'fail.js',
183
- `export const GET = async (_req, _res, next) => {
184
- const err = Object.assign(new Error('Invalid input'), { statusCode: 422 });
185
- next(err);
186
- };`,
187
- );
188
- server = await ignite({ apiDir, cluster: false });
189
- const res = await request(server).get('/fail');
190
- expect(res.status).toBe(422);
191
- expect(res.body).toMatchObject({ error: 'Invalid input', statusCode: 422 });
192
- });
193
- });
package/src/index.ts DELETED
@@ -1,162 +0,0 @@
1
- import 'dotenv/config';
2
- import http from 'node:http';
3
- import express from 'express';
4
- import cors from 'cors';
5
- import cookieParser from 'cookie-parser';
6
- import cluster from 'node:cluster';
7
- import os from 'node:os';
8
- import type { EFCConfig } from './types.js';
9
- import { scanDir } from './router/scan.js';
10
- import { mountRoutes } from './router/mount.js';
11
- import { runMaster } from './cluster/index.js';
12
- import { configureAuth } from './auth/index.js';
13
- import { HttpError } from './errors.js';
14
- import { connectMongo } from './db/mongo.js';
15
- import { setDbClient } from './db/index.js';
16
- import { scanTasks } from './tasks/scanner.js';
17
- import { initBullMQ } from './tasks/bullmq-backend.js';
18
-
19
- function detectDatabase(url?: string): 'mongodb' | 'postgresql' | undefined {
20
- if (!url) return undefined;
21
- if (url.startsWith('mongodb')) return 'mongodb';
22
- if (url.startsWith('postgres')) return 'postgresql';
23
- return undefined;
24
- }
25
-
26
- export async function ignite(config: EFCConfig): Promise<http.Server | undefined> {
27
- const {
28
- port: _port,
29
- workers,
30
- apiDir,
31
- globalMiddlewares = [],
32
- onWorkerReady,
33
- onWorkerCrash,
34
- onError,
35
- } = config;
36
-
37
- // All runtime values fall back to environment variables
38
- const port =
39
- _port != null && !Number.isNaN(_port) ? _port : Number(process.env['PORT']) || 3000;
40
- const databaseUrl = config.databaseUrl ?? process.env['DATABASE_URL'];
41
- const database = config.database ?? detectDatabase(databaseUrl);
42
- const jwtSecret = config.jwtSecret ?? process.env['JWT_SECRET'];
43
- const authStrategy = config.authStrategy ?? 'http-only';
44
- const clusterEnabled = config.cluster ?? process.env['NODE_ENV'] === 'production';
45
-
46
- if (clusterEnabled && cluster.isPrimary) {
47
- runMaster({
48
- workers: workers ?? os.cpus().length,
49
- ...(onWorkerReady !== undefined && { onWorkerReady }),
50
- ...(onWorkerCrash !== undefined && { onWorkerCrash }),
51
- });
52
- return;
53
- }
54
-
55
- const app = express();
56
-
57
- const corsOption = config.cors ?? true;
58
- if (corsOption !== false) {
59
- const envOrigins = process.env['CORS_ORIGINS'];
60
- let origin: string | string[] | boolean;
61
- if (envOrigins) {
62
- const list = envOrigins.split(',').map((o) => o.trim()).filter(Boolean);
63
- origin = list; // always array so cors validates against the request Origin
64
- } else if (typeof corsOption === 'object' && corsOption.origin !== undefined) {
65
- origin = corsOption.origin;
66
- } else {
67
- origin = true;
68
- }
69
- const corsOpts = typeof corsOption === 'object' ? { ...corsOption, origin } : { origin };
70
- app.use(cors(corsOpts));
71
- }
72
-
73
- app.use(express.json());
74
- app.use(express.urlencoded({ extended: true }));
75
- app.use(cookieParser());
76
-
77
- for (const mw of globalMiddlewares) {
78
- app.use(mw);
79
- }
80
-
81
- // Pre-Flight step 1: Connect database
82
- if (database === 'mongodb' && databaseUrl) {
83
- const conn = await connectMongo(databaseUrl);
84
- setDbClient(conn as unknown as Record<string, unknown>);
85
- }
86
-
87
- // Pre-Flight step 2: Configure auth
88
- if (jwtSecret) {
89
- const cookieDomain = process.env['COOKIE_DOMAIN'];
90
- configureAuth({
91
- secret: jwtSecret,
92
- strategy: authStrategy,
93
- expiresIn: process.env['JWT_EXPIRES_IN'] ?? '7d',
94
- ...(cookieDomain !== undefined && { cookieDomain }),
95
- });
96
- }
97
-
98
- // Pre-Flight step 3: Scan and register tasks
99
- if (config.tasksDir) {
100
- await scanTasks(config.tasksDir);
101
- }
102
-
103
- // Pre-Flight step 4: Start task queue backend
104
- if (config.tasks) {
105
- if (config.tasks.backend === 'bullmq') {
106
- await initBullMQ({
107
- redisUrl: config.tasks.redisUrl ?? 'redis://localhost:6379',
108
- concurrency: config.tasks.concurrency ?? 5,
109
- });
110
- }
111
- }
112
-
113
- // Pre-Flight step 5: Scan routes and mount
114
- const routes = scanDir(apiDir);
115
- await mountRoutes(app, routes);
116
-
117
- if (onError) {
118
- app.use(onError);
119
- } else {
120
- app.use(
121
- (
122
- err: unknown,
123
- _req: express.Request,
124
- res: express.Response,
125
- _next: express.NextFunction,
126
- ) => {
127
- const asHttp = err instanceof Error && 'statusCode' in err
128
- ? (err as HttpError) : null;
129
- if (asHttp) {
130
- res.status(asHttp.statusCode).json({ error: asHttp.message, statusCode: asHttp.statusCode });
131
- } else {
132
- console.error('[EFC] Unhandled error:', err);
133
- res.status(500).json({ error: 'Internal Server Error', statusCode: 500 });
134
- }
135
- },
136
- );
137
- }
138
-
139
- return new Promise<http.Server>((resolve) => {
140
- const server = app.listen(port, () => {
141
- const wid = (cluster.worker as { id: number } | undefined)?.id ?? 'primary';
142
- console.log(`[EFC] Worker ${wid} listening on :${port}`);
143
- resolve(server);
144
- });
145
- });
146
- }
147
-
148
- export { HttpError } from './errors.js';
149
- export { compose } from './compose.js';
150
- export { db, setDbClient, getDbClient, defineModel } from './db/index.js';
151
- export { scanDir } from './router/scan.js';
152
- export type {
153
- EFCConfig,
154
- CorsConfig,
155
- RouteEntry,
156
- TaskOptions,
157
- TaskDefinition,
158
- DatabaseEngine,
159
- AuthStrategy,
160
- ModelSchema,
161
- ModelCRUD,
162
- } from './types.js';