create-skweb 1.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.
Files changed (74) hide show
  1. package/.turbo/turbo-build.log +17 -0
  2. package/.turbo/turbo-check.log +4 -0
  3. package/.turbo/turbo-lint.log +4 -0
  4. package/.turbo/turbo-test.log +65 -0
  5. package/CHANGELOG.md +7 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +199 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/index.cjs +115 -0
  11. package/dist/index.cjs.map +1 -0
  12. package/dist/index.d.ts +12 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +102 -0
  15. package/dist/index.js.map +1 -0
  16. package/package.json +54 -0
  17. package/rollup.config.mjs +71 -0
  18. package/src/cli.ts +118 -0
  19. package/src/index.ts +122 -0
  20. package/templates/cjs/ecosystem.config.js +32 -0
  21. package/templates/cjs/eslint.config.js +25 -0
  22. package/templates/cjs/package.json +46 -0
  23. package/templates/cjs/src/app.js +91 -0
  24. package/templates/cjs/src/commands/migrate.js +35 -0
  25. package/templates/cjs/src/commands/seed.js +58 -0
  26. package/templates/cjs/src/controllers/hello.js +33 -0
  27. package/templates/cjs/src/controllers/user.js +101 -0
  28. package/templates/cjs/src/cronActions/cleanup.js +12 -0
  29. package/templates/cjs/src/cronWorker.js +85 -0
  30. package/templates/cjs/src/index.js +7 -0
  31. package/templates/cjs/src/models/SysUser.js +38 -0
  32. package/templates/cjs/src/seeds/SysUser.json +1 -0
  33. package/templates/cjs/src/services/database.js +15 -0
  34. package/templates/cjs/src/utils/config.js +65 -0
  35. package/templates/cjs/src/utils/logger.js +53 -0
  36. package/templates/cjs/test/index.js +13 -0
  37. package/templates/mjs/ecosystem.config.js +32 -0
  38. package/templates/mjs/eslint.config.js +25 -0
  39. package/templates/mjs/package.json +46 -0
  40. package/templates/mjs/src/app.js +97 -0
  41. package/templates/mjs/src/commands/migrate.js +38 -0
  42. package/templates/mjs/src/commands/seed.js +63 -0
  43. package/templates/mjs/src/controllers/hello.js +33 -0
  44. package/templates/mjs/src/controllers/user.js +101 -0
  45. package/templates/mjs/src/cronActions/cleanup.js +9 -0
  46. package/templates/mjs/src/cronWorker.js +91 -0
  47. package/templates/mjs/src/index.js +8 -0
  48. package/templates/mjs/src/models/SysUser.js +40 -0
  49. package/templates/mjs/src/seeds/SysUser.json +1 -0
  50. package/templates/mjs/src/services/database.js +16 -0
  51. package/templates/mjs/src/utils/config.js +63 -0
  52. package/templates/mjs/src/utils/logger.js +52 -0
  53. package/templates/mjs/test/index.mjs +13 -0
  54. package/templates/ts/ecosystem.config.js +36 -0
  55. package/templates/ts/eslint.config.js +17 -0
  56. package/templates/ts/package.json +57 -0
  57. package/templates/ts/rollup.config.mjs +39 -0
  58. package/templates/ts/src/app.ts +96 -0
  59. package/templates/ts/src/commands/migrate.ts +38 -0
  60. package/templates/ts/src/commands/seed.ts +63 -0
  61. package/templates/ts/src/controllers/hello.ts +33 -0
  62. package/templates/ts/src/controllers/user.ts +101 -0
  63. package/templates/ts/src/cronActions/cleanup.ts +10 -0
  64. package/templates/ts/src/cronWorker.ts +90 -0
  65. package/templates/ts/src/index.ts +8 -0
  66. package/templates/ts/src/models/SysUser.ts +49 -0
  67. package/templates/ts/src/seeds/SysUser.json +1 -0
  68. package/templates/ts/src/services/database.ts +16 -0
  69. package/templates/ts/src/utils/config.ts +68 -0
  70. package/templates/ts/src/utils/logger.ts +63 -0
  71. package/templates/ts/test/index.mjs +13 -0
  72. package/templates/ts/tsconfig.json +20 -0
  73. package/test/index.mjs +137 -0
  74. package/tsconfig.json +22 -0
