express-file-cluster 0.1.0
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/dist/auth/index.cjs +98 -0
- package/dist/auth/index.cjs.map +1 -0
- package/dist/auth/index.d.cts +16 -0
- package/dist/auth/index.d.ts +16 -0
- package/dist/auth/index.js +59 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/cli/index.cjs +380 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +357 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.cjs +541 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +498 -0
- package/dist/index.js.map +1 -0
- package/dist/tasks/index.cjs +70 -0
- package/dist/tasks/index.cjs.map +1 -0
- package/dist/tasks/index.d.cts +16 -0
- package/dist/tasks/index.d.ts +16 -0
- package/dist/tasks/index.js +41 -0
- package/dist/tasks/index.js.map +1 -0
- package/dist/types-DNdkDUPX.d.cts +70 -0
- package/dist/types-DNdkDUPX.d.ts +70 -0
- package/package.json +69 -0
- package/src/auth/index.ts +71 -0
- package/src/cli/commands/build.ts +43 -0
- package/src/cli/commands/diagnostics.ts +124 -0
- package/src/cli/commands/generate.ts +91 -0
- package/src/cli/commands/run.ts +28 -0
- package/src/cli/commands/start.ts +74 -0
- package/src/cli/index.ts +22 -0
- package/src/cluster/index.ts +31 -0
- package/src/compose.ts +26 -0
- package/src/db/index.ts +31 -0
- package/src/db/model.ts +95 -0
- package/src/db/mongo.ts +22 -0
- package/src/errors.ts +10 -0
- package/src/index.ts +127 -0
- package/src/router/mount.test.ts +75 -0
- package/src/router/mount.ts +49 -0
- package/src/router/scan.test.ts +23 -0
- package/src/router/scan.ts +55 -0
- package/src/tasks/bullmq-backend.ts +76 -0
- package/src/tasks/index.ts +51 -0
- package/src/tasks/scanner.test.ts +58 -0
- package/src/tasks/scanner.ts +30 -0
- package/src/tasks/thread-runner.ts +40 -0
- package/src/types.ts +68 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +26 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { T as TaskDefinition, a as TaskOptions } from '../types-DNdkDUPX.js';
|
|
2
|
+
import 'express';
|
|
3
|
+
|
|
4
|
+
declare const taskRegistry: Map<string, TaskDefinition<unknown>>;
|
|
5
|
+
type HandlerFn<T> = (payload: T) => Promise<void>;
|
|
6
|
+
type DefineTaskOverload = {
|
|
7
|
+
<T>(handler: HandlerFn<T>): TaskDefinition<T>;
|
|
8
|
+
<T>(options: TaskOptions, handler: HandlerFn<T>): TaskDefinition<T>;
|
|
9
|
+
};
|
|
10
|
+
declare const defineTask: DefineTaskOverload;
|
|
11
|
+
type EnqueueImpl = (name: string, payload: unknown) => Promise<void>;
|
|
12
|
+
declare function setEnqueueImpl(impl: EnqueueImpl): void;
|
|
13
|
+
declare function enqueue<T>(name: string, payload: T): Promise<void>;
|
|
14
|
+
declare function registerTask(name: string, def: TaskDefinition): void;
|
|
15
|
+
|
|
16
|
+
export { defineTask, enqueue, registerTask, setEnqueueImpl, taskRegistry };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/tasks/index.ts
|
|
2
|
+
var taskRegistry = /* @__PURE__ */ new Map();
|
|
3
|
+
var defineTask = (handlerOrOptions, maybeHandler) => {
|
|
4
|
+
let options = {};
|
|
5
|
+
let handler;
|
|
6
|
+
if (typeof handlerOrOptions === "function") {
|
|
7
|
+
handler = handlerOrOptions;
|
|
8
|
+
} else {
|
|
9
|
+
options = handlerOrOptions;
|
|
10
|
+
if (!maybeHandler) throw new Error("[EFC] defineTask: handler function is required");
|
|
11
|
+
handler = maybeHandler;
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
handler,
|
|
15
|
+
options,
|
|
16
|
+
name: ""
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
var _impl = null;
|
|
20
|
+
function setEnqueueImpl(impl) {
|
|
21
|
+
_impl = impl;
|
|
22
|
+
}
|
|
23
|
+
async function enqueue(name, payload) {
|
|
24
|
+
if (!_impl) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`[EFC] Task queue not initialised. Set tasks.backend in ignite() config.`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return _impl(name, payload);
|
|
30
|
+
}
|
|
31
|
+
function registerTask(name, def) {
|
|
32
|
+
taskRegistry.set(name, { ...def, name });
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
defineTask,
|
|
36
|
+
enqueue,
|
|
37
|
+
registerTask,
|
|
38
|
+
setEnqueueImpl,
|
|
39
|
+
taskRegistry
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/tasks/index.ts"],"sourcesContent":["import type { TaskDefinition, TaskOptions } from '../types.js';\n\nexport const taskRegistry = new Map<string, TaskDefinition>();\n\ntype HandlerFn<T> = (payload: T) => Promise<void>;\ntype DefineTaskOverload = {\n <T>(handler: HandlerFn<T>): TaskDefinition<T>;\n <T>(options: TaskOptions, handler: HandlerFn<T>): TaskDefinition<T>;\n};\n\nexport const defineTask: DefineTaskOverload = <T>(\n handlerOrOptions: HandlerFn<T> | TaskOptions,\n maybeHandler?: HandlerFn<T>,\n): TaskDefinition<T> => {\n let options: TaskOptions = {};\n let handler: HandlerFn<T>;\n\n if (typeof handlerOrOptions === 'function') {\n handler = handlerOrOptions;\n } else {\n options = handlerOrOptions;\n if (!maybeHandler) throw new Error('[EFC] defineTask: handler function is required');\n handler = maybeHandler;\n }\n\n return {\n handler: handler as (payload: unknown) => Promise<void>,\n options,\n name: '',\n };\n};\n\ntype EnqueueImpl = (name: string, payload: unknown) => Promise<void>;\nlet _impl: EnqueueImpl | null = null;\n\nexport function setEnqueueImpl(impl: EnqueueImpl): void {\n _impl = impl;\n}\n\nexport async function enqueue<T>(name: string, payload: T): Promise<void> {\n if (!_impl) {\n throw new Error(\n `[EFC] Task queue not initialised. Set tasks.backend in ignite() config.`,\n );\n }\n return _impl(name, payload as unknown);\n}\n\nexport function registerTask(name: string, def: TaskDefinition): void {\n taskRegistry.set(name, { ...def, name });\n}\n"],"mappings":";AAEO,IAAM,eAAe,oBAAI,IAA4B;AAQrD,IAAM,aAAiC,CAC5C,kBACA,iBACsB;AACtB,MAAI,UAAuB,CAAC;AAC5B,MAAI;AAEJ,MAAI,OAAO,qBAAqB,YAAY;AAC1C,cAAU;AAAA,EACZ,OAAO;AACL,cAAU;AACV,QAAI,CAAC,aAAc,OAAM,IAAI,MAAM,gDAAgD;AACnF,cAAU;AAAA,EACZ;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,MAAM;AAAA,EACR;AACF;AAGA,IAAI,QAA4B;AAEzB,SAAS,eAAe,MAAyB;AACtD,UAAQ;AACV;AAEA,eAAsB,QAAW,MAAc,SAA2B;AACxE,MAAI,CAAC,OAAO;AACV,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO,MAAM,MAAM,OAAkB;AACvC;AAEO,SAAS,aAAa,MAAc,KAA2B;AACpE,eAAa,IAAI,MAAM,EAAE,GAAG,KAAK,KAAK,CAAC;AACzC;","names":[]}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { RequestHandler, ErrorRequestHandler } from 'express';
|
|
2
|
+
|
|
3
|
+
type DatabaseEngine = 'mongodb' | 'postgresql';
|
|
4
|
+
type AuthStrategy = 'http-only' | 'localStorage';
|
|
5
|
+
type TaskBackend = 'bullmq' | 'pg-boss';
|
|
6
|
+
interface TaskConfig {
|
|
7
|
+
backend: TaskBackend;
|
|
8
|
+
redisUrl?: string;
|
|
9
|
+
concurrency?: number;
|
|
10
|
+
}
|
|
11
|
+
interface EFCConfig {
|
|
12
|
+
port?: number;
|
|
13
|
+
apiDir: string;
|
|
14
|
+
tasksDir?: string;
|
|
15
|
+
database?: DatabaseEngine;
|
|
16
|
+
databaseUrl?: string;
|
|
17
|
+
authStrategy?: AuthStrategy;
|
|
18
|
+
jwtSecret?: string;
|
|
19
|
+
cluster?: boolean;
|
|
20
|
+
workers?: number;
|
|
21
|
+
tasks?: TaskConfig | false;
|
|
22
|
+
globalMiddlewares?: RequestHandler[];
|
|
23
|
+
onWorkerReady?: (id: number) => void;
|
|
24
|
+
onWorkerCrash?: (id: number, code: number) => void;
|
|
25
|
+
onError?: ErrorRequestHandler;
|
|
26
|
+
}
|
|
27
|
+
interface RouteEntry {
|
|
28
|
+
urlPath: string;
|
|
29
|
+
filePath: string;
|
|
30
|
+
params: string[];
|
|
31
|
+
}
|
|
32
|
+
interface TaskOptions {
|
|
33
|
+
thread?: boolean;
|
|
34
|
+
retries?: number;
|
|
35
|
+
backoff?: 'fixed' | 'exponential';
|
|
36
|
+
concurrency?: number;
|
|
37
|
+
schedule?: string;
|
|
38
|
+
}
|
|
39
|
+
interface TaskDefinition<TPayload = unknown> {
|
|
40
|
+
handler: (payload: TPayload) => Promise<void>;
|
|
41
|
+
options: TaskOptions;
|
|
42
|
+
name: string;
|
|
43
|
+
filePath?: string;
|
|
44
|
+
}
|
|
45
|
+
interface FieldDefinition {
|
|
46
|
+
type: 'string' | 'number' | 'boolean' | 'date' | 'object' | 'array';
|
|
47
|
+
required?: boolean;
|
|
48
|
+
unique?: boolean;
|
|
49
|
+
default?: unknown;
|
|
50
|
+
}
|
|
51
|
+
type ModelSchema = Record<string, FieldDefinition>;
|
|
52
|
+
interface ModelCRUD<T extends Record<string, unknown>> {
|
|
53
|
+
find(filter?: Partial<T>): Promise<T[]>;
|
|
54
|
+
findById(id: string): Promise<(T & {
|
|
55
|
+
id: string;
|
|
56
|
+
}) | null>;
|
|
57
|
+
findOne(filter: Partial<T>): Promise<(T & {
|
|
58
|
+
id: string;
|
|
59
|
+
}) | null>;
|
|
60
|
+
create(data: Partial<T>): Promise<T & {
|
|
61
|
+
id: string;
|
|
62
|
+
}>;
|
|
63
|
+
update(id: string, data: Partial<T>): Promise<(T & {
|
|
64
|
+
id: string;
|
|
65
|
+
}) | null>;
|
|
66
|
+
delete(id: string): Promise<void>;
|
|
67
|
+
count(filter?: Partial<T>): Promise<number>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type { AuthStrategy as A, DatabaseEngine as D, EFCConfig as E, ModelSchema as M, RouteEntry as R, TaskDefinition as T, TaskOptions as a, ModelCRUD as b };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { RequestHandler, ErrorRequestHandler } from 'express';
|
|
2
|
+
|
|
3
|
+
type DatabaseEngine = 'mongodb' | 'postgresql';
|
|
4
|
+
type AuthStrategy = 'http-only' | 'localStorage';
|
|
5
|
+
type TaskBackend = 'bullmq' | 'pg-boss';
|
|
6
|
+
interface TaskConfig {
|
|
7
|
+
backend: TaskBackend;
|
|
8
|
+
redisUrl?: string;
|
|
9
|
+
concurrency?: number;
|
|
10
|
+
}
|
|
11
|
+
interface EFCConfig {
|
|
12
|
+
port?: number;
|
|
13
|
+
apiDir: string;
|
|
14
|
+
tasksDir?: string;
|
|
15
|
+
database?: DatabaseEngine;
|
|
16
|
+
databaseUrl?: string;
|
|
17
|
+
authStrategy?: AuthStrategy;
|
|
18
|
+
jwtSecret?: string;
|
|
19
|
+
cluster?: boolean;
|
|
20
|
+
workers?: number;
|
|
21
|
+
tasks?: TaskConfig | false;
|
|
22
|
+
globalMiddlewares?: RequestHandler[];
|
|
23
|
+
onWorkerReady?: (id: number) => void;
|
|
24
|
+
onWorkerCrash?: (id: number, code: number) => void;
|
|
25
|
+
onError?: ErrorRequestHandler;
|
|
26
|
+
}
|
|
27
|
+
interface RouteEntry {
|
|
28
|
+
urlPath: string;
|
|
29
|
+
filePath: string;
|
|
30
|
+
params: string[];
|
|
31
|
+
}
|
|
32
|
+
interface TaskOptions {
|
|
33
|
+
thread?: boolean;
|
|
34
|
+
retries?: number;
|
|
35
|
+
backoff?: 'fixed' | 'exponential';
|
|
36
|
+
concurrency?: number;
|
|
37
|
+
schedule?: string;
|
|
38
|
+
}
|
|
39
|
+
interface TaskDefinition<TPayload = unknown> {
|
|
40
|
+
handler: (payload: TPayload) => Promise<void>;
|
|
41
|
+
options: TaskOptions;
|
|
42
|
+
name: string;
|
|
43
|
+
filePath?: string;
|
|
44
|
+
}
|
|
45
|
+
interface FieldDefinition {
|
|
46
|
+
type: 'string' | 'number' | 'boolean' | 'date' | 'object' | 'array';
|
|
47
|
+
required?: boolean;
|
|
48
|
+
unique?: boolean;
|
|
49
|
+
default?: unknown;
|
|
50
|
+
}
|
|
51
|
+
type ModelSchema = Record<string, FieldDefinition>;
|
|
52
|
+
interface ModelCRUD<T extends Record<string, unknown>> {
|
|
53
|
+
find(filter?: Partial<T>): Promise<T[]>;
|
|
54
|
+
findById(id: string): Promise<(T & {
|
|
55
|
+
id: string;
|
|
56
|
+
}) | null>;
|
|
57
|
+
findOne(filter: Partial<T>): Promise<(T & {
|
|
58
|
+
id: string;
|
|
59
|
+
}) | null>;
|
|
60
|
+
create(data: Partial<T>): Promise<T & {
|
|
61
|
+
id: string;
|
|
62
|
+
}>;
|
|
63
|
+
update(id: string, data: Partial<T>): Promise<(T & {
|
|
64
|
+
id: string;
|
|
65
|
+
}) | null>;
|
|
66
|
+
delete(id: string): Promise<void>;
|
|
67
|
+
count(filter?: Partial<T>): Promise<number>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type { AuthStrategy as A, DatabaseEngine as D, EFCConfig as E, ModelSchema as M, RouteEntry as R, TaskDefinition as T, TaskOptions as a, ModelCRUD as b };
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "express-file-cluster",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Enterprise-grade, zero-boilerplate backend framework with file-based routing and multi-core clustering",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"bin": {
|
|
11
|
+
"efc": "./dist/cli/index.js"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"require": "./dist/index.cjs",
|
|
17
|
+
"types": "./dist/index.d.ts"
|
|
18
|
+
},
|
|
19
|
+
"./auth": {
|
|
20
|
+
"import": "./dist/auth/index.js",
|
|
21
|
+
"require": "./dist/auth/index.cjs",
|
|
22
|
+
"types": "./dist/auth/index.d.ts"
|
|
23
|
+
},
|
|
24
|
+
"./tasks": {
|
|
25
|
+
"import": "./dist/tasks/index.js",
|
|
26
|
+
"require": "./dist/tasks/index.cjs",
|
|
27
|
+
"types": "./dist/tasks/index.d.ts"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup",
|
|
32
|
+
"dev": "tsup --watch",
|
|
33
|
+
"typecheck": "tsc --noEmit"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"chalk": "^5.3.0",
|
|
37
|
+
"chokidar": "^4.0.0",
|
|
38
|
+
"commander": "^12.0.0",
|
|
39
|
+
"cookie-parser": "^1.4.6",
|
|
40
|
+
"express": "^4.19.0",
|
|
41
|
+
"jsonwebtoken": "^9.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/cookie-parser": "^1.4.7",
|
|
45
|
+
"@types/express": "^4.17.21",
|
|
46
|
+
"@types/jsonwebtoken": "^9.0.6",
|
|
47
|
+
"@types/node": "^22.0.0",
|
|
48
|
+
"bullmq": "^5.79.1",
|
|
49
|
+
"mongoose": "^9.7.2",
|
|
50
|
+
"tsup": "^8.2.0",
|
|
51
|
+
"typescript": "^5.5.0",
|
|
52
|
+
"vitest": "^2.0.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"bullmq": ">=5.0.0",
|
|
56
|
+
"mongoose": ">=8.0.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"bullmq": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"mongoose": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=18.0.0"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
}
|