@unsetsoft/ryunix-presets 1.0.24 → 1.0.25

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@unsetsoft/ryunix-presets",
3
3
  "description": "Package with presets for different development environments.",
4
- "version": "1.0.24",
4
+ "version": "1.0.25",
5
5
  "author": "Neyunse",
6
6
  "type": "module",
7
7
  "repository": "https://github.com/UnSetSoft/Ryunixjs",
@@ -49,7 +49,8 @@ const StartServer = async (cliSettings) => {
49
49
  `${defaultSettings.webpack.output.buildDirectory}/cache`,
50
50
  )
51
51
 
52
- const mode = cliSettings.production || defaultSettings.webpack.production ? true : false
52
+ const mode =
53
+ cliSettings.production || defaultSettings.webpack.production ? true : false
53
54
 
54
55
  if (!mode) {
55
56
  cleanCacheDir(cacheDir)
@@ -18,7 +18,7 @@ import fs from 'fs'
18
18
  import { fileURLToPath } from 'url'
19
19
  import { dirname, join } from 'path'
20
20
  import server from './prod.server.mjs'
21
- import config from '../utils/config.cjs';
21
+ import config from '../utils/config.cjs'
22
22
  const __filename = fileURLToPath(import.meta.url)
23
23
 
24
24
  const __dirname = dirname(__filename)
@@ -60,7 +60,9 @@ const dev = {
60
60
  describe: 'Run server for developer mode.',
61
61
  handler: async (arg) => {
62
62
  if (defaultSettings.webpack.production) {
63
- logger.error("You need use development mode! change webpack.production to false in ryunix.config.js.")
63
+ logger.error(
64
+ 'You need use development mode! change webpack.production to false in ryunix.config.js.',
65
+ )
64
66
  return
65
67
  }
66
68
  const open = Boolean(arg.browser) || false
@@ -77,19 +79,25 @@ const prod = {
77
79
  describe: 'Run server for production mode. Requiere .ryunix/static',
78
80
  handler: async (arg) => {
79
81
  if (!defaultSettings.webpack.production) {
80
- logger.error("You need use production mode!")
82
+ logger.error('You need use production mode!')
81
83
  return
82
84
  }
83
85
 
84
- if (!fs.existsSync(join(process.cwd(), config.webpack.output.buildDirectory, 'static'))) {
85
- logger.error("You need build first!")
86
+ if (
87
+ !fs.existsSync(
88
+ join(process.cwd(), config.webpack.output.buildDirectory, 'static'),
89
+ )
90
+ ) {
91
+ logger.error('You need build first!')
86
92
  return
87
93
  }
88
94
 
89
95
  server.listen(config.webpack.devServer.port, () => {
90
- console.log(`Server running at http://localhost:${config.webpack.devServer.port}/`);
91
- });
92
- }
96
+ console.log(
97
+ `Server running at http://localhost:${config.webpack.devServer.port}/`,
98
+ )
99
+ })
100
+ },
93
101
  }
94
102
 
95
103
  const build = {
@@ -130,8 +138,6 @@ const build = {
130
138
  minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`
131
139
 
132
140
  if (defaultSettings.webpack.production) {
133
-
134
-
135
141
  await Prerender(defaultSettings.webpack.output.buildDirectory)
136
142
  }
137
143
 
@@ -153,15 +159,26 @@ const extractHTML = {
153
159
  handler: async (arg) => {
154
160
  const runPath = process.cwd()
155
161
 
156
- fs.copyFile(join(__dirname, "..", "template/index.html"), join(runPath, "public/index.html"), (err) => {
157
- if (err) {
158
- console.error("Error extracting HTML: ", err.message);
159
- return;
160
- }
161
- console.log("File extracted successfully. Now you can enable the template with static.customTemplate inside ryunix.config.js");
162
- });
162
+ fs.copyFile(
163
+ join(__dirname, '..', 'template/index.html'),
164
+ join(runPath, 'public/index.html'),
165
+ (err) => {
166
+ if (err) {
167
+ console.error('Error extracting HTML: ', err.message)
168
+ return
169
+ }
170
+ console.log(
171
+ 'File extracted successfully. Now you can enable the template with static.customTemplate inside ryunix.config.js',
172
+ )
173
+ },
174
+ )
163
175
  },
164
176
  }
165
177
 
166
-
167
- yargs(hideBin(process.argv)).command(dev).command(build).command(prod).command(lint).command(extractHTML).parse()
178
+ yargs(hideBin(process.argv))
179
+ .command(dev)
180
+ .command(build)
181
+ .command(prod)
182
+ .command(lint)
183
+ .command(extractHTML)
184
+ .parse()
@@ -29,10 +29,10 @@ const Prerender = async (directory) => {
29
29
 
30
30
  const metaExist = routes.some((route) => route.meta)
31
31
  if (metaExist && defaultSettings.static.seo.meta.length > 0) {
32
-
33
- console.error("[Ryunix Error] You are mixing static and dynamic meta tags; you can only use one of the two. Remove static.seo.meta from ryunix.config.js.")
32
+ console.error(
33
+ '[Ryunix Error] You are mixing static and dynamic meta tags; you can only use one of the two. Remove static.seo.meta from ryunix.config.js.',
34
+ )
34
35
  process.exit(1)
35
-
36
36
  }
37
37
 
38
38
  if (routes.length === 0) {
@@ -1,393 +1,401 @@
1
- import http from 'http'
2
- import { promises as fs } from 'fs'
3
- import path from 'path'
4
- import { createHash } from 'crypto'
5
- import zlib from 'zlib'
6
- import { promisify } from 'util'
7
- import { createReadStream } from 'fs'
8
- import { pipeline } from 'stream/promises'
9
- import config from '../utils/config.cjs'
10
-
11
- const gzip = promisify(zlib.gzip)
12
- const brotliCompress = promisify(zlib.brotliCompress)
13
-
14
- // MIME types dictionary
15
- const MIME_TYPES = {
16
- '.js': 'application/javascript',
17
- '.mjs': 'application/javascript',
18
- '.css': 'text/css',
19
- '.html': 'text/html',
20
- '.json': 'application/json',
21
- '.png': 'image/png',
22
- '.jpg': 'image/jpeg',
23
- '.jpeg': 'image/jpeg',
24
- '.gif': 'image/gif',
25
- '.svg': 'image/svg+xml',
26
- '.woff': 'font/woff',
27
- '.woff2': 'font/woff2',
28
- '.ttf': 'font/ttf',
29
- '.eot': 'application/vnd.ms-fontobject',
30
- '.otf': 'font/otf',
31
- '.wasm': 'application/wasm',
32
- '.ico': 'image/x-icon',
33
- '.mp3': 'audio/mpeg',
34
- '.mp4': 'video/mp4',
35
- '.pdf': 'application/pdf',
36
- '.zip': 'application/zip',
37
- '.gz': 'application/gzip',
38
- '.tar': 'application/x-tar',
39
- '.7z': 'application/x-7z-compressed',
40
- '.rar': 'application/x-rar-compressed',
41
- '.avi': 'video/x-msvideo',
42
- '.mov': 'video/quicktime',
43
- '.wmv': 'video/x-ms-wmv',
44
- '.flv': 'video/x-flv',
45
- '.webm': 'video/webm',
46
- '.ogg': 'audio/ogg',
47
- '.ogv': 'video/ogg',
48
- '.m4v': 'video/mp4',
49
- '.3gp': 'video/3gpp',
50
- '.3g2': 'video/3gpp2',
51
- '.mkv': 'video/x-matroska',
52
- '.ts': 'video/mp2t',
53
- }
54
-
55
- // File cache for production server
56
- const fileCache = new Map()
57
- const MAX_CACHE_SIZE = 50 * 1024 * 1024 // 50MB
58
- let currentCacheSize = 0
59
-
60
- /**
61
- * Get MIME type from file extension
62
- */
63
- const getMimeType = (filePath) => {
64
- const ext = path.extname(filePath).toLowerCase()
65
- return MIME_TYPES[ext] || 'application/octet-stream'
66
- }
67
-
68
- /**
69
- * Validate path to prevent directory traversal attacks
70
- */
71
- const validatePath = (requestPath, rootDir) => {
72
- try {
73
- const normalizedPath = path.normalize(requestPath)
74
- const resolvedPath = path.resolve(rootDir, normalizedPath.slice(1))
75
-
76
- if (!resolvedPath.startsWith(rootDir)) {
77
- return null
78
- }
79
-
80
- return resolvedPath
81
- } catch {
82
- return null
83
- }
84
- }
85
-
86
- /**
87
- * Generate ETag from file content
88
- */
89
- const generateETag = (content) => {
90
- return createHash('md5').update(content).digest('hex')
91
- }
92
-
93
- /**
94
- * Check compression support (Brotli preferred over Gzip)
95
- */
96
- const getAcceptedEncoding = (headers) => {
97
- const encoding = headers['accept-encoding'] || ''
98
- if (encoding.includes('br')) return 'br'
99
- if (encoding.includes('gzip')) return 'gzip'
100
- return null
101
- }
102
-
103
- /**
104
- * Check if MIME type is compressible
105
- */
106
- const isCompressible = (mimeType) => {
107
- return mimeType.startsWith('text/') ||
108
- mimeType.includes('javascript') ||
109
- mimeType.includes('json') ||
110
- mimeType.includes('css')
111
- }
112
-
113
- /**
114
- * Parse Range header
115
- */
116
- const parseRange = (rangeHeader, fileSize) => {
117
- if (!rangeHeader) return null
118
-
119
- const parts = rangeHeader.replace(/bytes=/, '').split('-')
120
- const start = parseInt(parts[0], 10)
121
- const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1
122
-
123
- if (isNaN(start) || isNaN(end) || start > end || end >= fileSize) {
124
- return null
125
- }
126
-
127
- return { start, end, length: end - start + 1 }
128
- }
129
-
130
- /**
131
- * Check if file should support range requests (media files)
132
- */
133
- const supportsRangeRequests = (mimeType) => {
134
- return mimeType.startsWith('video/') ||
135
- mimeType.startsWith('audio/') ||
136
- mimeType === 'application/pdf'
137
- }
138
-
139
- /**
140
- * Serve file with range support (for video/audio)
141
- */
142
- const serveWithRange = async (filePath, req, res, stats) => {
143
- const mimeType = getMimeType(filePath)
144
- const range = parseRange(req.headers.range, stats.size)
145
-
146
- const headers = {
147
- 'Content-Type': mimeType,
148
- 'Accept-Ranges': 'bytes',
149
- 'Cache-Control': 'public, max-age=31536000',
150
- }
151
-
152
- if (range) {
153
- // Partial content
154
- headers['Content-Range'] = `bytes ${range.start}-${range.end}/${stats.size}`
155
- headers['Content-Length'] = range.length
156
-
157
- res.writeHead(206, headers)
158
-
159
- const stream = createReadStream(filePath, { start: range.start, end: range.end })
160
- await pipeline(stream, res)
161
- } else {
162
- // Full content
163
- headers['Content-Length'] = stats.size
164
- res.writeHead(200, headers)
165
-
166
- const stream = createReadStream(filePath)
167
- await pipeline(stream, res)
168
- }
169
-
170
- return true
171
- }
172
-
173
- /**
174
- * Serve static file with caching and compression
175
- */
176
- const serveStaticFile = async (filePath, req, res) => {
177
- try {
178
- let stats = await fs.stat(filePath)
179
-
180
- // 👉 If is a directory
181
- if (stats.isDirectory()) {
182
- const indexPath = path.join(filePath, 'index.html')
183
- await fs.access(indexPath)
184
- stats = await fs.stat(indexPath)
185
- filePath = indexPath
186
- }
187
-
188
- const mimeType = getMimeType(filePath)
189
-
190
- // Use range requests for media files or large files
191
- if (supportsRangeRequests(mimeType) || stats.size > 5 * 1024 * 1024) {
192
- return await serveWithRange(filePath, req, res, stats)
193
- }
194
-
195
- let cached = fileCache.get(filePath)
196
-
197
- if (!cached) {
198
- // Read and cache file
199
- const content = await fs.readFile(filePath)
200
- const etag = generateETag(content)
201
-
202
- // Compress if text-based content
203
- let brotli = null
204
- let gzipped = null
205
-
206
- if (isCompressible(mimeType)) {
207
- try {
208
- [brotli, gzipped] = await Promise.all([
209
- brotliCompress(content, {
210
- params: {
211
- [zlib.constants.BROTLI_PARAM_QUALITY]: 6,
212
- },
213
- }),
214
- gzip(content),
215
- ])
216
- } catch {
217
- // Compression failed, serve uncompressed
218
- }
219
- }
220
-
221
- cached = {
222
- content,
223
- brotli,
224
- gzipped,
225
- etag,
226
- mimeType,
227
- size: stats.size,
228
- }
229
-
230
- // Update cache
231
- if (currentCacheSize + stats.size < MAX_CACHE_SIZE) {
232
- fileCache.set(filePath, cached)
233
- currentCacheSize += stats.size
234
- }
235
- }
236
-
237
- // Check ETag for 304 Not Modified
238
- if (req.headers['if-none-match'] === cached.etag) {
239
- res.writeHead(304)
240
- res.end()
241
- return true
242
- }
243
-
244
- // Select best encoding
245
- const encoding = getAcceptedEncoding(req.headers)
246
- let responseContent = cached.content
247
- let contentEncoding = null
248
-
249
- if (encoding === 'br' && cached.brotli) {
250
- responseContent = cached.brotli
251
- contentEncoding = 'br'
252
- } else if (encoding === 'gzip' && cached.gzipped) {
253
- responseContent = cached.gzipped
254
- contentEncoding = 'gzip'
255
- }
256
-
257
- const headers = {
258
- 'Content-Type': cached.mimeType,
259
- 'Content-Length': responseContent.length,
260
- 'ETag': cached.etag,
261
- 'Cache-Control': 'public, max-age=31536000',
262
- }
263
-
264
- if (contentEncoding) {
265
- headers['Content-Encoding'] = contentEncoding
266
- }
267
-
268
- res.writeHead(200, headers)
269
- res.end(responseContent)
270
- return true
271
-
272
- } catch (error) {
273
- return false
274
- }
275
- }
276
-
277
- /**
278
- * Serve HTML page with SPA fallback support
279
- */
280
- const serveHTMLPage = async (pathname, staticDir, req, res) => {
281
- try {
282
- const candidates = []
283
-
284
- // / /index.html
285
- if (pathname === '/') {
286
- candidates.push(path.join(staticDir, 'index.html'))
287
- } else {
288
- // /test /test/index.html
289
- candidates.push(path.join(staticDir, pathname, 'index.html'))
290
-
291
- // /test /test.html
292
- candidates.push(path.join(staticDir, `${pathname}.html`))
293
-
294
- // SPA fallback
295
- candidates.push(path.join(staticDir, 'index.html'))
296
- }
297
-
298
- let pageFile = null
299
-
300
- for (const file of candidates) {
301
- try {
302
- await fs.access(file)
303
- pageFile = file
304
- break
305
- } catch { }
306
- }
307
-
308
- if (!pageFile) {
309
- res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' })
310
- res.end('404')
311
- return
312
- }
313
-
314
- const content = await fs.readFile(pageFile, 'utf-8')
315
- const etag = generateETag(Buffer.from(content))
316
-
317
- if (req.headers['if-none-match'] === etag) {
318
- res.writeHead(304)
319
- res.end()
320
- return
321
- }
322
-
323
- // Compress HTML
324
- let responseContent = content
325
- const headers = {
326
- 'Content-Type': 'text/html; charset=utf-8',
327
- 'ETag': etag,
328
- 'Cache-Control': 'no-cache',
329
- }
330
-
331
- const encoding = getAcceptedEncoding(req.headers)
332
-
333
- if (encoding === 'br') {
334
- try {
335
- responseContent = await brotliCompress(Buffer.from(content))
336
- headers['Content-Encoding'] = 'br'
337
- } catch {
338
- // Fallback to uncompressed
339
- }
340
- } else if (encoding === 'gzip') {
341
- try {
342
- responseContent = await gzip(Buffer.from(content))
343
- headers['Content-Encoding'] = 'gzip'
344
- } catch {
345
- // Fallback to uncompressed
346
- }
347
- }
348
-
349
- headers['Content-Length'] = Buffer.byteLength(responseContent)
350
-
351
- res.writeHead(200, headers)
352
- res.end(responseContent)
353
-
354
- } catch (error) {
355
- res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' })
356
- res.end('500')
357
- }
358
- }
359
-
360
- /**
361
- * Request handler
362
- */
363
- const requestHandler = async (req, res) => {
364
- const rootDir = process.cwd()
365
- const staticDir = path.join(rootDir, config.webpack.output.buildDirectory, 'static')
366
-
367
- try {
368
- const parsedUrl = new URL(req.url, `http://${req.headers.host}`)
369
- const pathname = decodeURIComponent(parsedUrl.pathname)
370
-
371
- const safePath = validatePath(pathname, staticDir)
372
- if (!safePath) {
373
- res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' })
374
- res.end('403')
375
- return
376
- }
377
-
378
- const fileServed = await serveStaticFile(safePath, req, res)
379
-
380
- if (!fileServed) {
381
- await serveHTMLPage(pathname, staticDir, req, res)
382
- }
383
-
384
- } catch (error) {
385
- console.error('[Ryunix Server Error]:', error.message)
386
- res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' })
387
- res.end('500')
388
- }
389
- }
390
-
391
- const httpServ = http.createServer(requestHandler)
392
-
393
- export default httpServ
1
+ import http from 'http'
2
+ import { promises as fs } from 'fs'
3
+ import path from 'path'
4
+ import { createHash } from 'crypto'
5
+ import zlib from 'zlib'
6
+ import { promisify } from 'util'
7
+ import { createReadStream } from 'fs'
8
+ import { pipeline } from 'stream/promises'
9
+ import config from '../utils/config.cjs'
10
+
11
+ const gzip = promisify(zlib.gzip)
12
+ const brotliCompress = promisify(zlib.brotliCompress)
13
+
14
+ // MIME types dictionary
15
+ const MIME_TYPES = {
16
+ '.js': 'application/javascript',
17
+ '.mjs': 'application/javascript',
18
+ '.css': 'text/css',
19
+ '.html': 'text/html',
20
+ '.json': 'application/json',
21
+ '.png': 'image/png',
22
+ '.jpg': 'image/jpeg',
23
+ '.jpeg': 'image/jpeg',
24
+ '.gif': 'image/gif',
25
+ '.svg': 'image/svg+xml',
26
+ '.woff': 'font/woff',
27
+ '.woff2': 'font/woff2',
28
+ '.ttf': 'font/ttf',
29
+ '.eot': 'application/vnd.ms-fontobject',
30
+ '.otf': 'font/otf',
31
+ '.wasm': 'application/wasm',
32
+ '.ico': 'image/x-icon',
33
+ '.mp3': 'audio/mpeg',
34
+ '.mp4': 'video/mp4',
35
+ '.pdf': 'application/pdf',
36
+ '.zip': 'application/zip',
37
+ '.gz': 'application/gzip',
38
+ '.tar': 'application/x-tar',
39
+ '.7z': 'application/x-7z-compressed',
40
+ '.rar': 'application/x-rar-compressed',
41
+ '.avi': 'video/x-msvideo',
42
+ '.mov': 'video/quicktime',
43
+ '.wmv': 'video/x-ms-wmv',
44
+ '.flv': 'video/x-flv',
45
+ '.webm': 'video/webm',
46
+ '.ogg': 'audio/ogg',
47
+ '.ogv': 'video/ogg',
48
+ '.m4v': 'video/mp4',
49
+ '.3gp': 'video/3gpp',
50
+ '.3g2': 'video/3gpp2',
51
+ '.mkv': 'video/x-matroska',
52
+ '.ts': 'video/mp2t',
53
+ }
54
+
55
+ // File cache for production server
56
+ const fileCache = new Map()
57
+ const MAX_CACHE_SIZE = 50 * 1024 * 1024 // 50MB
58
+ let currentCacheSize = 0
59
+
60
+ /**
61
+ * Get MIME type from file extension
62
+ */
63
+ const getMimeType = (filePath) => {
64
+ const ext = path.extname(filePath).toLowerCase()
65
+ return MIME_TYPES[ext] || 'application/octet-stream'
66
+ }
67
+
68
+ /**
69
+ * Validate path to prevent directory traversal attacks
70
+ */
71
+ const validatePath = (requestPath, rootDir) => {
72
+ try {
73
+ const normalizedPath = path.normalize(requestPath)
74
+ const resolvedPath = path.resolve(rootDir, normalizedPath.slice(1))
75
+
76
+ if (!resolvedPath.startsWith(rootDir)) {
77
+ return null
78
+ }
79
+
80
+ return resolvedPath
81
+ } catch {
82
+ return null
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Generate ETag from file content
88
+ */
89
+ const generateETag = (content) => {
90
+ return createHash('md5').update(content).digest('hex')
91
+ }
92
+
93
+ /**
94
+ * Check compression support (Brotli preferred over Gzip)
95
+ */
96
+ const getAcceptedEncoding = (headers) => {
97
+ const encoding = headers['accept-encoding'] || ''
98
+ if (encoding.includes('br')) return 'br'
99
+ if (encoding.includes('gzip')) return 'gzip'
100
+ return null
101
+ }
102
+
103
+ /**
104
+ * Check if MIME type is compressible
105
+ */
106
+ const isCompressible = (mimeType) => {
107
+ return (
108
+ mimeType.startsWith('text/') ||
109
+ mimeType.includes('javascript') ||
110
+ mimeType.includes('json') ||
111
+ mimeType.includes('css')
112
+ )
113
+ }
114
+
115
+ /**
116
+ * Parse Range header
117
+ */
118
+ const parseRange = (rangeHeader, fileSize) => {
119
+ if (!rangeHeader) return null
120
+
121
+ const parts = rangeHeader.replace(/bytes=/, '').split('-')
122
+ const start = parseInt(parts[0], 10)
123
+ const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1
124
+
125
+ if (isNaN(start) || isNaN(end) || start > end || end >= fileSize) {
126
+ return null
127
+ }
128
+
129
+ return { start, end, length: end - start + 1 }
130
+ }
131
+
132
+ /**
133
+ * Check if file should support range requests (media files)
134
+ */
135
+ const supportsRangeRequests = (mimeType) => {
136
+ return (
137
+ mimeType.startsWith('video/') ||
138
+ mimeType.startsWith('audio/') ||
139
+ mimeType === 'application/pdf'
140
+ )
141
+ }
142
+
143
+ /**
144
+ * Serve file with range support (for video/audio)
145
+ */
146
+ const serveWithRange = async (filePath, req, res, stats) => {
147
+ const mimeType = getMimeType(filePath)
148
+ const range = parseRange(req.headers.range, stats.size)
149
+
150
+ const headers = {
151
+ 'Content-Type': mimeType,
152
+ 'Accept-Ranges': 'bytes',
153
+ 'Cache-Control': 'public, max-age=31536000',
154
+ }
155
+
156
+ if (range) {
157
+ // Partial content
158
+ headers['Content-Range'] = `bytes ${range.start}-${range.end}/${stats.size}`
159
+ headers['Content-Length'] = range.length
160
+
161
+ res.writeHead(206, headers)
162
+
163
+ const stream = createReadStream(filePath, {
164
+ start: range.start,
165
+ end: range.end,
166
+ })
167
+ await pipeline(stream, res)
168
+ } else {
169
+ // Full content
170
+ headers['Content-Length'] = stats.size
171
+ res.writeHead(200, headers)
172
+
173
+ const stream = createReadStream(filePath)
174
+ await pipeline(stream, res)
175
+ }
176
+
177
+ return true
178
+ }
179
+
180
+ /**
181
+ * Serve static file with caching and compression
182
+ */
183
+ const serveStaticFile = async (filePath, req, res) => {
184
+ try {
185
+ let stats = await fs.stat(filePath)
186
+
187
+ // 👉 If is a directory
188
+ if (stats.isDirectory()) {
189
+ const indexPath = path.join(filePath, 'index.html')
190
+ await fs.access(indexPath)
191
+ stats = await fs.stat(indexPath)
192
+ filePath = indexPath
193
+ }
194
+
195
+ const mimeType = getMimeType(filePath)
196
+
197
+ // Use range requests for media files or large files
198
+ if (supportsRangeRequests(mimeType) || stats.size > 5 * 1024 * 1024) {
199
+ return await serveWithRange(filePath, req, res, stats)
200
+ }
201
+
202
+ let cached = fileCache.get(filePath)
203
+
204
+ if (!cached) {
205
+ // Read and cache file
206
+ const content = await fs.readFile(filePath)
207
+ const etag = generateETag(content)
208
+
209
+ // Compress if text-based content
210
+ let brotli = null
211
+ let gzipped = null
212
+
213
+ if (isCompressible(mimeType)) {
214
+ try {
215
+ ;[brotli, gzipped] = await Promise.all([
216
+ brotliCompress(content, {
217
+ params: {
218
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 6,
219
+ },
220
+ }),
221
+ gzip(content),
222
+ ])
223
+ } catch {
224
+ // Compression failed, serve uncompressed
225
+ }
226
+ }
227
+
228
+ cached = {
229
+ content,
230
+ brotli,
231
+ gzipped,
232
+ etag,
233
+ mimeType,
234
+ size: stats.size,
235
+ }
236
+
237
+ // Update cache
238
+ if (currentCacheSize + stats.size < MAX_CACHE_SIZE) {
239
+ fileCache.set(filePath, cached)
240
+ currentCacheSize += stats.size
241
+ }
242
+ }
243
+
244
+ // Check ETag for 304 Not Modified
245
+ if (req.headers['if-none-match'] === cached.etag) {
246
+ res.writeHead(304)
247
+ res.end()
248
+ return true
249
+ }
250
+
251
+ // Select best encoding
252
+ const encoding = getAcceptedEncoding(req.headers)
253
+ let responseContent = cached.content
254
+ let contentEncoding = null
255
+
256
+ if (encoding === 'br' && cached.brotli) {
257
+ responseContent = cached.brotli
258
+ contentEncoding = 'br'
259
+ } else if (encoding === 'gzip' && cached.gzipped) {
260
+ responseContent = cached.gzipped
261
+ contentEncoding = 'gzip'
262
+ }
263
+
264
+ const headers = {
265
+ 'Content-Type': cached.mimeType,
266
+ 'Content-Length': responseContent.length,
267
+ ETag: cached.etag,
268
+ 'Cache-Control': 'public, max-age=31536000',
269
+ }
270
+
271
+ if (contentEncoding) {
272
+ headers['Content-Encoding'] = contentEncoding
273
+ }
274
+
275
+ res.writeHead(200, headers)
276
+ res.end(responseContent)
277
+ return true
278
+ } catch (error) {
279
+ return false
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Serve HTML page with SPA fallback support
285
+ */
286
+ const serveHTMLPage = async (pathname, staticDir, req, res) => {
287
+ try {
288
+ const candidates = []
289
+
290
+ // / → /index.html
291
+ if (pathname === '/') {
292
+ candidates.push(path.join(staticDir, 'index.html'))
293
+ } else {
294
+ // /test → /test/index.html
295
+ candidates.push(path.join(staticDir, pathname, 'index.html'))
296
+
297
+ // /test → /test.html
298
+ candidates.push(path.join(staticDir, `${pathname}.html`))
299
+
300
+ // SPA fallback
301
+ candidates.push(path.join(staticDir, 'index.html'))
302
+ }
303
+
304
+ let pageFile = null
305
+
306
+ for (const file of candidates) {
307
+ try {
308
+ await fs.access(file)
309
+ pageFile = file
310
+ break
311
+ } catch {}
312
+ }
313
+
314
+ if (!pageFile) {
315
+ res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' })
316
+ res.end('404')
317
+ return
318
+ }
319
+
320
+ const content = await fs.readFile(pageFile, 'utf-8')
321
+ const etag = generateETag(Buffer.from(content))
322
+
323
+ if (req.headers['if-none-match'] === etag) {
324
+ res.writeHead(304)
325
+ res.end()
326
+ return
327
+ }
328
+
329
+ // Compress HTML
330
+ let responseContent = content
331
+ const headers = {
332
+ 'Content-Type': 'text/html; charset=utf-8',
333
+ ETag: etag,
334
+ 'Cache-Control': 'no-cache',
335
+ }
336
+
337
+ const encoding = getAcceptedEncoding(req.headers)
338
+
339
+ if (encoding === 'br') {
340
+ try {
341
+ responseContent = await brotliCompress(Buffer.from(content))
342
+ headers['Content-Encoding'] = 'br'
343
+ } catch {
344
+ // Fallback to uncompressed
345
+ }
346
+ } else if (encoding === 'gzip') {
347
+ try {
348
+ responseContent = await gzip(Buffer.from(content))
349
+ headers['Content-Encoding'] = 'gzip'
350
+ } catch {
351
+ // Fallback to uncompressed
352
+ }
353
+ }
354
+
355
+ headers['Content-Length'] = Buffer.byteLength(responseContent)
356
+
357
+ res.writeHead(200, headers)
358
+ res.end(responseContent)
359
+ } catch (error) {
360
+ res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' })
361
+ res.end('500')
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Request handler
367
+ */
368
+ const requestHandler = async (req, res) => {
369
+ const rootDir = process.cwd()
370
+ const staticDir = path.join(
371
+ rootDir,
372
+ config.webpack.output.buildDirectory,
373
+ 'static',
374
+ )
375
+
376
+ try {
377
+ const parsedUrl = new URL(req.url, `http://${req.headers.host}`)
378
+ const pathname = decodeURIComponent(parsedUrl.pathname)
379
+
380
+ const safePath = validatePath(pathname, staticDir)
381
+ if (!safePath) {
382
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' })
383
+ res.end('403')
384
+ return
385
+ }
386
+
387
+ const fileServed = await serveStaticFile(safePath, req, res)
388
+
389
+ if (!fileServed) {
390
+ await serveHTMLPage(pathname, staticDir, req, res)
391
+ }
392
+ } catch (error) {
393
+ console.error('[Ryunix Server Error]:', error.message)
394
+ res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' })
395
+ res.end('500')
396
+ }
397
+ }
398
+
399
+ const httpServ = http.createServer(requestHandler)
400
+
401
+ export default httpServ
@@ -67,7 +67,6 @@ const DEFAULT_SSG_SITEMAP_SETTINGS = {
67
67
  priority: '0.7',
68
68
  }
69
69
 
70
-
71
70
  // ============================================================================
72
71
  // Configuration Builder
73
72
  // ============================================================================
@@ -171,5 +170,4 @@ const defaultSettings = {
171
170
  },
172
171
  }
173
172
 
174
-
175
173
  module.exports = defaultSettings
@@ -9,7 +9,7 @@ import path from 'path'
9
9
  /**
10
10
  * Extract valid routes for SSG from routes configuration
11
11
  * Filters out dynamic routes and special routes
12
- *
12
+ *
13
13
  * @param {Array} routes - Array of route objects
14
14
  * @returns {Array} - Array of valid SSG routes
15
15
  */
@@ -46,7 +46,7 @@ const extractSSGRoutes = (routes) => {
46
46
 
47
47
  /**
48
48
  * Generate robots.txt content
49
- *
49
+ *
50
50
  * @param {string} baseURL - Base URL of the site
51
51
  * @param {Object} options - Configuration options
52
52
  * @param {Array<string>} options.disallow - Paths to disallow
@@ -73,7 +73,7 @@ const generateRobotsTxt = (baseURL, options = {}) => {
73
73
 
74
74
  /**
75
75
  * Generate XML sitemap with all routes
76
- *
76
+ *
77
77
  * @param {Array} routes - Array of route objects
78
78
  * @param {string} baseURL - Base URL of the site
79
79
  * @param {Object} defaultSettings - Default sitemap settings
@@ -115,7 +115,7 @@ ${urls}
115
115
 
116
116
  /**
117
117
  * Generate HTML meta tags from metadata object
118
- *
118
+ *
119
119
  * @param {Object} meta - Metadata object
120
120
  * @param {Object} defaultMeta - Default metadata
121
121
  * @returns {string} - HTML meta tags
@@ -181,7 +181,7 @@ const generateMetaTags = (meta, defaultMeta = {}) => {
181
181
 
182
182
  /**
183
183
  * Prerender a route to static HTML
184
- *
184
+ *
185
185
  * @param {Object} route - Route object
186
186
  * @param {string} template - HTML template
187
187
  * @param {Object} config - Configuration object
@@ -194,24 +194,15 @@ const prerenderRoute = async (route, template, config) => {
194
194
 
195
195
  // Replace title - use route meta or default
196
196
  const pageTitle = meta.title || defaultMeta.title || 'Ryunix App'
197
- html = html.replace(
198
- /<title>.*?<\/title>/,
199
- `<title>${pageTitle}</title>`,
200
- )
197
+ html = html.replace(/<title>.*?<\/title>/, `<title>${pageTitle}</title>`)
201
198
 
202
199
  // Generate and add meta tags
203
200
  const metaTags = generateMetaTags(meta, defaultMeta)
204
201
 
205
202
  // Remove existing meta tags (except framework/mode) and duplicate favicon
206
203
  // Remove all meta tags except framework and mode
207
- html = html.replace(
208
- /<meta\s+name="(?!framework|mode)[^"]*"[^>]*>/gi,
209
- ''
210
- )
211
- html = html.replace(
212
- /<meta\s+property="[^"]*"[^>]*>/gi,
213
- ''
214
- )
204
+ html = html.replace(/<meta\s+name="(?!framework|mode)[^"]*"[^>]*>/gi, '')
205
+ html = html.replace(/<meta\s+property="[^"]*"[^>]*>/gi, '')
215
206
 
216
207
  // Remove duplicate favicon links (keep only first one)
217
208
  const faviconMatches = html.match(/<link\s+rel="icon"[^>]*>/gi)
@@ -285,7 +276,7 @@ const prerenderRoute = async (route, template, config) => {
285
276
  /**
286
277
  * Full SSG build process
287
278
  * Generates prerendered HTML, sitemap, and robots.txt
288
- *
279
+ *
289
280
  * @param {Array} routesConfig - Routes configuration
290
281
  * @param {Object} config - Site configuration
291
282
  * @param {string} buildDir - Build output directory
@@ -295,7 +286,6 @@ const buildSSG = async (routesConfig, config, buildDir) => {
295
286
  const routes = extractSSGRoutes(routesConfig)
296
287
 
297
288
  if (routes.length === 0) {
298
-
299
289
  return
300
290
  }
301
291
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Ryunix Routes Plugin - SSG Manifest Generator
3
3
  * Extracts routes from routes.ryx file and generates manifest for static site generation
4
- *
4
+ *
5
5
  * Features:
6
6
  * - Template literal support (`/docs/${var.field}`)
7
7
  * - Frontmatter extraction from MDX files
@@ -24,39 +24,31 @@ class RyunixRoutesPlugin {
24
24
  compiler.hooks.emit.tapAsync(
25
25
  'RyunixRoutesPlugin',
26
26
  (compilation, callback) => {
27
-
28
-
29
27
  // Skip in development mode
30
28
  if (compiler.options.mode !== 'production') {
31
29
  callback()
32
30
  return
33
31
  }
34
32
 
35
-
36
-
37
33
  const routesFile = path.resolve(process.cwd(), this.routesPath)
38
34
 
39
35
  if (!fs.existsSync(routesFile)) {
40
- console.log(
41
- '[SSG] ❌ The route file was not found:', this.routesPath,
42
- )
36
+ console.log('[SSG] ❌ The route file was not found:', this.routesPath)
43
37
  callback()
44
38
  return
45
39
  }
46
40
 
47
-
48
41
  try {
49
42
  const content = fs.readFileSync(routesFile, 'utf-8')
50
43
 
51
44
  this.extractFrontmatter(content, path.dirname(routesFile))
52
45
 
53
-
54
-
55
46
  const routes = this.parseRoutes(content)
56
47
 
57
-
58
48
  // Count routes with meta
59
- const routesWithMeta = routes.filter(r => r.meta && Object.keys(r.meta).length > 0)
49
+ const routesWithMeta = routes.filter(
50
+ (r) => r.meta && Object.keys(r.meta).length > 0,
51
+ )
60
52
 
61
53
  const manifest = JSON.stringify(routes, null, 2)
62
54
  const outputPath = path.resolve(process.cwd(), this.outputPath)
@@ -64,10 +56,7 @@ class RyunixRoutesPlugin {
64
56
  fs.mkdirSync(path.dirname(outputPath), { recursive: true })
65
57
  fs.writeFileSync(outputPath, manifest)
66
58
 
67
-
68
-
69
59
  console.log('✅ [SSG Plugin] Process successfully completed')
70
-
71
60
  } catch (error) {
72
61
  console.error('\n' + '='.repeat(70))
73
62
  console.error('[SSG] ❌ ERROR generating route manifest:')
@@ -98,7 +87,6 @@ class RyunixRoutesPlugin {
98
87
  // Resolve absolute path
99
88
  const absolutePath = path.resolve(baseDir, mdxPath)
100
89
 
101
-
102
90
  if (!fs.existsSync(absolutePath)) {
103
91
  if (this.debug) {
104
92
  console.warn(`⚠️ File not found: ${absolutePath}`)
@@ -112,7 +100,6 @@ class RyunixRoutesPlugin {
112
100
 
113
101
  // Remove BOM if present
114
102
  if (mdxContent.charCodeAt(0) === 0xfeff) {
115
-
116
103
  mdxContent = mdxContent.slice(1)
117
104
  }
118
105
 
@@ -120,17 +107,11 @@ class RyunixRoutesPlugin {
120
107
 
121
108
  if (Object.keys(frontmatter).length > 0) {
122
109
  this.frontmatterCache.set(alias, frontmatter)
123
-
124
- }
110
+ }
125
111
  } catch (error) {
126
- console.error(
127
- `❌ Error reading ${absolutePath}:`,
128
- error.message,
129
- )
112
+ console.error(`❌ Error reading ${absolutePath}:`, error.message)
130
113
  }
131
114
  }
132
-
133
-
134
115
  }
135
116
 
136
117
  /**
@@ -230,7 +211,8 @@ class RyunixRoutesPlugin {
230
211
  const routes = []
231
212
 
232
213
  // Match route objects - capture the entire object
233
- const routeRegex = /\{\s*path:\s*[`"'][^`"']*[`"'](?:\s*\|\|\s*[`"'][^`"']*[`"'])?\s*,[\s\S]*?\}/g
214
+ const routeRegex =
215
+ /\{\s*path:\s*[`"'][^`"']*[`"'](?:\s*\|\|\s*[`"'][^`"']*[`"'])?\s*,[\s\S]*?\}/g
234
216
  let match
235
217
 
236
218
  while ((match = routeRegex.exec(content)) !== null) {
@@ -258,14 +240,11 @@ class RyunixRoutesPlugin {
258
240
 
259
241
  // Skip dynamic routes and wildcards
260
242
  if (evaluatedPath.includes(':') || evaluatedPath === '*') {
261
-
262
243
  continue
263
244
  }
264
245
 
265
246
  // Check for noRenderLink early
266
- const noRenderLinkMatch = routeBlock.match(
267
- /noRenderLink:\s*([^,}\s]+)/,
268
- )
247
+ const noRenderLinkMatch = routeBlock.match(/noRenderLink:\s*([^,}\s]+)/)
269
248
  if (noRenderLinkMatch) {
270
249
  const noRenderValue = this.evaluateExpression(
271
250
  noRenderLinkMatch[1].trim(),
@@ -280,7 +259,6 @@ class RyunixRoutesPlugin {
280
259
 
281
260
  // Check for NotFound property
282
261
  if (routeBlock.includes('NotFound:')) {
283
-
284
262
  continue
285
263
  }
286
264
 
@@ -293,8 +271,6 @@ class RyunixRoutesPlugin {
293
271
  const metaContent = this.extractMetaObject(routeBlock)
294
272
 
295
273
  if (metaContent) {
296
-
297
-
298
274
  route.meta = {}
299
275
 
300
276
  // Temporalmente activar debug para esta ruta
@@ -307,16 +283,11 @@ class RyunixRoutesPlugin {
307
283
  this.extractField(metaContent, 'image', route.meta)
308
284
  this.extractField(metaContent, 'author', route.meta)
309
285
  this.extractField(metaContent, 'canonical', route.meta)
310
-
311
-
312
286
  } else {
313
287
  console.log(` ❌ No object meta found in route: ${evaluatedPath}`)
314
-
315
288
  }
316
289
 
317
290
  routes.push(route)
318
-
319
-
320
291
  }
321
292
 
322
293
  return routes
@@ -526,7 +497,7 @@ class RyunixRoutesPlugin {
526
497
  // Only add field if value is meaningful
527
498
  if (value !== undefined && value !== null && value !== '') {
528
499
  target[fieldName] = value
529
- }
500
+ }
530
501
  }
531
502
 
532
503
  /**
@@ -70,7 +70,7 @@ export default {
70
70
  },
71
71
  context: resolveApp(dir, config.webpack.root),
72
72
  entry: './main.ryx',
73
- devtool: config.webpack.production ? 'source-map' : false,
73
+ devtool: config.webpack.production ? false : 'source-map',
74
74
  output: {
75
75
  path: resolveApp(dir, `${config.webpack.output.buildDirectory}/static`),
76
76
  publicPath: '/',