@unsetsoft/ryunix-presets 1.0.26-canary.21 → 1.0.26-canary.23

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.21",
4
+ "version": "1.0.26-canary.23",
5
5
  "author": "Neyunse",
6
6
  "type": "module",
7
7
  "repository": "https://github.com/UnSetSoft/Ryunixjs",
@@ -1,143 +1,588 @@
1
- import fs from 'fs'
2
- import path from 'path'
3
- import { transformSync } from '@swc/core'
1
+ import fs from 'fs';
2
+ import path from 'path';
4
3
 
5
- /**
6
- * Valid API route file names
7
- */
8
- const API_FILE_NAMES = ['route.js', 'route.ts', 'route.ryx', 'router.js', 'router.ts', 'router.ryx', 'endpoint.js', 'endpoint.ts']
9
-
10
- class ApiRouterPlugin {
4
+ class AppRouterPlugin {
11
5
  constructor(options = {}) {
12
- this.appDir = options.appDir || 'src/app'
13
- this.outputPath = options.outputPath || '.ryunix/server/api'
14
- this.debug = options.debug || false
6
+ this.appDir = options.appDir || 'src/app';
7
+ this.outputPath = options.outputPath || '.ryunix/server/app/app-router.js';
8
+ this.ssgOutputPath = options.ssgOutputPath || null; // explicit path for routes.json
9
+ this.debug = options.debug || false;
15
10
  }
16
11
 
17
12
  apply(compiler) {
18
- let lastScanTime = 0
19
- let isWatching = false
20
- let watcher = null
13
+ let lastScanTime = 0;
14
+ let lastRoutes = null;
21
15
 
22
- compiler.hooks.watchRun.tapAsync('ApiRouterPlugin', (comp, callback) => {
23
- isWatching = true
24
- callback()
25
- })
26
-
27
- compiler.hooks.beforeCompile.tapAsync('ApiRouterPlugin', (params, callback) => {
28
- const appDirPath = path.resolve(process.cwd(), this.appDir)
29
- const apiDirPath = path.join(appDirPath, 'api')
16
+ compiler.hooks.beforeCompile.tapAsync('AppRouterPlugin', (params, callback) => {
17
+ const appDirPath = path.resolve(process.cwd(), this.appDir);
30
18
 
31
- if (!fs.existsSync(apiDirPath)) {
32
- if (this.debug) console.log(`[ApiRouter] No api directory found at ${apiDirPath}`)
33
- callback()
34
- return
19
+ if (!fs.existsSync(appDirPath)) {
20
+ if (this.debug) console.log(`[AppRouter] No app directory found at ${appDirPath}`);
21
+ callback();
22
+ return;
35
23
  }
36
24
 
37
- // Add api directory to webpack's context dependencies so it detects new files/folders
38
25
  if (params && params.compilationDependencies) {
39
- params.contextDependencies.add(apiDirPath)
26
+ params.contextDependencies.add(appDirPath)
40
27
  }
41
28
 
42
29
  try {
43
- this.compileApiRoutes(apiDirPath, path.resolve(process.cwd(), this.outputPath))
30
+ // Simple optimization: check if any file in the directory has changed
31
+ // This is a bit coarse but better than scanning everything every time
32
+ const stats = fs.statSync(appDirPath);
33
+ const mtime = stats.mtimeMs;
34
+
35
+ if (mtime > lastScanTime || !lastRoutes) {
36
+ const routes = this.scanDirectory(appDirPath, '');
37
+ this.generateRouterFile(routes, path.resolve(process.cwd(), this.outputPath));
38
+ lastScanTime = mtime;
39
+ lastRoutes = routes;
40
+ }
44
41
  } catch (error) {
45
- console.error('[ApiRouter] ❌ ERROR compiling api routes:', error)
42
+ console.error('[AppRouter] ❌ ERROR generating app router:', error);
46
43
  }
47
44
 
48
- callback()
49
- })
45
+ callback();
46
+ });
50
47
 
51
- compiler.hooks.afterCompile.tapAsync('ApiRouterPlugin', (compilation, callback) => {
48
+ compiler.hooks.afterCompile.tapAsync('AppRouterPlugin', (compilation, callback) => {
52
49
  const appDirPath = path.resolve(process.cwd(), this.appDir)
53
- const apiDirPath = path.join(appDirPath, 'api')
54
- if (fs.existsSync(apiDirPath)) {
55
- compilation.contextDependencies.add(apiDirPath)
50
+ if (fs.existsSync(appDirPath)) {
51
+ compilation.contextDependencies.add(appDirPath)
56
52
  }
57
53
  callback()
58
54
  })
59
55
  }
60
56
 
61
- compileApiRoutes(sourceDir, outDir) {
62
- if (!fs.existsSync(outDir)) {
63
- fs.mkdirSync(outDir, { recursive: true })
57
+ scanDirectory(dir, basePath) {
58
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
59
+ let layout = null;
60
+ let index = null;
61
+ let errorFile = null;
62
+ let loadingFile = null;
63
+ const children = [];
64
+
65
+ // Find special files
66
+ for (const entry of entries) {
67
+ if (entry.isFile()) {
68
+ const ext = path.extname(entry.name);
69
+ if (!['.ryx', '.js', '.jsx', '.ts', '.tsx', '.mdx'].includes(ext)) continue;
70
+
71
+ const name = path.basename(entry.name, ext);
72
+ const fullPath = path.join(dir, entry.name).replace(/\\/g, '/');
73
+
74
+ if (name === 'layout') layout = fullPath;
75
+ else if (name === 'index') index = fullPath;
76
+ else if (name === 'error') errorFile = fullPath;
77
+ else if (name === 'loading') loadingFile = fullPath;
78
+ }
64
79
  }
65
80
 
66
- const compileDirectory = (currentDir, currentOutDir) => {
67
- if (!fs.existsSync(currentOutDir)) {
68
- fs.mkdirSync(currentOutDir, { recursive: true })
81
+ // Process subdirectories
82
+ for (const entry of entries) {
83
+ if (entry.isDirectory()) {
84
+ const routeSegment = entry.name;
85
+ // Convert [...slug] to :...slug and [slug] to :slug
86
+ const routePath = routeSegment.replace(/\[(\.\.\.)?([^\]]+)\]/g, ':$1$2');
87
+
88
+ let newBasePath = basePath;
89
+ if (newBasePath === '/') {
90
+ newBasePath = `/${routePath}`;
91
+ } else if (newBasePath === '') {
92
+ newBasePath = `/${routePath}`;
93
+ } else {
94
+ newBasePath = `${basePath}/${routePath}`;
95
+ }
96
+
97
+ const childRoutes = this.scanDirectory(
98
+ path.join(dir, entry.name),
99
+ newBasePath
100
+ );
101
+ if (childRoutes) {
102
+ // If the child is an array (flattened from a folder with only children but no index/layout), concat it.
103
+ // Otherwise, push it.
104
+ if (Array.isArray(childRoutes)) {
105
+ children.push(...childRoutes);
106
+ } else {
107
+ children.push(childRoutes);
108
+ }
109
+ }
69
110
  }
111
+ }
112
+
113
+ const extractMeta = (filePath) => {
114
+ if (!filePath) return null;
115
+ try {
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
+ }
70
145
 
71
- const entries = fs.readdirSync(currentDir, { withFileTypes: true })
146
+ const metatagMatch = content.match(/export\s+const\s+Metatags?\s*=\s*(\{[\s\S]*?\})(?=\s*(?:export|;|$))/);
147
+ if (metatagMatch) {
148
+ return new Function(`return ${metatagMatch[1]}`)();
149
+ }
150
+ } catch (e) {
151
+ if (this.debug) console.error(`[AppRouter] Error parsing Metatag in ${filePath}:`, e.message);
152
+ }
153
+ return null;
154
+ };
72
155
 
73
- for (const entry of entries) {
74
- const sourcePath = path.join(currentDir, entry.name)
156
+ const layoutMeta = extractMeta(layout) || {};
157
+ const indexMeta = extractMeta(index) || {};
158
+ let meta = {};
75
159
 
76
- if (entry.isDirectory()) {
77
- compileDirectory(sourcePath, path.join(currentOutDir, entry.name))
78
- } else if (entry.isFile() && API_FILE_NAMES.includes(entry.name)) {
79
- this.compileFile(sourcePath, currentOutDir, entry.name)
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];
80
173
  }
174
+ });
175
+ };
176
+
177
+ mergeMeta(meta, layoutMeta);
178
+ mergeMeta(meta, indexMeta);
179
+
180
+
181
+
182
+ if (Object.keys(meta).length === 0) {
183
+ meta = null;
184
+ }
185
+
186
+ if (!layout && !index && children.length === 0 && !errorFile && !loadingFile) {
187
+ return null;
188
+ }
189
+
190
+ const isAsync = (filePath) => {
191
+ if (!filePath) return false;
192
+ try {
193
+ const content = fs.readFileSync(filePath, 'utf8');
194
+ return content.includes('async function') || content.includes('async (');
195
+ } catch (e) {
196
+ return false;
81
197
  }
198
+ };
199
+
200
+ const node = {
201
+ path: basePath === '' ? '/' : basePath,
202
+ layout,
203
+ layoutIsAsync: isAsync(layout),
204
+ index,
205
+ indexIsAsync: isAsync(index),
206
+ meta,
207
+ error: errorFile,
208
+ loading: loadingFile,
209
+ children,
210
+ };
211
+
212
+ if (!layout && !index && !errorFile && !loadingFile) {
213
+ return children;
82
214
  }
83
215
 
84
- compileDirectory(sourceDir, outDir)
216
+ return node;
85
217
  }
86
218
 
87
- compileFile(sourcePath, currentOutDir, fileName) {
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
-
111
- const content = fs.readFileSync(sourcePath, 'utf8')
112
- const isTs = fileName.endsWith('.ts')
113
- const isRyx = fileName.endsWith('.ryx')
114
-
115
- const { code } = transformSync(content, {
116
- filename: fileName,
117
- jsc: {
118
- parser: {
119
- syntax: isTs ? 'typescript' : 'ecmascript',
120
- jsx: isRyx || true,
121
- },
122
- target: 'es2022',
123
- transform: {
124
- react: {
125
- pragma: 'Ryunix.createElement',
126
- pragmaFrag: 'Ryunix.Fragment',
127
- }
219
+ generateRouterFile(routeNode, outputPath) {
220
+ let importStatements = `import Ryunix, { RouterProvider, Children, useMetadata, useEffect, useStore } from '@unsetsoft/ryunixjs';\n`;
221
+ let routeDefinitions = '';
222
+
223
+ let componentIdCounter = 0;
224
+ const getNextId = () => componentIdCounter++;
225
+
226
+ // Flatten logic
227
+ const flattenedRoutes = [];
228
+ const ssgRoutes = [];
229
+
230
+ let rootLayouts = [];
231
+
232
+ const appDirPath = path.resolve(process.cwd(), this.appDir);
233
+ const errorsPath = Math.max(fs.existsSync(path.join(appDirPath, 'errors.ryx')), fs.existsSync(path.join(appDirPath, 'error.ryx')))
234
+ ? fs.existsSync(path.join(appDirPath, 'errors.ryx')) ? path.join(appDirPath, 'errors.ryx') : path.join(appDirPath, 'error.ryx')
235
+ : null;
236
+
237
+ let errorsId = null;
238
+ if (errorsPath) {
239
+ errorsId = `Errors_App`;
240
+ importStatements += `import * as ${errorsId} from '${this.getRelativeImport(errorsPath, outputPath)}';\n`;
241
+ }
242
+
243
+ const mergeStaticMeta = (target, source) => {
244
+ if (!source) return;
245
+ Object.keys(source).forEach(key => {
246
+ if (key === 'title') {
247
+ if (typeof source.title === 'object') {
248
+ target.titleTemplate = source.title.template || target.titleTemplate;
249
+ target.titleDefault = source.title.default || target.titleDefault;
250
+ target.title = source.title.default || target.title;
251
+ } else {
252
+ target.title = source.title;
128
253
  }
129
- },
130
- module: {
131
- type: 'es6'
254
+ } else {
255
+ target[key] = source[key];
132
256
  }
133
- })
257
+ });
258
+ };
134
259
 
135
- if (this.debug) console.log(`[ApiRouter] Compiled: ${sourcePath} -> ${outFilePath}`)
136
- fs.writeFileSync(outFilePath, code)
137
- } catch (e) {
138
- console.error(`[ApiRouter] Error compiling ${sourcePath}:`, e.message)
260
+ const traverse = (node, parentLayouts = [], inheritedMeta = {}) => {
261
+ if (Array.isArray(node)) {
262
+ for (const child of node) {
263
+ traverse(child, parentLayouts, inheritedMeta);
264
+ }
265
+ return;
266
+ }
267
+
268
+ const currentLayouts = [...parentLayouts];
269
+ const currentMeta = { ...inheritedMeta };
270
+ if (node.meta) Object.assign(currentMeta, JSON.parse(JSON.stringify(node.meta))); // deep clone workaround
271
+
272
+ if (node.meta) {
273
+ mergeStaticMeta(currentMeta, node.meta);
274
+ }
275
+ if (node.layout) {
276
+ const layoutId = `Layout_${getNextId()}`;
277
+ importStatements += `import * as ${layoutId} from '${this.getRelativeImport(node.layout, outputPath)}';\n`;
278
+ currentLayouts.push({ id: layoutId, isAsync: !!node.layoutIsAsync });
279
+ if (parentLayouts.length === 0 && !rootLayouts.some(l => l.id === layoutId)) {
280
+ rootLayouts.push({ id: layoutId, isAsync: !!node.layoutIsAsync });
281
+ }
282
+ }
283
+
284
+ if (node.index) {
285
+ const indexId = `Index_${getNextId()}`;
286
+ importStatements += `import * as ${indexId} from '${this.getRelativeImport(node.index, outputPath)}';\n`;
287
+
288
+ const layoutsArrayStr = `[${currentLayouts.map(l => `{ default: getOptExport(${l.id}, 'default'), isAsync: ${l.isAsync}, Metatags: getOptExport(${l.id}, 'Metatags') || getOptExport(${l.id}, 'frontmatter') || {} }`).join(', ')}]`;
289
+ const indexConfigStr = `{ default: getOptExport(${indexId}, 'default'), isAsync: ${!!node.indexIsAsync}, Metatags: getOptExport(${indexId}, 'Metatags') || getOptExport(${indexId}, 'frontmatter') || {} }`;
290
+ const errorPropStr = errorsId ? `Object.assign(${indexConfigStr}, { errorComponent: getOptExport(${errorsId}, 'UnknownError') || getOptExport(${errorsId}, 'UnknowError') || getOptExport(${errorsId}, 'default') })` : indexConfigStr;
291
+
292
+ let componentBody = `<RouteWrapper layouts={${layoutsArrayStr}} index={${errorPropStr}} props={props} />`;
293
+
294
+ flattenedRoutes.push(`
295
+ {
296
+ path: '${node.path}',
297
+ component: (props) => ${componentBody}
298
+ }`);
299
+
300
+ const finalMeta = { ...currentMeta };
301
+ if (finalMeta.titleTemplate && finalMeta.title && finalMeta.title !== finalMeta.titleDefault) {
302
+ finalMeta.title = finalMeta.titleTemplate.replace('%s', finalMeta.title);
303
+ } else if (finalMeta.titleDefault && !finalMeta.title) {
304
+ finalMeta.title = finalMeta.titleDefault;
305
+ }
306
+
307
+ ssgRoutes.push({
308
+ path: node.path,
309
+ meta: Object.keys(finalMeta).length > 0 ? finalMeta : {}
310
+ });
311
+ }
312
+
313
+ if (Array.isArray(node.children)) {
314
+ for (const child of node.children) {
315
+ traverse(child, currentLayouts, currentMeta);
316
+ }
317
+ }
318
+ };
319
+
320
+ if (routeNode) {
321
+ traverse(routeNode);
139
322
  }
323
+
324
+ if (errorsPath) {
325
+ const layoutsArrayStr = `[${rootLayouts.map(l => `{ default: getOptExport(${l.id}, 'default'), isAsync: ${l.isAsync}, Metatags: getOptExport(${l.id}, 'Metatags') || getOptExport(${l.id}, 'frontmatter') || {} }`).join(', ')}]`;
326
+ flattenedRoutes.push(`
327
+ {
328
+ path: '*',
329
+ NotFound: (props) => <RouteWrapper layouts={${layoutsArrayStr}} index={{ default: getOptExport(${errorsId}, 'NotFound') || getOptExport(${errorsId}, 'default'), isAsync: false, Metatags: getOptExport(${errorsId}, 'Metatags') || getOptExport(${errorsId}, 'frontmatter') || {} }} props={props} />,
330
+ ErrorBuild: (props) => <RouteWrapper layouts={${layoutsArrayStr}} index={{ default: getOptExport(${errorsId}, 'ErrorBuild') || getOptExport(${errorsId}, 'default'), isAsync: false, Metatags: getOptExport(${errorsId}, 'Metatags') || getOptExport(${errorsId}, 'frontmatter') || {} }} props={props} />
331
+ }`);
332
+ }
333
+
334
+ const fileContent = `/* AUTO-GENERATED APP ROUTER */
335
+ ${importStatements}
336
+ const getOptExport = (mod, key) => mod ? mod[key] : undefined;
337
+
338
+ const AsyncComponentRenderer = ({ Component, componentProps, ErrorFallback }) => {
339
+ const [content, setContent] = useStore(null);
340
+
341
+ useEffect(() => {
342
+ let active = true;
343
+ const run = async () => {
344
+ try {
345
+ const res = await Component(componentProps);
346
+ if (active) setContent(<Ryunix.Fragment>{res}</Ryunix.Fragment>);
347
+ } catch(err) {
348
+ console.error('Error rendering async component:', err);
349
+ if (ErrorFallback) {
350
+ if (active) setContent(<ErrorFallback error={err} />);
351
+ } else {
352
+ if (active) setContent(<div style={{ padding: '2rem', color: 'red' }}>Error rendering async component</div>);
353
+ }
354
+ }
355
+ };
356
+ run();
357
+ return () => { active = false; };
358
+ }, []); // Only run once on mount
359
+
360
+ return content;
361
+ };
362
+
363
+ const SyncComponentRenderer = ({ Component, componentProps, ErrorFallback }) => {
364
+ try {
365
+ const res = Component(componentProps);
366
+ return <Ryunix.Fragment>{res}</Ryunix.Fragment>;
367
+ } catch(err) {
368
+ console.error('Error rendering sync component:', err);
369
+ if (ErrorFallback) {
370
+ return <ErrorFallback error={err} />;
371
+ }
372
+ return <div style={{ padding: '2rem', color: 'red' }}>Error rendering component</div>;
373
+ }
374
+ };
375
+
376
+ const RouteWrapper = ({ layouts, index, props }) => {
377
+ const staticMeta = {};
378
+
379
+ const mergeMeta = (target, source) => {
380
+ if (!source) return;
381
+ Object.keys(source).forEach(key => {
382
+ if (key === 'title') {
383
+ if (typeof source.title === 'object') {
384
+ target.titleTemplate = source.title.template || target.titleTemplate;
385
+ target.titleDefault = source.title.default || target.titleDefault;
386
+ target.title = source.title.default || target.title;
387
+ } else {
388
+ target.title = source.title;
389
+ }
390
+ } else {
391
+ target[key] = source[key];
392
+ }
393
+ });
394
+ };
395
+
396
+ layouts.forEach(l => { if (l && l.Metatags) mergeMeta(staticMeta, l.Metatags); });
397
+ if (index && index.Metatags) mergeMeta(staticMeta, index.Metatags);
398
+
399
+ const formatMeta = (metaObj) => {
400
+ const formattedMeta = { ...metaObj };
401
+ if (metaObj.titleTemplate && metaObj.title) {
402
+ formattedMeta.title = metaObj.titleTemplate.replace('%s', metaObj.title);
403
+ } else if (metaObj.titleDefault && !metaObj.title) {
404
+ formattedMeta.title = metaObj.titleDefault;
405
+ }
406
+ return formattedMeta;
407
+ };
408
+
409
+ const [currentMeta, setCurrentMeta] = useStore(formatMeta(staticMeta));
410
+
411
+ // Ensure parameter proxies are available for both synchronous and asynchronous contexts
412
+ const promiseProps = (obj) => {
413
+ const promise = Promise.resolve(obj);
414
+ return new Proxy(promise, {
415
+ get(target, prop) {
416
+ if (prop === 'then' || prop === 'catch' || prop === 'finally') {
417
+ return target[prop].bind(target);
418
+ }
419
+ return obj[prop];
420
+ }
421
+ });
422
+ };
423
+
424
+ const asyncParams = promiseProps(props.params || {});
425
+ const asyncQuery = promiseProps(props.query || {});
426
+
427
+ useEffect(() => {
428
+ let active = true;
429
+ const loadMeta = async () => {
430
+ // Defer execution to cleanly escape the Ryunix commitWork synchronous phase
431
+ await Promise.resolve();
432
+
433
+ let resolvedMeta = { ...staticMeta };
434
+
435
+ for (const layout of layouts) {
436
+ if (layout?.DynamicMetadata) {
437
+ try {
438
+ const res = await layout.DynamicMetadata({ params: asyncParams, searchParams: asyncQuery }, resolvedMeta);
439
+ mergeMeta(resolvedMeta, res);
440
+ if (active) setCurrentMeta(formatMeta(resolvedMeta));
441
+ } catch (e) {
442
+ console.error('Error in layout DynamicMetadata:', e);
443
+ }
444
+ }
445
+ }
446
+
447
+ if (index?.DynamicMetadata) {
448
+ try {
449
+ const res = await index.DynamicMetadata({ params: asyncParams, searchParams: asyncQuery }, resolvedMeta);
450
+ mergeMeta(resolvedMeta, res);
451
+ if (active) setCurrentMeta(formatMeta(resolvedMeta));
452
+ } catch (e) {
453
+ console.error('Error in index DynamicMetadata:', e);
454
+ }
455
+ }
456
+ };
457
+
458
+ loadMeta();
459
+ return () => { active = false; };
460
+ }, [JSON.stringify(props.params), JSON.stringify(props.query)]);
461
+
462
+ useMetadata(currentMeta);
463
+
464
+ const ErrorFallback = index?.errorComponent;
465
+
466
+ // Build root content synchronously so Ryunix Fiber can track hooks for synchronous components
467
+ let content = null;
468
+ if (index?.default) {
469
+ const IndexComp = index.default;
470
+ const isAsync = index.isAsync || (IndexComp.constructor.name === 'AsyncFunction' || IndexComp[Symbol.toStringTag] === 'AsyncFunction');
471
+
472
+ if (isAsync) {
473
+ content = <AsyncComponentRenderer Component={IndexComp} componentProps={{ ...props, params: asyncParams, searchParams: asyncQuery }} ErrorFallback={ErrorFallback} />;
474
+ } else {
475
+ content = <SyncComponentRenderer Component={IndexComp} componentProps={{ ...props, params: asyncParams, searchParams: asyncQuery }} ErrorFallback={ErrorFallback} />;
476
+ }
477
+ }
478
+
479
+ // Wrap with Layouts
480
+ for (let i = layouts.length - 1; i >= 0; i--) {
481
+ const LayoutComp = layouts[i]?.default;
482
+ const isAsync = layouts[i]?.isAsync || (LayoutComp && (LayoutComp.constructor.name === 'AsyncFunction' || LayoutComp[Symbol.toStringTag] === 'AsyncFunction'));
483
+
484
+ if (LayoutComp) {
485
+ if (isAsync) {
486
+ content = <AsyncComponentRenderer Component={LayoutComp} componentProps={{ ...props, params: asyncParams, searchParams: asyncQuery, children: content }} ErrorFallback={ErrorFallback} />;
487
+ } else {
488
+ content = <SyncComponentRenderer Component={LayoutComp} componentProps={{ ...props, params: asyncParams, searchParams: asyncQuery, children: content }} ErrorFallback={ErrorFallback} />;
489
+ }
490
+ }
491
+ }
492
+
493
+ // Handle fallback if it's an error boundary route (optional, Ryunix handles its own suspense/errors generally)
494
+ return content;
495
+ };
496
+
497
+ const routes = [${flattenedRoutes.join(',\n')}
498
+ ];
499
+
500
+ export default function AppRouter() {
501
+ return (
502
+ <RouterProvider routes={routes}>
503
+ <Children />
504
+ </RouterProvider>
505
+ );
506
+ }
507
+ `;
508
+
509
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
510
+
511
+ // Only write if the content actually changed to avoid Webpack infinite loops
512
+ let shouldWrite = true;
513
+ if (fs.existsSync(outputPath)) {
514
+ const existingContent = fs.readFileSync(outputPath, 'utf8');
515
+ if (existingContent === fileContent) {
516
+ shouldWrite = false;
517
+ }
518
+ }
519
+
520
+ if (shouldWrite) {
521
+ if (this.debug) console.log(`[AppRouter] Generating routes at ${outputPath}`);
522
+ fs.writeFileSync(outputPath, fileContent);
523
+ }
524
+
525
+ const mainEntryPath = path.join(path.dirname(outputPath), 'main.ryx');
526
+ const mainEntryContent = `import Ryunix from '@unsetsoft/ryunixjs';
527
+ import AppRouter from './${path.basename(outputPath)}';
528
+
529
+ Ryunix.init(<AppRouter />);
530
+ `;
531
+ let shouldWriteMain = true;
532
+ if (fs.existsSync(mainEntryPath)) {
533
+ const existingMainContent = fs.readFileSync(mainEntryPath, 'utf8');
534
+ if (existingMainContent === mainEntryContent) {
535
+ shouldWriteMain = false;
536
+ }
537
+ }
538
+ if (shouldWriteMain) {
539
+ fs.writeFileSync(mainEntryPath, mainEntryContent);
540
+ if (this.debug) console.log(`[AppRouter] Generating main entry at ${mainEntryPath}`);
541
+ }
542
+
543
+ // Server Entry for SSG/SSR
544
+ const serverEntryPath = path.join(path.dirname(outputPath), 'app-router-server.js');
545
+ const serverEntryContent = `import AppRouter from './${path.basename(outputPath)}';
546
+ export const ssgRoutes = ${JSON.stringify(ssgRoutes, null, 2)};
547
+ export default AppRouter;
548
+ `;
549
+ let shouldWriteServer = true;
550
+ if (fs.existsSync(serverEntryPath)) {
551
+ const existingServerContent = fs.readFileSync(serverEntryPath, 'utf8');
552
+ if (existingServerContent === serverEntryContent) {
553
+ shouldWriteServer = false;
554
+ }
555
+ }
556
+ if (shouldWriteServer) {
557
+ fs.writeFileSync(serverEntryPath, serverEntryContent);
558
+ if (this.debug) console.log(`[AppRouter] Generating server entry at ${serverEntryPath}`);
559
+ }
560
+
561
+ // SSG Output
562
+ const ssgManifestPath = this.ssgOutputPath
563
+ ? path.resolve(process.cwd(), this.ssgOutputPath)
564
+ : path.join(path.dirname(outputPath), 'ssg', 'routes.json');
565
+ const ssgManifestContent = JSON.stringify(ssgRoutes, null, 2);
566
+
567
+ let shouldWriteSsg = true;
568
+ if (fs.existsSync(ssgManifestPath)) {
569
+ const existingSsgContent = fs.readFileSync(ssgManifestPath, 'utf8');
570
+ if (existingSsgContent === ssgManifestContent) {
571
+ shouldWriteSsg = false;
572
+ }
573
+ }
574
+
575
+ if (shouldWriteSsg) {
576
+ fs.mkdirSync(path.dirname(ssgManifestPath), { recursive: true });
577
+ if (this.debug) console.log(`[AppRouter] Generating SSG manifest at ${ssgManifestPath}`);
578
+ fs.writeFileSync(ssgManifestPath, ssgManifestContent);
579
+ }
580
+ }
581
+
582
+ getRelativeImport(targetPath, outputPath) {
583
+ const relativePath = path.relative(path.dirname(outputPath), targetPath).replace(/\\/g, '/');
584
+ return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
140
585
  }
141
586
  }
142
587
 
143
- export default ApiRouterPlugin
588
+ export default AppRouterPlugin;
@@ -407,12 +407,6 @@ const clientConfig = {
407
407
  return middlewares
408
408
  },
409
409
  },
410
- watchOptions: {
411
- ignored: [
412
- '**/node_modules/**',
413
- `**/${config.webpack.output.buildDirectory}/**`,
414
- ],
415
- },
416
410
  module: {
417
411
  ...sharedWebpackConfig.module,
418
412
  rules: [
@@ -462,12 +456,7 @@ const clientConfig = {
462
456
  cwd: dir,
463
457
  files: ['**/*.ryx', ...config.eslint.files],
464
458
  extensions: ['js', 'ryx', 'jsx'],
465
- exclude: [
466
- 'node_modules',
467
- '**/*.mdx',
468
- '**/*.md',
469
- `${config.webpack.output.buildDirectory}/**`,
470
- ],
459
+ exclude: ['node_modules', '**/*.mdx', '**/*.md'],
471
460
  emitError: true,
472
461
  emitWarning: true,
473
462
  failOnWarning: false,
@@ -495,12 +484,6 @@ const serverConfig = {
495
484
  // Keep api/ subdirectory — it's written by ApiRouterPlugin, not by webpack
496
485
  clean: { keep: /^api[\\/]/ },
497
486
  },
498
- watchOptions: {
499
- ignored: [
500
- '**/node_modules/**',
501
- `**/${config.webpack.output.buildDirectory}/**`,
502
- ],
503
- },
504
487
  experiments: {
505
488
  outputModule: true,
506
489
  },
@@ -545,4 +528,4 @@ const serverConfig = {
545
528
  const enableServerDualCompiler = config.experimental.ssr || (config.webpack.production && config.experimental.ssg?.prerender?.length > 0);
546
529
  export default enableServerDualCompiler
547
530
  ? [clientConfig, serverConfig]
548
- : clientConfig;
531
+ : clientConfig;