@atlashub/smartstack-cli 4.34.0 → 4.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlashub/smartstack-cli",
3
- "version": "4.34.0",
3
+ "version": "4.35.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -2,8 +2,8 @@
2
2
 
3
3
  > These templates generate React/TypeScript code for new applications/modules.
4
4
 
5
- > **DEPRECATED (v3.7+):** Pattern A (mergeRoutes) and Pattern B (JSX Routes) below
6
- > are replaced by PageRegistry.register() + DynamicRouter.
5
+ > **v3.7+:** Pattern A (mergeRoutes) and Pattern B (JSX Routes) were removed.
6
+ > The only supported routing pattern is PageRegistry.register() + DynamicRouter.
7
7
  > See references/frontend-route-wiring-app-tsx.md for the current pattern.
8
8
 
9
9
  ---
@@ -507,117 +507,86 @@ export const $moduleApi = {
507
507
 
508
508
  ---
509
509
 
510
- ## TEMPLATE: ROUTES (App.tsx)
510
+ ## TEMPLATE: ROUTES (PageRegistry + DynamicRouter)
511
511
 
512
- ### Detect App.tsx Routing Pattern FIRST
512
+ ### Default Pattern (v3.7+): PageRegistry + DynamicRouter
513
513
 
514
- Before adding routes, **read App.tsx** and detect which pattern is used:
514
+ > **This is the ONLY supported pattern** since v3.7.
515
+ > Pages are registered in `componentRegistry.generated.ts` via `PageRegistry.register()`.
516
+ > DynamicRouter resolves routes automatically from the navigation API + PageRegistry.
517
+ > **No App.tsx wiring needed.** No manual route duplication for tenant prefixes.
515
518
 
516
- | Pattern | How to detect | Action |
517
- |---------|---------------|--------|
518
- | **Pattern A** (mergeRoutes) | `applicationRoutes: ApplicationRouteExtensions` present | Add to `applicationRoutes.{application}[]` array |
519
- | **Pattern B** (JSX Routes) | `<Route path="/{application}" element={<{Layout} />}>` present | Insert `<Route>` children inside Layout wrapper |
519
+ **Steps:**
520
520
 
521
- ### Pattern A: mergeRoutes (applicationRoutes array)
522
-
523
- > **This is the DEFAULT pattern** generated by `smartstack init`.
524
- > Routes added to `applicationRoutes` are automatically injected into BOTH standard and tenant-prefixed route trees by `mergeRoutes()`. No manual duplication needed.
521
+ 1. Create your page component(s)
522
+ 2. Register in `componentRegistry.generated.ts` (or run `scaffold_routes outputFormat="componentRegistry"`)
523
+ 3. Ensure navigation seed data has matching `ComponentKey` values
524
+ 4. DynamicRouter does the rest
525
525
 
526
526
  ```tsx
527
- // Add to App.tsx imports at top
528
- import { $MODULE_PASCALPage } from '@/pages/$APPLICATION/$MODULE/$MODULE_PASCALPage';
529
- import { $MODULE_PASCALDetailPage } from '@/pages/$APPLICATION/$MODULE/$MODULE_PASCALDetailPage';
530
- import { Create$MODULE_PASCALPage } from '@/pages/$APPLICATION/$MODULE/Create$MODULE_PASCALPage';
531
-
532
- // Add routes to applicationRoutes.{application}[] with RELATIVE paths (no leading /)
533
- const applicationRoutes: ApplicationRouteExtensions = {
534
- '$APPLICATION_KEBAB': [
535
- // ... existing routes ...
536
- { path: '$MODULE_KEBAB', element: <$MODULE_PASCALPage /> },
537
- { path: '$MODULE_KEBAB/create', element: <Create$MODULE_PASCALPage /> },
538
- { path: '$MODULE_KEBAB/:id', element: <$MODULE_PASCALDetailPage /> },
539
- { path: '$MODULE_KEBAB/:id/edit', element: <Create$MODULE_PASCALPage /> },
540
- ],
541
- };
527
+ // 1. Create page component
528
+ // pages/$APPLICATION/$MODULE/$MODULE_PASCALPage.tsx
529
+ export function $MODULE_PASCALPage() { /* ... */ }
530
+
531
+ // 2. Register in componentRegistry.generated.ts
532
+ import { PageRegistry } from '@/extensions/PageRegistry';
533
+
534
+ PageRegistry.register(
535
+ '$APPLICATION_KEBAB.$MODULE_KEBAB',
536
+ lazy(() => import('@/pages/$APPLICATION/$MODULE/$MODULE_PASCALPage')
537
+ .then(m => ({ default: m.$MODULE_PASCALPage })))
538
+ );
539
+
540
+ // For CRUD sub-pages (detail, create):
541
+ PageRegistry.register(
542
+ '$APPLICATION_KEBAB.$MODULE_KEBAB.detail',
543
+ lazy(() => import('@/pages/$APPLICATION/$MODULE/$MODULE_PASCALDetailPage')
544
+ .then(m => ({ default: m.$MODULE_PASCALDetailPage })))
545
+ );
546
+
547
+ PageRegistry.register(
548
+ '$APPLICATION_KEBAB.$MODULE_KEBAB.create',
549
+ lazy(() => import('@/pages/$APPLICATION/$MODULE/Create$MODULE_PASCALPage')
550
+ .then(m => ({ default: m.Create$MODULE_PASCALPage })))
551
+ );
542
552
  ```
543
553
 
544
- **mergeRoutes auto-generates redirects** for intermediate paths (e.g., `$APPLICATION` → `$APPLICATION/$DEFAULT_MODULE_KEBAB`) so you don't need to add index redirects manually.
545
-
546
- **Application-to-Layout mapping (automatic via mergeRoutes):**
547
-
548
- | Application key | Injected into Layout | Standard path | Tenant path |
549
- |-----------------|---------------------|---------------|-------------|
550
- | `administration` | `AppLayout` | `/administration/...` | `/t/:slug/administration/...` |
551
- | `{application}` | `AppLayout` | `/{application}/...` | `/t/:slug/{application}/...` |
552
- | `myspace` | `AppLayout` | `/myspace/...` | `/t/:slug/myspace/...` |
553
-
554
- ### Pattern B: JSX Routes (inside Layout wrapper)
555
-
556
- > **Legacy pattern** — only used if App.tsx was manually restructured with JSX `<Route>` elements.
557
-
558
- The unified `AppLayout` provides the application shell: **header with AvatarMenu**, sidebar, navigation. It renders child pages via React Router's `<Outlet />`.
554
+ ```sql
555
+ -- 3. Navigation seed data (ComponentKey must match PageRegistry key)
556
+ INSERT INTO core.nav_Sections (...)
557
+ VALUES (..., ComponentKey = '$APPLICATION_KEBAB.$MODULE_KEBAB', ...);
558
+ ```
559
559
 
560
- **If routes are placed OUTSIDE the layout wrapper, the shell (header, sidebar, AvatarMenu) will NOT render. The page appears "naked" without any navigation.**
560
+ ### How DynamicRouter resolves routes
561
561
 
562
- **Step-by-step insertion:**
562
+ 1. Fetches menu from `GET /api/navigation/menu`
563
+ 2. For each Application → Module → Section → Resource, builds `<Route>` elements
564
+ 3. Looks up each item's `ComponentKey` in `PageRegistry` to get the React component
565
+ 4. Wraps routes in `<ProtectedRoute>` (API-driven permission check via `isOpen` flag)
566
+ 5. Generates both standard (`/$APPLICATION/$MODULE`) and tenant-prefixed (`/t/:slug/$APPLICATION/$MODULE`) routes automatically
563
567
 
564
- 1. Open `App.tsx`
565
- 2. Find the existing layout route for the target application:
566
- - `administration` → `<Route path="/administration" element={<AppLayout />}>`
567
- - `{application}` → `<Route path="/{application}" element={<AppLayout />}>`
568
- - `myspace` → `<Route path="/myspace" element={<AppLayout />}>`
569
- 3. Add the new routes **INSIDE** that `<Route>` block
570
- 4. If a tenant-prefixed block exists (`/t/:slug/...`), add the routes there too
568
+ ### ⚠️ CRITICAL RULES
571
569
 
570
+ **FORBIDDEN — Flat routes outside DynamicRouter:**
572
571
  ```tsx
573
- // Add to App.tsx
574
-
575
- import { $MODULE_PASCALPage } from '@/pages/$APPLICATION/$MODULE/$MODULE_PASCALPage';
576
- import { $MODULE_PASCALDetailPage } from '@/pages/$APPLICATION/$MODULE/$MODULE_PASCALDetailPage';
577
- import { Create$MODULE_PASCALPage } from '@/pages/$APPLICATION/$MODULE/Create$MODULE_PASCALPage';
578
-
579
- // Find the EXISTING layout route and add routes INSIDE it:
580
- <Route path="/$APPLICATION" element={<$APPLICATION_Layout />}>
581
- {/* ... existing routes stay here ... */}
582
-
583
- {/* NEW: $MODULE routes - added as children of the layout */}
584
- <Route path="$MODULE_KEBAB">
585
- <Route index element={<Navigate to="." replace />} />
586
- <Route index element={<$MODULE_PASCALPage />} />
587
- <Route path="create" element={<Create$MODULE_PASCALPage />} />
588
- <Route path=":id" element={<$MODULE_PASCALDetailPage />} />
589
- <Route path=":id/edit" element={<Create$MODULE_PASCALPage />} />
590
- </Route>
591
- </Route>
572
+ // WRONG — bypasses layout, permission checks, and tenant handling
573
+ <Route path="/$APPLICATION_KEBAB/$MODULE_KEBAB" element={<$MODULE_PASCALPage />} />
592
574
  ```
593
575
 
594
- ### ⚠️ CRITICAL RULES (both patterns)
595
-
596
- **FORBIDDEN — Adding to clientRoutes[] with absolute paths:**
576
+ **FORBIDDEN Registering pages without navigation seed data:**
597
577
  ```tsx
598
- // ❌ WRONG — bypasses layout entirely, shell will NOT render
599
- const clientRoutes: RouteConfig[] = [
600
- { path: '/$APPLICATION_KEBAB/$MODULE_KEBAB', element: <$MODULE_PASCALPage /> },
601
- ];
578
+ // ❌ WRONG — PageRegistry.register() alone does nothing without a matching ComponentKey in the DB
579
+ PageRegistry.register('my-app.my-module', MyPage);
580
+ // Must also have INSERT INTO core.nav_Sections with ComponentKey = 'my-app.my-module'
602
581
  ```
603
582
 
604
- `clientRoutes` is ONLY for routes **outside** SmartStack locked applications (e.g., `/about`, `/pricing`).
605
-
606
- **FORBIDDEN — Flat routes outside layout:**
607
- ```tsx
608
- // ❌ WRONG (Pattern B only) — flat route bypasses layout
609
- <Route path="/$APPLICATION_KEBAB/$MODULE_KEBAB" element={<$MODULE_PASCALPage />} />
610
- ```
583
+ ---
611
584
 
612
- ### Why nested/context routes?
585
+ ### Legacy Patterns (DEPRECATED — removed in v3.7)
613
586
 
614
- | Aspect | clientRoutes (wrong) | applicationRoutes / nested (correct) |
615
- |--------|---------------------|--------------------------------------|
616
- | Shell rendered | No (bypasses layout) | Yes (Outlet pattern) |
617
- | AvatarMenu visible | No | Yes |
618
- | Tenant prefix | Manual duplication | Automatic (mergeRoutes) |
619
- | Auto-redirects | None | Generated for intermediate paths |
620
- | Permission check | Bypassed (no RouteGuard) | Enforced by RouteGuard |
587
+ > **Pattern A (mergeRoutes)** and **Pattern B (JSX Routes)** were removed in v3.7.
588
+ > `smartstackRoutes.tsx`, `RouteGuard.tsx`, and `mergeRoutes()` no longer exist.
589
+ > If you encounter a pre-v3.7 project, upgrade to PageRegistry + DynamicRouter.
621
590
 
622
591
  ---
623
592
 
@@ -113,7 +113,7 @@ COMPARISON POINTS: {total}
113
113
  [OK] index.css — @custom-variant dark present
114
114
  [OK] DependencyInjection.Application — MediatR warning present
115
115
  [OK] DependencyInjection.Infrastructure — SQL Server pattern matches
116
- [SKIP] App.tsx — Intentionally different (client uses mergeRoutes legacy or PageRegistry+DynamicRouter)
116
+ [SKIP] App.tsx — Intentionally different (client uses PageRegistry+DynamicRouter v3.7+)
117
117
  [SKIP] ExtensionsDbContext — Intentionally simplified
118
118
 
119
119
  --------------------------------------------------------------------------------
@@ -161,7 +161,7 @@ These differences are **by design** and should always be marked `[SKIP]`:
161
161
 
162
162
  | File/Pattern | Reason |
163
163
  |---|---|
164
- | `App.tsx` — routing pattern (`mergeRoutes` legacy or `PageRegistry`+`DynamicRouter` v3.7+) | Client projects use their own routing entry point, not platform routing |
164
+ | `App.tsx` — routing pattern (`PageRegistry`+`DynamicRouter` v3.7+) | Client projects use their own routing entry point, not platform routing |
165
165
  | `ExtensionsDbContext` | Intentionally simplified vs `CoreDbContext` — different architectural role |
166
166
  | `api.ts` | Re-exports SmartStack client — wrapper pattern is correct |
167
167
  | `DependencyInjection.Infrastructure.cs` | SQL Server connection pattern identical by design |
@@ -208,7 +208,7 @@ These comparisons should **always** return `[SKIP]` — differences are by desig
208
208
 
209
209
  | Item | Reason |
210
210
  |---|---|
211
- | `App.tsx` | Client uses `mergeRoutes` (legacy) or `PageRegistry`+`DynamicRouter` (v3.7+); app uses direct routing |
211
+ | `App.tsx` | Client uses `PageRegistry`+`DynamicRouter` (v3.7+); app uses DynamicRouter exclusively (mergeRoutes removed in v3.7) |
212
212
  | `api.ts` | Client re-exports SmartStack client — wrapper pattern is correct |
213
213
  | `ExtensionsDbContext` | Intentionally simplified vs `CoreDbContext` |
214
214
  | `GlobalUsings.cs` | App has platform-wide usings; client has minimal set |
@@ -25,8 +25,7 @@ Validate that generated documentation is complete and accurate, then integrate i
25
25
  - [ ] Mock UI faithfully reproduces the real page interface (not generic KPI/table)
26
26
  - [ ] Every Mock UI section has `Annotation` components explaining each visual element
27
27
  - [ ] i18n JSON is FLAT (no root key) — `t('title')` resolves directly
28
- - [ ] Route added as **child** of `/docs/business` group (not standalone) in App.tsx
29
- - [ ] Route added in `smartstackRoutes.tsx` (locked routes)
28
+ - [ ] Route added as **child** of `/docs/business` group (not standalone) in DynamicRouter.tsx
30
29
  - [ ] DocsLayout sidebar updated (if applicable)
31
30
  - [ ] Module appears in UserIndexPage.tsx (if applicable)
32
31
  - [ ] DocPanelContext.tsx mapping updated for all module routes
@@ -38,8 +37,7 @@ Validate that generated documentation is complete and accurate, then integrate i
38
37
  - [ ] i18n JSON uses root key matching namespace
39
38
  - [ ] DocRenderer wrapper (index.tsx) passes correct `i18nNamespace` prop
40
39
  - [ ] doc-data.ts contains structured data (~50 lines)
41
- - [ ] Route added as **child** of `/system/docs` group (not standalone) in App.tsx
42
- - [ ] Route added in `smartstackRoutes.tsx` (locked routes)
40
+ - [ ] Route added as **child** of `/system/docs` group (not standalone) in DynamicRouter.tsx
43
41
  - [ ] DocsLayout sidebar updated (if applicable)
44
42
 
45
43
  #### For `update` Type
@@ -113,7 +111,7 @@ detects tenant-prefixed routes (`/t/:slug/...`), and builds lookup keys as `{app
113
111
  - `/support/tickets/123` → tries `support/tickets/123`, then `support/tickets`
114
112
  - `/t/my-company/administration/users` → strips tenant prefix, then tries `administration/users`
115
113
 
116
- #### App.tsx Routing
114
+ #### DynamicRouter.tsx Routing
117
115
 
118
116
  Documentation pages use a **separate routing system** from business pages. They render inside `DocsLayout` (sidebar navigation), NOT `AppLayout`. There are two route groups:
119
117
 
@@ -122,10 +120,10 @@ Documentation pages use a **separate routing system** from business pages. They
122
120
 
123
121
  > **Note:** The `<Suspense fallback={<PageLoader />}>` is already in place at the global level (wrapping the entire Routes tree). Do NOT add per-route `<Suspense>` wrappers.
124
122
 
125
- **For `user` type:** Add lazy import and **child route** inside the existing `/docs/business` group:
123
+ **For `user` type:** Add lazy import and **child route** inside the existing `/docs/business` group in `DynamicRouter.tsx`:
126
124
 
127
125
  ```tsx
128
- // 1. Add lazy import at top of App.tsx
126
+ // 1. Add lazy import at top of DynamicRouter.tsx
129
127
  const {Module}DocPage = lazy(() =>
130
128
  import('@/pages/docs/business/{application}/{module}')
131
129
  );
@@ -138,10 +136,10 @@ const {Module}DocPage = lazy(() =>
138
136
  </Route>
139
137
  ```
140
138
 
141
- **For `developer|database|testing` types:** Add **child route** inside the existing `/system/docs` group:
139
+ **For `developer|database|testing` types:** Add **child route** inside the existing `/system/docs` group in `DynamicRouter.tsx`:
142
140
 
143
141
  ```tsx
144
- // 1. Add lazy import at top of App.tsx
142
+ // 1. Add lazy import at top of DynamicRouter.tsx
145
143
  const {Tool}DocPage = lazy(() =>
146
144
  import('@/pages/docs/{category}/{tool}')
147
145
  );
@@ -154,9 +152,9 @@ const {Tool}DocPage = lazy(() =>
154
152
  </Route>
155
153
  ```
156
154
 
157
- #### smartstackRoutes.tsx Registration
155
+ #### DynamicRouter Documentation Routes
158
156
 
159
- **MANDATORY** for all doc types: Add the route in `smartstackRoutes.tsx` under the corresponding group (`/docs/business` or `/system/docs`). Documentation routes are part of `LOCKED_PREFIXES` and cannot be overridden by client extensions.
157
+ Documentation routes are statically defined in `DynamicRouter.tsx` (not dynamically generated from the navigation API). When adding new documentation pages, add the lazy import and `<Route>` entry in the documentation section of `DynamicRouter.tsx`.
160
158
 
161
159
  #### DocsLayout Sidebar Update
162
160
 
@@ -164,7 +162,7 @@ If the new documentation page should appear in the sidebar navigation, update th
164
162
 
165
163
  #### Important Notes
166
164
 
167
- - Documentation routes do **NOT** use `AppLayout`, `RouteGuard`, or `LicenseGuard`
165
+ - Documentation routes do **NOT** use `AppLayout`, `ProtectedRoute`, or `LicenseGuard`
168
166
  - Documentation routes have **no tenant prefix** (`/t/:slug/...`) — they are globally accessible
169
167
  - Documentation routes require **no specific permission** to access
170
168
 
@@ -259,8 +257,8 @@ Add or update entry with metadata:
259
257
  - [x] Mock UI faithfully reproduces the real page interface (user type)
260
258
  - [x] Every Mock UI section has `Annotation` components (user type)
261
259
  - [x] i18n files created in ALL 4 languages (FR, EN, DE, IT) with correct format
262
- - [x] Route added as child of correct DocsLayout group in App.tsx
263
- - [x] Route registered in smartstackRoutes.tsx (locked routes)
260
+ - [x] Route added as child of correct DocsLayout group in DynamicRouter.tsx
261
+ - [x] Route added in DynamicRouter.tsx documentation section
264
262
  - [x] DocsLayout sidebar updated (if applicable)
265
263
  - [x] docs-manifest.json updated
266
264
  - [x] i18n/config.ts updated with new namespace
@@ -1,126 +0,0 @@
1
- {{!-- SmartStack React Router Configuration Template --}}
2
- {{!-- Generates router configuration from NavRoute registry --}}
3
-
4
- /**
5
- * React Router Configuration
6
- *
7
- * Auto-generated by SmartStack MCP - DO NOT EDIT MANUALLY
8
- * Generated: {{generatedAt}}
9
- */
10
-
11
- import React, { Suspense, lazy } from 'react';
12
- import { createBrowserRouter, RouteObject, Navigate } from 'react-router-dom';
13
- import { ROUTES } from './navRoutes.generated';
14
- {{#if includeGuards}}
15
- import { ProtectedRoute, PermissionGuard } from './guards';
16
- {{/if}}
17
-
18
- // ============================================================================
19
- // Layouts
20
- // ============================================================================
21
-
22
- {{#each applications}}
23
- import { {{capitalize this}}Layout } from '../layouts/{{capitalize this}}Layout';
24
- {{/each}}
25
-
26
- // ============================================================================
27
- // Pages (lazy loaded for code splitting)
28
- // ============================================================================
29
-
30
- {{#each routes}}
31
- const {{pageName this.navRoute}}Page = lazy(() => import('../pages/{{pagePath this.navRoute}}'));
32
- {{/each}}
33
-
34
- // ============================================================================
35
- // Loading Component
36
- // ============================================================================
37
-
38
- const PageLoader: React.FC = () => (
39
- <div className="flex items-center justify-center h-64">
40
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
41
- </div>
42
- );
43
-
44
- // ============================================================================
45
- // Route Configuration
46
- // ============================================================================
47
-
48
- const routes: RouteObject[] = [
49
- // Root redirect
50
- {
51
- path: '/',
52
- element: <Navigate to="{{defaultPath}}" replace />,
53
- },
54
-
55
- {{#each applicationTree}}
56
- // {{uppercase @key}} Application
57
- {
58
- path: '{{@key}}',
59
- element: <{{capitalize @key}}Layout />,
60
- children: [
61
- {{#each this}}
62
- {
63
- path: '{{modulePath this.navRoute}}',
64
- {{#if this.permissions.length}}
65
- element: (
66
- <PermissionGuard permissions={ROUTES['{{this.navRoute}}'].permissions}>
67
- <Suspense fallback={<PageLoader />}>
68
- <{{pageName this.navRoute}}Page />
69
- </Suspense>
70
- </PermissionGuard>
71
- ),
72
- {{else}}
73
- element: (
74
- <Suspense fallback={<PageLoader />}>
75
- <{{pageName this.navRoute}}Page />
76
- </Suspense>
77
- ),
78
- {{/if}}
79
- },
80
- {{/each}}
81
- ],
82
- },
83
- {{/each}}
84
-
85
- // 404 Not Found
86
- {
87
- path: '*',
88
- element: (
89
- <div className="flex flex-col items-center justify-center h-screen">
90
- <h1 className="text-4xl font-bold text-gray-900">404</h1>
91
- <p className="mt-2 text-gray-600">Page not found</p>
92
- <a href="{{defaultPath}}" className="mt-4 text-blue-600 hover:underline">
93
- Return to home
94
- </a>
95
- </div>
96
- ),
97
- },
98
- ];
99
-
100
- // ============================================================================
101
- // Router Export
102
- // ============================================================================
103
-
104
- {{#if includeGuards}}
105
- export const router = createBrowserRouter([
106
- {
107
- element: <ProtectedRoute />,
108
- children: routes,
109
- },
110
- ]);
111
- {{else}}
112
- export const router = createBrowserRouter(routes);
113
- {{/if}}
114
-
115
- export default router;
116
-
117
- // ============================================================================
118
- // Type Exports
119
- // ============================================================================
120
-
121
- export type { RouteObject };
122
-
123
- /**
124
- * Get route configuration by NavRoute path
125
- */
126
- export const getRouteConfig = (navRoute: string) => ROUTES[navRoute];