@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 ADDED
@@ -0,0 +1,3 @@
1
+ # novel-tool
2
+
3
+ novel tool
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
+ })