@hyxf/novel-tool 0.0.9
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 +3 -0
- package/bin/novel.js +149 -0
- package/dist/assets/index-B1ISxojS.js +96 -0
- package/dist/assets/index-CihXRdtQ.css +1 -0
- package/dist/index.html +21 -0
- package/package.json +53 -0
package/README.md
ADDED
package/bin/novel.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import http from 'node:http'
|
|
4
|
+
import fs from 'node:fs/promises'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { createReadStream, existsSync } from 'node:fs'
|
|
8
|
+
import open from 'open'
|
|
9
|
+
import { Command } from 'commander'
|
|
10
|
+
|
|
11
|
+
/* =========================
|
|
12
|
+
* CLI
|
|
13
|
+
* ========================= */
|
|
14
|
+
|
|
15
|
+
const program = new Command()
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.name('novel')
|
|
19
|
+
.description('novel-tool static file server')
|
|
20
|
+
.option('-p, --port <number>', 'port to listen on', '3001')
|
|
21
|
+
.option('-h, --host <string>', 'host to bind to', '127.0.0.1')
|
|
22
|
+
.option('--no-open', 'do not open browser automatically')
|
|
23
|
+
.parse(process.argv)
|
|
24
|
+
|
|
25
|
+
const options = program.opts()
|
|
26
|
+
const port = Number(options.port)
|
|
27
|
+
const host = options.host
|
|
28
|
+
|
|
29
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
30
|
+
console.error('Invalid port number (must be 1-65535)')
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* =========================
|
|
35
|
+
* Paths
|
|
36
|
+
* ========================= */
|
|
37
|
+
|
|
38
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
39
|
+
const __dirname = path.dirname(__filename)
|
|
40
|
+
const distDir = path.resolve(__dirname, '../dist')
|
|
41
|
+
const indexHtml = path.join(distDir, 'index.html')
|
|
42
|
+
|
|
43
|
+
if (!existsSync(indexHtml)) {
|
|
44
|
+
console.error('ā dist/index.html not found. Run "npm run build" first.')
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* =========================
|
|
49
|
+
* Server
|
|
50
|
+
* ========================= */
|
|
51
|
+
|
|
52
|
+
const MIME_TYPES = {
|
|
53
|
+
'.html': 'text/html; charset=utf-8',
|
|
54
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
55
|
+
'.css': 'text/css; charset=utf-8',
|
|
56
|
+
'.svg': 'image/svg+xml',
|
|
57
|
+
'.json': 'application/json; charset=utf-8',
|
|
58
|
+
'.png': 'image/png',
|
|
59
|
+
'.jpg': 'image/jpeg',
|
|
60
|
+
'.jpeg': 'image/jpeg',
|
|
61
|
+
'.gif': 'image/gif',
|
|
62
|
+
'.ico': 'image/x-icon',
|
|
63
|
+
'.woff': 'font/woff',
|
|
64
|
+
'.woff2': 'font/woff2',
|
|
65
|
+
'.ttf': 'font/ttf'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const server = http.createServer(async (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
// Normalize URL and prevent directory traversal
|
|
71
|
+
const urlPath = req.url?.split('?')[0] || '/'
|
|
72
|
+
const safePath = urlPath === '/' ? '/index.html' : urlPath
|
|
73
|
+
const filePath = path.join(distDir, safePath)
|
|
74
|
+
|
|
75
|
+
// Security check
|
|
76
|
+
if (!filePath.startsWith(distDir)) {
|
|
77
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' })
|
|
78
|
+
res.end('Forbidden')
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if file exists
|
|
83
|
+
if (!existsSync(filePath)) {
|
|
84
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' })
|
|
85
|
+
res.end('Not Found')
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Get file stats
|
|
90
|
+
const stats = await fs.stat(filePath)
|
|
91
|
+
|
|
92
|
+
// Prevent directory listing
|
|
93
|
+
if (stats.isDirectory()) {
|
|
94
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' })
|
|
95
|
+
res.end('Forbidden')
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Set content type
|
|
100
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
101
|
+
const contentType = MIME_TYPES[ext] || 'application/octet-stream'
|
|
102
|
+
|
|
103
|
+
// Stream file for better performance
|
|
104
|
+
res.writeHead(200, {
|
|
105
|
+
'Content-Type': contentType,
|
|
106
|
+
'Content-Length': stats.size,
|
|
107
|
+
'Cache-Control': 'public, max-age=3600'
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
createReadStream(filePath).pipe(res)
|
|
111
|
+
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error('Server error:', err)
|
|
114
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' })
|
|
115
|
+
res.end('Internal Server Error')
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// Error handling
|
|
120
|
+
server.on('error', (err) => {
|
|
121
|
+
if (err.code === 'EADDRINUSE') {
|
|
122
|
+
console.error(`ā Port ${port} is already in use`)
|
|
123
|
+
} else {
|
|
124
|
+
console.error('ā Server error:', err.message)
|
|
125
|
+
}
|
|
126
|
+
process.exit(1)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
server.listen(port, host, () => {
|
|
130
|
+
const url = `http://${host}:${port}`
|
|
131
|
+
console.log(`š Server running at ${url}`)
|
|
132
|
+
console.log(`š Serving: ${distDir}`)
|
|
133
|
+
console.log('Press Ctrl+C to stop\n')
|
|
134
|
+
|
|
135
|
+
if (options.open) {
|
|
136
|
+
open(url).catch(err => {
|
|
137
|
+
console.warn('ā ļø Could not open browser:', err.message)
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Graceful shutdown
|
|
143
|
+
process.on('SIGINT', () => {
|
|
144
|
+
console.log('\nš Shutting down server...')
|
|
145
|
+
server.close(() => {
|
|
146
|
+
console.log('ā
Server closed')
|
|
147
|
+
process.exit(0)
|
|
148
|
+
})
|
|
149
|
+
})
|