@unsetsoft/ryunix-presets 1.0.23-canary.8 → 1.0.24
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 +24 -17
- package/webpack/bin/{serve.mjs → dev.server.mjs} +7 -5
- package/webpack/bin/index.mjs +57 -6
- package/webpack/bin/prerender.mjs +8 -1
- package/webpack/bin/prod.server.mjs +393 -0
- package/webpack/eslint.config.mjs +17 -5
- package/webpack/utils/config.cjs +140 -156
- package/webpack/utils/ssg.mjs +249 -65
- package/webpack/utils/ssgPlugin.mjs +553 -40
- package/webpack/webpack.config.mjs +56 -14
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.
|
|
4
|
+
"version": "1.0.24",
|
|
5
5
|
"author": "Neyunse",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": "https://github.com/UnSetSoft/Ryunixjs",
|
|
@@ -35,42 +35,49 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@babel/cli": "7.28.3",
|
|
37
37
|
"@babel/core": "7.28.5",
|
|
38
|
-
"@babel/plugin-proposal-class-properties": "7.18.6",
|
|
39
38
|
"@babel/plugin-transform-react-jsx": "7.27.1",
|
|
40
39
|
"@babel/preset-env": "7.28.5",
|
|
41
40
|
"@babel/preset-react": "7.28.5",
|
|
42
|
-
"@eslint/eslintrc": "3.3.
|
|
43
|
-
"@eslint/js": "9.
|
|
41
|
+
"@eslint/eslintrc": "3.3.3",
|
|
42
|
+
"@eslint/js": "9.39.2",
|
|
44
43
|
"@rollup/plugin-terser": "0.4.4",
|
|
45
|
-
"@swc/core": "1.
|
|
44
|
+
"@swc/core": "1.15.5",
|
|
46
45
|
"babel-loader": "10.0.0",
|
|
47
46
|
"chalk": "5.6.2",
|
|
48
47
|
"copy-webpack-plugin": "13.0.1",
|
|
49
48
|
"css-loader": "7.1.2",
|
|
50
|
-
"css-minimizer-webpack-plugin": "7.0.
|
|
51
|
-
"sass": "1.
|
|
49
|
+
"css-minimizer-webpack-plugin": "7.0.4",
|
|
50
|
+
"sass": "1.97.0",
|
|
52
51
|
"sass-loader": "16.0.6",
|
|
53
52
|
"dotenv-webpack": "8.1.1",
|
|
54
|
-
"eslint": "9.
|
|
53
|
+
"eslint": "9.39.2",
|
|
55
54
|
"eslint-plugin-react": "^7.37.5",
|
|
56
55
|
"eslint-webpack-plugin": "5.0.2",
|
|
57
56
|
"file-loader": "6.2.0",
|
|
58
|
-
"glob": "
|
|
59
|
-
"globals": "16.
|
|
57
|
+
"glob": "13.0.0",
|
|
58
|
+
"globals": "16.5.0",
|
|
60
59
|
"html-loader": "5.1.0",
|
|
61
|
-
"html-webpack-plugin": "5.6.
|
|
62
|
-
"image-webpack-loader": "^8.1.0",
|
|
60
|
+
"html-webpack-plugin": "5.6.5",
|
|
63
61
|
"lodash": "4.17.21",
|
|
64
62
|
"mini-css-extract-plugin": "2.9.4",
|
|
65
|
-
"rollup": "4.
|
|
63
|
+
"rollup": "4.53.5",
|
|
66
64
|
"style-loader": "4.0.0",
|
|
67
65
|
"terminal-log": "1.0.1",
|
|
68
|
-
"terser-webpack-plugin": "5.3.
|
|
69
|
-
"thread-loader": "4.0.4",
|
|
66
|
+
"terser-webpack-plugin": "5.3.16",
|
|
70
67
|
"url-loader": "4.1.1",
|
|
71
|
-
"webpack": "5.
|
|
68
|
+
"webpack": "5.104.0",
|
|
72
69
|
"webpack-cli": "6.0.1",
|
|
73
70
|
"webpack-dev-server": "5.2.2",
|
|
74
|
-
"
|
|
71
|
+
"thread-loader": "4.0.4",
|
|
72
|
+
"yargs": "18.0.0",
|
|
73
|
+
"@mdx-js/loader": "3.1.1",
|
|
74
|
+
"@mdx-js/rollup": "3.1.1",
|
|
75
|
+
"eslint-plugin-mdx": "3.6.2",
|
|
76
|
+
"remark-gfm": "4.0.1",
|
|
77
|
+
"rehype-highlight": "7.0.2",
|
|
78
|
+
"remark-frontmatter": "5.0.0",
|
|
79
|
+
"remark-mdx-frontmatter": "5.2.0",
|
|
80
|
+
"@remark-embedder/core": "3.0.3",
|
|
81
|
+
"@remark-embedder/transformer-oembed": "5.0.1"
|
|
75
82
|
}
|
|
76
83
|
}
|
|
@@ -49,11 +49,13 @@ const StartServer = async (cliSettings) => {
|
|
|
49
49
|
`${defaultSettings.webpack.output.buildDirectory}/cache`,
|
|
50
50
|
)
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
const mode = cliSettings.production || defaultSettings.webpack.production ? true : false
|
|
53
|
+
|
|
54
|
+
if (!mode) {
|
|
53
55
|
cleanCacheDir(cacheDir)
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
webpackConfig.mode = 'development'
|
|
58
|
+
webpackConfig.mode = mode ? 'production' : 'development'
|
|
57
59
|
const compiler = Webpack(webpackConfig)
|
|
58
60
|
let port = webpackConfig.devServer.port || 3000
|
|
59
61
|
|
|
@@ -65,7 +67,7 @@ const StartServer = async (cliSettings) => {
|
|
|
65
67
|
const devServerOptions = { ...webpackConfig.devServer, ...cliSettings }
|
|
66
68
|
const server = new WebpackDevServer(devServerOptions, compiler)
|
|
67
69
|
|
|
68
|
-
const devMode = Boolean(!
|
|
70
|
+
const devMode = Boolean(!mode)
|
|
69
71
|
|
|
70
72
|
const { version } = await getPackageVersion()
|
|
71
73
|
|
|
@@ -81,7 +83,7 @@ const StartServer = async (cliSettings) => {
|
|
|
81
83
|
const envStatus = envPath()
|
|
82
84
|
? chalk.green('loaded')
|
|
83
85
|
: chalk.yellow('not found')
|
|
84
|
-
const modeLabel =
|
|
86
|
+
const modeLabel = mode
|
|
85
87
|
? chalk.green('production')
|
|
86
88
|
: chalk.yellow('development')
|
|
87
89
|
|
|
@@ -109,4 +111,4 @@ const StartServer = async (cliSettings) => {
|
|
|
109
111
|
await startServer()
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
export { StartServer }
|
|
114
|
+
export { StartServer as StartDevServer }
|
package/webpack/bin/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
2
|
import yargs from 'yargs'
|
|
3
3
|
import { hideBin } from 'yargs/helpers'
|
|
4
|
-
import {
|
|
4
|
+
import { StartDevServer } from './dev.server.mjs'
|
|
5
5
|
import { compiler } from './compiler.mjs'
|
|
6
6
|
import logger from 'terminal-log'
|
|
7
7
|
import chalk from 'chalk'
|
|
@@ -15,6 +15,14 @@ import {
|
|
|
15
15
|
import { ESLint } from 'eslint'
|
|
16
16
|
import eslintConfig from '../eslint.config.mjs'
|
|
17
17
|
import fs from 'fs'
|
|
18
|
+
import { fileURLToPath } from 'url'
|
|
19
|
+
import { dirname, join } from 'path'
|
|
20
|
+
import server from './prod.server.mjs'
|
|
21
|
+
import config from '../utils/config.cjs';
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(__filename)
|
|
25
|
+
|
|
18
26
|
const lint = {
|
|
19
27
|
command: 'lint',
|
|
20
28
|
describe: 'Lint code',
|
|
@@ -47,19 +55,43 @@ const lint = {
|
|
|
47
55
|
},
|
|
48
56
|
}
|
|
49
57
|
|
|
50
|
-
const
|
|
51
|
-
command: '
|
|
52
|
-
describe: 'Run server',
|
|
58
|
+
const dev = {
|
|
59
|
+
command: 'dev',
|
|
60
|
+
describe: 'Run server for developer mode.',
|
|
53
61
|
handler: async (arg) => {
|
|
62
|
+
if (defaultSettings.webpack.production) {
|
|
63
|
+
logger.error("You need use development mode! change webpack.production to false in ryunix.config.js.")
|
|
64
|
+
return
|
|
65
|
+
}
|
|
54
66
|
const open = Boolean(arg.browser) || false
|
|
55
67
|
const settings = {
|
|
56
68
|
open,
|
|
57
69
|
}
|
|
58
70
|
|
|
59
|
-
|
|
71
|
+
StartDevServer(settings)
|
|
60
72
|
},
|
|
61
73
|
}
|
|
62
74
|
|
|
75
|
+
const prod = {
|
|
76
|
+
command: 'start',
|
|
77
|
+
describe: 'Run server for production mode. Requiere .ryunix/static',
|
|
78
|
+
handler: async (arg) => {
|
|
79
|
+
if (!defaultSettings.webpack.production) {
|
|
80
|
+
logger.error("You need use production mode!")
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!fs.existsSync(join(process.cwd(), config.webpack.output.buildDirectory, 'static'))) {
|
|
85
|
+
logger.error("You need build first!")
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
server.listen(config.webpack.devServer.port, () => {
|
|
90
|
+
console.log(`Server running at http://localhost:${config.webpack.devServer.port}/`);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
63
95
|
const build = {
|
|
64
96
|
command: 'build',
|
|
65
97
|
describe: 'Run builder',
|
|
@@ -98,6 +130,8 @@ const build = {
|
|
|
98
130
|
minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`
|
|
99
131
|
|
|
100
132
|
if (defaultSettings.webpack.production) {
|
|
133
|
+
|
|
134
|
+
|
|
101
135
|
await Prerender(defaultSettings.webpack.output.buildDirectory)
|
|
102
136
|
}
|
|
103
137
|
|
|
@@ -113,4 +147,21 @@ const build = {
|
|
|
113
147
|
},
|
|
114
148
|
}
|
|
115
149
|
|
|
116
|
-
|
|
150
|
+
const extractHTML = {
|
|
151
|
+
command: 'customHtml',
|
|
152
|
+
describe: 'Extract HTML for customization',
|
|
153
|
+
handler: async (arg) => {
|
|
154
|
+
const runPath = process.cwd()
|
|
155
|
+
|
|
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
|
+
});
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
yargs(hideBin(process.argv)).command(dev).command(build).command(prod).command(lint).command(extractHTML).parse()
|
|
@@ -22,12 +22,19 @@ const Prerender = async (directory) => {
|
|
|
22
22
|
if (fs.existsSync(manifestPath)) {
|
|
23
23
|
try {
|
|
24
24
|
routes = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
|
|
25
|
-
console.log(`[SSG] Found ${routes.length} routes in manifest`)
|
|
26
25
|
} catch (error) {
|
|
27
26
|
console.error('[SSG] Error reading routes manifest:', error)
|
|
28
27
|
}
|
|
29
28
|
}
|
|
30
29
|
|
|
30
|
+
const metaExist = routes.some((route) => route.meta)
|
|
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.")
|
|
34
|
+
process.exit(1)
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
if (routes.length === 0) {
|
|
32
39
|
routes = defaultSettings.experimental?.ssg?.prerender || []
|
|
33
40
|
if (routes.length > 0) {
|
|
@@ -0,0 +1,393 @@
|
|
|
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,15 +1,27 @@
|
|
|
1
1
|
import config from './utils/config.cjs'
|
|
2
|
-
import { resolveApp } from './utils/index.mjs'
|
|
3
2
|
import { defineConfig } from 'eslint/config'
|
|
4
|
-
const dir = process.cwd()
|
|
5
3
|
|
|
4
|
+
/**
|
|
5
|
+
* ESLint Configuration for Ryunix
|
|
6
|
+
*
|
|
7
|
+
* NOTE ABOUT MDX:
|
|
8
|
+
* .mdx and .md files are excluded from ESLint due to compatibility issues
|
|
9
|
+
* between eslint-plugin-mdx and ESM/flat config.
|
|
10
|
+
*
|
|
11
|
+
* Error: “Could not find ESLint Linter in require cache”
|
|
12
|
+
*
|
|
13
|
+
* MDX files are validated during compilation by @mdx-js/loader,
|
|
14
|
+
* which is sufficient for detecting syntax and JSX errors.
|
|
15
|
+
*/
|
|
6
16
|
const eslintConfig = defineConfig([
|
|
7
17
|
{
|
|
8
18
|
files: ['**/*.ryx', ...config?.eslint?.files],
|
|
19
|
+
|
|
20
|
+
ignores: ['**/*.mdx', '**/*.md', '**/node_modules/**'],
|
|
21
|
+
|
|
9
22
|
languageOptions: {
|
|
10
23
|
ecmaVersion: 2021,
|
|
11
24
|
sourceType: 'module',
|
|
12
|
-
|
|
13
25
|
parserOptions: {
|
|
14
26
|
ecmaFeatures: {
|
|
15
27
|
jsx: true,
|
|
@@ -19,8 +31,8 @@ const eslintConfig = defineConfig([
|
|
|
19
31
|
},
|
|
20
32
|
settings: {
|
|
21
33
|
react: {
|
|
22
|
-
pragma: 'Ryunix.createElement',
|
|
23
|
-
fragment: 'Ryunix.Fragment',
|
|
34
|
+
pragma: 'Ryunix.createElement',
|
|
35
|
+
fragment: 'Ryunix.Fragment',
|
|
24
36
|
},
|
|
25
37
|
},
|
|
26
38
|
plugins: config?.eslint?.plugins,
|