@mercurjs/dashboard-sdk 2.3.8-canary.1 → 2.3.8-canary.3

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/dist/vite.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  } from "./chunk-3RG5ZIWI.js";
4
4
 
5
5
  // src/plugin.ts
6
- import path8 from "path";
7
- import fs7 from "fs";
6
+ import path5 from "path";
7
+ import fs6 from "fs";
8
8
  import { createRequire } from "module";
9
9
 
10
10
  // src/utils.ts
@@ -26,7 +26,8 @@ import {
26
26
  isStringLiteral,
27
27
  isVariableDeclaration,
28
28
  isVariableDeclarator,
29
- isArrayExpression
29
+ isArrayExpression,
30
+ isUnaryExpression
30
31
  } from "@babel/types";
31
32
  var traverse;
32
33
  if (typeof _traverse === "function") {
@@ -44,6 +45,8 @@ var I18N_VIRTUAL_MODULE = "virtual:mercur/i18n";
44
45
  var WIDGETS_VIRTUAL_MODULE = "virtual:mercur/widgets";
45
46
  var NAVIGATION_VIRTUAL_MODULE = "virtual:mercur/navigation";
46
47
  var CUSTOM_FIELDS_VIRTUAL_MODULE = "virtual:mercur/custom-fields";
48
+ var LAYOUTS_VIRTUAL_MODULE = "virtual:mercur/layouts";
49
+ var PREFERENCES_VIRTUAL_MODULE = "virtual:mercur/preferences";
47
50
  var RESOLVED_CONFIG_MODULE = "\0" + CONFIG_VIRTUAL_MODULE;
48
51
  var RESOLVED_ROUTES_MODULE = "\0" + ROUTES_VIRTUAL_MODULE;
49
52
  var RESOLVED_MENU_ITEMS_MODULE = "\0" + MENU_ITEMS_VIRTUAL_MODULE;
@@ -51,6 +54,8 @@ var RESOLVED_I18N_MODULE = "\0" + I18N_VIRTUAL_MODULE;
51
54
  var RESOLVED_WIDGETS_MODULE = "\0" + WIDGETS_VIRTUAL_MODULE;
52
55
  var RESOLVED_NAVIGATION_MODULE = "\0" + NAVIGATION_VIRTUAL_MODULE;
53
56
  var RESOLVED_CUSTOM_FIELDS_MODULE = "\0" + CUSTOM_FIELDS_VIRTUAL_MODULE;
57
+ var RESOLVED_LAYOUTS_MODULE = "\0" + LAYOUTS_VIRTUAL_MODULE;
58
+ var RESOLVED_PREFERENCES_MODULE = "\0" + PREFERENCES_VIRTUAL_MODULE;
54
59
  var VIRTUAL_MODULES = [
55
60
  CONFIG_VIRTUAL_MODULE,
56
61
  ROUTES_VIRTUAL_MODULE,
@@ -58,8 +63,12 @@ var VIRTUAL_MODULES = [
58
63
  I18N_VIRTUAL_MODULE,
59
64
  WIDGETS_VIRTUAL_MODULE,
60
65
  NAVIGATION_VIRTUAL_MODULE,
61
- CUSTOM_FIELDS_VIRTUAL_MODULE
66
+ CUSTOM_FIELDS_VIRTUAL_MODULE,
67
+ LAYOUTS_VIRTUAL_MODULE,
68
+ PREFERENCES_VIRTUAL_MODULE
62
69
  ];
70
+ var LAYOUT_PREFERENCES_FILE = "_layouts";
71
+ var NAVIGATION_FILE = "_navigation";
63
72
 
64
73
  // src/utils.ts
65
74
  function normalizePath(filePath) {
@@ -98,9 +107,9 @@ function resolveExports(moduleExports) {
98
107
  }
99
108
  return moduleExports;
100
109
  }
101
- async function getFileExports(path10) {
110
+ async function getFileExports(path6) {
102
111
  const { unregister } = await safeRegister();
103
- const module = __require(path10);
112
+ const module = __require(path6);
104
113
  unregister();
105
114
  return resolveExports(module);
106
115
  }
@@ -126,13 +135,13 @@ function hasDefaultExport(ast) {
126
135
  ExportDefaultDeclaration() {
127
136
  found = true;
128
137
  },
129
- AssignmentExpression(path10) {
130
- if (path10.node.left.type === "MemberExpression" && path10.node.left.object.type === "Identifier" && path10.node.left.object.name === "exports" && path10.node.left.property.type === "Identifier" && path10.node.left.property.name === "default") {
138
+ AssignmentExpression(path6) {
139
+ if (path6.node.left.type === "MemberExpression" && path6.node.left.object.type === "Identifier" && path6.node.left.object.name === "exports" && path6.node.left.property.type === "Identifier" && path6.node.left.property.name === "default") {
131
140
  found = true;
132
141
  }
133
142
  },
134
- ExportNamedDeclaration(path10) {
135
- const specifiers = path10.node.specifiers;
143
+ ExportNamedDeclaration(path6) {
144
+ const specifiers = path6.node.specifiers;
136
145
  if (specifiers?.some(
137
146
  (s) => s.type === "ExportSpecifier" && s.exported.type === "Identifier" && s.exported.name === "default"
138
147
  )) {
@@ -144,252 +153,16 @@ function hasDefaultExport(ast) {
144
153
  }
145
154
 
146
155
  // src/routes.ts
147
- import fs2 from "fs";
148
- import path2 from "path";
149
- function getRoute(file, routesDir) {
150
- const importPath = normalizePath(file);
151
- const normalizedRoutesDir = normalizePath(routesDir);
152
- return importPath.replace(normalizedRoutesDir, "").replace(/\[\[\*\]\]/g, "*?").replace(/\[\*\]/g, "*").replace(/\(([^[\])]+)\)/g, "$1?").replace(/\[\[([^\]]+)\]\]/g, ":$1?").replace(/\[([^\]]+)\]/g, ":$1").replace(
153
- new RegExp(
154
- `/page\\.(${VALID_FILE_EXTENSIONS.map((ext) => ext.slice(1)).join("|")})$`
155
- ),
156
- ""
157
- ) || "/";
158
- }
159
- function crawlRoutes(dir, pattern = "page") {
160
- const files = [];
161
- if (!fs2.existsSync(dir)) {
162
- return files;
163
- }
164
- const entries = fs2.readdirSync(dir, { withFileTypes: true });
165
- for (const entry of entries) {
166
- const fullPath = path2.join(dir, entry.name);
167
- if (entry.isDirectory()) {
168
- files.push(...crawlRoutes(fullPath, pattern));
169
- } else if (entry.isFile()) {
170
- const ext = path2.extname(entry.name);
171
- const baseName = path2.basename(entry.name, ext);
172
- if (baseName === pattern && VALID_FILE_EXTENSIONS.includes(ext)) {
173
- files.push(fullPath);
174
- }
175
- }
176
- }
177
- return files;
178
- }
179
- function hasConfigPublic(ast) {
180
- let found = false;
181
- traverse(ast, {
182
- ExportNamedDeclaration(path10) {
183
- const declaration = path10.node.declaration;
184
- if (!isVariableDeclaration(declaration)) return;
185
- for (const decl of declaration.declarations) {
186
- if (isVariableDeclarator(decl) && isIdentifier(decl.id, { name: "config" }) && decl.init?.type === "ObjectExpression") {
187
- const publicProp = decl.init.properties.find(
188
- (prop) => isObjectProperty(prop) && isIdentifier(prop.key, { name: "public" }) && isBooleanLiteral(prop.value, { value: true })
189
- );
190
- if (publicProp) {
191
- found = true;
192
- }
193
- }
194
- }
195
- }
196
- });
197
- return found;
198
- }
199
- function getNamedExports(ast) {
200
- let hasHandle = false;
201
- let hasLoader = false;
202
- traverse(ast, {
203
- ExportNamedDeclaration(path10) {
204
- const declaration = path10.node.declaration;
205
- if (declaration?.type === "VariableDeclaration") {
206
- declaration.declarations.forEach((decl) => {
207
- if (decl.id.type === "Identifier" && decl.id.name === "handle") {
208
- hasHandle = true;
209
- }
210
- if (decl.id.type === "Identifier" && decl.id.name === "loader") {
211
- hasLoader = true;
212
- }
213
- });
214
- }
215
- if (declaration?.type === "FunctionDeclaration" && declaration.id?.name === "loader") {
216
- hasLoader = true;
217
- }
218
- if (declaration?.type === "FunctionDeclaration" && declaration.id?.name === "handle") {
219
- hasHandle = true;
220
- }
221
- }
222
- });
223
- return { hasHandle, hasLoader };
224
- }
225
- function generateRouteComponentName(index) {
226
- return `RouteComponent${index}`;
227
- }
228
- function generateHandleName(index) {
229
- return `RouteHandle${index}`;
230
- }
231
- function generateLoaderName(index) {
232
- return `RouteLoader${index}`;
233
- }
234
- function generateImports(file, index, hasHandle, hasLoader) {
235
- const imports = [];
236
- const componentName = generateRouteComponentName(index);
237
- const importPath = normalizePath(file);
238
- if (!hasHandle && !hasLoader) {
239
- imports.push(`import ${componentName} from "${importPath}"`);
240
- } else {
241
- const namedImports = [
242
- hasHandle && `handle as ${generateHandleName(index)}`,
243
- hasLoader && `loader as ${generateLoaderName(index)}`
244
- ].filter(Boolean).join(", ");
245
- imports.push(`import ${componentName}, { ${namedImports} } from "${importPath}"`);
246
- }
247
- return imports;
248
- }
249
- function generateRouteObject(routePath, index, hasHandle, hasLoader, isPublic) {
250
- return {
251
- Component: generateRouteComponentName(index),
252
- path: routePath,
253
- handle: hasHandle ? generateHandleName(index) : void 0,
254
- loader: hasLoader ? generateLoaderName(index) : void 0,
255
- isPublic
256
- };
257
- }
258
- function formatRoute(route, indent = " ") {
259
- let result = `${indent}{
260
- `;
261
- result += `${indent} Component: ${route.Component},
262
- `;
263
- result += `${indent} path: "${route.path}"`;
264
- if (route.handle) {
265
- result += `,
266
- ${indent} handle: ${route.handle}`;
267
- }
268
- if (route.loader) {
269
- result += `,
270
- ${indent} loader: ${route.loader}`;
271
- }
272
- if (route.isPublic) {
273
- result += `,
274
- ${indent} isPublic: true`;
275
- }
276
- if (route.children?.length) {
277
- result += `,
278
- ${indent} children: [
279
- `;
280
- result += route.children.map((child) => formatRoute(child, indent + " ")).join(",\n");
281
- result += `
282
- ${indent} ]`;
283
- }
284
- result += `
285
- ${indent}}`;
286
- return result;
287
- }
288
- function parseFile(file, routesDir, index) {
289
- try {
290
- const code = fs2.readFileSync(file, "utf-8");
291
- const ast = parse(code, getParserOptions(file));
292
- if (!hasDefaultExport(ast)) {
293
- return null;
294
- }
295
- const { hasHandle, hasLoader } = getNamedExports(ast);
296
- const isPublic = hasConfigPublic(ast);
297
- const routePath = getRoute(file, routesDir);
298
- const imports = generateImports(file, index, hasHandle, hasLoader);
299
- const route = generateRouteObject(routePath, index, hasHandle, hasLoader, isPublic);
300
- return {
301
- imports,
302
- route
303
- };
304
- } catch {
305
- return null;
306
- }
307
- }
308
- function buildRouteTree(results) {
309
- const routeMap = /* @__PURE__ */ new Map();
310
- const sortedResults = [...results].sort(
311
- (a, b) => a.route.path.split("/").length - b.route.path.split("/").length
312
- );
313
- for (const result of sortedResults) {
314
- const routePath = result.route.path;
315
- const isParallel = routePath.includes("/@");
316
- if (isParallel) {
317
- const parentPath = routePath.split("/@")[0];
318
- const parent = routeMap.get(parentPath);
319
- if (parent) {
320
- parent.route.children = parent.route.children ?? [];
321
- parent.route.children.push({
322
- ...result.route,
323
- path: result.route.path.replace("/@", "/")
324
- });
325
- parent.imports.push(...result.imports);
326
- } else {
327
- routeMap.set(routePath, result);
328
- }
329
- } else {
330
- routeMap.set(routePath, result);
331
- }
332
- }
333
- return Array.from(routeMap.values());
334
- }
335
- function generateRoutes({ srcDir, pluginExtensions }) {
336
- const routesDir = path2.join(srcDir, "routes");
337
- let index = 0;
338
- const results = [];
339
- for (const file of crawlRoutes(routesDir)) {
340
- const result = parseFile(file, routesDir, index);
341
- if (result) {
342
- results.push(result);
343
- index++;
344
- }
345
- }
346
- const pluginDeclarations = pluginExtensions.map(
347
- (ext, i) => `import __plugin${i} from "${normalizePath(ext)}"`
348
- );
349
- const pluginSpreads = pluginExtensions.map(
350
- (_, i) => ` ...(__plugin${i}.routeModule?.routes ?? [])`
351
- );
352
- const routeTree = buildRouteTree(results);
353
- const appImports = routeTree.flatMap((r) => r.imports);
354
- const appRoutes = routeTree.map((r) => formatRoute(r.route));
355
- const allImports = [...appImports];
356
- const allRoutes = [...appRoutes, ...pluginSpreads];
357
- if (allImports.length === 0 && pluginDeclarations.length === 0 && allRoutes.length === 0) {
358
- return `export const customRoutes = []`;
359
- }
360
- return `${allImports.join("\n")}
361
-
362
- ${pluginDeclarations.join("\n")}
156
+ import fs5 from "fs";
157
+ import path4 from "path";
363
158
 
364
- export const customRoutes = [
365
- ${allRoutes.join(",\n")}
366
- ]`;
367
- }
159
+ // src/layers.ts
160
+ import fs4 from "fs";
161
+ import path3 from "path";
368
162
 
369
163
  // src/menu-items.ts
370
- import fs3 from "fs";
371
- import path3 from "path";
372
- function crawlRoutes2(dir, pattern = "page") {
373
- const files = [];
374
- if (!fs3.existsSync(dir)) {
375
- return files;
376
- }
377
- const entries = fs3.readdirSync(dir, { withFileTypes: true });
378
- for (const entry of entries) {
379
- const fullPath = path3.join(dir, entry.name);
380
- if (entry.isDirectory()) {
381
- files.push(...crawlRoutes2(fullPath, pattern));
382
- } else if (entry.isFile()) {
383
- const ext = path3.extname(entry.name);
384
- const baseName = path3.basename(entry.name, ext);
385
- if (baseName === pattern && VALID_FILE_EXTENSIONS.includes(ext)) {
386
- files.push(fullPath);
387
- }
388
- }
389
- }
390
- return files;
391
- }
392
- function getRoute2(file, routesDir) {
164
+ import fs2 from "fs";
165
+ function getRoute(file, routesDir) {
393
166
  const importPath = normalizePath(file);
394
167
  const normalizedRoutesDir = normalizePath(routesDir);
395
168
  return importPath.replace(normalizedRoutesDir, "").replace(/\[\[\*\]\]/g, "*?").replace(/\[\*\]/g, "*").replace(/\(([^[\])]+)\)/g, "$1?").replace(/\[\[([^\]]+)\]\]/g, ":$1?").replace(/\[([^\]]+)\]/g, ":$1").replace(
@@ -399,9 +172,9 @@ function getRoute2(file, routesDir) {
399
172
  ""
400
173
  ) || "/";
401
174
  }
402
- function getConfigObjectProperties(path10) {
403
- if (isVariableDeclarator(path10.node)) {
404
- const decl = isIdentifier(path10.node.id, { name: "config" }) ? path10.node : null;
175
+ function getConfigObjectProperties(path6) {
176
+ if (isVariableDeclarator(path6.node)) {
177
+ const decl = isIdentifier(path6.node.id, { name: "config" }) ? path6.node : null;
405
178
  if (!decl) return null;
406
179
  if (isCallExpression(decl.init) && decl.init.arguments.length > 0 && isObjectExpression(decl.init.arguments[0])) {
407
180
  return decl.init.arguments[0].properties;
@@ -411,7 +184,7 @@ function getConfigObjectProperties(path10) {
411
184
  }
412
185
  return null;
413
186
  }
414
- const declaration = path10.node.declaration;
187
+ const declaration = path6.node.declaration;
415
188
  if (isVariableDeclaration(declaration)) {
416
189
  const configDecl = declaration.declarations.find(
417
190
  (d) => isVariableDeclarator(d) && isIdentifier(d.id, { name: "config" })
@@ -487,21 +260,21 @@ function processConfigProperties(properties) {
487
260
  }
488
261
  function getRouteConfig(file) {
489
262
  try {
490
- const code = fs3.readFileSync(file, "utf-8");
263
+ const code = fs2.readFileSync(file, "utf-8");
491
264
  const ast = parse(code, getParserOptions(file));
492
265
  let config = null;
493
266
  let configFound = false;
494
267
  traverse(ast, {
495
- VariableDeclarator(path10) {
268
+ VariableDeclarator(path6) {
496
269
  if (configFound) return;
497
- const properties = getConfigObjectProperties(path10);
270
+ const properties = getConfigObjectProperties(path6);
498
271
  if (!properties) return;
499
272
  config = processConfigProperties(properties);
500
273
  if (config) configFound = true;
501
274
  },
502
- ExportNamedDeclaration(path10) {
275
+ ExportNamedDeclaration(path6) {
503
276
  if (configFound) return;
504
- const properties = getConfigObjectProperties(path10);
277
+ const properties = getConfigObjectProperties(path6);
505
278
  if (!properties) return;
506
279
  config = processConfigProperties(properties);
507
280
  if (config) configFound = true;
@@ -524,7 +297,7 @@ function generateMenuItem(config, file, routesDir, index) {
524
297
  return {
525
298
  label: `${configName}.label`,
526
299
  icon: config.icon ? `${configName}.icon` : void 0,
527
- path: getRoute2(file, routesDir),
300
+ path: getRoute(file, routesDir),
528
301
  rank: config.rank,
529
302
  nested: config.nested,
530
303
  group: config.group,
@@ -554,75 +327,485 @@ function parseMenuItemFile(file, routesDir, index) {
554
327
  if (!config) {
555
328
  return null;
556
329
  }
557
- return {
558
- import: generateImport(file, index),
559
- menuItem: generateMenuItem(config, file, routesDir, index)
560
- };
330
+ return {
331
+ import: generateImport(file, index),
332
+ menuItem: generateMenuItem(config, file, routesDir, index)
333
+ };
334
+ }
335
+ function generateMenuItems(config) {
336
+ const { header, names } = emitLayers(config);
337
+ return `${header}
338
+
339
+ const byPath = new Map()
340
+ for (const item of [
341
+ ${spreadLayers(names, "menuItemModule", "menuItems")}
342
+ ]) {
343
+ byPath.set(item.path, item)
344
+ }
345
+
346
+ export default { menuItems: [...byPath.values()] }`;
347
+ }
348
+
349
+ // src/widgets.ts
350
+ import fs3 from "fs";
351
+ import path2 from "path";
352
+ function readNumber(value) {
353
+ if (isNumericLiteral(value)) return value.value;
354
+ if (isUnaryExpression(value) && value.operator === "-" && isNumericLiteral(value.argument)) {
355
+ return -value.argument.value;
356
+ }
357
+ return void 0;
358
+ }
359
+ function extractStrings(value, zones) {
360
+ if (isStringLiteral(value)) {
361
+ zones.push(value.value);
362
+ } else if (isArrayExpression(value)) {
363
+ for (const el of value.elements) {
364
+ if (isStringLiteral(el)) zones.push(el.value);
365
+ }
366
+ }
367
+ }
368
+ function readWidgetConfig(properties) {
369
+ const zones = [];
370
+ const permissions = [];
371
+ let id;
372
+ let label;
373
+ let section;
374
+ let order;
375
+ let replace;
376
+ for (const prop of properties) {
377
+ if (!isObjectProperty(prop)) continue;
378
+ if (isIdentifier(prop.key, { name: "zone" })) {
379
+ extractStrings(prop.value, zones);
380
+ }
381
+ if (isIdentifier(prop.key, { name: "permission" })) {
382
+ extractStrings(prop.value, permissions);
383
+ }
384
+ if (isIdentifier(prop.key, { name: "id" }) && isStringLiteral(prop.value)) {
385
+ id = prop.value.value;
386
+ }
387
+ if (isIdentifier(prop.key, { name: "label" }) && isStringLiteral(prop.value)) {
388
+ label = prop.value.value;
389
+ }
390
+ if (isIdentifier(prop.key, { name: "section" }) && isStringLiteral(prop.value)) {
391
+ section = prop.value.value;
392
+ }
393
+ if (isIdentifier(prop.key, { name: "order" })) {
394
+ order = readNumber(prop.value);
395
+ }
396
+ if (isIdentifier(prop.key, { name: "replace" }) && isStringLiteral(prop.value)) {
397
+ replace = prop.value.value;
398
+ }
399
+ }
400
+ if (zones.length === 0) return null;
401
+ return { zones, id, label, permissions, section, order, replace };
402
+ }
403
+ function getConfigProperties(node) {
404
+ if (isCallExpression(node) && node.arguments.length > 0 && isObjectExpression(node.arguments[0])) {
405
+ return node.arguments[0].properties;
406
+ }
407
+ if (isObjectExpression(node)) {
408
+ return node.properties;
409
+ }
410
+ return null;
411
+ }
412
+ function getWidgetConfig(file) {
413
+ try {
414
+ const code = fs3.readFileSync(file, "utf-8");
415
+ const ast = parse(code, getParserOptions(file));
416
+ if (!hasDefaultExport(ast)) return null;
417
+ let info = null;
418
+ const visit = (declaration) => {
419
+ if (info) return;
420
+ if (!isVariableDeclaration(declaration)) return;
421
+ for (const decl of declaration.declarations) {
422
+ if (isVariableDeclarator(decl) && isIdentifier(decl.id, { name: "config" })) {
423
+ const props = getConfigProperties(decl.init);
424
+ if (props) info = readWidgetConfig(props);
425
+ }
426
+ }
427
+ };
428
+ traverse(ast, {
429
+ ExportNamedDeclaration(p) {
430
+ visit(p.node.declaration);
431
+ },
432
+ VariableDeclaration(p) {
433
+ visit(p.node);
434
+ }
435
+ });
436
+ return info;
437
+ } catch {
438
+ return null;
439
+ }
440
+ }
441
+ function hashPath(relative) {
442
+ let hash = 0;
443
+ for (let i = 0; i < relative.length; i++) {
444
+ hash = (hash << 5) - hash + relative.charCodeAt(i);
445
+ hash |= 0;
446
+ }
447
+ return Math.abs(hash).toString(36).slice(0, 6);
448
+ }
449
+ function widgetId(info, file, widgetsDir) {
450
+ if (info.id) return info.id;
451
+ const relative = normalizePath(path2.relative(widgetsDir, file));
452
+ return `Widget-${hashPath(relative)}`;
453
+ }
454
+ function parseWidgetFile(file, widgetsDir, index) {
455
+ const info = getWidgetConfig(file);
456
+ if (!info) return null;
457
+ const name = `Widget${index}`;
458
+ const importPath = normalizePath(file);
459
+ const zonesLiteral = JSON.stringify(info.zones);
460
+ const id = widgetId(info, file, widgetsDir);
461
+ return {
462
+ import: `import ${name} from "${importPath}"`,
463
+ entry: ` { Component: ${name}, zone: ${zonesLiteral}, widgetId: ${JSON.stringify(
464
+ id
465
+ )}${info.label ? `, label: ${JSON.stringify(info.label)}` : ""}${info.permissions.length ? `, permission: ${JSON.stringify(info.permissions)}` : ""}${info.section ? `, section: ${JSON.stringify(info.section)}` : ""}${info.order !== void 0 ? `, order: ${info.order}` : ""}${info.replace ? `, replace: ${JSON.stringify(info.replace)}` : ""} }`
466
+ };
467
+ }
468
+ function collectWidgets(srcDir, startIndex = 0) {
469
+ const widgetsDir = path2.join(srcDir, "widgets");
470
+ let index = startIndex;
471
+ const results = [];
472
+ for (const file of crawlModuleFiles(widgetsDir)) {
473
+ const result = parseWidgetFile(file, widgetsDir, index);
474
+ if (result) {
475
+ results.push(result);
476
+ index++;
477
+ }
478
+ }
479
+ return {
480
+ imports: results.map((r) => r.import),
481
+ entries: results.map((r) => r.entry)
482
+ };
483
+ }
484
+ function generateWidgets(config) {
485
+ const { header, names } = emitLayers(config);
486
+ return `${header}
487
+
488
+ const byId = new Map()
489
+ for (const widget of [
490
+ ${spreadLayers(names, "widgetModule", "widgets")}
491
+ ]) {
492
+ byId.set(widget.widgetId, widget)
493
+ }
494
+
495
+ export default { widgets: [...byId.values()] }`;
496
+ }
497
+
498
+ // src/layers.ts
499
+ function layerSources({
500
+ pluginExtensions,
501
+ projectDir,
502
+ srcDir
503
+ }) {
504
+ return [
505
+ ...pluginExtensions.map((entry) => ({ entry })),
506
+ ...projectDir ? [{ srcDir: projectDir }] : [],
507
+ { srcDir }
508
+ ];
509
+ }
510
+ function findFile(dir, stem) {
511
+ for (const ext of VALID_FILE_EXTENSIONS) {
512
+ const file = path3.join(dir, `${stem}${ext}`);
513
+ if (fs4.existsSync(file)) return file;
514
+ }
515
+ return null;
516
+ }
517
+ function collectLayer(srcDir, layer, startIndex = 0) {
518
+ const name = `__layer${layer}`;
519
+ const routesDir = path3.join(srcDir, "routes");
520
+ let index = startIndex;
521
+ const routeResults = [];
522
+ const menuItemResults = [];
523
+ for (const file of crawlRoutes(routesDir)) {
524
+ const route = parseFile(file, routesDir, index);
525
+ if (route) routeResults.push(route);
526
+ const menuItem = parseMenuItemFile(file, routesDir, index);
527
+ if (menuItem) menuItemResults.push(menuItem);
528
+ index++;
529
+ }
530
+ const routeTree = buildRouteTree(routeResults);
531
+ const routeImports = routeTree.flatMap((r) => r.imports);
532
+ const routes = routeTree.map((r) => formatRoute(r.route));
533
+ const menuItemImports = menuItemResults.map((r) => r.import);
534
+ const menuItems = menuItemResults.map((r) => formatMenuItem(r.menuItem));
535
+ const widgets = collectWidgets(srcDir, index);
536
+ index += widgets.entries.length;
537
+ const cfFiles = crawlModuleFiles(path3.join(srcDir, "custom-fields"));
538
+ const cfImports = cfFiles.map(
539
+ (file, i) => `import __cf${layer}_${i} from "${normalizePath(file)}"`
540
+ );
541
+ const cfRefs = cfFiles.map((_, i) => ` __cf${layer}_${i}`);
542
+ const layoutFiles = crawlModuleFiles(path3.join(srcDir, "layouts"));
543
+ const layoutImports = layoutFiles.map(
544
+ (file, i) => `import __layout${layer}_${i} from "${normalizePath(file)}"`
545
+ );
546
+ const layoutRefs = layoutFiles.map((_, i) => ` __layout${layer}_${i}`);
547
+ const i18nFile = findFile(path3.join(srcDir, "i18n"), "index");
548
+ const i18nName = `__i18n${layer}`;
549
+ const navigationFile = findFile(srcDir, NAVIGATION_FILE);
550
+ const navigationName = `__navigation${layer}`;
551
+ const preferencesFile = findFile(srcDir, LAYOUT_PREFERENCES_FILE);
552
+ const preferencesName = `__preferences${layer}`;
553
+ const imports = [
554
+ ...routeImports,
555
+ ...menuItemImports,
556
+ ...widgets.imports,
557
+ ...cfImports,
558
+ ...layoutImports,
559
+ ...i18nFile ? [`import ${i18nName} from "${normalizePath(i18nFile)}"`] : [],
560
+ ...navigationFile ? [`import ${navigationName} from "${normalizePath(navigationFile)}"`] : [],
561
+ ...preferencesFile ? [`import ${preferencesName} from "${normalizePath(preferencesFile)}"`] : []
562
+ ];
563
+ const declaration = `const ${name} = {
564
+ routeModule: { routes: [
565
+ ${routes.join(",\n")}
566
+ ] },
567
+ menuItemModule: { menuItems: [
568
+ ${menuItems.join(",\n")}
569
+ ] },
570
+ widgetModule: { widgets: [
571
+ ${widgets.entries.join(",\n")}
572
+ ] },
573
+ customFieldsModule: { configs: [
574
+ ${cfRefs.join(",\n")}
575
+ ] },
576
+ layoutModule: { layouts: [
577
+ ${layoutRefs.join(",\n")}
578
+ ] },
579
+ i18nModule: ${i18nFile ? i18nName : "{}"},
580
+ navigationModule: ${navigationFile ? navigationName : "{}"},
581
+ preferenceModule: ${preferencesFile ? preferencesName : "{}"},
582
+ }`;
583
+ return { imports, declaration, name, nextIndex: index };
584
+ }
585
+ function emitLayers(config) {
586
+ const lines = [];
587
+ const declarations = [];
588
+ const names = [];
589
+ let index = 0;
590
+ layerSources(config).forEach((source, layer) => {
591
+ if ("entry" in source) {
592
+ const name = `__layer${layer}`;
593
+ lines.push(`import ${name} from "${normalizePath(source.entry)}"`);
594
+ names.push(name);
595
+ return;
596
+ }
597
+ const collected = collectLayer(source.srcDir, layer, index);
598
+ index = collected.nextIndex;
599
+ lines.push(...collected.imports);
600
+ declarations.push(collected.declaration);
601
+ names.push(collected.name);
602
+ });
603
+ return { header: [...lines, ...declarations].join("\n"), names };
604
+ }
605
+ function spreadLayers(names, key, list) {
606
+ return names.map((name) => ` ...(${name}.${key}?.${list} ?? [])`).join(",\n");
607
+ }
608
+
609
+ // src/routes.ts
610
+ function getRoute2(file, routesDir) {
611
+ const importPath = normalizePath(file);
612
+ const normalizedRoutesDir = normalizePath(routesDir);
613
+ return importPath.replace(normalizedRoutesDir, "").replace(/\[\[\*\]\]/g, "*?").replace(/\[\*\]/g, "*").replace(/\(([^[\])]+)\)/g, "$1?").replace(/\[\[([^\]]+)\]\]/g, ":$1?").replace(/\[([^\]]+)\]/g, ":$1").replace(
614
+ new RegExp(
615
+ `/page\\.(${VALID_FILE_EXTENSIONS.map((ext) => ext.slice(1)).join("|")})$`
616
+ ),
617
+ ""
618
+ ) || "/";
619
+ }
620
+ function crawlRoutes(dir, pattern = "page") {
621
+ const files = [];
622
+ if (!fs5.existsSync(dir)) {
623
+ return files;
624
+ }
625
+ const entries = fs5.readdirSync(dir, { withFileTypes: true });
626
+ for (const entry of entries) {
627
+ const fullPath = path4.join(dir, entry.name);
628
+ if (entry.isDirectory()) {
629
+ files.push(...crawlRoutes(fullPath, pattern));
630
+ } else if (entry.isFile()) {
631
+ const ext = path4.extname(entry.name);
632
+ const baseName = path4.basename(entry.name, ext);
633
+ if (baseName === pattern && VALID_FILE_EXTENSIONS.includes(ext)) {
634
+ files.push(fullPath);
635
+ }
636
+ }
637
+ }
638
+ return files;
639
+ }
640
+ function hasConfigPublic(ast) {
641
+ let found = false;
642
+ traverse(ast, {
643
+ ExportNamedDeclaration(path6) {
644
+ const declaration = path6.node.declaration;
645
+ if (!isVariableDeclaration(declaration)) return;
646
+ for (const decl of declaration.declarations) {
647
+ if (isVariableDeclarator(decl) && isIdentifier(decl.id, { name: "config" }) && decl.init?.type === "ObjectExpression") {
648
+ const publicProp = decl.init.properties.find(
649
+ (prop) => isObjectProperty(prop) && isIdentifier(prop.key, { name: "public" }) && isBooleanLiteral(prop.value, { value: true })
650
+ );
651
+ if (publicProp) {
652
+ found = true;
653
+ }
654
+ }
655
+ }
656
+ }
657
+ });
658
+ return found;
659
+ }
660
+ function getNamedExports(ast) {
661
+ let hasHandle = false;
662
+ let hasLoader = false;
663
+ traverse(ast, {
664
+ ExportNamedDeclaration(path6) {
665
+ const declaration = path6.node.declaration;
666
+ if (declaration?.type === "VariableDeclaration") {
667
+ declaration.declarations.forEach((decl) => {
668
+ if (decl.id.type === "Identifier" && decl.id.name === "handle") {
669
+ hasHandle = true;
670
+ }
671
+ if (decl.id.type === "Identifier" && decl.id.name === "loader") {
672
+ hasLoader = true;
673
+ }
674
+ });
675
+ }
676
+ if (declaration?.type === "FunctionDeclaration" && declaration.id?.name === "loader") {
677
+ hasLoader = true;
678
+ }
679
+ if (declaration?.type === "FunctionDeclaration" && declaration.id?.name === "handle") {
680
+ hasHandle = true;
681
+ }
682
+ }
683
+ });
684
+ return { hasHandle, hasLoader };
685
+ }
686
+ function generateRouteComponentName(index) {
687
+ return `RouteComponent${index}`;
688
+ }
689
+ function generateHandleName(index) {
690
+ return `RouteHandle${index}`;
691
+ }
692
+ function generateLoaderName(index) {
693
+ return `RouteLoader${index}`;
694
+ }
695
+ function generateImports(file, index, hasHandle, hasLoader) {
696
+ const imports = [];
697
+ const componentName = generateRouteComponentName(index);
698
+ const importPath = normalizePath(file);
699
+ if (!hasHandle && !hasLoader) {
700
+ imports.push(`import ${componentName} from "${importPath}"`);
701
+ } else {
702
+ const namedImports = [
703
+ hasHandle && `handle as ${generateHandleName(index)}`,
704
+ hasLoader && `loader as ${generateLoaderName(index)}`
705
+ ].filter(Boolean).join(", ");
706
+ imports.push(`import ${componentName}, { ${namedImports} } from "${importPath}"`);
707
+ }
708
+ return imports;
709
+ }
710
+ function generateRouteObject(routePath, index, hasHandle, hasLoader, isPublic) {
711
+ return {
712
+ Component: generateRouteComponentName(index),
713
+ path: routePath,
714
+ handle: hasHandle ? generateHandleName(index) : void 0,
715
+ loader: hasLoader ? generateLoaderName(index) : void 0,
716
+ isPublic
717
+ };
718
+ }
719
+ function formatRoute(route, indent = " ") {
720
+ let result = `${indent}{
721
+ `;
722
+ result += `${indent} Component: ${route.Component},
723
+ `;
724
+ result += `${indent} path: "${route.path}"`;
725
+ if (route.handle) {
726
+ result += `,
727
+ ${indent} handle: ${route.handle}`;
728
+ }
729
+ if (route.loader) {
730
+ result += `,
731
+ ${indent} loader: ${route.loader}`;
732
+ }
733
+ if (route.isPublic) {
734
+ result += `,
735
+ ${indent} isPublic: true`;
736
+ }
737
+ if (route.children?.length) {
738
+ result += `,
739
+ ${indent} children: [
740
+ `;
741
+ result += route.children.map((child) => formatRoute(child, indent + " ")).join(",\n");
742
+ result += `
743
+ ${indent} ]`;
744
+ }
745
+ result += `
746
+ ${indent}}`;
747
+ return result;
561
748
  }
562
- function generateMenuItems({ srcDir, pluginExtensions }) {
563
- const routesDir = path3.join(srcDir, "routes");
564
- let index = 0;
565
- const results = [];
566
- for (const file of crawlRoutes2(routesDir)) {
567
- const result = parseMenuItemFile(file, routesDir, index);
568
- if (result) {
569
- results.push(result);
570
- index++;
749
+ function parseFile(file, routesDir, index) {
750
+ try {
751
+ const code = fs5.readFileSync(file, "utf-8");
752
+ const ast = parse(code, getParserOptions(file));
753
+ if (!hasDefaultExport(ast)) {
754
+ return null;
571
755
  }
756
+ const { hasHandle, hasLoader } = getNamedExports(ast);
757
+ const isPublic = hasConfigPublic(ast);
758
+ const routePath = getRoute2(file, routesDir);
759
+ const imports = generateImports(file, index, hasHandle, hasLoader);
760
+ const route = generateRouteObject(routePath, index, hasHandle, hasLoader, isPublic);
761
+ return {
762
+ imports,
763
+ route
764
+ };
765
+ } catch {
766
+ return null;
572
767
  }
573
- const pluginDeclarations = pluginExtensions.map(
574
- (ext, i) => `import __plugin${i} from "${normalizePath(ext)}"`
575
- );
576
- const pluginSpreads = pluginExtensions.map(
577
- (_, i) => ` ...(__plugin${i}.menuItemModule?.menuItems ?? [])`
768
+ }
769
+ function buildRouteTree(results) {
770
+ const routeMap = /* @__PURE__ */ new Map();
771
+ const sortedResults = [...results].sort(
772
+ (a, b) => a.route.path.split("/").length - b.route.path.split("/").length
578
773
  );
579
- const appImports = results.map((r) => r.import);
580
- const appMenuItems = results.map((r) => formatMenuItem(r.menuItem));
581
- const allImports = [...appImports];
582
- const allMenuItems = [...appMenuItems, ...pluginSpreads];
583
- if (allImports.length === 0 && pluginDeclarations.length === 0 && allMenuItems.length === 0) {
584
- return `export default { menuItems: [] }`;
774
+ for (const result of sortedResults) {
775
+ const routePath = result.route.path;
776
+ const isParallel = routePath.includes("/@");
777
+ if (isParallel) {
778
+ const parentPath = routePath.split("/@")[0];
779
+ const parent = routeMap.get(parentPath);
780
+ if (parent) {
781
+ parent.route.children = parent.route.children ?? [];
782
+ parent.route.children.push({
783
+ ...result.route,
784
+ path: result.route.path.replace("/@", "/")
785
+ });
786
+ parent.imports.push(...result.imports);
787
+ } else {
788
+ routeMap.set(routePath, result);
789
+ }
790
+ } else {
791
+ routeMap.set(routePath, result);
792
+ }
585
793
  }
586
- return `${allImports.join("\n")}
587
-
588
- ${pluginDeclarations.join("\n")}
794
+ return Array.from(routeMap.values());
795
+ }
796
+ function generateRoutes(config) {
797
+ const { header, names } = emitLayers(config);
798
+ return `${header}
589
799
 
590
- export default {
591
- menuItems: [
592
- ${allMenuItems.join(",\n")}
593
- ]
594
- }`;
800
+ export const customRoutes = [
801
+ ${spreadLayers(names, "routeModule", "routes")}
802
+ ]`;
595
803
  }
596
804
 
597
805
  // src/i18n.ts
598
- import fs4 from "fs";
599
- import path4 from "path";
600
- function findI18nIndex(srcDir) {
601
- const i18nDir = path4.join(srcDir, "i18n");
602
- if (!fs4.existsSync(i18nDir)) {
603
- return null;
604
- }
605
- for (const ext of VALID_FILE_EXTENSIONS) {
606
- const filePath = path4.join(i18nDir, `index${ext}`);
607
- if (fs4.existsSync(filePath)) {
608
- return filePath;
609
- }
610
- }
611
- return null;
612
- }
613
- function generateI18n({ srcDir, pluginExtensions }) {
614
- const indexFile = findI18nIndex(srcDir);
615
- const imports = [
616
- ...indexFile ? [`import appI18n from "${normalizePath(indexFile)}"`] : [],
617
- ...pluginExtensions.map(
618
- (ext, i) => `import __plugin${i} from "${normalizePath(ext)}"`
619
- )
620
- ];
621
- const sources = [
622
- ...pluginExtensions.map((_, i) => `(__plugin${i}.i18nModule ?? {})`),
623
- ...indexFile ? ["appI18n"] : []
624
- ];
625
- return `${imports.join("\n")}
806
+ function generateI18n(config) {
807
+ const { header, names } = emitLayers(config);
808
+ return `${header}
626
809
 
627
810
  function deepMerge(target, source) {
628
811
  const result = { ...target }
@@ -648,205 +831,62 @@ function mergeResources(target, source) {
648
831
  return result
649
832
  }
650
833
 
651
- export default [${sources.join(", ")}].reduce(mergeResources, {})
834
+ export default [${names.map((name) => `(${name}.i18nModule ?? {})`).join(", ")}].reduce(mergeResources, {})
652
835
  `;
653
836
  }
654
837
 
655
- // src/widgets.ts
656
- import fs5 from "fs";
657
- import path5 from "path";
658
- function extractStrings(value, zones) {
659
- if (isStringLiteral(value)) {
660
- zones.push(value.value);
661
- } else if (isArrayExpression(value)) {
662
- for (const el of value.elements) {
663
- if (isStringLiteral(el)) zones.push(el.value);
664
- }
665
- }
666
- }
667
- function readWidgetConfig(properties) {
668
- const zones = [];
669
- const permissions = [];
670
- let id;
671
- let label;
672
- for (const prop of properties) {
673
- if (!isObjectProperty(prop)) continue;
674
- if (isIdentifier(prop.key, { name: "zone" })) {
675
- extractStrings(prop.value, zones);
676
- }
677
- if (isIdentifier(prop.key, { name: "permission" })) {
678
- extractStrings(prop.value, permissions);
679
- }
680
- if (isIdentifier(prop.key, { name: "id" }) && isStringLiteral(prop.value)) {
681
- id = prop.value.value;
838
+ // src/navigation.ts
839
+ function generateNavigation(config) {
840
+ const { header, names } = emitLayers(config);
841
+ return `${header}
842
+
843
+ const groups = new Map()
844
+ const items = new Map()
845
+ for (const navigation of [${names.map((name) => `${name}.navigationModule ?? {}`).join(", ")}]) {
846
+ for (const group of navigation.groups ?? []) {
847
+ if (group?.id) groups.set(group.id, { ...(groups.get(group.id) ?? {}), ...group })
682
848
  }
683
- if (isIdentifier(prop.key, { name: "label" }) && isStringLiteral(prop.value)) {
684
- label = prop.value.value;
849
+ for (const item of navigation.items ?? []) {
850
+ if (item?.id) items.set(item.id, { ...(items.get(item.id) ?? {}), ...item })
685
851
  }
686
- }
687
- if (zones.length === 0) return null;
688
- return { zones, id, label, permissions };
689
- }
690
- function getConfigProperties(node) {
691
- if (isCallExpression(node) && node.arguments.length > 0 && isObjectExpression(node.arguments[0])) {
692
- return node.arguments[0].properties;
693
- }
694
- if (isObjectExpression(node)) {
695
- return node.properties;
696
- }
697
- return null;
698
- }
699
- function getWidgetConfig(file) {
700
- try {
701
- const code = fs5.readFileSync(file, "utf-8");
702
- const ast = parse(code, getParserOptions(file));
703
- if (!hasDefaultExport(ast)) return null;
704
- let info = null;
705
- const visit = (declaration) => {
706
- if (info) return;
707
- if (!isVariableDeclaration(declaration)) return;
708
- for (const decl of declaration.declarations) {
709
- if (isVariableDeclarator(decl) && isIdentifier(decl.id, { name: "config" })) {
710
- const props = getConfigProperties(decl.init);
711
- if (props) info = readWidgetConfig(props);
712
- }
713
- }
714
- };
715
- traverse(ast, {
716
- ExportNamedDeclaration(p) {
717
- visit(p.node.declaration);
718
- },
719
- VariableDeclaration(p) {
720
- visit(p.node);
721
- }
722
- });
723
- return info;
724
- } catch {
725
- return null;
726
- }
727
- }
728
- function hashPath(relative) {
729
- let hash = 0;
730
- for (let i = 0; i < relative.length; i++) {
731
- hash = (hash << 5) - hash + relative.charCodeAt(i);
732
- hash |= 0;
733
- }
734
- return Math.abs(hash).toString(36).slice(0, 6);
735
- }
736
- function widgetId(info, file, widgetsDir) {
737
- if (info.id) return info.id;
738
- const relative = normalizePath(path5.relative(widgetsDir, file));
739
- return `Widget-${hashPath(relative)}`;
740
- }
741
- function parseWidgetFile(file, widgetsDir, index) {
742
- const info = getWidgetConfig(file);
743
- if (!info) return null;
744
- const name = `Widget${index}`;
745
- const importPath = normalizePath(file);
746
- const zonesLiteral = JSON.stringify(info.zones);
747
- const id = widgetId(info, file, widgetsDir);
748
- return {
749
- import: `import ${name} from "${importPath}"`,
750
- entry: ` { Component: ${name}, zone: ${zonesLiteral}, widgetId: ${JSON.stringify(
751
- id
752
- )}${info.label ? `, label: ${JSON.stringify(info.label)}` : ""}${info.permissions.length ? `, permission: ${JSON.stringify(info.permissions)}` : ""} }`
753
- };
754
852
  }
755
- function collectWidgets(srcDir, startIndex = 0) {
756
- const widgetsDir = path5.join(srcDir, "widgets");
757
- let index = startIndex;
758
- const results = [];
759
- for (const file of crawlModuleFiles(widgetsDir)) {
760
- const result = parseWidgetFile(file, widgetsDir, index);
761
- if (result) {
762
- results.push(result);
763
- index++;
764
- }
765
- }
766
- return {
767
- imports: results.map((r) => r.import),
768
- entries: results.map((r) => r.entry)
769
- };
853
+
854
+ export default { groups: [...groups.values()], items: [...items.values()] }`;
770
855
  }
771
- function generateWidgets({
772
- srcDir,
773
- pluginExtensions
774
- }) {
775
- const { imports, entries: widgetEntries } = collectWidgets(srcDir);
776
- const pluginDeclarations = pluginExtensions.map(
777
- (ext, i) => `import __plugin${i} from "${normalizePath(ext)}"`
778
- );
779
- const pluginSpreads = pluginExtensions.map(
780
- (_, i) => ` ...(__plugin${i}.widgetModule?.widgets ?? [])`
781
- );
782
- const entries = [...widgetEntries, ...pluginSpreads];
783
- if (imports.length === 0 && pluginDeclarations.length === 0 && entries.length === 0) {
784
- return `export default { widgets: [] }`;
785
- }
786
- return `${imports.join("\n")}
787
856
 
788
- ${pluginDeclarations.join("\n")}
857
+ // src/custom-fields.ts
858
+ function generateCustomFields(config) {
859
+ const { header, names } = emitLayers(config);
860
+ return `${header}
789
861
 
790
862
  export default {
791
- widgets: [
792
- ${entries.join(",\n")}
863
+ configs: [
864
+ ${spreadLayers(names, "customFieldsModule", "configs")}
793
865
  ]
794
866
  }`;
795
867
  }
796
868
 
797
- // src/navigation.ts
798
- import fs6 from "fs";
799
- import path6 from "path";
800
- function findNavigationFile(srcDir) {
801
- for (const ext of VALID_FILE_EXTENSIONS) {
802
- const filePath = path6.join(srcDir, `_navigation${ext}`);
803
- if (fs6.existsSync(filePath)) return filePath;
804
- }
805
- return null;
806
- }
807
- function generateNavigation({ srcDir }) {
808
- const file = findNavigationFile(srcDir);
809
- if (!file) {
810
- return `export default { items: [] }`;
811
- }
812
- return `import navigationConfig from "${normalizePath(file)}"
813
- export default navigationConfig`;
869
+ // src/layouts-module.ts
870
+ function generateLayouts(config) {
871
+ const { header, names } = emitLayers(config);
872
+ return `${header}
873
+
874
+ const byId = new Map()
875
+ for (const layout of [
876
+ ${spreadLayers(names, "layoutModule", "layouts")}
877
+ ]) {
878
+ if (layout?.id) byId.set(layout.id, layout)
814
879
  }
815
880
 
816
- // src/custom-fields.ts
817
- import path7 from "path";
818
- function generateCustomFields({
819
- srcDir,
820
- pluginExtensions
821
- }) {
822
- const dir = path7.join(srcDir, "custom-fields");
823
- const files = crawlModuleFiles(dir);
824
- const imports = files.map(
825
- (file, i) => `import __cf${i} from "${normalizePath(file)}"`
826
- );
827
- const localRefs = files.map((_, i) => `__cf${i}`);
828
- const pluginDeclarations = pluginExtensions.map(
829
- (ext, i) => `const __plugin${i} = (await import("${normalizePath(ext)}")).default`
830
- );
831
- const pluginSpreads = pluginExtensions.map(
832
- (_, i) => ` ...(__plugin${i}.customFieldsModule?.configs ?? [])`
833
- );
834
- const configs = [
835
- ...pluginSpreads,
836
- ...localRefs.map((ref) => ` ${ref}`)
837
- ];
838
- if (configs.length === 0 && pluginDeclarations.length === 0) {
839
- return `export default { configs: [] }`;
840
- }
841
- return `${imports.join("\n")}
881
+ export default { layouts: [...byId.values()] }`;
882
+ }
842
883
 
843
- ${pluginDeclarations.join("\n")}
884
+ // src/preferences-module.ts
885
+ function generatePreferences(config) {
886
+ const { header, names } = emitLayers(config);
887
+ return `${header}
844
888
 
845
- export default {
846
- configs: [
847
- ${configs.join(",\n")}
848
- ]
849
- }`;
889
+ export default { layers: [${names.map((name) => `${name}.preferenceModule ?? {}`).join(", ")}] }`;
850
890
  }
851
891
 
852
892
  // src/virtual-modules.ts
@@ -881,6 +921,12 @@ function loadVirtualModule({
881
921
  if (id === RESOLVED_CUSTOM_FIELDS_MODULE) {
882
922
  return generateCustomFields(mercurConfig);
883
923
  }
924
+ if (id === RESOLVED_LAYOUTS_MODULE) {
925
+ return generateLayouts(mercurConfig);
926
+ }
927
+ if (id === RESOLVED_PREFERENCES_MODULE) {
928
+ return generatePreferences(mercurConfig);
929
+ }
884
930
  return null;
885
931
  }
886
932
  function loadConfigModule(mercurConfig) {
@@ -898,19 +944,42 @@ function loadI18nModule(mercurConfig) {
898
944
 
899
945
  // src/plugin.ts
900
946
  function isRouteFile(file) {
901
- const basename = path8.basename(file, path8.extname(file));
947
+ const basename = path5.basename(file, path5.extname(file));
902
948
  return basename === "page";
903
949
  }
904
950
  function isWidgetFile(file) {
905
951
  return normalizeSep(file).includes("/src/widgets/");
906
952
  }
907
953
  function isNavigationFile(file) {
908
- const basename = path8.basename(file, path8.extname(file));
909
- return basename === "_navigation";
954
+ const basename = path5.basename(file, path5.extname(file));
955
+ return basename === NAVIGATION_FILE;
956
+ }
957
+ function isPreferencesFile(file) {
958
+ const basename = path5.basename(file, path5.extname(file));
959
+ return basename === LAYOUT_PREFERENCES_FILE;
910
960
  }
911
961
  function isCustomFieldFile(file) {
912
962
  return normalizeSep(file).includes("/src/custom-fields/");
913
963
  }
964
+ function isLayoutFile(file) {
965
+ return normalizeSep(file).includes("/layouts/");
966
+ }
967
+ function isI18nFile(file) {
968
+ return normalizeSep(file).includes("/i18n/");
969
+ }
970
+ var LAYERED_MODULES = [
971
+ RESOLVED_ROUTES_MODULE,
972
+ RESOLVED_MENU_ITEMS_MODULE,
973
+ RESOLVED_WIDGETS_MODULE,
974
+ RESOLVED_NAVIGATION_MODULE,
975
+ RESOLVED_CUSTOM_FIELDS_MODULE,
976
+ RESOLVED_LAYOUTS_MODULE,
977
+ RESOLVED_PREFERENCES_MODULE,
978
+ RESOLVED_I18N_MODULE
979
+ ];
980
+ function isExtensionFile(file) {
981
+ return isRouteFile(file) || isWidgetFile(file) || isNavigationFile(file) || isPreferencesFile(file) || isCustomFieldFile(file) || isLayoutFile(file) || isI18nFile(file);
982
+ }
914
983
  function normalizeSep(file) {
915
984
  return file.replace(/\\/g, "/");
916
985
  }
@@ -931,7 +1000,7 @@ function isMedusaVirtualModule(id) {
931
1000
  return MEDUSA_VIRTUAL_MODULES.includes(id);
932
1001
  }
933
1002
  function filterResolvableDeps(specifiers, fromDir) {
934
- const require2 = createRequire(path8.join(fromDir, "noop.js"));
1003
+ const require2 = createRequire(path5.join(fromDir, "noop.js"));
935
1004
  return specifiers.filter((specifier) => {
936
1005
  try {
937
1006
  require2.resolve(specifier);
@@ -949,44 +1018,44 @@ function isResolvedMedusaVirtualModule(id) {
949
1018
  }
950
1019
  function findNodeModulesRoot(configDir) {
951
1020
  let dir = configDir;
952
- while (dir !== path8.dirname(dir)) {
953
- const candidate = path8.join(dir, "node_modules");
954
- if (fs7.existsSync(candidate) && fs7.statSync(candidate).isDirectory()) {
1021
+ while (dir !== path5.dirname(dir)) {
1022
+ const candidate = path5.join(dir, "node_modules");
1023
+ if (fs6.existsSync(candidate) && fs6.statSync(candidate).isDirectory()) {
955
1024
  return candidate;
956
1025
  }
957
- dir = path8.dirname(dir);
1026
+ dir = path5.dirname(dir);
958
1027
  }
959
- return path8.join(configDir, "node_modules");
1028
+ return path5.join(configDir, "node_modules");
960
1029
  }
961
1030
  function resolvePluginRoot(resolve, configDir, nodeModulesRoot) {
962
1031
  if (resolve.startsWith(".")) {
963
1032
  try {
964
- const resolved = path8.resolve(configDir, resolve);
965
- return fs7.existsSync(resolved) ? fs7.realpathSync(resolved) : null;
1033
+ const resolved = path5.resolve(configDir, resolve);
1034
+ return fs6.existsSync(resolved) ? fs6.realpathSync(resolved) : null;
966
1035
  } catch {
967
1036
  return null;
968
1037
  }
969
1038
  }
970
- const require2 = createRequire(path8.join(configDir, "noop.js"));
1039
+ const require2 = createRequire(path5.join(configDir, "noop.js"));
971
1040
  try {
972
- return fs7.realpathSync(
973
- path8.dirname(require2.resolve(`${resolve}/package.json`))
1041
+ return fs6.realpathSync(
1042
+ path5.dirname(require2.resolve(`${resolve}/package.json`))
974
1043
  );
975
1044
  } catch {
976
1045
  }
977
1046
  try {
978
- let dir = path8.dirname(require2.resolve(resolve));
979
- while (dir !== path8.dirname(dir)) {
980
- if (fs7.existsSync(path8.join(dir, "package.json"))) {
981
- return fs7.realpathSync(dir);
1047
+ let dir = path5.dirname(require2.resolve(resolve));
1048
+ while (dir !== path5.dirname(dir)) {
1049
+ if (fs6.existsSync(path5.join(dir, "package.json"))) {
1050
+ return fs6.realpathSync(dir);
982
1051
  }
983
- dir = path8.dirname(dir);
1052
+ dir = path5.dirname(dir);
984
1053
  }
985
1054
  } catch {
986
1055
  }
987
1056
  try {
988
- const packagePath = path8.join(nodeModulesRoot, resolve);
989
- return fs7.existsSync(packagePath) ? fs7.realpathSync(packagePath) : null;
1057
+ const packagePath = path5.join(nodeModulesRoot, resolve);
1058
+ return fs6.existsSync(packagePath) ? fs6.realpathSync(packagePath) : null;
990
1059
  } catch {
991
1060
  return null;
992
1061
  }
@@ -1004,13 +1073,13 @@ function resolvePluginExtensions(plugins, configDir, appType) {
1004
1073
  );
1005
1074
  continue;
1006
1075
  }
1007
- const extFile = path8.join(
1076
+ const extFile = path5.join(
1008
1077
  pluginRoot,
1009
1078
  ".medusa/server/src",
1010
1079
  appType,
1011
1080
  "index.mjs"
1012
1081
  );
1013
- if (fs7.existsSync(extFile)) {
1082
+ if (fs6.existsSync(extFile)) {
1014
1083
  extensions.push(extFile);
1015
1084
  }
1016
1085
  }
@@ -1024,7 +1093,7 @@ function trimTrailingSlashes(value) {
1024
1093
  return end === value.length ? value : value.slice(0, end);
1025
1094
  }
1026
1095
  async function loadMedusaConfig(medusaConfigPath, root, options) {
1027
- const configDir = path8.dirname(medusaConfigPath);
1096
+ const configDir = path5.dirname(medusaConfigPath);
1028
1097
  try {
1029
1098
  const previousCwd = process.cwd();
1030
1099
  let mod;
@@ -1054,7 +1123,7 @@ async function loadMedusaConfig(medusaConfigPath, root, options) {
1054
1123
  const value = modules[key];
1055
1124
  if (!value || typeof value !== "object" || !value.options?.appDir)
1056
1125
  continue;
1057
- const appDir = path8.resolve(configDir, value.options.appDir);
1126
+ const appDir = path5.resolve(configDir, value.options.appDir);
1058
1127
  if (appDir === root) {
1059
1128
  base = value.options.path;
1060
1129
  appType = key === "vendor_ui" ? "vendor" : "admin";
@@ -1065,7 +1134,9 @@ async function loadMedusaConfig(medusaConfigPath, root, options) {
1065
1134
  (plugin) => plugin.resolve !== "@medusajs/draft-order"
1066
1135
  ) ?? [];
1067
1136
  const pluginExtensions = resolvePluginExtensions(plugins, configDir, appType);
1068
- return { base, pluginExtensions, vendorAppUrl };
1137
+ const projectCandidate = path5.join(configDir, "src", appType);
1138
+ const projectDir = fs6.existsSync(projectCandidate) && fs6.realpathSync(projectCandidate) !== root ? projectCandidate : void 0;
1139
+ return { base, pluginExtensions, projectDir, vendorAppUrl };
1069
1140
  } catch (error) {
1070
1141
  console.warn(
1071
1142
  `[@mercurjs/dashboard-sdk] Could not load the Medusa config from "${medusaConfigPath}": ${error instanceof Error ? error.message : String(error)}. Building with base "/" and no plugin extensions \u2014 if this panel is served under a sub-path (e.g. /dashboard), its assets will not resolve.`
@@ -1081,11 +1152,11 @@ function mercurDashboardPlugin(pluginConfig) {
1081
1152
  async config(viteConfig) {
1082
1153
  root = viteConfig.root || process.cwd();
1083
1154
  const isDevelopment = (viteConfig.mode || process.env.NODE_ENV || "development") !== "production";
1084
- const medusaConfigPath = path8.resolve(
1155
+ const medusaConfigPath = path5.resolve(
1085
1156
  root,
1086
1157
  pluginConfig.medusaConfigPath
1087
1158
  );
1088
- const { base, pluginExtensions, vendorAppUrl } = await loadMedusaConfig(
1159
+ const { base, pluginExtensions, projectDir, vendorAppUrl } = await loadMedusaConfig(
1089
1160
  medusaConfigPath,
1090
1161
  root,
1091
1162
  {
@@ -1093,7 +1164,7 @@ function mercurDashboardPlugin(pluginConfig) {
1093
1164
  vendorUrl: pluginConfig.vendorUrl
1094
1165
  }
1095
1166
  );
1096
- const srcDir = path8.join(root, "src");
1167
+ const srcDir = path5.join(root, "src");
1097
1168
  const backendUrl = pluginConfig.backendUrl ?? "http://localhost:9000";
1098
1169
  const imageLimit = pluginConfig.imageLimit ?? 2 * 1024 * 1024;
1099
1170
  config = {
@@ -1102,6 +1173,7 @@ function mercurDashboardPlugin(pluginConfig) {
1102
1173
  base,
1103
1174
  root,
1104
1175
  srcDir,
1176
+ projectDir,
1105
1177
  pluginExtensions,
1106
1178
  imageLimit
1107
1179
  };
@@ -1116,16 +1188,7 @@ function mercurDashboardPlugin(pluginConfig) {
1116
1188
  dedupe: ["i18next", "react-i18next", "react", "react-dom"]
1117
1189
  },
1118
1190
  optimizeDeps: {
1119
- exclude: [
1120
- "virtual:mercur/config",
1121
- "virtual:mercur/routes",
1122
- "virtual:mercur/menu-items",
1123
- "virtual:mercur/i18n",
1124
- "virtual:mercur/widgets",
1125
- "virtual:mercur/navigation",
1126
- "virtual:mercur/custom-fields",
1127
- ...MEDUSA_VIRTUAL_MODULES
1128
- ],
1191
+ exclude: [...VIRTUAL_MODULES, ...MEDUSA_VIRTUAL_MODULES],
1129
1192
  include: filterResolvableDeps(
1130
1193
  [
1131
1194
  "react",
@@ -1171,10 +1234,9 @@ function mercurDashboardPlugin(pluginConfig) {
1171
1234
  }
1172
1235
  };
1173
1236
  const handleChange = (file) => {
1174
- if (isRouteFile(file)) invalidate(RESOLVED_ROUTES_MODULE, true);
1175
- if (isWidgetFile(file)) invalidate(RESOLVED_WIDGETS_MODULE, true);
1176
- if (isNavigationFile(file)) invalidate(RESOLVED_NAVIGATION_MODULE, true);
1177
- if (isCustomFieldFile(file)) invalidate(RESOLVED_CUSTOM_FIELDS_MODULE, true);
1237
+ if (!isExtensionFile(file)) return;
1238
+ for (const moduleId of LAYERED_MODULES) invalidate(moduleId, false);
1239
+ server.ws.send({ type: "full-reload" });
1178
1240
  };
1179
1241
  server.watcher.on("add", handleChange);
1180
1242
  server.watcher.on("unlink", handleChange);
@@ -1184,103 +1246,21 @@ function mercurDashboardPlugin(pluginConfig) {
1184
1246
  const mod = server.moduleGraph.getModuleById(moduleId);
1185
1247
  if (mod) server.moduleGraph.invalidateModule(mod);
1186
1248
  };
1187
- if (isRouteFile(file)) invalidate(RESOLVED_ROUTES_MODULE);
1188
- if (isWidgetFile(file)) invalidate(RESOLVED_WIDGETS_MODULE);
1189
- if (isNavigationFile(file)) invalidate(RESOLVED_NAVIGATION_MODULE);
1190
- if (isCustomFieldFile(file)) invalidate(RESOLVED_CUSTOM_FIELDS_MODULE);
1249
+ if (!isExtensionFile(file)) return;
1250
+ for (const moduleId of LAYERED_MODULES) invalidate(moduleId);
1191
1251
  }
1192
1252
  };
1193
1253
  }
1194
1254
 
1195
1255
  // src/generate-plugin-entry.ts
1196
- import path9 from "path";
1197
- import fs8 from "fs";
1198
- function findI18nIndex2(srcDir) {
1199
- const i18nDir = path9.join(srcDir, "i18n");
1200
- if (!fs8.existsSync(i18nDir)) return null;
1201
- for (const ext of VALID_FILE_EXTENSIONS) {
1202
- const filePath = path9.join(i18nDir, `index${ext}`);
1203
- if (fs8.existsSync(filePath)) return filePath;
1204
- }
1205
- return null;
1206
- }
1207
1256
  function generatePluginEntryModule(srcDir) {
1208
- const routesDir = path9.join(srcDir, "routes");
1209
- const files = crawlRoutes(routesDir);
1210
- let index = 0;
1211
- const routeResults = [];
1212
- const menuItemResults = [];
1213
- for (const file of files) {
1214
- const route = parseFile(file, routesDir, index);
1215
- if (route) {
1216
- routeResults.push(route);
1217
- }
1218
- const menuItem = parseMenuItemFile(file, routesDir, index);
1219
- if (menuItem) {
1220
- menuItemResults.push(menuItem);
1221
- }
1222
- index++;
1223
- }
1224
- const routeTree = buildRouteTree(routeResults.filter(Boolean));
1225
- const routeImports = routeTree.flatMap((r) => r.imports);
1226
- const routes = routeTree.map((r) => formatRoute(r.route));
1227
- const menuItemImports = menuItemResults.filter(Boolean).map((r) => r.import);
1228
- const menuItems = menuItemResults.filter(Boolean).map((r) => formatMenuItem(r.menuItem));
1229
- const { imports: widgetImports, entries: widgetEntries } = collectWidgets(
1230
- srcDir,
1231
- index
1232
- );
1233
- const cfFiles = crawlModuleFiles(path9.join(srcDir, "custom-fields"));
1234
- const cfImports = cfFiles.map(
1235
- (file, i) => `import __cf${i} from "${normalizePath(file)}"`
1236
- );
1237
- const cfRefs = cfFiles.map((_, i) => ` __cf${i}`);
1238
- const i18nFile = findI18nIndex2(srcDir);
1239
- const i18nImport = i18nFile ? `import i18nResources from "${normalizePath(i18nFile)}"` : "";
1240
- const i18nValue = i18nFile ? "i18nResources" : "{}";
1241
- const allImports = [
1242
- ...routeImports,
1243
- ...menuItemImports,
1244
- ...widgetImports,
1245
- ...cfImports
1246
- ];
1247
- if (i18nImport) allImports.push(i18nImport);
1257
+ const { imports, declaration, name } = collectLayer(srcDir, 0);
1248
1258
  return `// Auto-generated plugin extensions entry
1249
- ${allImports.join("\n")}
1250
-
1251
- const routeModule = {
1252
- routes: [
1253
- ${routes.join(",\n")}
1254
- ]
1255
- }
1256
-
1257
- const menuItemModule = {
1258
- menuItems: [
1259
- ${menuItems.join(",\n")}
1260
- ]
1261
- }
1262
-
1263
- const widgetModule = {
1264
- widgets: [
1265
- ${widgetEntries.join(",\n")}
1266
- ]
1267
- }
1268
-
1269
- const customFieldsModule = {
1270
- configs: [
1271
- ${cfRefs.join(",\n")}
1272
- ]
1273
- }
1259
+ ${imports.join("\n")}
1274
1260
 
1275
- const plugin = {
1276
- routeModule,
1277
- menuItemModule,
1278
- widgetModule,
1279
- customFieldsModule,
1280
- i18nModule: ${i18nValue},
1281
- }
1261
+ ${declaration}
1282
1262
 
1283
- export default plugin
1263
+ export default ${name}
1284
1264
  `;
1285
1265
  }
1286
1266
  export {