@@ -0,0 +1,96 @@
1
+ import 'dotenv/config'
2
+
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+ import express from 'skweb-express'
6
+ import expressLogger from 'skweb-express-logger'
7
+ import { WebFramework } from 'skweb-framework'
8
+ import logger from './utils/logger.js'
9
+ import { checkRequiredEnv } from './utils/config.js'
10
+
11
+ const __filename = fileURLToPath(import.meta.url)
12
+ const __dirname = path.dirname(__filename)
13
+
14
+ async function bootstrap(): Promise<void> {
15
+ logger.info('[app] Initializing web process...')
16
+
17
+ if (!checkRequiredEnv()) {
18
+ logger.error('[app] Configuration validation failed, exiting...')
19
+ process.exit(1)
20
+ }
21
+
22
+ const framework = new WebFramework({
23
+ db: {
24
+ host: process.env.DB_HOST || '127.0.0.1',
25
+ port: Number(process.env.DB_PORT) || 3306,
26
+ username: process.env.DB_USER || 'root',
27
+ password: process.env.DB_PASS || '',
28
+ database: process.env.DB_NAME || 'test-project',
29
+ dialect: (process.env.DB_DIALECT as 'mysql' | 'postgres' | 'sqlite' | 'mariadb') || 'mysql',
30
+ charset: process.env.DB_CHARSET || 'utf8mb4',
31
+ underscored: true,
32
+ logging: process.env.DEBUG_DB === 'true' ? (msg: string) => logger.debug(msg) : false
33
+ },
34
+ redis: {
35
+ host: process.env.REDIS_HOST || '127.0.0.1',
36
+ port: Number(process.env.REDIS_PORT) || 6379,
37
+ password: process.env.REDIS_PASSWORD || undefined
38
+ }
39
+ })
40
+
41
+ await framework.runPluginBeforeSetup()
42
+ await framework.setupModels(path.join(__dirname, './models'))
43
+ await framework.loadPluginModels()
44
+ await framework.runPluginAfterSetup()
45
+
46
+ const app = framework.createApp() as express.Express & Record<string, any>
47
+
48
+ app.disable('x-powered-by')
49
+ app.disable('etag')
50
+
51
+ app.set('trust proxy', true)
52
+
53
+ app.set('wrap response', true)
54
+ app.setOkCode(0)
55
+ app.setOkMessage('success')
56
+
57
+ app.setJWT('default', {
58
+ secret: process.env.JWT_SECRET || 'please-change-this-in-production',
59
+ algorithms: ['HS256'],
60
+ sign: { expiresIn: process.env.JWT_EXPIRES_IN || '7d' },
61
+ requestProperty: 'resolvedToken'
62
+ })
63
+ app.setJwtHintHeader('JwtHint')
64
+
65
+ app.use(expressLogger())
66
+ app.use(express.static(path.join(__dirname, '..', 'public')))
67
+
68
+ await framework.loadControllers(path.join(__dirname, './controllers'))
69
+ await framework.loadPluginControllers()
70
+
71
+ framework.start(Number(process.env.PORT) || 3000)
72
+
73
+ logger.info('[app] Initialization complete (web process)')
74
+
75
+ process.on('SIGINT', async () => {
76
+ logger.info('[app] Received SIGINT, shutting down gracefully...')
77
+ await framework.stop()
78
+ process.exit(0)
79
+ })
80
+
81
+ process.on('SIGTERM', async () => {
82
+ logger.info('[app] Received SIGTERM, shutting down gracefully...')
83
+ await framework.stop()
84
+ process.exit(0)
85
+ })
86
+ }
87
+
88
+ const isMainModule = import.meta.url === `file://${process.argv[1]}`
89
+ if (isMainModule) {
90
+ bootstrap().catch((err) => {
91
+ logger.error('[app] Fatal error:', err)
92
+ process.exit(1)
93
+ })
94
+ }
95
+
96
+ export { bootstrap }
@@ -0,0 +1,38 @@
1
+ import 'dotenv/config'
2
+
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+ import { Database } from 'skweb-sequelize'
6
+ import logger from '../utils/logger.js'
7
+
8
+ const __filename = fileURLToPath(import.meta.url)
9
+ const __dirname = path.dirname(__filename)
10
+
11
+ async function runMigrate(): Promise<void> {
12
+ logger.info('[migrate] Running database migrations...')
13
+
14
+ const db = new Database({
15
+ host: process.env.DB_HOST || '127.0.0.1',
16
+ port: Number(process.env.DB_PORT) || 3306,
17
+ username: process.env.DB_USER || 'root',
18
+ password: process.env.DB_PASS || '',
19
+ database: process.env.DB_NAME || 'test-project',
20
+ dialect: (process.env.DB_DIALECT as 'mysql' | 'postgres' | 'sqlite' | 'mariadb') || 'mysql',
21
+ charset: process.env.DB_CHARSET || 'utf8mb4',
22
+ underscored: true,
23
+ logging: process.env.DEBUG_DB === 'true' ? (msg: string) => logger.debug(msg) : false
24
+ })
25
+
26
+ await db.loadModelsDir(path.join(__dirname, '..', 'models'))
27
+ db.associateAllModels()
28
+ await db.sequelize.sync({ alter: true })
29
+ logger.info('[migrate] Database schema synced')
30
+ }
31
+
32
+ const isMainModule = import.meta.url === `file://${process.argv[1]}`
33
+ if (isMainModule) {
34
+ runMigrate().catch((err) => {
35
+ logger.error('[migrate] Fatal error:', err)
36
+ process.exit(1)
37
+ })
38
+ }
@@ -0,0 +1,63 @@
1
+ import 'dotenv/config'
2
+
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+ import { Database } from 'skweb-sequelize'
6
+ import logger from '../utils/logger.js'
7
+
8
+ const __filename = fileURLToPath(import.meta.url)
9
+ const __dirname = path.dirname(__filename)
10
+
11
+ async function runSeed(): Promise<void> {
12
+ logger.info('[seed] Initializing database with seed data...')
13
+
14
+ const db = new Database({
15
+ host: process.env.DB_HOST || '127.0.0.1',
16
+ port: Number(process.env.DB_PORT) || 3306,
17
+ username: process.env.DB_USER || 'root',
18
+ password: process.env.DB_PASS || '',
19
+ database: process.env.DB_NAME || 'test-project',
20
+ dialect: (process.env.DB_DIALECT as 'mysql' | 'postgres' | 'sqlite' | 'mariadb') || 'mysql',
21
+ charset: process.env.DB_CHARSET || 'utf8mb4',
22
+ underscored: true,
23
+ logging: process.env.DEBUG_DB === 'true' ? (msg: string) => logger.debug(msg) : false
24
+ })
25
+
26
+ await db.loadModelsDir(path.join(__dirname, '..', 'models'))
27
+ db.associateAllModels()
28
+
29
+ const seedsDir = path.join(__dirname, '..', 'seeds')
30
+ const fs = await import('fs/promises')
31
+ let files: string[]
32
+ try {
33
+ files = await fs.readdir(seedsDir)
34
+ } catch {
35
+ logger.warn('[seed] No seeds directory found')
36
+ return
37
+ }
38
+
39
+ for (const file of files) {
40
+ if (!file.endsWith('.json')) continue
41
+ const modelName = path.basename(file, '.json')
42
+ const Model = db.sequelize.models[modelName]
43
+ if (!Model) {
44
+ logger.warn(`[seed] Model ${modelName} not found`)
45
+ continue
46
+ }
47
+ const data = JSON.parse(await fs.readFile(path.join(seedsDir, file), 'utf-8'))
48
+ if (Array.isArray(data) && data.length > 0) {
49
+ await Model.bulkCreate(data, { ignoreDuplicates: true })
50
+ logger.info(`[seed] Seeded ${modelName} with ${data.length} records`)
51
+ }
52
+ }
53
+
54
+ logger.info('[seed] Database initialization complete')
55
+ }
56
+
57
+ const isMainModule = import.meta.url === `file://${process.argv[1]}`
58
+ if (isMainModule) {
59
+ runSeed().catch((err) => {
60
+ logger.error('[seed] Fatal error:', err)
61
+ process.exit(1)
62
+ })
63
+ }
@@ -0,0 +1,33 @@
1
+ export default [
2
+ {
3
+ method: 'get',
4
+ path: '/',
5
+ async handler() {
6
+ return {
7
+ message: 'Welcome to SKWeb!',
8
+ version: '1.0.0',
9
+ timestamp: new Date().toISOString()
10
+ }
11
+ }
12
+ },
13
+ {
14
+ method: 'get',
15
+ path: '/health',
16
+ async handler() {
17
+ return {
18
+ status: 'ok',
19
+ timestamp: new Date().toISOString()
20
+ }
21
+ }
22
+ },
23
+ {
24
+ method: 'post',
25
+ path: '/echo',
26
+ async handler(req: any) {
27
+ return {
28
+ message: 'Echo received',
29
+ data: req.body
30
+ }
31
+ }
32
+ }
33
+ ]
@@ -0,0 +1,101 @@
1
+ import createHttpError from 'http-errors'
2
+ import { db } from '../services/database.js'
3
+
4
+ const { SysUser } = db.sequelize.models
5
+
6
+ export default [
7
+ {
8
+ method: 'get',
9
+ path: '/api/users',
10
+ async handler(req: any) {
11
+ const { page = 1, limit = 10 } = req.query
12
+ const users = await SysUser.findAndCountAll({
13
+ offset: (Number(page) - 1) * Number(limit),
14
+ limit: Number(limit)
15
+ })
16
+ return {
17
+ list: users.rows,
18
+ total: users.count
19
+ }
20
+ }
21
+ },
22
+ {
23
+ method: 'get',
24
+ path: '/api/users/:id',
25
+ paramsSchema: {
26
+ type: 'object',
27
+ required: ['id'],
28
+ properties: { id: { type: 'integer', minimum: 1 } }
29
+ },
30
+ async handler(req: any) {
31
+ const { id } = req.validatedParams
32
+ const user = await SysUser.findByPk(id)
33
+ if (!user) {
34
+ throw new createHttpError.NotFound('User not found')
35
+ }
36
+ return user
37
+ }
38
+ },
39
+ {
40
+ method: 'post',
41
+ path: '/api/users',
42
+ bodySchema: {
43
+ type: 'object',
44
+ required: ['username', 'password'],
45
+ properties: {
46
+ username: { type: 'string', minLength: 3 },
47
+ password: { type: 'string', minLength: 6 },
48
+ email: { type: 'string', format: 'email' },
49
+ phone: { type: 'string' }
50
+ }
51
+ },
52
+ async handler(req: any) {
53
+ return await SysUser.create(req.validatedBody)
54
+ }
55
+ },
56
+ {
57
+ method: 'put',
58
+ path: '/api/users/:id',
59
+ paramsSchema: {
60
+ type: 'object',
61
+ required: ['id'],
62
+ properties: { id: { type: 'integer', minimum: 1 } }
63
+ },
64
+ bodySchema: {
65
+ type: 'object',
66
+ properties: {
67
+ password: { type: 'string', minLength: 6 },
68
+ email: { type: 'string', format: 'email' },
69
+ phone: { type: 'string' },
70
+ status: { type: 'integer' }
71
+ }
72
+ },
73
+ async handler(req: any) {
74
+ const { id } = req.validatedParams
75
+ const user = await SysUser.findByPk(id)
76
+ if (!user) {
77
+ throw new createHttpError.NotFound('User not found')
78
+ }
79
+ await user.update(req.validatedBody)
80
+ return user
81
+ }
82
+ },
83
+ {
84
+ method: 'delete',
85
+ path: '/api/users/:id',
86
+ paramsSchema: {
87
+ type: 'object',
88
+ required: ['id'],
89
+ properties: { id: { type: 'integer', minimum: 1 } }
90
+ },
91
+ async handler(req: any) {
92
+ const { id } = req.validatedParams
93
+ const user = await SysUser.findByPk(id)
94
+ if (!user) {
95
+ throw new createHttpError.NotFound('User not found')
96
+ }
97
+ await user.destroy()
98
+ return { message: 'User deleted' }
99
+ }
100
+ }
101
+ ]
@@ -0,0 +1,10 @@
1
+ import logger from '../utils/logger.js'
2
+
3
+ /**
4
+ * 清理任务示例 - 每天凌晨3点清理过期数据
5
+ */
6
+ export default async function cleanup(): Promise<void> {
7
+ logger.info(`[cron] Running cleanup task at ${new Date().toISOString()}`)
8
+ // 业务逻辑:清理 7 天前的临时文件、过期 token 等
9
+ logger.info('[cron] Cleanup task completed')
10
+ }
@@ -0,0 +1,90 @@
1
+ import 'dotenv/config'
2
+
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+ import { CronFramework } from 'skweb-framework'
6
+ import logger from './utils/logger.js'
7
+ import { checkRequiredEnv } from './utils/config.js'
8
+
9
+ const __filename = fileURLToPath(import.meta.url)
10
+ const __dirname = path.dirname(__filename)
11
+
12
+ async function startCronWorker(): Promise<void> {
13
+ logger.info('[cron-worker] Starting cron worker process...')
14
+
15
+ if (!checkRequiredEnv()) {
16
+ logger.error('[cron-worker] Configuration validation failed, exiting...')
17
+ process.exit(1)
18
+ }
19
+
20
+ const framework = new CronFramework({
21
+ db: {
22
+ host: process.env.DB_HOST || '127.0.0.1',
23
+ port: Number(process.env.DB_PORT) || 3306,
24
+ username: process.env.DB_USER || 'root',
25
+ password: process.env.DB_PASS || '',
26
+ database: process.env.DB_NAME || 'test-project',
27
+ dialect: (process.env.DB_DIALECT as 'mysql' | 'postgres' | 'sqlite' | 'mariadb') || 'mysql',
28
+ charset: process.env.DB_CHARSET || 'utf8mb4',
29
+ underscored: true,
30
+ logging: process.env.DEBUG_DB === 'true' ? (msg: string) => logger.debug(msg) : false
31
+ },
32
+ redis: {
33
+ host: process.env.REDIS_HOST || '127.0.0.1',
34
+ port: Number(process.env.REDIS_PORT) || 6379,
35
+ password: process.env.REDIS_PASSWORD || undefined
36
+ },
37
+ channel: 'cron:task:cmd',
38
+ logger,
39
+ onSaveTask: async (taskData: { taskId: string; cache: unknown; data: unknown }) => {
40
+ const SysCronTask = framework.db?.sequelize?.models.SysCronTask as any
41
+ if (SysCronTask) {
42
+ await SysCronTask.update(
43
+ { cache: taskData.cache, data: taskData.data },
44
+ { where: { id: taskData.taskId } }
45
+ )
46
+ }
47
+ },
48
+ loadTasks: async () => {
49
+ const SysCronTask = framework.db?.sequelize?.models.SysCronTask as any
50
+ if (!SysCronTask) return []
51
+ return (await SysCronTask.findAll()).map((t: any) => ({
52
+ id: String(t.id),
53
+ cronTime: t.cronTime,
54
+ method: t.method
55
+ }))
56
+ }
57
+ })
58
+
59
+ await framework.runPluginBeforeSetup()
60
+ await framework.setupModels(path.join(__dirname, './models'))
61
+ await framework.loadPluginModels()
62
+ await framework.runPluginAfterSetup()
63
+
64
+ await framework.loadCronActions(path.join(__dirname, './cronActions'))
65
+ await framework.startWorker()
66
+ framework.start()
67
+ logger.info('[cron-worker] Cron worker started')
68
+
69
+ process.on('SIGINT', async () => {
70
+ logger.info('[cron-worker] Received SIGINT, shutting down...')
71
+ await framework.stop()
72
+ process.exit(0)
73
+ })
74
+
75
+ process.on('SIGTERM', async () => {
76
+ logger.info('[cron-worker] Received SIGTERM, shutting down...')
77
+ await framework.stop()
78
+ process.exit(0)
79
+ })
80
+ }
81
+
82
+ const isMainModule = import.meta.url === `file://${process.argv[1]}`
83
+ if (isMainModule) {
84
+ startCronWorker().catch((err) => {
85
+ logger.error('[cron-worker] Fatal error:', err)
86
+ process.exit(1)
87
+ })
88
+ }
89
+
90
+ export { startCronWorker }
@@ -0,0 +1,8 @@
1
+ import 'dotenv/config'
2
+
3
+ import { bootstrap } from './app.js'
4
+
5
+ const isMainModule = import.meta.url === `file://${process.argv[1]}`
6
+ if (isMainModule) {
7
+ bootstrap()
8
+ }
@@ -0,0 +1,49 @@
1
+ import { Model, DataTypes, type Sequelize, type ModelStatic } from 'sequelize'
2
+
3
+ export default (sequelize: Sequelize): ModelStatic<Model> => {
4
+ class SysUser extends Model {
5
+ declare id: number
6
+ declare username: string
7
+ declare password: string
8
+ declare email: string
9
+ declare phone: string
10
+ declare status: number
11
+ declare createdAt: Date
12
+ declare updatedAt: Date
13
+ }
14
+
15
+ SysUser.init({
16
+ id: {
17
+ type: DataTypes.INTEGER,
18
+ primaryKey: true,
19
+ autoIncrement: true
20
+ },
21
+ username: {
22
+ type: DataTypes.STRING(50),
23
+ allowNull: false,
24
+ unique: true
25
+ },
26
+ password: {
27
+ type: DataTypes.STRING(255),
28
+ allowNull: false
29
+ },
30
+ email: {
31
+ type: DataTypes.STRING(100)
32
+ },
33
+ phone: {
34
+ type: DataTypes.STRING(20)
35
+ },
36
+ status: {
37
+ type: DataTypes.TINYINT,
38
+ defaultValue: 1
39
+ }
40
+ }, {
41
+ sequelize,
42
+ modelName: 'SysUser',
43
+ tableName: 'sys_users',
44
+ timestamps: true,
45
+ underscored: true
46
+ })
47
+
48
+ return SysUser
49
+ }
@@ -0,0 +1 @@
1
+ [{"id": 1, "username": "admin", "password": "$2b$10$example", "email": "admin@example.com", "phone": "13800138000", "status": 1}]
@@ -0,0 +1,16 @@
1
+ import { Database } from 'skweb-sequelize'
2
+
3
+ const db = new Database({
4
+ host: process.env.DB_HOST || '127.0.0.1',
5
+ port: Number(process.env.DB_PORT) || 3306,
6
+ username: process.env.DB_USER || 'root',
7
+ password: process.env.DB_PASS || '',
8
+ database: process.env.DB_NAME || '{{name}}',
9
+ dialect: (process.env.DB_DIALECT as any) || 'mysql',
10
+ charset: process.env.DB_CHARSET || 'utf8mb4',
11
+ underscored: true,
12
+ logging: process.env.DEBUG_DB === 'true'
13
+ })
14
+
15
+ export { db }
16
+ export default db
@@ -0,0 +1,68 @@
1
+ import logger from './logger.js'
2
+
3
+ interface ConfigValidationResult {
4
+ isValid: boolean
5
+ errors: string[]
6
+ }
7
+
8
+ export function validateConfig(): ConfigValidationResult {
9
+ const errors: string[] = []
10
+
11
+ if (!process.env.DB_HOST) {
12
+ logger.warn('[config] DB_HOST not set, using default: 127.0.0.1')
13
+ }
14
+ if (!process.env.DB_NAME) {
15
+ logger.warn('[config] DB_NAME not set, using default: test-project')
16
+ }
17
+ if (!process.env.DB_USER) {
18
+ logger.warn('[config] DB_USER not set, using default: root')
19
+ }
20
+
21
+ if (process.env.JWT_SECRET === 'please-change-this-in-production') {
22
+ logger.warn('[config] WARNING: JWT_SECRET is using default value, please change in production!')
23
+ }
24
+
25
+ if (!process.env.REDIS_HOST) {
26
+ logger.warn('[config] REDIS_HOST not set, using default: 127.0.0.1')
27
+ }
28
+
29
+ const port = Number(process.env.PORT)
30
+ if (port && (port < 1 || port > 65535)) {
31
+ errors.push('PORT must be between 1 and 65535')
32
+ }
33
+
34
+ const dbPort = Number(process.env.DB_PORT)
35
+ if (dbPort && (dbPort < 1 || dbPort > 65535)) {
36
+ errors.push('DB_PORT must be between 1 and 65535')
37
+ }
38
+
39
+ const redisPort = Number(process.env.REDIS_PORT)
40
+ if (redisPort && (redisPort < 1 || redisPort > 65535)) {
41
+ errors.push('REDIS_PORT must be between 1 and 65535')
42
+ }
43
+
44
+ return {
45
+ isValid: errors.length === 0,
46
+ errors
47
+ }
48
+ }
49
+
50
+ export function checkRequiredEnv(): boolean {
51
+ const result = validateConfig()
52
+
53
+ if (result.errors.length > 0) {
54
+ logger.error('[config] Configuration validation failed:')
55
+ result.errors.forEach((err) => logger.error(` - ${err}`))
56
+ return false
57
+ }
58
+
59
+ if (process.env.NODE_ENV === 'production') {
60
+ if (!process.env.JWT_SECRET || process.env.JWT_SECRET === 'please-change-this-in-production') {
61
+ logger.error('[config] JWT_SECRET must be set in production!')
62
+ return false
63
+ }
64
+ }
65
+
66
+ logger.info('[config] Configuration validation passed')
67
+ return true
68
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Winston 日志配置
3
+ *
4
+ * 支持通过环境变量控制:
5
+ * - LOG_LEVEL: 日志级别 (debug/info/warn/error),默认 info
6
+ * - LOG_DIR: 日志文件目录,默认 ./logs
7
+ * - LOG_CONSOLE: 是否同时输出到控制台 (true/false),默认 true
8
+ */
9
+ import winston from 'winston'
10
+ import path from 'path'
11
+ import fs from 'fs'
12
+
13
+ const LOG_LEVEL = process.env.LOG_LEVEL || 'info'
14
+ const LOG_DIR = process.env.LOG_DIR || path.join(process.cwd(), 'logs')
15
+ const LOG_CONSOLE = process.env.LOG_CONSOLE !== 'false'
16
+
17
+ // 确保日志目录存在
18
+ if (!fs.existsSync(LOG_DIR)) {
19
+ fs.mkdirSync(LOG_DIR, { recursive: true })
20
+ }
21
+
22
+ const { combine, timestamp, printf, colorize, errors, splat, json } = winston.format
23
+
24
+ // 控制台输出格式:带颜色 + 易读
25
+ const consoleFormat = printf(({ level, message, timestamp: ts, stack }) => {
26
+ return `${ts} [${level}] ${stack || message}`
27
+ })
28
+
29
+ // 文件输出格式:JSON 结构(便于日志分析系统采集)
30
+ const fileFormat = combine(timestamp(), errors({ stack: true }), splat(), json())
31
+
32
+ export const logger = winston.createLogger({
33
+ level: LOG_LEVEL,
34
+ format: combine(timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), errors({ stack: true }), splat()),
35
+ transports: [
36
+ // 错误日志单独输出
37
+ new winston.transports.File({
38
+ filename: path.join(LOG_DIR, 'error.log'),
39
+ level: 'error',
40
+ format: fileFormat,
41
+ maxsize: 10 * 1024 * 1024, // 10MB
42
+ maxFiles: 10
43
+ }),
44
+ // 所有日志
45
+ new winston.transports.File({
46
+ filename: path.join(LOG_DIR, 'combined.log'),
47
+ format: fileFormat,
48
+ maxsize: 10 * 1024 * 1024, // 10MB
49
+ maxFiles: 10
50
+ })
51
+ ]
52
+ })
53
+
54
+ // 生产环境:输出到控制台
55
+ if (LOG_CONSOLE) {
56
+ logger.add(
57
+ new winston.transports.Console({
58
+ format: combine(colorize(), consoleFormat)
59
+ })
60
+ )
61
+ }
62
+
63
+ export default logger
@@ -0,0 +1,13 @@
1
+ import { expect } from 'chai'
2
+
3
+ describe('SKWeb App Tests', function() {
4
+ describe('Basic Tests', function() {
5
+ it('should pass basic test', function() {
6
+ expect(true).to.equal(true)
7
+ })
8
+
9
+ it('should have correct package structure', function() {
10
+ expect('SKWeb').to.be.a('string')
11
+ })
12
+ })
13
+ })
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "lib": ["ES2020"],
6
+ "moduleResolution": "Node",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "declaration": true,
13
+ "sourceMap": true,
14
+ "rootDir": "src",
15
+ "outDir": "dist",
16
+ "baseUrl": "."
17
+ },
18
+ "include": ["src/**/*.ts"],
19
+ "exclude": ["node_modules", "dist", "test"]
20
+ }