create-forkly 0.1.1

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/index.js ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ /** create-forkly — Forkly 项目脚手架 CLI */
3
+ import { cac } from 'cac';
4
+ import * as p from '@clack/prompts';
5
+ import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
6
+ import { resolve, dirname } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { execSync } from 'node:child_process';
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const templateDir = resolve(__dirname, '../template');
11
+ function copyDir(src, dest, replaces) {
12
+ mkdirSync(dest, { recursive: true });
13
+ for (const entry of readdirSync(src)) {
14
+ const srcPath = resolve(src, entry);
15
+ const destPath = resolve(dest, entry);
16
+ if (statSync(srcPath).isDirectory()) {
17
+ copyDir(srcPath, destPath, replaces);
18
+ }
19
+ else {
20
+ let content = readFileSync(srcPath, 'utf-8');
21
+ for (const [k, v] of Object.entries(replaces)) {
22
+ content = content.replace(new RegExp(k, 'g'), v);
23
+ }
24
+ writeFileSync(destPath, content);
25
+ }
26
+ }
27
+ }
28
+ const cli = cac('create-forkly');
29
+ cli.command('[dir]', 'Create a new Forkly project')
30
+ .action(async (dir = '.') => {
31
+ p.intro('Forkly Project Scaffolder');
32
+ const projectName = await p.text({
33
+ message: 'Project name:',
34
+ placeholder: 'my-forkly-app',
35
+ defaultValue: dir === '.' ? 'my-forkly-app' : dir,
36
+ });
37
+ if (p.isCancel(projectName)) {
38
+ p.cancel('Cancelled');
39
+ process.exit(0);
40
+ }
41
+ const useTypeScript = await p.confirm({
42
+ message: 'Use TypeScript?',
43
+ initialValue: true,
44
+ });
45
+ if (p.isCancel(useTypeScript)) {
46
+ p.cancel('Cancelled');
47
+ process.exit(0);
48
+ }
49
+ const targetDir = resolve(process.cwd(), dir === '.' ? projectName : dir);
50
+ const replaces = { 'forkly-app': projectName };
51
+ p.log.step('Copying template files...');
52
+ copyDir(templateDir, targetDir, replaces);
53
+ // 更新 package.json 中的项目名
54
+ const pkgPath = resolve(targetDir, 'package.json');
55
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
56
+ pkg.name = projectName;
57
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
58
+ if (!useTypeScript) {
59
+ // 如果不用 TS,移除 tsconfig,把 .ts 文件改为 .js
60
+ p.log.step('Setting up JavaScript project...');
61
+ }
62
+ p.log.step('Installing dependencies...');
63
+ try {
64
+ execSync('npm install', { cwd: targetDir, stdio: 'inherit' });
65
+ }
66
+ catch {
67
+ p.log.warn('npm install failed. You can run it manually.');
68
+ }
69
+ p.outro(`Done! Run:\n cd ${projectName}\n npm run dev`);
70
+ });
71
+ cli.help();
72
+ cli.version('0.1.1');
73
+ cli.parse();
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "create-forkly",
3
+ "version": "0.1.1",
4
+ "description": "CLI to scaffold a new Forkly project",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-forkly": "./dist/index.js"
8
+ },
9
+ "files": ["dist/", "template/"],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsx src/index.ts"
13
+ },
14
+ "license": "MIT",
15
+ "dependencies": {
16
+ "@clack/prompts": "^0.10.1",
17
+ "cac": "^6.7.14"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^25.9.1",
21
+ "tsx": "^4.22.4",
22
+ "typescript": "^5.8.0"
23
+ }
24
+ }
@@ -0,0 +1,33 @@
1
+ /** Forkly 应用入口 — 启动 App 并注册钩子与定时任务 */
2
+ import { App } from 'forkly';
3
+
4
+ const app = new App({
5
+ port: 3000,
6
+ workers: 4,
7
+ });
8
+
9
+ // 注册全局日志回调
10
+ app.onLog((level, message) => {
11
+ console.log(`[${level.toUpperCase()}] ${message}`);
12
+ });
13
+
14
+ // 注册全局前置钩子
15
+ app.beforeRequest(async (router) => {
16
+ router.state.startTime = Date.now();
17
+ });
18
+
19
+ // 注册全局后置钩子
20
+ app.afterResponse(async (router) => {
21
+ const elapsed = Date.now() - ((router.state.startTime as number) ?? 0);
22
+ console.log(`${router.request.method} ${router.request.pathname} ${router.response.status} ${elapsed}ms`);
23
+ });
24
+
25
+ // 注册定时任务:每天凌晨 3 点清理过期 session
26
+ app.schedule('0 3 * * *', async () => {
27
+ console.log('Running daily cleanup...');
28
+ });
29
+
30
+ // 启动应用
31
+ app.start().then(() => {
32
+ console.log('Forkly app started');
33
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "port": 3000,
3
+ "workers": 4,
4
+ "session": { "secret": "your-secret-key-change-me", "maxAge": 86400000 },
5
+ "errorFormat": "json",
6
+ "logger": { "level": "info" }
7
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "NOT_FOUND": { "zh": "资源未找到", "en": "Not Found" },
3
+ "VALIDATION_ERROR": { "zh": "参数校验失败", "en": "Validation Error" },
4
+ "INTERNAL_ERROR": { "zh": "服务器内部错误", "en": "Internal Server Error" },
5
+ "TIMEOUT": { "zh": "请求超时", "en": "Request Timeout" },
6
+ "UNAUTHORIZED": { "zh": "未授权", "en": "Unauthorized" },
7
+ "FORBIDDEN": { "zh": "禁止访问", "en": "Forbidden" },
8
+ "TOO_MANY_REQUESTS": { "zh": "请求频繁,请稍后再试", "en": "Too Many Requests" },
9
+ "USER_NOT_FOUND": { "zh": "用户不存在", "en": "User Not Found" },
10
+ "INVALID_INPUT": { "zh": "输入参数无效", "en": "Invalid Input" }
11
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "port": 8080,
3
+ "session": { "secret": "production-secret-change-me" },
4
+ "errorFormat": "json",
5
+ "logger": { "level": "warn" }
6
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "forkly-app",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "tsx watch app.ts",
7
+ "build": "tsc"
8
+ },
9
+ "dependencies": {
10
+ "forkly": "^0.2.0"
11
+ },
12
+ "devDependencies": {
13
+ "@types/node": "^25.9.1",
14
+ "tsx": "^4.22.4",
15
+ "typescript": "^5.8.0"
16
+ }
17
+ }
@@ -0,0 +1,19 @@
1
+ /** Hello 路由 — GET /hello?name=world,含 dataType 校验 */
2
+ import { Router } from 'forkly';
3
+
4
+ export class HelloRouter extends Router {
5
+ static channel = 'async' as const;
6
+ static dataType = {
7
+ query: { name: 'string' },
8
+ } as const;
9
+
10
+ async get() {
11
+ const name = this.request.query.name ?? 'World';
12
+ return { message: `Hello, ${name}!` };
13
+ }
14
+
15
+ async post() {
16
+ const { name } = this.request.body as { name?: string } ?? {};
17
+ return { message: `Hello, ${name ?? 'World'}! (POST)` };
18
+ }
19
+ }
@@ -0,0 +1,14 @@
1
+ /** 首页路由 — GET / */
2
+ import { Router } from 'forkly';
3
+
4
+ export class IndexRouter extends Router {
5
+ static channel = 'async' as const;
6
+
7
+ async get() {
8
+ return {
9
+ message: 'Welcome to Forkly!',
10
+ version: '0.1.0',
11
+ timestamp: Date.now(),
12
+ };
13
+ }
14
+ }
@@ -0,0 +1,35 @@
1
+ /** 动态路由示例 — GET /users/:id */
2
+ import { Router } from 'forkly';
3
+
4
+ // 模拟数据库
5
+ const users: Record<string, { name: string; email: string }> = {
6
+ '1': { name: 'Alice', email: 'alice@example.com' },
7
+ '2': { name: 'Bob', email: 'bob@example.com' },
8
+ };
9
+
10
+ export class UserRouter extends Router {
11
+ static channel = 'async' as const;
12
+
13
+ async get() {
14
+ const id = this.request.params.id;
15
+ const user = users[id];
16
+ if (!user) { this.resError('USER_NOT_FOUND'); return; }
17
+ return { id, ...user };
18
+ }
19
+
20
+ async put() {
21
+ const id = this.request.params.id;
22
+ const body = this.request.body as { name?: string; email?: string };
23
+ if (!users[id]) { this.resError('USER_NOT_FOUND'); return; }
24
+ if (body.name) users[id].name = body.name;
25
+ if (body.email) users[id].email = body.email;
26
+ return { id, ...users[id], updated: true };
27
+ }
28
+
29
+ async delete() {
30
+ const id = this.request.params.id;
31
+ if (!users[id]) { this.resError('USER_NOT_FOUND'); return; }
32
+ delete users[id];
33
+ return { deleted: true, id };
34
+ }
35
+ }
@@ -0,0 +1,22 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Forkly App</title>
7
+ <style>
8
+ body { font-family: system-ui, sans-serif; max-width: 640px; margin: 80px auto; padding: 0 20px; color: #333; }
9
+ h1 { font-size: 2rem; margin-bottom: 0.5rem; }
10
+ code { background: #f4f4f4; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; }
11
+ .card { border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; margin: 16px 0; }
12
+ </style>
13
+ </head>
14
+ <body>
15
+ <h1>Forkly</h1>
16
+ <p>A multi-process Node.js backend framework — minimal, fast, production-ready.</p>
17
+ <div class="card">
18
+ <p>Visit <code>/hello?name=forkly</code> to try the API</p>
19
+ <p>Visit <code>/users/1</code> to try dynamic routing</p>
20
+ </div>
21
+ </body>
22
+ </html>
@@ -0,0 +1,26 @@
1
+ /** 定时清理任务 — 每天凌晨 3 点执行,清理过期日志 */
2
+ import { Task } from 'forkly';
3
+ import { readdir, unlink, stat } from 'node:fs/promises';
4
+ import { resolve } from 'node:path';
5
+
6
+ const LOG_DIR = resolve(process.cwd(), 'logs');
7
+ const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 天
8
+
9
+ export const cleanupTask = new Task('0 3 * * *', async () => {
10
+ try {
11
+ const files = await readdir(LOG_DIR);
12
+ const now = Date.now();
13
+ let deleted = 0;
14
+ for (const file of files) {
15
+ const filePath = resolve(LOG_DIR, file);
16
+ const info = await stat(filePath);
17
+ if (now - info.mtimeMs > MAX_AGE_MS) {
18
+ await unlink(filePath);
19
+ deleted++;
20
+ }
21
+ }
22
+ console.log(`Cleanup: deleted ${deleted} old log files`);
23
+ } catch {
24
+ // 日志目录可能不存在
25
+ }
26
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": ".",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "resolveJsonModule": true
11
+ },
12
+ "include": ["**/*.ts"],
13
+ "exclude": ["node_modules", "dist"]
14
+ }