webspresso 0.0.98 → 0.0.99
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/.agents/ADMIN.md +11 -1
- package/.agents/AGENTS.md +1 -1
- package/.agents/AUTH.md +169 -0
- package/.agents/ORM.md +88 -4
- package/.agents/PLUGINS.md +73 -5
- package/.agents/QUEUE.md +84 -0
- package/.agents/ROUTING.md +127 -7
- package/AGENTS.md +1 -0
- package/core/auth/index.js +60 -0
- package/core/auth/manager.js +134 -0
- package/core/auth/middleware.js +88 -0
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/totp.js +504 -0
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +8 -0
- package/core/orm/model.js +4 -0
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +44 -3
- package/core/orm/search.js +213 -0
- package/core/queue/cron.js +331 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +82 -1
- package/core/queue/scheduler.js +359 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/index.d.ts +528 -0
- package/index.js +44 -0
- package/package.json +1 -1
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +323 -31
- package/plugins/admin-panel/client/parts/08-record-list.js +5 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- package/plugins/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -70
- package/plugins/audit-log/api-handlers.js +114 -0
- package/plugins/audit-log/index.js +24 -0
- package/plugins/file-manager/admin-component.js +20 -4
- package/plugins/file-manager/index.js +21 -4
- package/plugins/index.js +6 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/queue/index.js +4 -0
- package/src/api/api-loader.js +1 -0
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +137 -100
- package/src/helpers.js +9 -0
- package/src/routing/cascading.js +378 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +6 -0
|
@@ -158,8 +158,11 @@ function parseFileRoute(relativePath, options = {}) {
|
|
|
158
158
|
// Replace last segment with cleanFilename
|
|
159
159
|
const routeSegments = [...segments.slice(0, -1), cleanFilename];
|
|
160
160
|
|
|
161
|
+
// Filter out route group segments (e.g. (marketing), (auth))
|
|
162
|
+
const filteredSegments = routeSegments.filter((seg) => !isRouteGroupSegment(seg));
|
|
163
|
+
|
|
161
164
|
// Process dynamic brackets in segments
|
|
162
|
-
const transformedSegments =
|
|
165
|
+
const transformedSegments = filteredSegments.map(transformDynamicSegment);
|
|
163
166
|
|
|
164
167
|
// Handle 'index' at the end of path
|
|
165
168
|
if (transformedSegments[transformedSegments.length - 1] === 'index') {
|
|
@@ -190,8 +193,48 @@ function parseFileRoute(relativePath, options = {}) {
|
|
|
190
193
|
};
|
|
191
194
|
}
|
|
192
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Checks if a path segment is a route group, e.g. '(marketing)', '(auth)'
|
|
198
|
+
* @param {string} segment
|
|
199
|
+
* @returns {boolean}
|
|
200
|
+
*/
|
|
201
|
+
function isRouteGroupSegment(segment) {
|
|
202
|
+
return (
|
|
203
|
+
typeof segment === 'string' &&
|
|
204
|
+
segment.length > 2 &&
|
|
205
|
+
segment.charCodeAt(0) === 40 /* '(' */ &&
|
|
206
|
+
segment.charCodeAt(segment.length - 1) === 41 /* ')' */
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Checks if a file path belongs to an API route, accounting for optional parent route groups.
|
|
212
|
+
* E.g., 'api/users.get.js' -> true
|
|
213
|
+
* 'api/(v1)/users.get.js' -> true
|
|
214
|
+
* '(marketing)/api/newsletter.post.js' -> true
|
|
215
|
+
* 'about.njk' -> false
|
|
216
|
+
* @param {string} file
|
|
217
|
+
* @returns {boolean}
|
|
218
|
+
*/
|
|
219
|
+
function isApiRouteFile(file) {
|
|
220
|
+
if (!file || typeof file !== 'string') return false;
|
|
221
|
+
const normalized = file.split(/[/\\]/).join('/');
|
|
222
|
+
if (normalized.startsWith('api/')) return true;
|
|
223
|
+
const parts = normalized.split('/');
|
|
224
|
+
const apiIndex = parts.indexOf('api');
|
|
225
|
+
if (apiIndex > 0) {
|
|
226
|
+
const prefix = parts.slice(0, apiIndex);
|
|
227
|
+
if (prefix.every(isRouteGroupSegment)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
193
234
|
module.exports = {
|
|
194
235
|
parseFileRoute,
|
|
236
|
+
isRouteGroupSegment,
|
|
237
|
+
isApiRouteFile,
|
|
195
238
|
isPrivateOrIgnored,
|
|
196
239
|
normalizePrefix,
|
|
197
240
|
transformDynamicSegment,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const { parseFileRoute, isPrivateOrIgnored } = require('./file-route-parser');
|
|
11
|
+
const { parseFileRoute, isPrivateOrIgnored, isApiRouteFile } = require('./file-route-parser');
|
|
12
12
|
const { discoverModuleDetails } = require('../modules/module-discovery');
|
|
13
13
|
|
|
14
14
|
const SUPPORTED_EXTENSIONS = new Set(['.js', '.njk']);
|
|
@@ -99,7 +99,7 @@ function scanRoutes(options = {}) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// Skip api directory inside pages (handled by classic mountPages or explicit api scan)
|
|
102
|
-
const isUnderApi =
|
|
102
|
+
const isUnderApi = isApiRouteFile(relativePath);
|
|
103
103
|
if (isUnderApi) {
|
|
104
104
|
continue;
|
|
105
105
|
}
|
package/src/file-router.js
CHANGED
|
@@ -17,6 +17,19 @@ const {
|
|
|
17
17
|
registerClearHook,
|
|
18
18
|
} = require('./njk-frontmatter');
|
|
19
19
|
const { renderStream } = require('../core/ssr/stream');
|
|
20
|
+
const {
|
|
21
|
+
loadCascadingMiddlewares,
|
|
22
|
+
runMiddlewareChain,
|
|
23
|
+
findCascadingLayouts,
|
|
24
|
+
executeCascadingLayoutLoads,
|
|
25
|
+
applyCascadingLayouts,
|
|
26
|
+
templateHasExtends,
|
|
27
|
+
} = require('./routing/cascading');
|
|
28
|
+
const {
|
|
29
|
+
isMiddlewareFactory,
|
|
30
|
+
resolveNamedMiddleware,
|
|
31
|
+
resolveMiddlewares,
|
|
32
|
+
} = require('./routing/middleware-resolver');
|
|
20
33
|
|
|
21
34
|
const NOOP = () => {};
|
|
22
35
|
const EMPTY_OBJECT = Object.freeze({});
|
|
@@ -173,6 +186,73 @@ function escapeRegExp(s) {
|
|
|
173
186
|
return String(s).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
174
187
|
}
|
|
175
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Checks if a path segment is a route group, e.g. '(marketing)', '(auth)'.
|
|
191
|
+
* Route groups are enclosed in parentheses and do not affect the public URL path.
|
|
192
|
+
* @param {string} segment
|
|
193
|
+
* @returns {boolean}
|
|
194
|
+
*/
|
|
195
|
+
function isRouteGroupSegment(segment) {
|
|
196
|
+
return (
|
|
197
|
+
typeof segment === 'string' &&
|
|
198
|
+
segment.length > 2 &&
|
|
199
|
+
segment.charCodeAt(0) === 40 /* '(' */ &&
|
|
200
|
+
segment.charCodeAt(segment.length - 1) === 41 /* ')' */
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Strips route group segments from a slash-separated route path.
|
|
206
|
+
* E.g., '(marketing)/about' -> 'about'
|
|
207
|
+
* '/(auth)/login' -> '/login'
|
|
208
|
+
* '/(marketing)/(sub)/page' -> '/page'
|
|
209
|
+
* '(marketing)/index' -> 'index'
|
|
210
|
+
* @param {string} route
|
|
211
|
+
* @returns {string}
|
|
212
|
+
*/
|
|
213
|
+
function stripRouteGroups(route) {
|
|
214
|
+
if (!route || typeof route !== 'string' || !route.includes('(')) {
|
|
215
|
+
return route;
|
|
216
|
+
}
|
|
217
|
+
const hadLeadingSlash = route.startsWith('/');
|
|
218
|
+
const rawSegments = route.split('/');
|
|
219
|
+
const filtered = rawSegments.filter((seg) => !isRouteGroupSegment(seg));
|
|
220
|
+
|
|
221
|
+
if (filtered.length === 0 || (filtered.length === 1 && filtered[0] === '')) {
|
|
222
|
+
return hadLeadingSlash ? '/' : '';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let res = filtered.join('/');
|
|
226
|
+
if (hadLeadingSlash && !res.startsWith('/')) {
|
|
227
|
+
res = '/' + res;
|
|
228
|
+
}
|
|
229
|
+
return res;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Checks if a file path belongs to an API route, accounting for optional parent route groups.
|
|
234
|
+
* E.g., 'api/users.get.js' -> true
|
|
235
|
+
* 'api/(v1)/users.get.js' -> true
|
|
236
|
+
* '(marketing)/api/newsletter.post.js' -> true
|
|
237
|
+
* 'about.njk' -> false
|
|
238
|
+
* @param {string} file
|
|
239
|
+
* @returns {boolean}
|
|
240
|
+
*/
|
|
241
|
+
function isApiRouteFile(file) {
|
|
242
|
+
if (!file || typeof file !== 'string') return false;
|
|
243
|
+
const normalized = file.split(/[/\\]/).join('/');
|
|
244
|
+
if (normalized.startsWith('api/')) return true;
|
|
245
|
+
const parts = normalized.split('/');
|
|
246
|
+
const apiIndex = parts.indexOf('api');
|
|
247
|
+
if (apiIndex > 0) {
|
|
248
|
+
const prefix = parts.slice(0, apiIndex);
|
|
249
|
+
if (prefix.every(isRouteGroupSegment)) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
176
256
|
/**
|
|
177
257
|
* Convert a file path to an Express route pattern
|
|
178
258
|
* @param {string} filePath - Relative path from pages/
|
|
@@ -186,11 +266,14 @@ function filePathToRoute(filePath, ext) {
|
|
|
186
266
|
// Normalize path separators to forward slashes (handle both / and \)
|
|
187
267
|
route = route.split(path.sep).join('/');
|
|
188
268
|
route = route.split('\\').join('/'); // Also handle literal backslashes
|
|
269
|
+
|
|
270
|
+
// Strip route group segments (e.g. (marketing), (auth), (dashboard))
|
|
271
|
+
route = stripRouteGroups(route);
|
|
189
272
|
|
|
190
273
|
// Handle index files
|
|
191
274
|
if (route.endsWith('/index')) {
|
|
192
275
|
route = route.slice(0, -6) || '/';
|
|
193
|
-
} else if (route === 'index') {
|
|
276
|
+
} else if (route === 'index' || route === '') {
|
|
194
277
|
route = '/';
|
|
195
278
|
}
|
|
196
279
|
|
|
@@ -870,86 +953,6 @@ function detectLocale(req, supported = parseSupportedLocaleSet(), def = getDefau
|
|
|
870
953
|
return def;
|
|
871
954
|
}
|
|
872
955
|
|
|
873
|
-
/**
|
|
874
|
-
* True when the registry entry is (options) => (req, res, next) => …
|
|
875
|
-
* Express handlers typically have length >= 2 (req, res) or 3 (req, res, next).
|
|
876
|
-
*/
|
|
877
|
-
function isMiddlewareFactory(fn) {
|
|
878
|
-
return typeof fn === 'function' && fn.length <= 1;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
/**
|
|
882
|
-
* Resolve a named middleware from createApp({ middlewares }).
|
|
883
|
-
* @param {string} name
|
|
884
|
-
* @param {Function} entry
|
|
885
|
-
* @param {boolean} fromTuple - true when route used ['name', options]
|
|
886
|
-
* @param {unknown} tupleOptions - second element of the tuple (only when fromTuple)
|
|
887
|
-
* @param {Object} middlewareRegistry - for error messages
|
|
888
|
-
* @returns {Function} Express middleware
|
|
889
|
-
*/
|
|
890
|
-
function resolveNamedMiddleware(name, entry, fromTuple, tupleOptions, middlewareRegistry) {
|
|
891
|
-
if (!entry) {
|
|
892
|
-
throw new Error(`Middleware "${name}" not found in registry. Available: ${Object.keys(middlewareRegistry).join(', ') || 'none'}`);
|
|
893
|
-
}
|
|
894
|
-
if (typeof entry !== 'function') {
|
|
895
|
-
throw new Error(`Middleware "${name}" must be a function`);
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
if (fromTuple) {
|
|
899
|
-
if (!isMiddlewareFactory(entry)) {
|
|
900
|
-
throw new Error(
|
|
901
|
-
`Middleware "${name}" must be a factory (options) => (req, res, next) => … when using ["${name}", options] tuple form`
|
|
902
|
-
);
|
|
903
|
-
}
|
|
904
|
-
const produced = entry(tupleOptions);
|
|
905
|
-
if (typeof produced !== 'function') {
|
|
906
|
-
throw new Error(`Middleware factory "${name}" must return an Express middleware function`);
|
|
907
|
-
}
|
|
908
|
-
return produced;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
if (isMiddlewareFactory(entry)) {
|
|
912
|
-
const produced = entry({});
|
|
913
|
-
if (typeof produced !== 'function') {
|
|
914
|
-
throw new Error(`Middleware factory "${name}" must return an Express middleware function`);
|
|
915
|
-
}
|
|
916
|
-
return produced;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
return entry;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
/**
|
|
923
|
-
* Resolve middleware from config — functions, string names, or [name, options] tuples
|
|
924
|
-
* @param {Array} middlewareConfig - middleware functions, names, or ['name', options] tuples
|
|
925
|
-
* @param {Object} middlewareRegistry - Named middleware registry (plain handlers or option factories)
|
|
926
|
-
* @returns {Array} Array of resolved middleware functions
|
|
927
|
-
*/
|
|
928
|
-
function resolveMiddlewares(middlewareConfig, middlewareRegistry = EMPTY_OBJECT) {
|
|
929
|
-
if (!middlewareConfig || !Array.isArray(middlewareConfig) || middlewareConfig.length === 0) {
|
|
930
|
-
return EMPTY_ARRAY;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
return middlewareConfig.map((mw, index) => {
|
|
934
|
-
if (typeof mw === 'function') {
|
|
935
|
-
return mw;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
if (typeof mw === 'string') {
|
|
939
|
-
return resolveNamedMiddleware(mw, middlewareRegistry[mw], false, undefined, middlewareRegistry);
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
if (Array.isArray(mw) && mw.length === 2 && typeof mw[0] === 'string') {
|
|
943
|
-
const name = mw[0];
|
|
944
|
-
return resolveNamedMiddleware(name, middlewareRegistry[name], true, mw[1], middlewareRegistry);
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
throw new Error(
|
|
948
|
-
`Invalid middleware at index ${index}: must be a function, string name, or [name, options] tuple`
|
|
949
|
-
);
|
|
950
|
-
});
|
|
951
|
-
}
|
|
952
|
-
|
|
953
956
|
/**
|
|
954
957
|
* Mount all pages as routes on the Express app
|
|
955
958
|
* @param {Object} app - Express app
|
|
@@ -1073,7 +1076,7 @@ function mountPages(app, options) {
|
|
|
1073
1076
|
|
|
1074
1077
|
for (const file of files) {
|
|
1075
1078
|
const ext = path.extname(file);
|
|
1076
|
-
const isApi =
|
|
1079
|
+
const isApi = isApiRouteFile(file);
|
|
1077
1080
|
|
|
1078
1081
|
if (isApi && ext === '.js') {
|
|
1079
1082
|
// API route
|
|
@@ -1209,19 +1212,32 @@ function mountPages(app, options) {
|
|
|
1209
1212
|
throw err;
|
|
1210
1213
|
}
|
|
1211
1214
|
|
|
1215
|
+
// Run cascading folder middlewares (_middleware.js)
|
|
1216
|
+
const cascadingMw = loadCascadingMiddlewares(absolutePagesDir, route.file, middlewares, isDev);
|
|
1217
|
+
if (cascadingMw.length) {
|
|
1218
|
+
await runMiddlewareChain(cascadingMw, req, res);
|
|
1219
|
+
if (res.headersSent) return;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1212
1222
|
// Run middleware if defined (resolved at route registration — required for stateful middleware like express-rate-limit)
|
|
1213
1223
|
if (preResolvedMw.length) {
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
mw(req, res, (err) => {
|
|
1217
|
-
if (err) reject(err);
|
|
1218
|
-
else resolve();
|
|
1219
|
-
});
|
|
1220
|
-
});
|
|
1221
|
-
}
|
|
1224
|
+
await runMiddlewareChain(preResolvedMw, req, res);
|
|
1225
|
+
if (res.headersSent) return;
|
|
1222
1226
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1227
|
+
const apiCtx = Object.assign((err) => next(err), {
|
|
1228
|
+
req,
|
|
1229
|
+
res,
|
|
1230
|
+
next,
|
|
1231
|
+
params: req.params || {},
|
|
1232
|
+
query: req.query || {},
|
|
1233
|
+
body: req.body || {},
|
|
1234
|
+
input: req.input,
|
|
1235
|
+
service: req.service,
|
|
1236
|
+
db,
|
|
1237
|
+
sse: res.sse,
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
await fn(req, res, apiCtx);
|
|
1225
1241
|
} catch (err) {
|
|
1226
1242
|
console.error(`API error ${route.routePath}:`, err);
|
|
1227
1243
|
const hookCtx = { req, res, error: err };
|
|
@@ -1354,16 +1370,17 @@ function mountPages(app, options) {
|
|
|
1354
1370
|
hookPromise = runHooks(globalHooks, routeHooks, 'beforeMiddleware', ctx);
|
|
1355
1371
|
if (hookPromise) await hookPromise;
|
|
1356
1372
|
|
|
1373
|
+
// Run cascading folder middlewares (_middleware.js)
|
|
1374
|
+
const cascadingMw = loadCascadingMiddlewares(absolutePagesDir, route.file, middlewares, isDev);
|
|
1375
|
+
if (cascadingMw.length) {
|
|
1376
|
+
await runMiddlewareChain(cascadingMw, req, res);
|
|
1377
|
+
if (res.headersSent) return;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1357
1380
|
// Run route middleware (chain fixed at route registration; edit middleware in dev → restart)
|
|
1358
1381
|
if (preResolvedPageMw.length) {
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
mw(req, res, (err) => {
|
|
1362
|
-
if (err) reject(err);
|
|
1363
|
-
else resolve();
|
|
1364
|
-
});
|
|
1365
|
-
});
|
|
1366
|
-
}
|
|
1382
|
+
await runMiddlewareChain(preResolvedPageMw, req, res);
|
|
1383
|
+
if (res.headersSent) return;
|
|
1367
1384
|
}
|
|
1368
1385
|
|
|
1369
1386
|
// Execute hooks: afterMiddleware
|
|
@@ -1380,6 +1397,14 @@ function mountPages(app, options) {
|
|
|
1380
1397
|
hookPromise = runHooks(globalHooks, routeHooks, 'beforeLoad', ctx);
|
|
1381
1398
|
if (hookPromise) await hookPromise;
|
|
1382
1399
|
|
|
1400
|
+
// Find cascading layouts (_layout.njk)
|
|
1401
|
+
const cascadingLayouts = findCascadingLayouts(absolutePagesDir, route.file, isDev);
|
|
1402
|
+
|
|
1403
|
+
// Run cascading _layout.js load() functions in top-down order (outermost first)
|
|
1404
|
+
if (cascadingLayouts.length > 0) {
|
|
1405
|
+
await executeCascadingLayoutLoads(cascadingLayouts, req, ctx, isDev);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1383
1408
|
// Run load function
|
|
1384
1409
|
if (config?.load && typeof config.load === 'function') {
|
|
1385
1410
|
const loadData = await config.load(req, ctx);
|
|
@@ -1465,6 +1490,16 @@ function mountPages(app, options) {
|
|
|
1465
1490
|
html = nunjucks.render(templatePath, renderContext);
|
|
1466
1491
|
}
|
|
1467
1492
|
|
|
1493
|
+
// Apply cascading nested layouts if not opted-out and template does not extend another template
|
|
1494
|
+
const shouldApplyLayout = config?.layout !== false &&
|
|
1495
|
+
ctx.data?.layout !== false &&
|
|
1496
|
+
ctx.meta?.layout !== false &&
|
|
1497
|
+
!templateHasExtends(njkTpl.templateBody, route.fullPath);
|
|
1498
|
+
|
|
1499
|
+
if (shouldApplyLayout && cascadingLayouts.length > 0) {
|
|
1500
|
+
html = applyCascadingLayouts(nunjucks, cascadingLayouts, html, renderContext, isDev);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1468
1503
|
if (pluginManager) {
|
|
1469
1504
|
const contentApi = pluginManager.getPluginAPI('content');
|
|
1470
1505
|
if (contentApi?.maybeInjectInlineEdit) {
|
|
@@ -1535,6 +1570,8 @@ module.exports = {
|
|
|
1535
1570
|
mountPages,
|
|
1536
1571
|
filePathToRoute,
|
|
1537
1572
|
extractMethodFromFilename,
|
|
1573
|
+
isRouteGroupSegment,
|
|
1574
|
+
stripRouteGroups,
|
|
1538
1575
|
scanDirectory,
|
|
1539
1576
|
loadI18n,
|
|
1540
1577
|
createTranslator,
|
package/src/helpers.js
CHANGED
|
@@ -278,6 +278,15 @@ class AssetManager {
|
|
|
278
278
|
candidates.add(staticParts.join('/'));
|
|
279
279
|
}
|
|
280
280
|
}
|
|
281
|
+
if (cleanFile.includes('(')) {
|
|
282
|
+
const withoutGroups = cleanFile.split('/').filter(p => !(p.startsWith('(') && p.endsWith(')'))).join('/');
|
|
283
|
+
if (withoutGroups) {
|
|
284
|
+
candidates.add(withoutGroups);
|
|
285
|
+
if (withoutGroups.endsWith('/index')) {
|
|
286
|
+
candidates.add(withoutGroups.slice(0, -6));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
281
290
|
if (cleanFile === 'index') {
|
|
282
291
|
candidates.add('home');
|
|
283
292
|
}
|