@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 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.2",
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.3-canary.0 || ^1.2.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
- "rollup": "4.53.5",
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
- "url-loader": "4.1.1",
68
- "webpack": "5.104.0",
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
- "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"
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
- cleanCacheDir(cacheDir)
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 = webpackConfig.devServer.port || 3000
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
- webpackConfig.devServer.port = port
68
- const devServerOptions = { ...webpackConfig.devServer, ...cliSettings }
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)
@@ -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
- if (fs.existsSync(resolveApp(process.cwd(), 'src/pages/routes.ryx'))) {
117
- await cleanBuildDirectory(
118
- resolveApp(
119
- process.cwd(),
120
- `${defaultSettings.webpack.output.buildDirectory}/static`,
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
- logger.error(err || stats.toString('errors-only'))
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
- const buildTimeMs = stats.endTime - stats.startTime
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
- const formattedTime =
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
 
@@ -13,7 +13,8 @@
13
13
  <title><%= htmlWebpackPlugin.options.title %></title>
14
14
  </head>
15
15
  <body>
16
- <noscript>You need to enable JavaScript to run this app.</noscript>
16
+ <%= htmlWebpackPlugin.options.ssrScript %>
17
+
17
18
  <div id="__ryunix"></div>
18
19
  </body>
19
20
  </html>
@@ -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
- const stats = fs.statSync(apiDirPath)
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
- // Always output as .mjs for native Node ESM support
120
- const outFileName = fileName.replace(/\.(ts|ryx|js)$/, '.mjs')
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
- const content = fs.readFileSync(filePath, 'utf8');
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 = null;
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(layoutMeta).length > 0 || Object.keys(indexMeta).length > 0) {
131
- meta = { ...layoutMeta, ...indexMeta };
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
- return content.includes('async function') || content.includes('async (');
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 traverse = (node, parentLayouts = []) => {
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: node.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}.default, isAsync: ${l.isAsync} }`).join(', ')}]`;
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
- }, []); // Only run once on mount
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 = path.join(path.dirname(outputPath), 'ssg', 'routes.json');
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;
@@ -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: {