jj.js 1.0.0-rc.2 → 1.0.0-rc.3

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 CHANGED
@@ -37,9 +37,23 @@ jj.js 是一个模仿 ThinkPHP5 设计的轻量级 Node.js MVC 框架。基于 P
37
37
  npm i jj.js
38
38
  ```
39
39
 
40
- > **运行环境要求**:Node.js >= 18
40
+ > **运行环境要求**:Node.js >= 20.19.0
41
41
 
42
- ### Hello World
42
+ ### 方式一:CLI 初始化(推荐)
43
+
44
+ ```bash
45
+ # 创建项目(交互式选择模板)
46
+ npx jj.js init myapp
47
+
48
+ # 进入项目并启动
49
+ cd myapp
50
+ npm install
51
+ npm start
52
+ ```
53
+
54
+ ### 方式二:手动创建
55
+
56
+ #### Hello World
43
57
 
44
58
  **1. 创建控制器** `./app/controller/index.js`
45
59
 
package/bin/jj.js ADDED
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+
7
+ const templates = {
8
+ hello: 'Hello World 示例项目',
9
+ todo: 'Todo List 完整示例项目'
10
+ };
11
+
12
+ function showHelp() {
13
+ console.log(`
14
+ jj.js - 轻量级 Node.js MVC 框架
15
+
16
+ 用法:
17
+ npx jj.js init <project-name>
18
+
19
+ 示例:
20
+ npx jj.js init myapp
21
+ `);
22
+ }
23
+
24
+ function selectTemplate() {
25
+ return new Promise((resolve) => {
26
+ const rl = readline.createInterface({
27
+ input: process.stdin,
28
+ output: process.stdout
29
+ });
30
+
31
+ console.log('\n请选择项目模板:');
32
+ const templateList = Object.entries(templates);
33
+ templateList.forEach(([name, desc], index) => {
34
+ console.log(` ${index + 1}. ${name.padEnd(10)} - ${desc}`);
35
+ });
36
+
37
+ rl.question('\n请输入序号 (1): ', (answer) => {
38
+ rl.close();
39
+ const index = parseInt(answer) - 1;
40
+ if (index >= 0 && index < templateList.length) {
41
+ resolve(templateList[index][0]);
42
+ } else {
43
+ resolve('hello'); // 默认选择第一个
44
+ }
45
+ });
46
+ });
47
+ }
48
+
49
+ function copyDir(src, dest) {
50
+ if (!fs.existsSync(dest)) {
51
+ fs.mkdirSync(dest, { recursive: true });
52
+ }
53
+
54
+ const entries = fs.readdirSync(src, { withFileTypes: true });
55
+
56
+ for (const entry of entries) {
57
+ const srcPath = path.join(src, entry.name);
58
+ const destPath = path.join(dest, entry.name);
59
+
60
+ if (entry.isDirectory()) {
61
+ copyDir(srcPath, destPath);
62
+ } else {
63
+ fs.copyFileSync(srcPath, destPath);
64
+ }
65
+ }
66
+ }
67
+
68
+ function initProject(template, projectName) {
69
+ if (!templates[template]) {
70
+ console.error(`错误: 未知的模板 "${template}"`);
71
+ console.log('\n可用模板:');
72
+ for (const [name, desc] of Object.entries(templates)) {
73
+ console.log(` ${name.padEnd(10)} ${desc}`);
74
+ }
75
+ process.exit(1);
76
+ }
77
+
78
+ const projectPath = path.resolve(process.cwd(), projectName || template + '-app');
79
+
80
+ if (fs.existsSync(projectPath)) {
81
+ console.error(`错误: 目录 "${projectName}" 已存在`);
82
+ process.exit(1);
83
+ }
84
+
85
+ console.log(`正在创建 ${templates[template]}...`);
86
+
87
+ // 复制模板文件
88
+ const templatePath = path.join(__dirname, '..', 'templates', template);
89
+ copyDir(templatePath, projectPath);
90
+
91
+ // 更新 package.json 中的项目名称
92
+ const pkgPath = path.join(projectPath, 'package.json');
93
+ if (fs.existsSync(pkgPath)) {
94
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
95
+ pkg.name = projectName || template + '-app';
96
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
97
+ }
98
+
99
+ console.log(`\n✓ 项目创建成功: ${projectPath}`);
100
+ console.log('\n下一步:');
101
+ console.log(` cd ${projectName || template + '-app'}`);
102
+ console.log(' npm install');
103
+ console.log(' npm start');
104
+ }
105
+
106
+ // 解析命令行参数
107
+ const args = process.argv.slice(2);
108
+ const command = args[0];
109
+
110
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
111
+ showHelp();
112
+ process.exit(0);
113
+ }
114
+
115
+ if (command === 'init') {
116
+ const projectName = args[1];
117
+
118
+ if (!projectName) {
119
+ console.error('错误: 请指定项目名称');
120
+ showHelp();
121
+ process.exit(1);
122
+ }
123
+
124
+ // 交互式选择模板
125
+ selectTemplate().then(template => {
126
+ initProject(template, projectName);
127
+ });
128
+ } else {
129
+ console.error(`错误: 未知的命令 "${command}"`);
130
+ showHelp();
131
+ process.exit(1);
132
+ }
package/lib/controller.js CHANGED
@@ -14,12 +14,12 @@ class Controller extends Middleware
14
14
  /**
15
15
  * 初始化方法,在控制器方法执行前自动执行
16
16
  * @protected
17
- * @returns {any} - 返回Bool值false,则不会执行控制器及后续方法
17
+ * @returns {('__EXIT__'|any)} - 如果返回__EXIT__,则不会执行控制器及后续方法
18
18
  */
19
19
  _init() {}
20
20
 
21
21
  /**
22
- * 结束方法,在控制器方法执行后自动执行。如果控制器方法返回Bool值false,则不会执行此方法
22
+ * 结束方法,在控制器方法执行后自动执行。如果控制器方法返回__EXIT__,则不会执行此方法
23
23
  * @protected
24
24
  */
25
25
  _end() {}
package/lib/mvc.js CHANGED
@@ -106,8 +106,8 @@ async function mvc(ctx, next) {
106
106
  await compose(middlewares)(ctx, async () => {
107
107
  let result;
108
108
  typeof $controller['_init'] == 'function' && (result = await $controller['_init']());
109
- result !== false && (result = await $controller[action]());
110
- result !== false && typeof $controller['_end'] == 'function' && await $controller['_end']();
109
+ result !== '__EXIT__' && (result = await $controller[action]());
110
+ result !== '__EXIT__' && typeof $controller['_end'] == 'function' && await $controller['_end']();
111
111
  });
112
112
  }
113
113
 
package/lib/request.js CHANGED
@@ -194,7 +194,7 @@ class Request extends Context
194
194
  * @returns {*}
195
195
  */
196
196
  header(key, default_value='') {
197
- return this.ctx.headers[key] || default_value;
197
+ return this._getValue(this.ctx.headers[key], default_value);
198
198
  }
199
199
 
200
200
  /**
@@ -212,7 +212,7 @@ class Request extends Context
212
212
  * @returns {*}
213
213
  */
214
214
  _getValue(value, default_value) {
215
- return value === undefined ? default_value : typeof value === 'number' ? Number(value) : value;
215
+ return value === undefined ? default_value : typeof default_value === 'number' ? Number(value) : value;
216
216
  }
217
217
  }
218
218
 
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "jj.js",
3
- "version": "1.0.0-rc.2",
3
+ "version": "1.0.0-rc.3",
4
4
  "description": "A super simple lightweight NodeJS MVC framework(一个超级简单轻量的NodeJS MVC框架)",
5
5
  "main": "jj.js",
6
+ "bin": {
7
+ "init": "./bin/jj.js"
8
+ },
6
9
  "scripts": {
7
10
  "demo": "node ./tests/_demo/server.js",
8
11
  "test": "node --test"
@@ -0,0 +1,9 @@
1
+ const { Controller } = require('jj.js');
2
+
3
+ class IndexController extends Controller {
4
+ index() {
5
+ this.$show('Hello World!');
6
+ }
7
+ }
8
+
9
+ module.exports = IndexController;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @module config
3
+ * @type {import('jj.js').AppConfig}
4
+ */
5
+ module.exports = {
6
+ app_debug: true,
7
+ static_dir: 'static'
8
+ };
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "target": "es6",
5
+ "checkJs": true
6
+ },
7
+ "exclude": ["node_modules"]
8
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "hello-app",
3
+ "version": "1.0.0",
4
+ "description": "A jj.js Hello World application",
5
+ "main": "server.js",
6
+ "scripts": {
7
+ "start": "node server.js"
8
+ },
9
+ "dependencies": {
10
+ "jj.js": "*"
11
+ }
12
+ }
@@ -0,0 +1,8 @@
1
+ const { App, Logger } = require('jj.js');
2
+
3
+ const port = 3000;
4
+ const app = new App();
5
+
6
+ app.listen(port, () => {
7
+ Logger.system(`Hello World 应用已启动: http://localhost:${port}`);
8
+ });
@@ -0,0 +1,9 @@
1
+ const { Controller } = require('jj.js');
2
+
3
+ class IndexController extends Controller {
4
+ index() {
5
+ this.$redirect('/todo');
6
+ }
7
+ }
8
+
9
+ module.exports = IndexController;
@@ -0,0 +1,45 @@
1
+ const { Controller } = require('jj.js');
2
+
3
+ class TodoController extends Controller {
4
+ async index() {
5
+ const db = this.$db;
6
+ // 确保表存在
7
+ await db.execute(`
8
+ CREATE TABLE IF NOT EXISTS todo_todo (
9
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10
+ title TEXT NOT NULL DEFAULT '',
11
+ completed INTEGER NOT NULL DEFAULT 0,
12
+ add_time INTEGER NOT NULL DEFAULT 0
13
+ )
14
+ `);
15
+ const todos = await db.table('todo').order('id', 'desc').select();
16
+ this.$assign('todos', todos);
17
+ await this.$fetch('todo/index');
18
+ }
19
+
20
+ async add() {
21
+ const title = this.$request.post('title');
22
+ if (title) {
23
+ const db = this.$db;
24
+ await db.table('todo').insert({ title, completed: 0, add_time: Date.now() });
25
+ }
26
+ this.$redirect('/todo');
27
+ }
28
+
29
+ async toggle() {
30
+ const id = this.$request.query('id');
31
+ const completed = this.$request.query('completed', 0);
32
+ const db = this.$db;
33
+ await db.table('todo').where({ id }).update({ completed: completed ? 0 : 1 });
34
+ this.$redirect('/todo');
35
+ }
36
+
37
+ async delete() {
38
+ const id = this.$request.query('id');
39
+ const db = this.$db;
40
+ await db.table('todo').where({ id }).delete();
41
+ this.$redirect('/todo');
42
+ }
43
+ }
44
+
45
+ module.exports = TodoController;
@@ -0,0 +1,55 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Todo List - jj.js</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f5f5f5; padding: 20px; }
10
+ .container { max-width: 600px; margin: 0 auto; }
11
+ h1 { text-align: center; color: #333; margin-bottom: 30px; }
12
+ .add-form { display: flex; gap: 10px; margin-bottom: 20px; }
13
+ .add-form input { flex: 1; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; }
14
+ .add-form button { padding: 12px 24px; background: #4CAF50; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; }
15
+ .add-form button:hover { background: #45a049; }
16
+ .todo-list { list-style: none; }
17
+ .todo-item { display: flex; align-items: center; gap: 12px; padding: 15px; background: white; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
18
+ .todo-item.completed .todo-text { text-decoration: line-through; color: #999; }
19
+ .todo-text { flex: 1; font-size: 16px; }
20
+ .todo-actions { display: flex; gap: 8px; }
21
+ .btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
22
+ .btn-toggle { background: #2196F3; color: white; }
23
+ .btn-delete { background: #f44336; color: white; }
24
+ .empty { text-align: center; color: #999; padding: 40px; }
25
+ </style>
26
+ </head>
27
+ <body>
28
+ <div class="container">
29
+ <h1>📝 Todo List</h1>
30
+
31
+ <form class="add-form" action="/todo/add" method="POST">
32
+ <input type="text" name="title" placeholder="添加新任务..." required>
33
+ <button type="submit">添加</button>
34
+ </form>
35
+
36
+ {{if todos.length === 0}}
37
+ <div class="empty">暂无任务,添加一个吧!</div>
38
+ {{else}}
39
+ <ul class="todo-list">
40
+ {{each todos todo}}
41
+ <li class="todo-item {{if todo.completed}}completed{{/if}}">
42
+ <span class="todo-text">{{todo.title}}</span>
43
+ <div class="todo-actions">
44
+ <a href="/todo/toggle?id={{todo.id}}&completed={{todo.completed}}" class="btn btn-toggle">
45
+ {{if todo.completed}}撤销{{else}}完成{{/if}}
46
+ </a>
47
+ <a href="/todo/delete?id={{todo.id}}" class="btn btn-delete" onclick="return confirm('确定删除吗?')">删除</a>
48
+ </div>
49
+ </li>
50
+ {{/each}}
51
+ </ul>
52
+ {{/if}}
53
+ </div>
54
+ </body>
55
+ </html>
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @module config
3
+ * @type {import('jj.js').AppConfig}
4
+ */
5
+ module.exports = {
6
+ app_debug: true,
7
+ static_dir: 'static',
8
+ koa_body: {}
9
+ };
@@ -0,0 +1,17 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ const dataDir = path.join(__dirname, '../data');
5
+ if(!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
6
+
7
+ /**
8
+ * @module config
9
+ * @type {import('jj.js').DbConfig}
10
+ */
11
+ module.exports = {
12
+ default: {
13
+ type: 'sqlite',
14
+ database: path.join(dataDir, 'todo.db'),
15
+ prefix: 'todo_'
16
+ }
17
+ };
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "target": "es6",
5
+ "checkJs": true
6
+ },
7
+ "exclude": ["node_modules"]
8
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "todo-app",
3
+ "version": "1.0.0",
4
+ "description": "A jj.js Todo List application",
5
+ "main": "server.js",
6
+ "scripts": {
7
+ "start": "node server.js"
8
+ },
9
+ "dependencies": {
10
+ "jj.js": "*"
11
+ }
12
+ }
@@ -0,0 +1,8 @@
1
+ const { App, Logger } = require('jj.js');
2
+
3
+ const port = 3000;
4
+ const app = new App();
5
+
6
+ app.listen(port, () => {
7
+ Logger.system(`Todo 应用已启动: http://localhost:${port}`);
8
+ });