@whxcctv/create-tony-app 1.0.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/Readme.md +142 -0
- package/bin/back.js +66 -0
- package/bin/dev-server.js +42 -0
- package/bin/index.js +39 -0
- package/helper.js +18 -0
- package/jsconfig.json +10 -0
- package/package.json +12 -0
- package/src/generator.js +221 -0
- package/tony/error.js +7 -0
- package/tony/helpers.js +12 -0
- package/tony.config.example.js +65 -0
package/Readme.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Tony Server 🚀
|
|
2
|
+
|
|
3
|
+
**Tony** is an extremely lightweight, configuration‑driven Node.js Web
|
|
4
|
+
API server scaffold. It is designed for developers who pursue maximal
|
|
5
|
+
performance and code purity.
|
|
6
|
+
|
|
7
|
+
Unlike traditional frameworks, Tony embraces a **No TypeScript**
|
|
8
|
+
philosophy, offering type safety through **JSDoc**, and using **static
|
|
9
|
+
code generation** to transform route configuration into a physical
|
|
10
|
+
directory structure, completely eliminating runtime route‑lookup
|
|
11
|
+
overhead.
|
|
12
|
+
|
|
13
|
+
------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
## Core Features
|
|
16
|
+
|
|
17
|
+
- **Zero Framework**: Built on native Node.js `http` module with no
|
|
18
|
+
heavy runtime framework dependencies.
|
|
19
|
+
- **Static Routing**: Generates nested `if-else` dispatch logic;
|
|
20
|
+
routing performance approaches $O(1)$.
|
|
21
|
+
- **ESM First**: Fully embraces modern ES6 module standards
|
|
22
|
+
(`import/export`).
|
|
23
|
+
- **JSDoc Powered**: Full IDE autocomplete and type checking, even in
|
|
24
|
+
pure JavaScript.
|
|
25
|
+
- **Object‑Driven Config**: Elegant object-based routing configuration
|
|
26
|
+
that makes API structure instantly understandable.
|
|
27
|
+
|
|
28
|
+
------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
## Project Architecture
|
|
31
|
+
|
|
32
|
+
The generated project follows a highly decoupled physical directory
|
|
33
|
+
structure:
|
|
34
|
+
|
|
35
|
+
- **`/tony`**: Core helper library. Injects utilities like
|
|
36
|
+
`res.json()` and `res.status()`.
|
|
37
|
+
- **`/middlewares`**: Stores business middlewares (auth, rate
|
|
38
|
+
limiting, logging, etc.).
|
|
39
|
+
- **`/routers`**: Core routing tree. Each folder represents an API
|
|
40
|
+
path:
|
|
41
|
+
- `index.js`: Auto‑generated router dispatcher handling middleware
|
|
42
|
+
chains and child route matching.
|
|
43
|
+
- `*.service.js`: Actual business logic processors.
|
|
44
|
+
|
|
45
|
+
------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
### 1. Prepare Environment
|
|
50
|
+
|
|
51
|
+
Ensure your `package.json` is configured for ESM:
|
|
52
|
+
|
|
53
|
+
``` json
|
|
54
|
+
{
|
|
55
|
+
"type": "module",
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "node generator.js",
|
|
58
|
+
"dev": "node dev-server.js"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 2. Define API (tony.config.js)
|
|
64
|
+
|
|
65
|
+
Example Audiobooks API structure:
|
|
66
|
+
|
|
67
|
+
``` js
|
|
68
|
+
export default {
|
|
69
|
+
server: { port: 3000 },
|
|
70
|
+
routes: {
|
|
71
|
+
"/api/v1/books": {
|
|
72
|
+
services: ["list", "search"],
|
|
73
|
+
middlewares: [["./cache.js"]]
|
|
74
|
+
},
|
|
75
|
+
"/api/v1/me/library": {
|
|
76
|
+
middlewares: [["./jwtAuth.js"]],
|
|
77
|
+
services: ["getCollection", "addBook"]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 3. Generate & Run
|
|
84
|
+
|
|
85
|
+
``` bash
|
|
86
|
+
npm run build # Generate static directories & code
|
|
87
|
+
npm run dev # Start dev server (with hot reload)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
## Development Guide
|
|
93
|
+
|
|
94
|
+
### Writing a Service
|
|
95
|
+
|
|
96
|
+
Tony automatically generates JSDoc‑rich templates to give pure JS a
|
|
97
|
+
strong typing experience:
|
|
98
|
+
|
|
99
|
+
``` js
|
|
100
|
+
/**
|
|
101
|
+
* @param {import('../../../tony/helpers').EnhancedRequest} req
|
|
102
|
+
* @param {import('../../../tony/helpers').EnhancedResponse} res
|
|
103
|
+
*/
|
|
104
|
+
export async function list(req, res) {
|
|
105
|
+
res.status(200).json({
|
|
106
|
+
success: true,
|
|
107
|
+
data: [{ id: 1, title: "The Great Gatsby" }]
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Writing Middleware
|
|
113
|
+
|
|
114
|
+
Middlewares return `false` to stop the request chain:
|
|
115
|
+
|
|
116
|
+
``` js
|
|
117
|
+
export default function auth(options) {
|
|
118
|
+
return async (req, res) => {
|
|
119
|
+
if (!req.headers.authorization) {
|
|
120
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
121
|
+
return false; // Stop chain
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
## Performance
|
|
130
|
+
|
|
131
|
+
Traditional frameworks often rely on dynamic regex‑based routing. Tony
|
|
132
|
+
transforms routing into static, precompiled branch code.
|
|
133
|
+
|
|
134
|
+
This reduces CPU overhead and makes the execution path fully
|
|
135
|
+
observable---you can directly inspect the generated `index.js` to
|
|
136
|
+
understand the full request flow.
|
|
137
|
+
|
|
138
|
+
------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
package/bin/back.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { pathToFileURL } from 'url';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import { createApp } from '../src/generator.js'; // 将之前的逻辑移到这里
|
|
8
|
+
import { log } from 'console';
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const configPath = path.resolve(process.cwd(), 'tony.config.js');
|
|
12
|
+
|
|
13
|
+
async function loadConfig() {
|
|
14
|
+
// 1. 获取绝对路径
|
|
15
|
+
const absolutePath = path.resolve(process.cwd(), 'tony.config.js');
|
|
16
|
+
|
|
17
|
+
// 2. 将 C:\... 转换为 file:///C:/...
|
|
18
|
+
const fileUrl = pathToFileURL(absolutePath).href;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// 3. 加上时间戳缓存消除,动态导入
|
|
22
|
+
const configModule = await import(`${fileUrl}?update=${Date.now()}`);
|
|
23
|
+
return configModule.default;
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error('❌ Failed to load config:', err);
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// 核心执行逻辑
|
|
30
|
+
const runGeneration = async () => {
|
|
31
|
+
console.log('🏗️ Generating server structure...');
|
|
32
|
+
try {
|
|
33
|
+
// 动态加载配置(清除 ESM 缓存需要加 query)
|
|
34
|
+
const config = await loadConfig()
|
|
35
|
+
console.log(config);
|
|
36
|
+
|
|
37
|
+
createApp(config);
|
|
38
|
+
console.log('✅ Structure updated.');
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error('❌ Generation failed:', err.message);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// 监听模式
|
|
45
|
+
// if (args.includes('--watch')) {
|
|
46
|
+
// console.log('👀 Watching for changes in tony.config.js...');
|
|
47
|
+
// fs.watchFile(configPath, { interval: 500 }, () => {
|
|
48
|
+
// runGeneration();
|
|
49
|
+
// });
|
|
50
|
+
// }
|
|
51
|
+
|
|
52
|
+
// 初始化模式
|
|
53
|
+
// if (args.includes('init')) {
|
|
54
|
+
// const template = `export default {
|
|
55
|
+
// middlewares: [],
|
|
56
|
+
// routes: {
|
|
57
|
+
// "/": [[], ["home"]]
|
|
58
|
+
// }
|
|
59
|
+
// };`;
|
|
60
|
+
// if (!fs.existsSync(configPath)) {
|
|
61
|
+
// fs.writeFileSync(configPath, template);
|
|
62
|
+
// console.log('🆕 Created tony.config.js');
|
|
63
|
+
// }
|
|
64
|
+
// } else {
|
|
65
|
+
// runGeneration();
|
|
66
|
+
// }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { createApp } from '../src/generator.js';
|
|
5
|
+
|
|
6
|
+
let serverProcess = null;
|
|
7
|
+
|
|
8
|
+
// 启动/重启生成的 app.js
|
|
9
|
+
function restartServer() {
|
|
10
|
+
if (serverProcess) {
|
|
11
|
+
serverProcess.kill('SIGTERM');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
console.log('🚀 Starting Tony server...');
|
|
15
|
+
serverProcess = spawn('node', ['app.js'], { stdio: 'inherit' });
|
|
16
|
+
|
|
17
|
+
serverProcess.on('close', (code) => {
|
|
18
|
+
if (code && code !== 0) console.error(`Server exited with code ${code}`);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 核心工作流
|
|
23
|
+
async function workflow() {
|
|
24
|
+
// 1. 生成代码
|
|
25
|
+
const config = (await import(`./tony.config.js?t=${Date.now()}`)).default;
|
|
26
|
+
createApp(config);
|
|
27
|
+
|
|
28
|
+
// 2. 重启服务器
|
|
29
|
+
restartServer();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 监听配置变化
|
|
33
|
+
fs.watchFile(path.resolve('tony.config.js'), { interval: 500 }, workflow);
|
|
34
|
+
|
|
35
|
+
// 监听业务代码变化 (routers/middlewares)
|
|
36
|
+
// 这里简单监听整个目录,实际可以更精细
|
|
37
|
+
fs.watch(path.resolve('routers'), { recursive: true }, () => {
|
|
38
|
+
console.log('📄 Business code changed, restarting...');
|
|
39
|
+
restartServer();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
workflow();
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { createApp } from "../src/generator.js";
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
|
|
8
|
+
// 获取 --config 参数
|
|
9
|
+
function getArgValue(name) {
|
|
10
|
+
const flag = args.find(a => a.startsWith(`--${name}`));
|
|
11
|
+
if (!flag) return null;
|
|
12
|
+
const [, value] = flag.split("=");
|
|
13
|
+
return value || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const projectName = args.find(a => !a.startsWith("--")) || "tony-app";
|
|
17
|
+
const userConfigPath = getArgValue("config");
|
|
18
|
+
|
|
19
|
+
if (!userConfigPath) {
|
|
20
|
+
console.error("❌ Missing --config parameter.");
|
|
21
|
+
console.error("Usage: npx create-tony-app <project> --config=./tony.config.js");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
26
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
27
|
+
|
|
28
|
+
// 把用户的 config 复制到新项目目录
|
|
29
|
+
const destConfigPath = path.join(targetDir, "tony.config.js");
|
|
30
|
+
fs.copyFileSync(userConfigPath, destConfigPath);
|
|
31
|
+
console.log(`📄 Copied config to ${destConfigPath}`);
|
|
32
|
+
|
|
33
|
+
// 进入目录执行生成器
|
|
34
|
+
process.chdir(targetDir);
|
|
35
|
+
const config = (await import(pathToFileURL(destConfigPath).href)).default;
|
|
36
|
+
|
|
37
|
+
createApp(config);
|
|
38
|
+
|
|
39
|
+
console.log("🎉 Project created successfully!");
|
package/helper.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// tony/helpers.js
|
|
2
|
+
export function enhanceRequest(req) {
|
|
3
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
4
|
+
req.path = url.pathname;
|
|
5
|
+
req.query = Object.fromEntries(url.searchParams);
|
|
6
|
+
// 可以在这里扩展更多,比如解析 cookie
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function enhanceResponse(res) {
|
|
10
|
+
res.json = (data) => {
|
|
11
|
+
res.setHeader('Content-Type', 'application/json');
|
|
12
|
+
res.end(JSON.stringify(data));
|
|
13
|
+
};
|
|
14
|
+
res.status = (code) => {
|
|
15
|
+
res.statusCode = code;
|
|
16
|
+
return res;
|
|
17
|
+
};
|
|
18
|
+
}
|
package/jsconfig.json
ADDED
package/package.json
ADDED
package/src/generator.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 核心工具集
|
|
10
|
+
*/
|
|
11
|
+
const utils = {
|
|
12
|
+
ensureDir: (dirPath) => fs.mkdirSync(dirPath, { recursive: true }),
|
|
13
|
+
|
|
14
|
+
writeFile: (filePath, content) => {
|
|
15
|
+
utils.ensureDir(path.dirname(filePath));
|
|
16
|
+
fs.writeFileSync(filePath, content.trim() + "\n");
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
pascal: (str) => str.split(/[^a-zA-Z0-9]/).filter(Boolean)
|
|
20
|
+
.map(w => w[0].toUpperCase() + w.slice(1)).join(""),
|
|
21
|
+
|
|
22
|
+
toCamel: (str) => str.replace(/[^a-zA-Z0-9]/g, ' ').split(' ').filter(Boolean)
|
|
23
|
+
.map((w, i) => i === 0 ? w.toLowerCase() : w[0].toUpperCase() + w.slice(1))
|
|
24
|
+
.join('') || 'module',
|
|
25
|
+
|
|
26
|
+
// 关键:统一 ESM 导入路径,必须是 Posix 格式并带后缀
|
|
27
|
+
toImportPath: (p) => {
|
|
28
|
+
let posix = p.split(path.sep).join(path.posix.sep);
|
|
29
|
+
return posix.endsWith('.js') ? posix : `${posix}.js`;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
jsDoc: `/**
|
|
33
|
+
* @param {import('http').IncomingMessage & { path: string, query: Object }} req
|
|
34
|
+
* @param {import('http').ServerResponse & { json: Function, status: Function }} res
|
|
35
|
+
*/`
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 递归生成路由
|
|
40
|
+
*/
|
|
41
|
+
function createRouter(route, routeConfig, fullConfig) {
|
|
42
|
+
const segments = route.split("/").filter(Boolean);
|
|
43
|
+
const routerDir = path.join("routers", ...segments);
|
|
44
|
+
const level = segments.length;
|
|
45
|
+
const isRoot = route === "/";
|
|
46
|
+
|
|
47
|
+
// 优雅的配置解构
|
|
48
|
+
const {
|
|
49
|
+
middlewares = [],
|
|
50
|
+
services = []
|
|
51
|
+
} = routeConfig;
|
|
52
|
+
|
|
53
|
+
const mName = isRoot ? "main" : segments[level - 1];
|
|
54
|
+
const defaultService = services[0] || (isRoot ? "home" : segments[level - 1]);
|
|
55
|
+
const allServices = Array.from(new Set([defaultService, ...services]));
|
|
56
|
+
|
|
57
|
+
let imports = "";
|
|
58
|
+
let calls = "";
|
|
59
|
+
|
|
60
|
+
// 1. 生成中间件代码
|
|
61
|
+
middlewares.forEach(([modPath, options], idx) => {
|
|
62
|
+
const name = path.basename(modPath, ".js");
|
|
63
|
+
const varPrefix = utils.toCamel(name);
|
|
64
|
+
const varName = `${varPrefix}_${idx}`;
|
|
65
|
+
|
|
66
|
+
const relPath = modPath.startsWith(".")
|
|
67
|
+
? utils.toImportPath(path.join("../".repeat(level + 1), "middlewares", name))
|
|
68
|
+
: modPath;
|
|
69
|
+
|
|
70
|
+
imports += `import ${varPrefix}Fn from "${relPath}";\n`;
|
|
71
|
+
imports += `const ${varName} = ${varPrefix}Fn(${options ? JSON.stringify(options) : ""});\n`;
|
|
72
|
+
calls += ` if (await ${varName}(req, res) === false) return;\n`;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// 2. 生成 Service 导入
|
|
76
|
+
allServices.forEach(srv => {
|
|
77
|
+
imports += `import { ${srv} } from "./${srv}.service.js";\n`;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// 3. 自动查找并生成子路由分发
|
|
81
|
+
const childrenPaths = Object.keys(fullConfig.routes).filter(k => {
|
|
82
|
+
const kSegs = k.split("/").filter(Boolean);
|
|
83
|
+
const baseMatch = k.startsWith(isRoot ? "/" : route + "/");
|
|
84
|
+
return baseMatch && kSegs.length === level + 1;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
childrenPaths.forEach(childPath => {
|
|
88
|
+
const childName = childPath.split("/").filter(Boolean).pop();
|
|
89
|
+
const alias = utils.pascal(childName);
|
|
90
|
+
imports += `import ${alias} from "./${childName}/index.js";\n`;
|
|
91
|
+
calls += ` if (req.path.startsWith("${childPath}")) return await ${alias}(req, res);\n`;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const errorRelPath = utils.toImportPath(path.join("../".repeat(level), "error/index.js"));
|
|
95
|
+
imports += `import error from "${errorRelPath.startsWith('.') ? errorRelPath : './' + errorRelPath}";\n`;
|
|
96
|
+
|
|
97
|
+
// 4. 生成 Router index.js
|
|
98
|
+
const content = `
|
|
99
|
+
${imports}
|
|
100
|
+
|
|
101
|
+
${utils.jsDoc}
|
|
102
|
+
export default async function ${mName}(req, res) {
|
|
103
|
+
${calls}
|
|
104
|
+
try {
|
|
105
|
+
return await ${defaultService}(req, res);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error("[Router Error at ${route}]:", err);
|
|
108
|
+
return await error(req, res);
|
|
109
|
+
}
|
|
110
|
+
}`;
|
|
111
|
+
|
|
112
|
+
utils.writeFile(path.join(routerDir, "index.js"), content);
|
|
113
|
+
|
|
114
|
+
// 5. 生成 Service 模板
|
|
115
|
+
allServices.forEach(srv => {
|
|
116
|
+
const srvFile = path.join(routerDir, `${srv}.service.js`);
|
|
117
|
+
if (!fs.existsSync(srvFile)) {
|
|
118
|
+
utils.writeFile(srvFile, `
|
|
119
|
+
${utils.jsDoc}
|
|
120
|
+
export async function ${srv}(req, res) {
|
|
121
|
+
res.end("Response from ${srv} at ${route}");
|
|
122
|
+
}`);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 主构建函数
|
|
129
|
+
*/
|
|
130
|
+
export function createApp(config) {
|
|
131
|
+
const root = process.cwd();
|
|
132
|
+
["middlewares", "routers", "tony"].forEach(d => utils.ensureDir(d));
|
|
133
|
+
|
|
134
|
+
// 1. 生成 app.js
|
|
135
|
+
let appImports = `import http from "http";\nimport { enhanceRequest, enhanceResponse } from "./tony/helpers.js";\nimport { handleError } from "./tony/error.js";\nimport main from "./routers/index.js";\n`;
|
|
136
|
+
let appCalls = "";
|
|
137
|
+
|
|
138
|
+
(config.middlewares || []).forEach(([modPath, options], idx) => {
|
|
139
|
+
const name = path.basename(modPath, ".js");
|
|
140
|
+
const varPrefix = utils.toCamel(name);
|
|
141
|
+
const varName = `_global_${varPrefix}_${idx}`;
|
|
142
|
+
const impPath = modPath.startsWith(".") ? `./middlewares/${name}.js` : modPath;
|
|
143
|
+
appImports += `import ${varPrefix}Fn from "${impPath}";\nconst ${varName} = ${varPrefix}Fn(${JSON.stringify(options)});\n`;
|
|
144
|
+
appCalls += ` if (await ${varName}(req, res) === false) return;\n`;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const appContent = `${appImports}
|
|
148
|
+
http.createServer(async (req, res) => {
|
|
149
|
+
try {
|
|
150
|
+
enhanceRequest(req);
|
|
151
|
+
enhanceResponse(res);
|
|
152
|
+
${appCalls}
|
|
153
|
+
await main(req, res);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
handleError(err, res);
|
|
156
|
+
}
|
|
157
|
+
}).listen(config.server?.port || 3000, () => {
|
|
158
|
+
console.log(\`🚀 Tony server running at http://localhost:\${config.server?.port || 3000}\`);
|
|
159
|
+
});`;
|
|
160
|
+
|
|
161
|
+
utils.writeFile("app.js", appContent);
|
|
162
|
+
|
|
163
|
+
// 2. 递归生成所有路由
|
|
164
|
+
Object.entries(config.routes).forEach(([route, routeConfig]) => {
|
|
165
|
+
createRouter(route, routeConfig, config);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// 3. 生成固定辅助代码 (helpers, error, jsconfig)
|
|
169
|
+
generateHelpers();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function generateHelpers() {
|
|
173
|
+
utils.writeFile("tony/helpers.js", `
|
|
174
|
+
export function enhanceRequest(req) {
|
|
175
|
+
const url = new URL(req.url, \`http://\${req.headers.host}\`);
|
|
176
|
+
req.path = url.pathname;
|
|
177
|
+
req.query = Object.fromEntries(url.searchParams);
|
|
178
|
+
}
|
|
179
|
+
export function enhanceResponse(res) {
|
|
180
|
+
res.json = (data) => {
|
|
181
|
+
res.setHeader("Content-Type", "application/json");
|
|
182
|
+
res.end(JSON.stringify(data));
|
|
183
|
+
};
|
|
184
|
+
res.status = (code) => { res.statusCode = code; return res; };
|
|
185
|
+
}`);
|
|
186
|
+
|
|
187
|
+
utils.writeFile("tony/error.js", `
|
|
188
|
+
export function handleError(err, res) {
|
|
189
|
+
console.error(err);
|
|
190
|
+
if (!res.writableEnded) {
|
|
191
|
+
res.writeHead(500);
|
|
192
|
+
res.end("Internal Server Error");
|
|
193
|
+
}
|
|
194
|
+
}`);
|
|
195
|
+
|
|
196
|
+
utils.writeFile("routers/error/index.js", `
|
|
197
|
+
export default async function _error(req, res) {
|
|
198
|
+
res.writeHead(404);
|
|
199
|
+
res.end("404 Not Found");
|
|
200
|
+
}`);
|
|
201
|
+
|
|
202
|
+
utils.writeFile("jsconfig.json", JSON.stringify({
|
|
203
|
+
compilerOptions: { checkJs: true, target: "ESNext", module: "ESNext" },
|
|
204
|
+
exclude: ["node_modules"]
|
|
205
|
+
}, null, 2));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 启动器:修复 Windows 路径并加载配置
|
|
210
|
+
*/
|
|
211
|
+
export async function startGenerator() {
|
|
212
|
+
const configPath = path.resolve(process.cwd(), "tony.config.js");
|
|
213
|
+
const fileUrl = pathToFileURL(configPath).href;
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const configModule = await import(`${fileUrl}?update=${Date.now()}`);
|
|
217
|
+
createApp(configModule.default);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.error("❌ Generation failed:", err);
|
|
220
|
+
}
|
|
221
|
+
}
|
package/tony/error.js
ADDED
package/tony/helpers.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function enhanceRequest(req) {
|
|
2
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
3
|
+
req.path = url.pathname;
|
|
4
|
+
req.query = Object.fromEntries(url.searchParams);
|
|
5
|
+
}
|
|
6
|
+
export function enhanceResponse(res) {
|
|
7
|
+
res.json = (data) => {
|
|
8
|
+
res.setHeader("Content-Type", "application/json");
|
|
9
|
+
res.end(JSON.stringify(data));
|
|
10
|
+
};
|
|
11
|
+
res.status = (code) => { res.statusCode = code; return res; };
|
|
12
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** @type {import('./tony/types').TonyConfig} */
|
|
2
|
+
export default {
|
|
3
|
+
server: {
|
|
4
|
+
port: 3000,
|
|
5
|
+
apiPrefix: "/api/v1" // 逻辑前缀
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
// API 专用全局中间件
|
|
9
|
+
middlewares: [
|
|
10
|
+
["./tony/cors.js"], // 跨域处理
|
|
11
|
+
["./tony/bodyParser.js"], // 解析 JSON Body
|
|
12
|
+
["./tony/rateLimiter.js", { windowMs: 15 * 60 * 1000, max: 100 }] // 防刷限流
|
|
13
|
+
],
|
|
14
|
+
|
|
15
|
+
routes: {
|
|
16
|
+
// --- 公共资源接口 ---
|
|
17
|
+
"/api/v1/books": {
|
|
18
|
+
services: ["list", "search"],
|
|
19
|
+
middlewares: [["./cache.js", { expire: "10m" }]]
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
// 资源详情:在 service 中通过 req.query.id 处理
|
|
23
|
+
"/api/v1/books/detail": {
|
|
24
|
+
services: ["getById", "getChapters", "getRecommendations"]
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// --- 认证与令牌管理 ---
|
|
28
|
+
"/api/v1/auth": {
|
|
29
|
+
services: ["issueToken", "refreshToken", "revokeToken"]
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// --- 需鉴权的用户私有接口 ---
|
|
33
|
+
"/api/v1/me": {
|
|
34
|
+
middlewares: [["./jwtAuth.js"]], // JWT 校验
|
|
35
|
+
services: ["getProfile", "updateProfile"]
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// 播放记录(高频同步)
|
|
39
|
+
"/api/v1/me/progress": {
|
|
40
|
+
middlewares: [["./jwtAuth.js"]],
|
|
41
|
+
services: ["sync", "lastPlayed"]
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// 收藏夹
|
|
45
|
+
"/api/v1/me/library": {
|
|
46
|
+
middlewares: [["./jwtAuth.js"]],
|
|
47
|
+
services: ["addBook", "removeBook", "getCollection"]
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// --- 媒体分发接口 (通常指向 CDN 或云存储签名地址) ---
|
|
51
|
+
"/api/v1/stream": {
|
|
52
|
+
middlewares: [
|
|
53
|
+
["./jwtAuth.js"],
|
|
54
|
+
["./checkSubscription.js"] // 检查是否已购买或会员
|
|
55
|
+
],
|
|
56
|
+
services: ["getSignedUrl", "getQualityOptions"]
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// --- 支付与账单 ---
|
|
60
|
+
"/api/v1/billing": {
|
|
61
|
+
middlewares: [["./jwtAuth.js"]],
|
|
62
|
+
services: ["createOrder", "getInvoices", "verifyWebHook"]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|