@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 +1 -1
- package/webpack/utils/ApiRouterPlugin.mjs +546 -101
- package/webpack/webpack.config.mjs +2 -19
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.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/
|
|
14
|
-
this.
|
|
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
|
|
20
|
-
let watcher = null
|
|
13
|
+
let lastScanTime = 0;
|
|
14
|
+
let lastRoutes = null;
|
|
21
15
|
|
|
22
|
-
compiler.hooks.
|
|
23
|
-
|
|
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(
|
|
32
|
-
if (this.debug) console.log(`[
|
|
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(
|
|
26
|
+
params.contextDependencies.add(appDirPath)
|
|
40
27
|
}
|
|
41
28
|
|
|
42
29
|
try {
|
|
43
|
-
|
|
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('[
|
|
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('
|
|
48
|
+
compiler.hooks.afterCompile.tapAsync('AppRouterPlugin', (compilation, callback) => {
|
|
52
49
|
const appDirPath = path.resolve(process.cwd(), this.appDir)
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
156
|
+
const layoutMeta = extractMeta(layout) || {};
|
|
157
|
+
const indexMeta = extractMeta(index) || {};
|
|
158
|
+
let meta = {};
|
|
75
159
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
216
|
+
return node;
|
|
85
217
|
}
|
|
86
218
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
131
|
-
type: 'es6'
|
|
254
|
+
} else {
|
|
255
|
+
target[key] = source[key];
|
|
132
256
|
}
|
|
133
|
-
})
|
|
257
|
+
});
|
|
258
|
+
};
|
|
134
259
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
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;
|