@unsetsoft/ryunix-presets 1.0.20 → 1.0.23-canary.3
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 +23 -7
- package/webpack/bin/index.mjs +4 -3
- package/webpack/bin/prerender.mjs +23 -114
- package/webpack/eslint.config.mjs +12 -5
- package/webpack/utils/index.mjs +7 -12
- package/webpack/utils/mdx.mjs +49 -0
- package/webpack/utils/ssg.mjs +197 -0
- package/webpack/utils/ssgPlugin.mjs +118 -0
- package/webpack/webpack.config.mjs +22 -8
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unsetsoft/ryunix-presets",
|
|
3
|
-
"description": "Package with presets for different development environments.",
|
|
4
|
-
"version": "1.0.
|
|
3
|
+
"description": "Package with presets for different development environments with MDX support.",
|
|
4
|
+
"version": "1.0.23-canary.3",
|
|
5
5
|
"author": "Neyunse",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": "https://github.com/UnSetSoft/Ryunixjs",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"keywords": [
|
|
10
|
-
"ryunixjs"
|
|
10
|
+
"ryunixjs",
|
|
11
|
+
"mdx",
|
|
12
|
+
"markdown"
|
|
11
13
|
],
|
|
12
14
|
"files": [
|
|
13
15
|
"vite/",
|
|
@@ -15,7 +17,7 @@
|
|
|
15
17
|
"webpack/"
|
|
16
18
|
],
|
|
17
19
|
"peerDependencies": {
|
|
18
|
-
"@unsetsoft/ryunixjs": "^1.2.
|
|
20
|
+
"@unsetsoft/ryunixjs": "^1.2.2"
|
|
19
21
|
},
|
|
20
22
|
"publishConfig": {
|
|
21
23
|
"registry": "https://registry.npmjs.org"
|
|
@@ -41,6 +43,8 @@
|
|
|
41
43
|
"@babel/preset-react": "7.28.5",
|
|
42
44
|
"@eslint/eslintrc": "3.3.1",
|
|
43
45
|
"@eslint/js": "9.38.0",
|
|
46
|
+
"@mdx-js/loader": "^3.1.1",
|
|
47
|
+
"@mdx-js/rollup": "^3.1.1",
|
|
44
48
|
"@rollup/plugin-terser": "0.4.4",
|
|
45
49
|
"@swc/core": "1.12.14",
|
|
46
50
|
"babel-loader": "10.0.0",
|
|
@@ -48,10 +52,9 @@
|
|
|
48
52
|
"copy-webpack-plugin": "13.0.1",
|
|
49
53
|
"css-loader": "7.1.2",
|
|
50
54
|
"css-minimizer-webpack-plugin": "7.0.2",
|
|
51
|
-
"sass": "1.93.2",
|
|
52
|
-
"sass-loader": "16.0.6",
|
|
53
55
|
"dotenv-webpack": "8.1.1",
|
|
54
56
|
"eslint": "9.38.0",
|
|
57
|
+
"eslint-plugin-mdx": "^3.1.5",
|
|
55
58
|
"eslint-plugin-react": "^7.37.5",
|
|
56
59
|
"eslint-webpack-plugin": "5.0.2",
|
|
57
60
|
"file-loader": "6.2.0",
|
|
@@ -61,8 +64,17 @@
|
|
|
61
64
|
"html-webpack-plugin": "5.6.4",
|
|
62
65
|
"image-webpack-loader": "^8.1.0",
|
|
63
66
|
"lodash": "4.17.21",
|
|
67
|
+
"markdown-loader": "^8.0.0",
|
|
64
68
|
"mini-css-extract-plugin": "2.9.4",
|
|
69
|
+
"rehype-highlight": "^7.0.1",
|
|
70
|
+
"rehype-katex": "^7.0.1",
|
|
71
|
+
"rehype-slug": "^6.0.0",
|
|
72
|
+
"remark-frontmatter": "^5.0.0",
|
|
73
|
+
"remark-gfm": "^4.0.0",
|
|
74
|
+
"remark-math": "^6.0.0",
|
|
65
75
|
"rollup": "4.52.5",
|
|
76
|
+
"sass": "1.93.2",
|
|
77
|
+
"sass-loader": "16.0.6",
|
|
66
78
|
"style-loader": "4.0.0",
|
|
67
79
|
"terminal-log": "1.0.1",
|
|
68
80
|
"terser-webpack-plugin": "5.3.14",
|
|
@@ -72,5 +84,9 @@
|
|
|
72
84
|
"webpack-cli": "6.0.1",
|
|
73
85
|
"webpack-dev-server": "5.2.2",
|
|
74
86
|
"yargs": "18.0.0"
|
|
87
|
+
},
|
|
88
|
+
"optionalDependencies": {
|
|
89
|
+
"rehype-highlight": "^7.0.1",
|
|
90
|
+
"remark-gfm": "^4.0.0"
|
|
75
91
|
}
|
|
76
|
-
}
|
|
92
|
+
}
|
package/webpack/bin/index.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from '../utils/index.mjs'
|
|
15
15
|
import { ESLint } from 'eslint'
|
|
16
16
|
import eslintConfig from '../eslint.config.mjs'
|
|
17
|
+
import fs from 'fs'
|
|
17
18
|
const lint = {
|
|
18
19
|
command: 'lint',
|
|
19
20
|
describe: 'Lint code',
|
|
@@ -72,7 +73,7 @@ const build = {
|
|
|
72
73
|
return
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
if (
|
|
76
|
+
if (fs.existsSync(resolveApp(process.cwd(), 'src/pages/routes.ryx'))) {
|
|
76
77
|
await cleanBuildDirectory(
|
|
77
78
|
resolveApp(
|
|
78
79
|
process.cwd(),
|
|
@@ -96,8 +97,8 @@ const build = {
|
|
|
96
97
|
const formattedTime =
|
|
97
98
|
minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`
|
|
98
99
|
|
|
99
|
-
if (defaultSettings.
|
|
100
|
-
await Prerender()
|
|
100
|
+
if (defaultSettings.webpack.production) {
|
|
101
|
+
await Prerender(defaultSettings.webpack.output.buildDirectory)
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
logger.info(chalk.green('Compilation successful! 🎉'))
|
|
@@ -1,138 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Automatic Prerender - reads routes from manifest
|
|
4
3
|
*/
|
|
4
|
+
import { buildSSG } from '../utils/ssg.mjs'
|
|
5
5
|
import { configFileExist } from '../utils/settingfile.cjs'
|
|
6
6
|
import defaultSettings from '../utils/config.cjs'
|
|
7
7
|
import { resolveApp } from '../utils/index.mjs'
|
|
8
8
|
import fs from 'fs'
|
|
9
9
|
import path from 'path'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
const buildDirectory = resolveApp(process.cwd(),
|
|
13
|
-
const indexFile = path.join(buildDirectory, 'index.html')
|
|
11
|
+
const Prerender = async (directory) => {
|
|
12
|
+
const buildDirectory = resolveApp(process.cwd(), directory)
|
|
14
13
|
|
|
15
|
-
const siteMap = async (routes) => {
|
|
16
|
-
if (!defaultSettings.experimental.ssg.sitemap.baseURL) {
|
|
17
|
-
console.error(
|
|
18
|
-
'❌ Base URL is not defined in the configuration file. Please set `experimental.ssg.sitemap.baseURL`.',
|
|
19
|
-
)
|
|
20
|
-
process.exit(1)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
|
24
|
-
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
25
|
-
${routes
|
|
26
|
-
.map((route) => {
|
|
27
|
-
const url = `${defaultSettings.experimental.ssg.sitemap.baseURL}${route.path === '/' ? '' : route.path}`
|
|
28
|
-
const meta = route.meta || {}
|
|
29
|
-
const sitemap_settings = route.sitemap || {}
|
|
30
|
-
const lastmod = meta.lastmod || new Date().toISOString().split('T')[0]
|
|
31
|
-
|
|
32
|
-
// sitemap settings
|
|
33
|
-
const changefreq =
|
|
34
|
-
sitemap_settings.changefreq ||
|
|
35
|
-
meta.changefreq || // TODO: Remove meta.changefreq
|
|
36
|
-
defaultSettings.experimental.ssg.sitemap.settings.changefreq
|
|
37
|
-
const priority =
|
|
38
|
-
sitemap_settings.priority ||
|
|
39
|
-
meta.priority || // TODO: Remove meta.priority
|
|
40
|
-
defaultSettings.experimental.ssg.sitemap.settings.priority
|
|
41
|
-
|
|
42
|
-
return `<url>
|
|
43
|
-
<loc>${url}</loc>
|
|
44
|
-
<lastmod>${lastmod}</lastmod>
|
|
45
|
-
<changefreq>${changefreq}</changefreq>
|
|
46
|
-
<priority>${priority}</priority>
|
|
47
|
-
</url>`
|
|
48
|
-
})
|
|
49
|
-
.join('\n')}
|
|
50
|
-
</urlset>`
|
|
51
|
-
|
|
52
|
-
await fs.writeFileSync(
|
|
53
|
-
path.resolve(buildDirectory, 'sitemap.xml'),
|
|
54
|
-
sitemap,
|
|
55
|
-
'utf-8',
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
console.log('✅ Sitemap created')
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const Prerender = async () => {
|
|
62
14
|
if (!configFileExist()) {
|
|
63
15
|
console.error('❌ No configuration file found.')
|
|
64
16
|
process.exit(1)
|
|
65
17
|
}
|
|
66
18
|
|
|
67
|
-
const
|
|
19
|
+
const manifestPath = path.join(process.cwd(), directory, 'ssg', 'routes.json')
|
|
20
|
+
let routes = []
|
|
68
21
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (meta.title) {
|
|
76
|
-
html = html.replace(/<title>.*<\/title>/, `<title>${meta.title}<\/title>`)
|
|
22
|
+
if (fs.existsSync(manifestPath)) {
|
|
23
|
+
try {
|
|
24
|
+
routes = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
|
|
25
|
+
console.log(`[SSG] Found ${routes.length} routes in manifest`)
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error('[SSG] Error reading routes manifest:', error)
|
|
77
28
|
}
|
|
29
|
+
}
|
|
78
30
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const isProperty = name.startsWith('og:') || name.startsWith('twitter:')
|
|
84
|
-
const attr = isProperty ? 'property' : 'name'
|
|
85
|
-
const regex = new RegExp(`<meta ${attr}=["']${name}["'][^>]*>`, 'i')
|
|
86
|
-
const tag = `<meta ${attr}="${name}" content="${value}">`
|
|
87
|
-
|
|
88
|
-
if (regex.test(html)) {
|
|
89
|
-
return html.replace(regex, tag)
|
|
90
|
-
} else {
|
|
91
|
-
return html.replace(/<\/head>/i, `${tag}\n<\/head>`)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Description
|
|
96
|
-
html = upsertMetaTag(html, 'description', meta.description)
|
|
97
|
-
// Keywords
|
|
98
|
-
html = upsertMetaTag(html, 'keywords', meta.keywords)
|
|
99
|
-
|
|
100
|
-
// dynamic metatags (excluding title, description, keywords, framework, mode)
|
|
101
|
-
for (const [key, value] of Object.entries(meta)) {
|
|
102
|
-
if (
|
|
103
|
-
[
|
|
104
|
-
'title',
|
|
105
|
-
'description',
|
|
106
|
-
'keywords',
|
|
107
|
-
'framework',
|
|
108
|
-
'mode',
|
|
109
|
-
'viewport',
|
|
110
|
-
].includes(key)
|
|
111
|
-
)
|
|
112
|
-
continue
|
|
113
|
-
html = upsertMetaTag(html, key, value)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const outputDir =
|
|
117
|
-
route.path === '/'
|
|
118
|
-
? buildDirectory
|
|
119
|
-
: path.join(buildDirectory, route.path)
|
|
120
|
-
|
|
121
|
-
if (!fs.existsSync(outputDir)) {
|
|
122
|
-
await fs.mkdirSync(outputDir, { recursive: true, force: true })
|
|
31
|
+
if (routes.length === 0) {
|
|
32
|
+
routes = defaultSettings.experimental?.ssg?.prerender || []
|
|
33
|
+
if (routes.length > 0) {
|
|
34
|
+
console.log(`[SSG] Using ${routes.length} routes from config`)
|
|
123
35
|
}
|
|
124
|
-
|
|
125
|
-
await fs.writeFileSync(path.join(outputDir, 'index.html'), html)
|
|
126
|
-
console.log(`✅ Prerendered ${route.path}`)
|
|
127
36
|
}
|
|
128
37
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
defaultSettings.experimental.ssg.sitemap.baseURL
|
|
133
|
-
) {
|
|
134
|
-
await siteMap(defaultSettings.experimental.ssg.prerender)
|
|
38
|
+
if (routes.length === 0) {
|
|
39
|
+
console.log('[SSG] No routes to prerender, skipping SSG generation.')
|
|
40
|
+
return
|
|
135
41
|
}
|
|
42
|
+
|
|
43
|
+
await buildSSG(routes, defaultSettings, buildDirectory)
|
|
44
|
+
console.log('✅ SSG build complete')
|
|
136
45
|
}
|
|
137
46
|
|
|
138
47
|
export default Prerender
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import config from './utils/config.cjs'
|
|
2
2
|
import { resolveApp } from './utils/index.mjs'
|
|
3
3
|
import { defineConfig } from 'eslint/config'
|
|
4
|
+
|
|
4
5
|
const dir = process.cwd()
|
|
5
6
|
|
|
6
7
|
const eslintConfig = defineConfig([
|
|
7
8
|
{
|
|
8
|
-
files: ['**/*.ryx', ...config?.eslint?.files],
|
|
9
|
+
files: ['**/*.ryx', '**/*.mdx', ...config?.eslint?.files],
|
|
9
10
|
languageOptions: {
|
|
10
11
|
ecmaVersion: 2021,
|
|
11
12
|
sourceType: 'module',
|
|
@@ -14,17 +15,23 @@ const eslintConfig = defineConfig([
|
|
|
14
15
|
ecmaFeatures: {
|
|
15
16
|
jsx: true,
|
|
16
17
|
},
|
|
17
|
-
extraFileExtensions: ['.ryx'],
|
|
18
|
+
extraFileExtensions: ['.ryx', '.mdx'],
|
|
18
19
|
},
|
|
19
20
|
},
|
|
20
21
|
settings: {
|
|
21
22
|
react: {
|
|
22
|
-
pragma: 'Ryunix.createElement',
|
|
23
|
-
fragment: 'Ryunix.Fragment',
|
|
23
|
+
pragma: 'Ryunix.createElement',
|
|
24
|
+
fragment: 'Ryunix.Fragment',
|
|
24
25
|
},
|
|
26
|
+
// MDX-specific settings
|
|
27
|
+
'mdx/code-blocks': true,
|
|
25
28
|
},
|
|
26
29
|
plugins: config?.eslint?.plugins,
|
|
27
|
-
rules:
|
|
30
|
+
rules: {
|
|
31
|
+
...config?.eslint?.rules,
|
|
32
|
+
// Disable some rules for MDX files that might conflict
|
|
33
|
+
'react/jsx-no-undef': 'off', // MDX might use components not explicitly imported
|
|
34
|
+
},
|
|
28
35
|
},
|
|
29
36
|
])
|
|
30
37
|
|
package/webpack/utils/index.mjs
CHANGED
|
@@ -6,6 +6,8 @@ import { join, dirname } from 'path'
|
|
|
6
6
|
import { fileURLToPath } from 'url'
|
|
7
7
|
import logger from 'terminal-log'
|
|
8
8
|
import chalk from 'chalk'
|
|
9
|
+
import { createRequire } from 'node:module'
|
|
10
|
+
import { readFile } from 'node:fs/promises'
|
|
9
11
|
|
|
10
12
|
const resolveApp = (appDirectory, relativePath) =>
|
|
11
13
|
resolve(appDirectory, relativePath)
|
|
@@ -58,19 +60,12 @@ const getEnviroment = () =>
|
|
|
58
60
|
},
|
|
59
61
|
)
|
|
60
62
|
|
|
63
|
+
const require = createRequire(import.meta.url)
|
|
64
|
+
|
|
61
65
|
const getPackageVersion = async () => {
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
'..',
|
|
66
|
-
'..',
|
|
67
|
-
'..',
|
|
68
|
-
'ryunixjs',
|
|
69
|
-
'package.json',
|
|
70
|
-
)
|
|
71
|
-
const data = await fs.readFile(packageJsonPath, 'utf-8')
|
|
72
|
-
const packageJson = JSON.parse(data)
|
|
73
|
-
return packageJson
|
|
66
|
+
const packageJsonPath = require.resolve('@unsetsoft/ryunixjs/package.json')
|
|
67
|
+
const data = await readFile(packageJsonPath, 'utf-8')
|
|
68
|
+
return JSON.parse(data)
|
|
74
69
|
}
|
|
75
70
|
|
|
76
71
|
async function cleanCacheDir(dirPath) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const getMDXLoaderConfig = (options = {}) => {
|
|
2
|
+
const {
|
|
3
|
+
buildDirectory = '.ryunix',
|
|
4
|
+
remarkPlugins = [],
|
|
5
|
+
rehypePlugins = [],
|
|
6
|
+
} = options
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
test: /\.mdx$/,
|
|
10
|
+
exclude: /node_modules/,
|
|
11
|
+
use: [
|
|
12
|
+
{
|
|
13
|
+
loader: 'babel-loader',
|
|
14
|
+
options: {
|
|
15
|
+
presets: [
|
|
16
|
+
[
|
|
17
|
+
'@babel/preset-env',
|
|
18
|
+
{
|
|
19
|
+
targets: 'defaults and not IE 11',
|
|
20
|
+
useBuiltIns: false,
|
|
21
|
+
modules: false,
|
|
22
|
+
bugfixes: true,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
],
|
|
26
|
+
cacheDirectory: `${buildDirectory}/cache/babel`,
|
|
27
|
+
plugins: [
|
|
28
|
+
[
|
|
29
|
+
'@babel/plugin-transform-react-jsx',
|
|
30
|
+
{
|
|
31
|
+
pragma: 'Ryunix.createElement',
|
|
32
|
+
pragmaFrag: 'Ryunix.Fragment',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
loader: '@mdx-js/loader',
|
|
40
|
+
options: {
|
|
41
|
+
remarkPlugins: [...remarkPlugins],
|
|
42
|
+
rehypePlugins: [...rehypePlugins],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default getMDXLoaderConfig
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSG Utilities - Improved static site generation
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'fs'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extract routes from routes config for SSG
|
|
9
|
+
*/
|
|
10
|
+
const extractSSGRoutes = (routes) => {
|
|
11
|
+
const ssgRoutes = []
|
|
12
|
+
|
|
13
|
+
const processRoute = (route, parentPath = '') => {
|
|
14
|
+
if (!route.path || route.path.includes(':')) return // Skip dynamic routes
|
|
15
|
+
if (route.NotFound || route.noRenderLink) return // Skip special routes
|
|
16
|
+
|
|
17
|
+
const fullPath = parentPath + route.path
|
|
18
|
+
|
|
19
|
+
ssgRoutes.push({
|
|
20
|
+
path: fullPath === '' ? '/' : fullPath,
|
|
21
|
+
component: route.component,
|
|
22
|
+
meta: route.meta || {},
|
|
23
|
+
sitemap: route.sitemap || {},
|
|
24
|
+
label: route.label,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Process nested routes
|
|
28
|
+
if (route.subRoutes) {
|
|
29
|
+
route.subRoutes.forEach((subRoute) => {
|
|
30
|
+
processRoute(subRoute, fullPath)
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
routes.forEach((route) => processRoute(route))
|
|
36
|
+
return ssgRoutes
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generate robots.txt
|
|
41
|
+
*/
|
|
42
|
+
const generateRobotsTxt = (baseURL, options = {}) => {
|
|
43
|
+
const { disallow = [], allow = [], userAgents = ['*'] } = options
|
|
44
|
+
|
|
45
|
+
let content = ''
|
|
46
|
+
|
|
47
|
+
userAgents.forEach((agent) => {
|
|
48
|
+
content += `User-agent: ${agent}\n`
|
|
49
|
+
allow.forEach((path) => (content += `Allow: ${path}\n`))
|
|
50
|
+
disallow.forEach((path) => (content += `Disallow: ${path}\n`))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
content += `\nSitemap: ${baseURL}/sitemap.xml\n`
|
|
54
|
+
|
|
55
|
+
return content
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate enhanced sitemap with all routes
|
|
60
|
+
*/
|
|
61
|
+
const generateSitemap = (routes, baseURL, defaultSettings = {}) => {
|
|
62
|
+
const { changefreq = 'weekly', priority = '0.7' } = defaultSettings
|
|
63
|
+
|
|
64
|
+
const urls = routes
|
|
65
|
+
.map((route) => {
|
|
66
|
+
const url = `${baseURL}${route.path === '/' ? '' : route.path}`
|
|
67
|
+
const meta = route.meta || {}
|
|
68
|
+
const sitemap = route.sitemap || {}
|
|
69
|
+
|
|
70
|
+
const lastmod =
|
|
71
|
+
meta.lastmod ||
|
|
72
|
+
sitemap.lastmod ||
|
|
73
|
+
new Date().toISOString().split('T')[0]
|
|
74
|
+
const freq = sitemap.changefreq || meta.changefreq || changefreq
|
|
75
|
+
const prio = sitemap.priority || meta.priority || priority
|
|
76
|
+
|
|
77
|
+
return ` <url>
|
|
78
|
+
<loc>${url}</loc>
|
|
79
|
+
<lastmod>${lastmod}</lastmod>
|
|
80
|
+
<changefreq>${freq}</changefreq>
|
|
81
|
+
<priority>${prio}</priority>
|
|
82
|
+
</url>`
|
|
83
|
+
})
|
|
84
|
+
.join('\n')
|
|
85
|
+
|
|
86
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
87
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
88
|
+
${urls}
|
|
89
|
+
</urlset>`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Generate meta tags HTML
|
|
94
|
+
*/
|
|
95
|
+
const generateMetaTags = (meta, defaultMeta = {}) => {
|
|
96
|
+
const tags = { ...defaultMeta, ...meta }
|
|
97
|
+
let html = ''
|
|
98
|
+
|
|
99
|
+
Object.entries(tags).forEach(([key, value]) => {
|
|
100
|
+
if (['title', 'canonical'].includes(key)) return
|
|
101
|
+
|
|
102
|
+
const isProperty = key.startsWith('og:') || key.startsWith('twitter:')
|
|
103
|
+
const attr = isProperty ? 'property' : 'name'
|
|
104
|
+
html += ` <meta ${attr}="${key}" content="${value}">\n`
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return html
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Prerender route to HTML
|
|
112
|
+
*/
|
|
113
|
+
const prerenderRoute = async (route, template, config) => {
|
|
114
|
+
const meta = route.meta || config.static.seo.meta
|
|
115
|
+
let html = template
|
|
116
|
+
|
|
117
|
+
// Replace title
|
|
118
|
+
if (meta.title) {
|
|
119
|
+
html = html.replace(/<title>.*?<\/title>/, `<title>${meta.title}</title>`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Add/replace meta tags
|
|
123
|
+
const metaTags = generateMetaTags(meta, config.static.seo.meta)
|
|
124
|
+
|
|
125
|
+
// Remove existing meta tags (except framework/mode)
|
|
126
|
+
html = html.replace(/<meta name="(?!framework|mode).*?>\n/g, '')
|
|
127
|
+
html = html.replace(/<meta property=.*?>\n/g, '')
|
|
128
|
+
|
|
129
|
+
// Add new meta tags before </head>
|
|
130
|
+
html = html.replace(/<\/head>/, `${metaTags} </head>`)
|
|
131
|
+
|
|
132
|
+
// Add canonical if provided
|
|
133
|
+
if (meta.canonical) {
|
|
134
|
+
const canonical = ` <link rel="canonical" href="${meta.canonical}">\n`
|
|
135
|
+
html = html.replace(/<\/head>/, `${canonical} </head>`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return html
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Full SSG build process
|
|
143
|
+
*/
|
|
144
|
+
const buildSSG = async (routesConfig, config, buildDir) => {
|
|
145
|
+
const routes = extractSSGRoutes(routesConfig)
|
|
146
|
+
|
|
147
|
+
// Template is in buildDir/static/index.html
|
|
148
|
+
const templatePath = path.join(buildDir, 'static', 'index.html')
|
|
149
|
+
|
|
150
|
+
if (!fs.existsSync(templatePath)) {
|
|
151
|
+
console.error('❌ Template not found at:', templatePath)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const template = fs.readFileSync(templatePath, 'utf-8')
|
|
156
|
+
|
|
157
|
+
for (const route of routes) {
|
|
158
|
+
const html = await prerenderRoute(route, template, config)
|
|
159
|
+
|
|
160
|
+
const outputDir =
|
|
161
|
+
route.path === '/'
|
|
162
|
+
? path.join(buildDir, 'static')
|
|
163
|
+
: path.join(buildDir, 'static', route.path)
|
|
164
|
+
|
|
165
|
+
if (!fs.existsSync(outputDir)) {
|
|
166
|
+
fs.mkdirSync(outputDir, { recursive: true })
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fs.writeFileSync(path.join(outputDir, 'index.html'), html)
|
|
170
|
+
console.log(`✅ Prerendered ${route.path}`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Generate sitemap in static/
|
|
174
|
+
if (config.experimental?.ssg?.sitemap?.enable) {
|
|
175
|
+
const baseURL = config.experimental.ssg.sitemap.baseURL
|
|
176
|
+
const sitemap = generateSitemap(
|
|
177
|
+
routes,
|
|
178
|
+
baseURL,
|
|
179
|
+
config.experimental.ssg.sitemap.settings,
|
|
180
|
+
)
|
|
181
|
+
fs.writeFileSync(path.join(buildDir, 'static', 'sitemap.xml'), sitemap)
|
|
182
|
+
console.log('✅ Sitemap created')
|
|
183
|
+
|
|
184
|
+
const robots = generateRobotsTxt(baseURL, config.experimental?.ssg?.robots)
|
|
185
|
+
fs.writeFileSync(path.join(buildDir, 'static', 'robots.txt'), robots)
|
|
186
|
+
console.log('✅ Robots.txt created')
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export {
|
|
191
|
+
extractSSGRoutes,
|
|
192
|
+
generateSitemap,
|
|
193
|
+
generateRobotsTxt,
|
|
194
|
+
generateMetaTags,
|
|
195
|
+
prerenderRoute,
|
|
196
|
+
buildSSG,
|
|
197
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webpack plugin to extract routes for SSG
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'fs'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
|
|
7
|
+
class RyunixRoutesPlugin {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.routesPath = options.routesPath || 'src/pages/routes.ryx'
|
|
10
|
+
this.outputPath = options.outputPath || '.ryunix/ssg/routes.json'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
apply(compiler) {
|
|
14
|
+
compiler.hooks.emit.tapAsync(
|
|
15
|
+
'RyunixRoutesPlugin',
|
|
16
|
+
(compilation, callback) => {
|
|
17
|
+
// Skip in development mode
|
|
18
|
+
if (compiler.options.mode !== 'production') {
|
|
19
|
+
callback()
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const routesFile = path.resolve(process.cwd(), this.routesPath)
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(routesFile)) {
|
|
26
|
+
console.log(
|
|
27
|
+
'[SSG] No routes file found, skipping manifest generation.',
|
|
28
|
+
)
|
|
29
|
+
callback()
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const content = fs.readFileSync(routesFile, 'utf-8')
|
|
35
|
+
console.log('📄 Routes file size:', content.length, 'bytes')
|
|
36
|
+
|
|
37
|
+
const routes = this.parseRoutes(content)
|
|
38
|
+
console.log('✅ Extracted routes:', JSON.stringify(routes, null, 2))
|
|
39
|
+
|
|
40
|
+
const manifest = JSON.stringify(routes, null, 2)
|
|
41
|
+
const outputPath = path.resolve(process.cwd(), this.outputPath)
|
|
42
|
+
|
|
43
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true })
|
|
44
|
+
fs.writeFileSync(outputPath, manifest)
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('❌ Error generating routes manifest:', error)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
callback()
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
parseRoutes(content) {
|
|
55
|
+
const routes = []
|
|
56
|
+
|
|
57
|
+
// Match route objects more loosely
|
|
58
|
+
const routeRegex = /\{\s*path:\s*["']([^"']+)["'][\s\S]*?\}/g
|
|
59
|
+
let match
|
|
60
|
+
|
|
61
|
+
while ((match = routeRegex.exec(content)) !== null) {
|
|
62
|
+
const path = match[1]
|
|
63
|
+
|
|
64
|
+
// Skip dynamic and special routes
|
|
65
|
+
if (path.includes(':') || path === '*') continue
|
|
66
|
+
|
|
67
|
+
const route = { path }
|
|
68
|
+
|
|
69
|
+
// Get full route block (find next closing brace at same level)
|
|
70
|
+
const startIdx = match.index
|
|
71
|
+
let braceCount = 1
|
|
72
|
+
let endIdx = startIdx + 1
|
|
73
|
+
|
|
74
|
+
for (let i = startIdx + 1; i < content.length; i++) {
|
|
75
|
+
if (content[i] === '{') braceCount++
|
|
76
|
+
if (content[i] === '}') braceCount--
|
|
77
|
+
if (braceCount === 0) {
|
|
78
|
+
endIdx = i
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const routeBlock = content.substring(startIdx, endIdx + 1)
|
|
84
|
+
|
|
85
|
+
// Extract meta with nested braces support
|
|
86
|
+
const metaMatch = routeBlock.match(/meta:\s*\{([\s\S]*?)\}(?:\s*,|\s*\})/)
|
|
87
|
+
if (metaMatch) {
|
|
88
|
+
route.meta = this.parseObject(metaMatch[1])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Extract sitemap
|
|
92
|
+
const sitemapMatch = routeBlock.match(/sitemap:\s*\{([^}]+)\}/)
|
|
93
|
+
if (sitemapMatch) {
|
|
94
|
+
route.sitemap = this.parseObject(sitemapMatch[1])
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
routes.push(route)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return routes
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
parseObject(str) {
|
|
104
|
+
const obj = {}
|
|
105
|
+
const pairs = str.match(/["']?[\w:]+["']?\s*:\s*["'][^"']*["']/g) || []
|
|
106
|
+
|
|
107
|
+
pairs.forEach((pair) => {
|
|
108
|
+
const [key, value] = pair
|
|
109
|
+
.split(':')
|
|
110
|
+
.map((s) => s.trim().replace(/["']/g, ''))
|
|
111
|
+
obj[key] = value
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
return obj
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default RyunixRoutesPlugin
|
|
@@ -15,13 +15,14 @@ import {
|
|
|
15
15
|
resolveApp,
|
|
16
16
|
RYUNIX_APP,
|
|
17
17
|
} from './utils/index.mjs'
|
|
18
|
+
import { getMDXLoaderConfig } from './utils/mdx.mjs'
|
|
18
19
|
import fs from 'fs'
|
|
19
20
|
import config from './utils/config.cjs'
|
|
20
21
|
import Dotenv from 'dotenv-webpack'
|
|
21
22
|
import { getPackageVersion } from './utils/index.mjs'
|
|
23
|
+
import RyunixRoutesPlugin from './utils/ssgPlugin.mjs'
|
|
22
24
|
|
|
23
25
|
const __filename = fileURLToPath(import.meta.url)
|
|
24
|
-
|
|
25
26
|
const __dirname = dirname(__filename)
|
|
26
27
|
|
|
27
28
|
let dir
|
|
@@ -48,7 +49,6 @@ function getAlias(object) {
|
|
|
48
49
|
const { version } = await getPackageVersion()
|
|
49
50
|
|
|
50
51
|
export default {
|
|
51
|
-
// context: src
|
|
52
52
|
experiments: {
|
|
53
53
|
lazyCompilation: config.webpack.experiments.lazyCompilation,
|
|
54
54
|
},
|
|
@@ -56,7 +56,6 @@ export default {
|
|
|
56
56
|
entry: './main.ryx',
|
|
57
57
|
devtool: config.webpack.production ? 'source-map' : false,
|
|
58
58
|
output: {
|
|
59
|
-
// path: .ryunix
|
|
60
59
|
path: resolveApp(dir, `${config.webpack.output.buildDirectory}/static`),
|
|
61
60
|
publicPath: '/',
|
|
62
61
|
chunkFilename: './assets/js/[name].[fullhash:8].bundle.js',
|
|
@@ -126,8 +125,9 @@ export default {
|
|
|
126
125
|
stats: 'errors-warnings',
|
|
127
126
|
module: {
|
|
128
127
|
rules: [
|
|
128
|
+
// JavaScript/JSX/Ryunix files
|
|
129
129
|
{
|
|
130
|
-
test: /\.(js|jsx|ryx)$/,
|
|
130
|
+
test: /\.(js|jsx|ryx|mdx|md)$/,
|
|
131
131
|
exclude: /node_modules/,
|
|
132
132
|
use: [
|
|
133
133
|
'thread-loader',
|
|
@@ -163,6 +163,11 @@ export default {
|
|
|
163
163
|
},
|
|
164
164
|
],
|
|
165
165
|
},
|
|
166
|
+
// MDX files (with JSX support)
|
|
167
|
+
getMDXLoaderConfig({
|
|
168
|
+
buildDirectory: config.webpack.output.buildDirectory,
|
|
169
|
+
}),
|
|
170
|
+
// CSS/SASS files
|
|
166
171
|
{
|
|
167
172
|
test: /\.s[ac]ss|css$/i,
|
|
168
173
|
exclude: /node_modules/,
|
|
@@ -171,9 +176,9 @@ export default {
|
|
|
171
176
|
? MiniCssExtractPlugin.loader
|
|
172
177
|
: 'style-loader',
|
|
173
178
|
'css-loader',
|
|
174
|
-
|
|
175
179
|
],
|
|
176
180
|
},
|
|
181
|
+
// Images
|
|
177
182
|
{
|
|
178
183
|
test: /\.(jpg|jpeg|png|gif|svg|ico)$/,
|
|
179
184
|
exclude: /node_modules/,
|
|
@@ -182,6 +187,7 @@ export default {
|
|
|
182
187
|
filename: 'assets/images/[name].[hash][ext]',
|
|
183
188
|
},
|
|
184
189
|
},
|
|
190
|
+
// Media files
|
|
185
191
|
{
|
|
186
192
|
test: /\.(mp3|mp4|pdf)$/,
|
|
187
193
|
exclude: /node_modules/,
|
|
@@ -190,6 +196,7 @@ export default {
|
|
|
190
196
|
filename: 'assets/files/[name].[hash][ext]',
|
|
191
197
|
},
|
|
192
198
|
},
|
|
199
|
+
// User-defined rules from config
|
|
193
200
|
...config.webpack.module.rules,
|
|
194
201
|
],
|
|
195
202
|
},
|
|
@@ -201,6 +208,8 @@ export default {
|
|
|
201
208
|
'.js',
|
|
202
209
|
'.jsx',
|
|
203
210
|
'.ryx',
|
|
211
|
+
'.mdx', // Add MDX extension
|
|
212
|
+
'.md', // Add MD extension
|
|
204
213
|
...config.webpack.resolve.extensions,
|
|
205
214
|
],
|
|
206
215
|
fallback: config.webpack.resolve.fallback,
|
|
@@ -215,13 +224,20 @@ export default {
|
|
|
215
224
|
systemvars: false,
|
|
216
225
|
ignoreStub: true,
|
|
217
226
|
}),
|
|
227
|
+
new RyunixRoutesPlugin({
|
|
228
|
+
routesPath: resolveApp(dir, `${config.webpack.root}/pages/routes.ryx`),
|
|
229
|
+
outputPath: resolveApp(
|
|
230
|
+
dir,
|
|
231
|
+
`${config.webpack.output.buildDirectory}/ssg/routes.json`,
|
|
232
|
+
),
|
|
233
|
+
}),
|
|
218
234
|
new webpack.DefinePlugin({
|
|
219
235
|
'ryunix.config.env': JSON.stringify(config.experimental.env),
|
|
220
236
|
}),
|
|
221
237
|
new ESLintPlugin({
|
|
222
238
|
cwd: dir,
|
|
223
239
|
files: ['**/*.ryx', ...config.eslint.files],
|
|
224
|
-
extensions: ['js', 'ryx', 'jsx'],
|
|
240
|
+
extensions: ['js', 'ryx', 'jsx', 'mdx'], // Add MDX to linting
|
|
225
241
|
emitError: true,
|
|
226
242
|
emitWarning: true,
|
|
227
243
|
failOnWarning: false,
|
|
@@ -254,8 +270,6 @@ export default {
|
|
|
254
270
|
{
|
|
255
271
|
from: resolveApp(dir, 'public'),
|
|
256
272
|
to: resolveApp(dir, `${config.webpack.output.buildDirectory}/static`),
|
|
257
|
-
// Exclude any html files (index.html or others) to avoid duplicate emission
|
|
258
|
-
// when HtmlWebpackPlugin also generates index.html from a template.
|
|
259
273
|
globOptions: {
|
|
260
274
|
ignore: [
|
|
261
275
|
'**/template.html',
|