@unsetsoft/ryunix-presets 1.0.26-canary.2 → 1.0.26-canary.20
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 +19 -20
- package/webpack/bin/dev.server.mjs +19 -5
- package/webpack/bin/index.mjs +51 -15
- package/webpack/bin/prerender.mjs +1 -1
- package/webpack/bin/prod.server.mjs +1 -1
- package/webpack/template/index.html +2 -1
- package/webpack/utils/ApiRouterPlugin.mjs +27 -24
- package/webpack/utils/appRouterPlugin.mjs +128 -19
- package/webpack/utils/config.cjs +33 -0
- package/webpack/utils/ssg.mjs +353 -60
- package/webpack/utils/ssgPlugin.mjs +6 -1
- package/webpack/utils/ssrDevHandler.mjs +98 -0
- package/webpack/webpack.config.mjs +310 -138
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.26-canary.
|
|
4
|
+
"version": "1.0.26-canary.20",
|
|
5
5
|
"author": "Neyunse",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": "https://github.com/UnSetSoft/Ryunixjs",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"webpack/"
|
|
16
16
|
],
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"@unsetsoft/ryunixjs": "^1.2.
|
|
18
|
+
"@unsetsoft/ryunixjs": "^1.2.4-canary.0 || ^1.2.4"
|
|
19
19
|
},
|
|
20
20
|
"publishConfig": {
|
|
21
21
|
"registry": "https://registry.npmjs.org"
|
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
"@babel/preset-react": "7.28.5",
|
|
41
41
|
"@eslint/eslintrc": "3.3.3",
|
|
42
42
|
"@eslint/js": "9.39.2",
|
|
43
|
+
"@mdx-js/loader": "3.1.1",
|
|
44
|
+
"@mdx-js/rollup": "3.1.1",
|
|
45
|
+
"@remark-embedder/core": "3.0.3",
|
|
46
|
+
"@remark-embedder/transformer-oembed": "5.0.1",
|
|
43
47
|
"@rollup/plugin-terser": "0.4.4",
|
|
44
48
|
"@swc/core": "1.15.5",
|
|
45
49
|
"babel-loader": "10.0.0",
|
|
@@ -47,37 +51,32 @@
|
|
|
47
51
|
"copy-webpack-plugin": "13.0.1",
|
|
48
52
|
"css-loader": "7.1.2",
|
|
49
53
|
"css-minimizer-webpack-plugin": "7.0.4",
|
|
50
|
-
"sass": "1.97.0",
|
|
51
|
-
"sass-loader": "16.0.6",
|
|
52
54
|
"dotenv-webpack": "8.1.1",
|
|
53
55
|
"eslint": "9.39.2",
|
|
56
|
+
"eslint-plugin-mdx": "3.6.2",
|
|
54
57
|
"eslint-plugin-react": "^7.37.5",
|
|
55
58
|
"eslint-webpack-plugin": "5.0.2",
|
|
56
|
-
"file-loader": "6.2.0",
|
|
57
59
|
"glob": "13.0.0",
|
|
58
60
|
"globals": "16.5.0",
|
|
59
61
|
"html-loader": "5.1.0",
|
|
60
62
|
"html-webpack-plugin": "5.6.5",
|
|
61
|
-
"lodash": "4.17.21",
|
|
62
63
|
"mini-css-extract-plugin": "2.9.4",
|
|
63
|
-
"
|
|
64
|
+
"postcss": "^8.5.6",
|
|
65
|
+
"postcss-loader": "^8.2.1",
|
|
66
|
+
"rehype-highlight": "7.0.2",
|
|
67
|
+
"remark-frontmatter": "5.0.0",
|
|
68
|
+
"remark-gfm": "4.0.1",
|
|
69
|
+
"remark-mdx-frontmatter": "5.2.0",
|
|
70
|
+
"rollup": "4.59.0",
|
|
71
|
+
"sass": "1.97.0",
|
|
72
|
+
"sass-loader": "16.0.6",
|
|
64
73
|
"style-loader": "4.0.0",
|
|
65
74
|
"terminal-log": "1.0.1",
|
|
66
75
|
"terser-webpack-plugin": "5.3.16",
|
|
67
|
-
"
|
|
68
|
-
"webpack": "5.
|
|
76
|
+
"thread-loader": "4.0.4",
|
|
77
|
+
"webpack": "5.105.2",
|
|
69
78
|
"webpack-cli": "6.0.1",
|
|
70
79
|
"webpack-dev-server": "5.2.2",
|
|
71
|
-
"
|
|
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"
|
|
80
|
+
"yargs": "18.0.0"
|
|
82
81
|
}
|
|
83
82
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getPackageVersion,
|
|
8
8
|
resolveApp,
|
|
9
9
|
cleanCacheDir,
|
|
10
|
+
cleanBuildDirectory,
|
|
10
11
|
} from '../utils/index.mjs'
|
|
11
12
|
import logger from 'terminal-log'
|
|
12
13
|
import chalk from 'chalk'
|
|
@@ -53,19 +54,32 @@ const StartServer = async (cliSettings) => {
|
|
|
53
54
|
cliSettings.production || defaultSettings.webpack.production ? true : false
|
|
54
55
|
|
|
55
56
|
if (!mode) {
|
|
56
|
-
|
|
57
|
+
const buildDirectory =
|
|
58
|
+
defaultSettings.webpack.output.buildDirectory || '.ryunix'
|
|
59
|
+
await cleanCacheDir(cacheDir)
|
|
60
|
+
await cleanBuildDirectory(resolveApp(process.cwd(), join(buildDirectory, 'static')))
|
|
61
|
+
await cleanBuildDirectory(resolveApp(process.cwd(), join(buildDirectory, 'server')))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const clientConfig = Array.isArray(webpackConfig) ? webpackConfig.find(c => c.name === 'client') || webpackConfig[0] : webpackConfig
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(webpackConfig)) {
|
|
67
|
+
webpackConfig.forEach(c => c.mode = mode ? 'production' : 'development')
|
|
68
|
+
} else {
|
|
69
|
+
webpackConfig.mode = mode ? 'production' : 'development'
|
|
57
70
|
}
|
|
58
71
|
|
|
59
|
-
webpackConfig.mode = mode ? 'production' : 'development'
|
|
60
72
|
const compiler = Webpack(webpackConfig)
|
|
61
|
-
let port =
|
|
73
|
+
let port = clientConfig.devServer?.port || 3000
|
|
62
74
|
|
|
63
75
|
// Encontrar un puerto disponible
|
|
64
76
|
port = await findAvailablePort(port)
|
|
65
77
|
|
|
66
78
|
// Modificamos el puerto en la configuración
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
if (clientConfig.devServer) {
|
|
80
|
+
clientConfig.devServer.port = port
|
|
81
|
+
}
|
|
82
|
+
const devServerOptions = { ...(clientConfig.devServer || {}), ...cliSettings }
|
|
69
83
|
const server = new WebpackDevServer(devServerOptions, compiler)
|
|
70
84
|
|
|
71
85
|
const devMode = Boolean(!mode)
|
package/webpack/bin/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#! /usr/bin/env node
|
|
1
|
+
#! /usr/bin/env node
|
|
2
2
|
import yargs from 'yargs'
|
|
3
3
|
import { hideBin } from 'yargs/helpers'
|
|
4
4
|
import { StartDevServer } from './dev.server.mjs'
|
|
@@ -113,34 +113,69 @@ const build = {
|
|
|
113
113
|
return
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
122
|
-
|
|
116
|
+
// ── Clean build output before each production build ───────────────────
|
|
117
|
+
// Clears static/ and server/ (except server/api/) but keeps cache/ intact.
|
|
118
|
+
const buildRoot = resolveApp(process.cwd(), defaultSettings.webpack.output.buildDirectory)
|
|
119
|
+
const clean = (dir) => {
|
|
120
|
+
if (fs.existsSync(dir)) {
|
|
121
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
122
|
+
}
|
|
123
123
|
}
|
|
124
|
+
clean(join(buildRoot, 'static'))
|
|
125
|
+
clean(join(buildRoot, 'server', 'app'))
|
|
126
|
+
clean(join(buildRoot, 'server', 'app-router-server.bundle.mjs'))
|
|
127
|
+
// Note: server/api/ is cleaned by ApiRouterPlugin on its own (incremental recompile)
|
|
128
|
+
|
|
129
|
+
const buildStart = Date.now()
|
|
124
130
|
|
|
125
131
|
compiler.run(async (err, stats) => {
|
|
126
132
|
if (err || stats.hasErrors()) {
|
|
127
|
-
logger.error(chalk.red('Error during compilation:'))
|
|
128
|
-
|
|
133
|
+
logger.error(chalk.red('✘ Error during compilation:'))
|
|
134
|
+
if (err) {
|
|
135
|
+
logger.error(err)
|
|
136
|
+
} else {
|
|
137
|
+
// MultiStats or Stats — toString works on both
|
|
138
|
+
const output = stats.toString('errors-only')
|
|
139
|
+
const lines = output.split('\n').filter(Boolean)
|
|
140
|
+
lines.forEach((line) => logger.error(line))
|
|
141
|
+
}
|
|
142
|
+
compiler.close(() => process.exit(1))
|
|
129
143
|
return
|
|
130
144
|
}
|
|
131
145
|
|
|
132
|
-
|
|
133
|
-
|
|
146
|
+
// ── Build time ─────────────────────────────────────────────────────────
|
|
147
|
+
const buildTimeMs = Date.now() - buildStart
|
|
134
148
|
const minutes = Math.floor(buildTimeMs / 60000)
|
|
135
149
|
const seconds = ((buildTimeMs % 60000) / 1000).toFixed(1)
|
|
150
|
+
const formattedTime = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`
|
|
136
151
|
|
|
137
|
-
|
|
138
|
-
minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`
|
|
139
|
-
|
|
152
|
+
// ── SSG Prerender ──────────────────────────────────────────────────────
|
|
140
153
|
if (defaultSettings.webpack.production) {
|
|
141
154
|
await Prerender(defaultSettings.webpack.output.buildDirectory)
|
|
142
155
|
}
|
|
143
156
|
|
|
157
|
+
// ── API Routes log ─────────────────────────────────────────────────────
|
|
158
|
+
const apiOutputDir = join(buildRoot, 'server', 'api')
|
|
159
|
+
if (fs.existsSync(apiOutputDir)) {
|
|
160
|
+
const collectRoutes = (dir, base = '') => {
|
|
161
|
+
const routes = []
|
|
162
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
163
|
+
const fullPath = join(dir, entry.name)
|
|
164
|
+
if (entry.isDirectory()) {
|
|
165
|
+
routes.push(...collectRoutes(fullPath, `${base}/${entry.name}`))
|
|
166
|
+
} else if (/^(route|router|endpoint)\.(mjs|js)$/.test(entry.name)) {
|
|
167
|
+
routes.push(base || '/')
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return routes
|
|
171
|
+
}
|
|
172
|
+
const apiRoutes = collectRoutes(apiOutputDir)
|
|
173
|
+
if (apiRoutes.length > 0) {
|
|
174
|
+
console.log(`✅ API routes (${apiRoutes.length}):`)
|
|
175
|
+
apiRoutes.forEach((r) => console.log(` - /api${r}`))
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
144
179
|
logger.info(chalk.green('Compilation successful! 🎉'))
|
|
145
180
|
logger.info(`Done in ${formattedTime}`)
|
|
146
181
|
|
|
@@ -148,6 +183,7 @@ const build = {
|
|
|
148
183
|
if (closeErr) {
|
|
149
184
|
logger.error(chalk.red('Error closing the compiler:'), closeErr)
|
|
150
185
|
}
|
|
186
|
+
process.exit(0)
|
|
151
187
|
})
|
|
152
188
|
})
|
|
153
189
|
},
|
|
@@ -16,7 +16,7 @@ const Prerender = async (directory) => {
|
|
|
16
16
|
process.exit(1)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const manifestPath = path.join(process.cwd(), directory, 'ssg', 'routes.json')
|
|
19
|
+
const manifestPath = path.join(process.cwd(), directory, 'cache/ssg', 'routes.json')
|
|
20
20
|
let routes = []
|
|
21
21
|
|
|
22
22
|
if (fs.existsSync(manifestPath)) {
|
|
@@ -379,7 +379,7 @@ const requestHandler = async (req, res) => {
|
|
|
379
379
|
const pathname = decodeURIComponent(parsedUrl.pathname)
|
|
380
380
|
|
|
381
381
|
// Check for API Request
|
|
382
|
-
const apiRootPath = path.join(rootDir, config.webpack.output.buildDirectory, 'api')
|
|
382
|
+
const apiRootPath = path.join(rootDir, config.webpack.output.buildDirectory, 'server/api')
|
|
383
383
|
const handledApi = await handleApiRequest(req, res, apiRootPath)
|
|
384
384
|
if (handledApi) return
|
|
385
385
|
|
|
@@ -5,12 +5,12 @@ import { transformSync } from '@swc/core'
|
|
|
5
5
|
/**
|
|
6
6
|
* Valid API route file names
|
|
7
7
|
*/
|
|
8
|
-
const API_FILE_NAMES = ['route.js', 'route.ryx', 'endpoint.js']
|
|
8
|
+
const API_FILE_NAMES = ['route.js', 'route.ts', 'route.ryx', 'router.js', 'router.ts', 'router.ryx', 'endpoint.js', 'endpoint.ts']
|
|
9
9
|
|
|
10
10
|
class ApiRouterPlugin {
|
|
11
11
|
constructor(options = {}) {
|
|
12
12
|
this.appDir = options.appDir || 'src/app'
|
|
13
|
-
this.outputPath = options.outputPath || '.ryunix/api'
|
|
13
|
+
this.outputPath = options.outputPath || '.ryunix/server/api'
|
|
14
14
|
this.debug = options.debug || false
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -40,13 +40,7 @@ class ApiRouterPlugin {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
try {
|
|
43
|
-
|
|
44
|
-
const mtime = stats.mtimeMs
|
|
45
|
-
|
|
46
|
-
if (mtime > lastScanTime) {
|
|
47
|
-
this.compileApiRoutes(apiDirPath, path.resolve(process.cwd(), this.outputPath))
|
|
48
|
-
lastScanTime = mtime
|
|
49
|
-
}
|
|
43
|
+
this.compileApiRoutes(apiDirPath, path.resolve(process.cwd(), this.outputPath))
|
|
50
44
|
} catch (error) {
|
|
51
45
|
console.error('[ApiRouter] ❌ ERROR compiling api routes:', error)
|
|
52
46
|
}
|
|
@@ -92,6 +86,28 @@ class ApiRouterPlugin {
|
|
|
92
86
|
|
|
93
87
|
compileFile(sourcePath, currentOutDir, fileName) {
|
|
94
88
|
try {
|
|
89
|
+
// Always output as .mjs for native Node ESM support
|
|
90
|
+
const outFileName = fileName.replace(/\.(ts|ryx|js)$/, '.mjs')
|
|
91
|
+
const outFilePath = path.join(currentOutDir, outFileName)
|
|
92
|
+
|
|
93
|
+
// Get stats for both files
|
|
94
|
+
const sourceStats = fs.statSync(sourcePath)
|
|
95
|
+
const sourceMtime = sourceStats.mtimeMs
|
|
96
|
+
|
|
97
|
+
let shouldCompile = true
|
|
98
|
+
if (fs.existsSync(outFilePath)) {
|
|
99
|
+
const outStats = fs.statSync(outFilePath)
|
|
100
|
+
const outMtime = outStats.mtimeMs
|
|
101
|
+
// If the output file is newer than the source file, skip compilation
|
|
102
|
+
if (outMtime > sourceMtime) {
|
|
103
|
+
shouldCompile = false
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!shouldCompile) {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
95
111
|
const content = fs.readFileSync(sourcePath, 'utf8')
|
|
96
112
|
const isTs = fileName.endsWith('.ts')
|
|
97
113
|
const isRyx = fileName.endsWith('.ryx')
|
|
@@ -116,21 +132,8 @@ class ApiRouterPlugin {
|
|
|
116
132
|
}
|
|
117
133
|
})
|
|
118
134
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const outFilePath = path.join(currentOutDir, outFileName)
|
|
122
|
-
|
|
123
|
-
// Only write if changed to avoid unnecessary reloads
|
|
124
|
-
let shouldWrite = true
|
|
125
|
-
if (fs.existsSync(outFilePath)) {
|
|
126
|
-
const existing = fs.readFileSync(outFilePath, 'utf8')
|
|
127
|
-
if (existing === code) shouldWrite = false
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (shouldWrite) {
|
|
131
|
-
if (this.debug) console.log(`[ApiRouter] Compiled: ${sourcePath} -> ${outFilePath}`)
|
|
132
|
-
fs.writeFileSync(outFilePath, code)
|
|
133
|
-
}
|
|
135
|
+
if (this.debug) console.log(`[ApiRouter] Compiled: ${sourcePath} -> ${outFilePath}`)
|
|
136
|
+
fs.writeFileSync(outFilePath, code)
|
|
134
137
|
} catch (e) {
|
|
135
138
|
console.error(`[ApiRouter] Error compiling ${sourcePath}:`, e.message)
|
|
136
139
|
}
|
|
@@ -4,7 +4,8 @@ import path from 'path';
|
|
|
4
4
|
class AppRouterPlugin {
|
|
5
5
|
constructor(options = {}) {
|
|
6
6
|
this.appDir = options.appDir || 'src/app';
|
|
7
|
-
this.outputPath = options.outputPath || '.ryunix/app-router.js';
|
|
7
|
+
this.outputPath = options.outputPath || '.ryunix/server/app/app-router.js';
|
|
8
|
+
this.ssgOutputPath = options.ssgOutputPath || null; // explicit path for routes.json
|
|
8
9
|
this.debug = options.debug || false;
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -65,7 +66,7 @@ class AppRouterPlugin {
|
|
|
65
66
|
for (const entry of entries) {
|
|
66
67
|
if (entry.isFile()) {
|
|
67
68
|
const ext = path.extname(entry.name);
|
|
68
|
-
if (!['.ryx', '.js', '.jsx'].includes(ext)) continue;
|
|
69
|
+
if (!['.ryx', '.js', '.jsx', '.ts', '.tsx', '.mdx'].includes(ext)) continue;
|
|
69
70
|
|
|
70
71
|
const name = path.basename(entry.name, ext);
|
|
71
72
|
const fullPath = path.join(dir, entry.name).replace(/\\/g, '/');
|
|
@@ -112,7 +113,36 @@ class AppRouterPlugin {
|
|
|
112
113
|
const extractMeta = (filePath) => {
|
|
113
114
|
if (!filePath) return null;
|
|
114
115
|
try {
|
|
115
|
-
|
|
116
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
117
|
+
|
|
118
|
+
// Remove BOM if present
|
|
119
|
+
if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1);
|
|
120
|
+
content = content.replace(/\r\n/g, '\n');
|
|
121
|
+
|
|
122
|
+
// Check for MDX YAML frontmatter
|
|
123
|
+
if (filePath.endsWith('.mdx')) {
|
|
124
|
+
const mdxMatch = content.match(/^---\s*\n([\s\S]*?)\n\s*---/);
|
|
125
|
+
if (mdxMatch) {
|
|
126
|
+
const yamlContent = mdxMatch[1];
|
|
127
|
+
const frontmatter = {};
|
|
128
|
+
const lines = yamlContent.split('\n').filter(line => line.trim());
|
|
129
|
+
|
|
130
|
+
for (const line of lines) {
|
|
131
|
+
const keyValueMatch = line.match(/^\s*(\w+)\s*:\s*(.+)$/);
|
|
132
|
+
if (keyValueMatch) {
|
|
133
|
+
const key = keyValueMatch[1].trim();
|
|
134
|
+
let value = keyValueMatch[2].trim();
|
|
135
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
136
|
+
value = value.slice(1, -1);
|
|
137
|
+
}
|
|
138
|
+
frontmatter[key] = value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Title and description are the standard keys in YAML used for metatags
|
|
142
|
+
if (Object.keys(frontmatter).length > 0) return frontmatter;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
116
146
|
const metatagMatch = content.match(/export\s+const\s+Metatags?\s*=\s*(\{[\s\S]*?\})(?=\s*(?:export|;|$))/);
|
|
117
147
|
if (metatagMatch) {
|
|
118
148
|
return new Function(`return ${metatagMatch[1]}`)();
|
|
@@ -125,10 +155,32 @@ class AppRouterPlugin {
|
|
|
125
155
|
|
|
126
156
|
const layoutMeta = extractMeta(layout) || {};
|
|
127
157
|
const indexMeta = extractMeta(index) || {};
|
|
128
|
-
let meta =
|
|
158
|
+
let meta = {};
|
|
159
|
+
|
|
160
|
+
const mergeMeta = (target, source) => {
|
|
161
|
+
if (!source) return;
|
|
162
|
+
Object.keys(source).forEach(key => {
|
|
163
|
+
if (key === 'title') {
|
|
164
|
+
if (typeof source.title === 'object') {
|
|
165
|
+
target.titleTemplate = source.title.template || target.titleTemplate;
|
|
166
|
+
target.titleDefault = source.title.default || target.titleDefault;
|
|
167
|
+
target.title = source.title.default || target.title;
|
|
168
|
+
} else {
|
|
169
|
+
target.title = source.title;
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
target[key] = source[key];
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
mergeMeta(meta, layoutMeta);
|
|
178
|
+
mergeMeta(meta, indexMeta);
|
|
179
|
+
|
|
180
|
+
|
|
129
181
|
|
|
130
|
-
if (Object.keys(
|
|
131
|
-
meta =
|
|
182
|
+
if (Object.keys(meta).length === 0) {
|
|
183
|
+
meta = null;
|
|
132
184
|
}
|
|
133
185
|
|
|
134
186
|
if (!layout && !index && children.length === 0 && !errorFile && !loadingFile) {
|
|
@@ -139,7 +191,14 @@ class AppRouterPlugin {
|
|
|
139
191
|
if (!filePath) return false;
|
|
140
192
|
try {
|
|
141
193
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
142
|
-
|
|
194
|
+
// Improved detection: look for async exports or async function declarations outside comments/strings
|
|
195
|
+
// This is still a heuristic but more targeted towards Ryunix components
|
|
196
|
+
return (
|
|
197
|
+
/export\s+default\s+async\s+/.test(content) ||
|
|
198
|
+
/export\s+const\s+Metatags\s*=\s*async/.test(content) ||
|
|
199
|
+
/export\s+const\s+DynamicMetadata\s*=\s*async/.test(content) ||
|
|
200
|
+
(/async\s+function/.test(content) && content.includes('export default'))
|
|
201
|
+
);
|
|
143
202
|
} catch (e) {
|
|
144
203
|
return false;
|
|
145
204
|
}
|
|
@@ -188,15 +247,38 @@ class AppRouterPlugin {
|
|
|
188
247
|
importStatements += `import * as ${errorsId} from '${this.getRelativeImport(errorsPath, outputPath)}';\n`;
|
|
189
248
|
}
|
|
190
249
|
|
|
191
|
-
const
|
|
250
|
+
const mergeStaticMeta = (target, source) => {
|
|
251
|
+
if (!source) return;
|
|
252
|
+
Object.keys(source).forEach(key => {
|
|
253
|
+
if (key === 'title') {
|
|
254
|
+
if (typeof source.title === 'object') {
|
|
255
|
+
target.titleTemplate = source.title.template || target.titleTemplate;
|
|
256
|
+
target.titleDefault = source.title.default || target.titleDefault;
|
|
257
|
+
target.title = source.title.default || target.title;
|
|
258
|
+
} else {
|
|
259
|
+
target.title = source.title;
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
target[key] = source[key];
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const traverse = (node, parentLayouts = [], inheritedMeta = {}) => {
|
|
192
268
|
if (Array.isArray(node)) {
|
|
193
269
|
for (const child of node) {
|
|
194
|
-
traverse(child, parentLayouts);
|
|
270
|
+
traverse(child, parentLayouts, inheritedMeta);
|
|
195
271
|
}
|
|
196
272
|
return;
|
|
197
273
|
}
|
|
198
274
|
|
|
199
275
|
const currentLayouts = [...parentLayouts];
|
|
276
|
+
const currentMeta = { ...inheritedMeta };
|
|
277
|
+
if (node.meta) Object.assign(currentMeta, JSON.parse(JSON.stringify(node.meta))); // deep clone workaround
|
|
278
|
+
|
|
279
|
+
if (node.meta) {
|
|
280
|
+
mergeStaticMeta(currentMeta, node.meta);
|
|
281
|
+
}
|
|
200
282
|
if (node.layout) {
|
|
201
283
|
const layoutId = `Layout_${getNextId()}`;
|
|
202
284
|
importStatements += `import * as ${layoutId} from '${this.getRelativeImport(node.layout, outputPath)}';\n`;
|
|
@@ -210,8 +292,8 @@ class AppRouterPlugin {
|
|
|
210
292
|
const indexId = `Index_${getNextId()}`;
|
|
211
293
|
importStatements += `import * as ${indexId} from '${this.getRelativeImport(node.index, outputPath)}';\n`;
|
|
212
294
|
|
|
213
|
-
const layoutsArrayStr = `[${currentLayouts.map(l => `{ default: getOptExport(${l.id}, 'default'), isAsync: ${l.isAsync} }`).join(', ')}]`;
|
|
214
|
-
const indexConfigStr = `{ default: getOptExport(${indexId}, 'default'), isAsync: ${!!node.indexIsAsync}, Metatags: getOptExport(${indexId}, 'Metatags') || {} }`;
|
|
295
|
+
const layoutsArrayStr = `[${currentLayouts.map(l => `{ default: getOptExport(${l.id}, 'default'), isAsync: ${l.isAsync}, Metatags: getOptExport(${l.id}, 'Metatags') || getOptExport(${l.id}, 'frontmatter') || {} }`).join(', ')}]`;
|
|
296
|
+
const indexConfigStr = `{ default: getOptExport(${indexId}, 'default'), isAsync: ${!!node.indexIsAsync}, Metatags: getOptExport(${indexId}, 'Metatags') || getOptExport(${indexId}, 'frontmatter') || {} }`;
|
|
215
297
|
const errorPropStr = errorsId ? `Object.assign(${indexConfigStr}, { errorComponent: getOptExport(${errorsId}, 'UnknownError') || getOptExport(${errorsId}, 'UnknowError') || getOptExport(${errorsId}, 'default') })` : indexConfigStr;
|
|
216
298
|
|
|
217
299
|
let componentBody = `<RouteWrapper layouts={${layoutsArrayStr}} index={${errorPropStr}} props={props} />`;
|
|
@@ -222,15 +304,22 @@ class AppRouterPlugin {
|
|
|
222
304
|
component: (props) => ${componentBody}
|
|
223
305
|
}`);
|
|
224
306
|
|
|
307
|
+
const finalMeta = { ...currentMeta };
|
|
308
|
+
if (finalMeta.titleTemplate && finalMeta.title && finalMeta.title !== finalMeta.titleDefault) {
|
|
309
|
+
finalMeta.title = finalMeta.titleTemplate.replace('%s', finalMeta.title);
|
|
310
|
+
} else if (finalMeta.titleDefault && !finalMeta.title) {
|
|
311
|
+
finalMeta.title = finalMeta.titleDefault;
|
|
312
|
+
}
|
|
313
|
+
|
|
225
314
|
ssgRoutes.push({
|
|
226
315
|
path: node.path,
|
|
227
|
-
meta:
|
|
316
|
+
meta: Object.keys(finalMeta).length > 0 ? finalMeta : {}
|
|
228
317
|
});
|
|
229
318
|
}
|
|
230
319
|
|
|
231
320
|
if (Array.isArray(node.children)) {
|
|
232
321
|
for (const child of node.children) {
|
|
233
|
-
traverse(child, currentLayouts);
|
|
322
|
+
traverse(child, currentLayouts, currentMeta);
|
|
234
323
|
}
|
|
235
324
|
}
|
|
236
325
|
};
|
|
@@ -240,12 +329,12 @@ class AppRouterPlugin {
|
|
|
240
329
|
}
|
|
241
330
|
|
|
242
331
|
if (errorsPath) {
|
|
243
|
-
const layoutsArrayStr = `[${rootLayouts.map(l => `{ default: ${l.id}
|
|
332
|
+
const layoutsArrayStr = `[${rootLayouts.map(l => `{ default: getOptExport(${l.id}, 'default'), isAsync: ${l.isAsync}, Metatags: getOptExport(${l.id}, 'Metatags') || getOptExport(${l.id}, 'frontmatter') || {} }`).join(', ')}]`;
|
|
244
333
|
flattenedRoutes.push(`
|
|
245
334
|
{
|
|
246
335
|
path: '*',
|
|
247
|
-
NotFound: (props) => <RouteWrapper layouts={${layoutsArrayStr}} index={{ default: getOptExport(${errorsId}, 'NotFound') || getOptExport(${errorsId}, 'default'), isAsync: false, Metatags: getOptExport(${errorsId}, 'Metatags') || {} }} props={props} />,
|
|
248
|
-
ErrorBuild: (props) => <RouteWrapper layouts={${layoutsArrayStr}} index={{ default: getOptExport(${errorsId}, 'ErrorBuild') || getOptExport(${errorsId}, 'default'), isAsync: false, Metatags: getOptExport(${errorsId}, 'Metatags') || {} }} props={props} />
|
|
336
|
+
NotFound: (props) => <RouteWrapper layouts={${layoutsArrayStr}} index={{ default: getOptExport(${errorsId}, 'NotFound') || getOptExport(${errorsId}, 'default'), isAsync: false, Metatags: getOptExport(${errorsId}, 'Metatags') || getOptExport(${errorsId}, 'frontmatter') || {} }} props={props} />,
|
|
337
|
+
ErrorBuild: (props) => <RouteWrapper layouts={${layoutsArrayStr}} index={{ default: getOptExport(${errorsId}, 'ErrorBuild') || getOptExport(${errorsId}, 'default'), isAsync: false, Metatags: getOptExport(${errorsId}, 'Metatags') || getOptExport(${errorsId}, 'frontmatter') || {} }} props={props} />
|
|
249
338
|
}`);
|
|
250
339
|
}
|
|
251
340
|
|
|
@@ -273,7 +362,7 @@ const AsyncComponentRenderer = ({ Component, componentProps, ErrorFallback }) =>
|
|
|
273
362
|
};
|
|
274
363
|
run();
|
|
275
364
|
return () => { active = false; };
|
|
276
|
-
}, []); //
|
|
365
|
+
}, [Component, JSON.stringify(componentProps)]); // Update when component or props change
|
|
277
366
|
|
|
278
367
|
return content;
|
|
279
368
|
};
|
|
@@ -443,7 +532,7 @@ export default function AppRouter() {
|
|
|
443
532
|
const mainEntryPath = path.join(path.dirname(outputPath), 'main.ryx');
|
|
444
533
|
const mainEntryContent = `import Ryunix from '@unsetsoft/ryunixjs';
|
|
445
534
|
import AppRouter from './${path.basename(outputPath)}';
|
|
446
|
-
|
|
535
|
+
|
|
447
536
|
Ryunix.init(<AppRouter />);
|
|
448
537
|
`;
|
|
449
538
|
let shouldWriteMain = true;
|
|
@@ -458,8 +547,28 @@ Ryunix.init(<AppRouter />);
|
|
|
458
547
|
if (this.debug) console.log(`[AppRouter] Generating main entry at ${mainEntryPath}`);
|
|
459
548
|
}
|
|
460
549
|
|
|
550
|
+
// Server Entry for SSG/SSR
|
|
551
|
+
const serverEntryPath = path.join(path.dirname(outputPath), 'app-router-server.js');
|
|
552
|
+
const serverEntryContent = `import AppRouter from './${path.basename(outputPath)}';
|
|
553
|
+
export const ssgRoutes = ${JSON.stringify(ssgRoutes, null, 2)};
|
|
554
|
+
export default AppRouter;
|
|
555
|
+
`;
|
|
556
|
+
let shouldWriteServer = true;
|
|
557
|
+
if (fs.existsSync(serverEntryPath)) {
|
|
558
|
+
const existingServerContent = fs.readFileSync(serverEntryPath, 'utf8');
|
|
559
|
+
if (existingServerContent === serverEntryContent) {
|
|
560
|
+
shouldWriteServer = false;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (shouldWriteServer) {
|
|
564
|
+
fs.writeFileSync(serverEntryPath, serverEntryContent);
|
|
565
|
+
if (this.debug) console.log(`[AppRouter] Generating server entry at ${serverEntryPath}`);
|
|
566
|
+
}
|
|
567
|
+
|
|
461
568
|
// SSG Output
|
|
462
|
-
const ssgManifestPath =
|
|
569
|
+
const ssgManifestPath = this.ssgOutputPath
|
|
570
|
+
? path.resolve(process.cwd(), this.ssgOutputPath)
|
|
571
|
+
: path.join(path.dirname(outputPath), 'ssg', 'routes.json');
|
|
463
572
|
const ssgManifestContent = JSON.stringify(ssgRoutes, null, 2);
|
|
464
573
|
|
|
465
574
|
let shouldWriteSsg = true;
|
package/webpack/utils/config.cjs
CHANGED
|
@@ -4,6 +4,38 @@ const reactPlugin = require('eslint-plugin-react')
|
|
|
4
4
|
|
|
5
5
|
const userConfig = getConfig()
|
|
6
6
|
|
|
7
|
+
const path = require('path')
|
|
8
|
+
const fs = require('fs')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if a config path exists in userConfig and warn if it's deprecated.
|
|
12
|
+
*/
|
|
13
|
+
const warnDeprecated = (configPath, message) => {
|
|
14
|
+
if (!userConfig) return
|
|
15
|
+
|
|
16
|
+
const keys = configPath.split('.')
|
|
17
|
+
let value = userConfig
|
|
18
|
+
for (const key of keys) {
|
|
19
|
+
if (value?.[key] === undefined) return
|
|
20
|
+
value = value[key]
|
|
21
|
+
}
|
|
22
|
+
if (value !== undefined) {
|
|
23
|
+
console.warn(`\x1b[33m[Ryunix Warn]\x1b[0m \x1b[1m${configPath}\x1b[0m is deprecated. ${message}`)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check for App Router presence
|
|
28
|
+
const rootDir = userConfig?.webpack?.root ?? 'src'
|
|
29
|
+
const hasAppDir = fs.existsSync(path.resolve(process.cwd(), 'app')) || fs.existsSync(path.resolve(process.cwd(), `${rootDir}/app`))
|
|
30
|
+
|
|
31
|
+
// Global old SSG deprecations
|
|
32
|
+
warnDeprecated('experimental.ssg', 'The old SSG configuration is deprecated and will be removed in future versions in favor of App Router SSG conventions.')
|
|
33
|
+
|
|
34
|
+
// App Router specific deprecations
|
|
35
|
+
if (hasAppDir) {
|
|
36
|
+
warnDeprecated('static.seo', 'The global static.seo configuration is redundant when using the App Router. Use exported metadata in layout.ryx/page.ryx instead.')
|
|
37
|
+
warnDeprecated('static.customTemplate', 'Custom templates are replaced by the root layout.ryx when using the App Router.')
|
|
38
|
+
}
|
|
7
39
|
// ============================================================================
|
|
8
40
|
// Helpers
|
|
9
41
|
// ============================================================================
|
|
@@ -74,6 +106,7 @@ const DEFAULT_SSG_SITEMAP_SETTINGS = {
|
|
|
74
106
|
const defaultSettings = {
|
|
75
107
|
experimental: {
|
|
76
108
|
mdx: getConfigValue('experimental.mdx', false),
|
|
109
|
+
ssr: getConfigValue('experimental.ssr', false),
|
|
77
110
|
|
|
78
111
|
ssg: {
|
|
79
112
|
sitemap: {
|