@atlashub/smartstack-cli 4.31.0 → 4.33.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/.documentation/commands.html +952 -116
- package/.documentation/index.html +2 -2
- package/.documentation/init.html +358 -174
- package/dist/mcp-entry.mjs +271 -44
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/mcp-scaffolding/controller.cs.hbs +54 -128
- package/templates/project/README.md +19 -0
- package/templates/skills/apex/SKILL.md +16 -10
- package/templates/skills/apex/_shared.md +1 -1
- package/templates/skills/apex/references/checks/architecture-checks.sh +154 -0
- package/templates/skills/apex/references/checks/backend-checks.sh +194 -0
- package/templates/skills/apex/references/checks/frontend-checks.sh +448 -0
- package/templates/skills/apex/references/checks/infrastructure-checks.sh +255 -0
- package/templates/skills/apex/references/checks/security-checks.sh +153 -0
- package/templates/skills/apex/references/checks/seed-checks.sh +536 -0
- package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +49 -192
- package/templates/skills/apex/references/parallel-execution.md +18 -5
- package/templates/skills/apex/references/post-checks.md +124 -2156
- package/templates/skills/apex/references/smartstack-api.md +160 -957
- package/templates/skills/apex/references/smartstack-frontend-compliance.md +23 -1
- package/templates/skills/apex/references/smartstack-frontend.md +134 -1022
- package/templates/skills/apex/references/smartstack-layers.md +12 -6
- package/templates/skills/apex/steps/step-00-init.md +81 -238
- package/templates/skills/apex/steps/step-03-execute.md +25 -751
- package/templates/skills/apex/steps/step-03a-layer0-domain.md +118 -0
- package/templates/skills/apex/steps/step-03b-layer1-seed.md +91 -0
- package/templates/skills/apex/steps/step-03c-layer2-backend.md +240 -0
- package/templates/skills/apex/steps/step-03d-layer3-frontend.md +300 -0
- package/templates/skills/apex/steps/step-03e-layer4-devdata.md +44 -0
- package/templates/skills/apex/steps/step-04-examine.md +70 -150
- package/templates/skills/application/references/frontend-i18n-and-output.md +2 -2
- package/templates/skills/application/references/frontend-route-naming.md +5 -1
- package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +49 -198
- package/templates/skills/application/references/frontend-verification.md +11 -11
- package/templates/skills/application/steps/step-05-frontend.md +26 -15
- package/templates/skills/application/templates-frontend.md +4 -0
- package/templates/skills/cli-app-sync/SKILL.md +2 -2
- package/templates/skills/cli-app-sync/references/comparison-map.md +1 -1
- package/templates/skills/controller/references/controller-code-templates.md +70 -67
- package/templates/skills/controller/references/mcp-scaffold-workflow.md +5 -1
package/dist/mcp-entry.mjs
CHANGED
|
@@ -25984,6 +25984,7 @@ var init_types3 = __esm({
|
|
|
25984
25984
|
entityProperties: external_exports.array(EntityPropertySchema).optional().describe("Entity properties for DTO/Validator generation"),
|
|
25985
25985
|
navRoute: external_exports.string().optional().describe('Navigation route path for controller (e.g., "administration.users"). Required for controllers.'),
|
|
25986
25986
|
navRouteSuffix: external_exports.string().optional().describe('Optional suffix for NavRoute (e.g., "dashboard" for sub-resources)'),
|
|
25987
|
+
customSegment: external_exports.string().optional().describe('Optional custom URL segment for NavRoute (e.g., "user/dashboard")'),
|
|
25987
25988
|
withHierarchyFunction: external_exports.boolean().optional().describe("For entity type with self-reference (ParentId): generate TVF SQL script for hierarchy traversal"),
|
|
25988
25989
|
hierarchyDirection: external_exports.enum(["ancestors", "descendants", "both"]).optional().describe("Direction for hierarchy traversal function (default: both)"),
|
|
25989
25990
|
isPersonRole: external_exports.boolean().optional().describe("If true, entity is a Person Extension linked to User via UserId FK"),
|
|
@@ -26101,7 +26102,7 @@ var init_types3 = __esm({
|
|
|
26101
26102
|
includeLayouts: external_exports.boolean().default(true).describe("Generate layout components"),
|
|
26102
26103
|
includeGuards: external_exports.boolean().default(true).describe("Include route guards for permissions"),
|
|
26103
26104
|
generateRegistry: external_exports.boolean().default(true).describe("Generate navRoutes.generated.ts"),
|
|
26104
|
-
outputFormat: external_exports.enum(["standalone", "applicationRoutes", "clientRoutes"]).default("
|
|
26105
|
+
outputFormat: external_exports.enum(["standalone", "applicationRoutes", "clientRoutes", "componentRegistry"]).default("componentRegistry").describe('Output format: "componentRegistry" (v3.7+) generates PageRegistry.register() calls for DynamicRouter. "applicationRoutes" (deprecated) generates RouteObject[] arrays for App.tsx. "standalone" generates createBrowserRouter(). "clientRoutes" is a deprecated alias for "applicationRoutes".'),
|
|
26105
26106
|
dryRun: external_exports.boolean().default(false).describe("Preview without writing files")
|
|
26106
26107
|
}).optional()
|
|
26107
26108
|
});
|
|
@@ -27984,6 +27985,60 @@ async function validateFrontendRoutes(structure, _config, result) {
|
|
|
27984
27985
|
return;
|
|
27985
27986
|
}
|
|
27986
27987
|
const appContent = await readText(appFiles[0]);
|
|
27988
|
+
const registryFiles = await findFiles("**/componentRegistry.generated.ts", { cwd: structure.web });
|
|
27989
|
+
const usesDynamicRouter = registryFiles.length > 0;
|
|
27990
|
+
if (usesDynamicRouter) {
|
|
27991
|
+
const registryContent = await readText(registryFiles[0]);
|
|
27992
|
+
const registeredKeys = /* @__PURE__ */ new Set();
|
|
27993
|
+
const registerMatches = registryContent.matchAll(/PageRegistry\.register\s*\(\s*['"]([^'"]+)['"]/g);
|
|
27994
|
+
for (const match2 of registerMatches) {
|
|
27995
|
+
registeredKeys.add(match2[1]);
|
|
27996
|
+
}
|
|
27997
|
+
const seedFiles2 = await findFiles("**/*NavigationSeedData.cs", { cwd: structure.root });
|
|
27998
|
+
const seedNavRoutes2 = [];
|
|
27999
|
+
for (const file of seedFiles2) {
|
|
28000
|
+
const content = await readText(file);
|
|
28001
|
+
const routeMatches = content.matchAll(/(?:route:\s*|Route\s*=\s*)["']([^"']+)["']/g);
|
|
28002
|
+
for (const match2 of routeMatches) {
|
|
28003
|
+
seedNavRoutes2.push({
|
|
28004
|
+
route: match2[1],
|
|
28005
|
+
file: path8.relative(structure.root, file)
|
|
28006
|
+
});
|
|
28007
|
+
}
|
|
28008
|
+
}
|
|
28009
|
+
const missingRoutes2 = [];
|
|
28010
|
+
for (const nav of seedNavRoutes2) {
|
|
28011
|
+
const segments = nav.route.split("/").filter(Boolean);
|
|
28012
|
+
const componentKey = segments.join(".");
|
|
28013
|
+
if (!registeredKeys.has(componentKey)) {
|
|
28014
|
+
missingRoutes2.push(nav);
|
|
28015
|
+
}
|
|
28016
|
+
}
|
|
28017
|
+
if (missingRoutes2.length > 0) {
|
|
28018
|
+
for (const missing of missingRoutes2.slice(0, 5)) {
|
|
28019
|
+
result.errors.push({
|
|
28020
|
+
type: "error",
|
|
28021
|
+
category: "frontend-routes",
|
|
28022
|
+
message: `Navigation route "${missing.route}" from seed data has no PageRegistry.register() entry`,
|
|
28023
|
+
file: path8.relative(structure.root, registryFiles[0]),
|
|
28024
|
+
suggestion: `Run scaffold_routes (outputFormat: "componentRegistry") to regenerate (defined in ${missing.file})`
|
|
28025
|
+
});
|
|
28026
|
+
}
|
|
28027
|
+
if (missingRoutes2.length > 5) {
|
|
28028
|
+
result.errors.push({
|
|
28029
|
+
type: "error",
|
|
28030
|
+
category: "frontend-routes",
|
|
28031
|
+
message: `... and ${missingRoutes2.length - 5} more seed data routes missing from componentRegistry`
|
|
28032
|
+
});
|
|
28033
|
+
}
|
|
28034
|
+
}
|
|
28035
|
+
result.warnings.push({
|
|
28036
|
+
type: "warning",
|
|
28037
|
+
category: "frontend-routes",
|
|
28038
|
+
message: `Frontend routes summary (DynamicRouter): ${registeredKeys.size} registered keys, ${missingRoutes2.length} missing from componentRegistry`
|
|
28039
|
+
});
|
|
28040
|
+
return;
|
|
28041
|
+
}
|
|
27987
28042
|
const hasApplicationRoutes = appContent.includes("applicationRoutes") || appContent.includes("clientRoutes");
|
|
27988
28043
|
const hasRouteComponents = /<Route\s/.test(appContent);
|
|
27989
28044
|
if (!hasApplicationRoutes && !hasRouteComponents) {
|
|
@@ -34705,7 +34760,14 @@ function resolveHierarchy(navRoute) {
|
|
|
34705
34760
|
domainPath = application;
|
|
34706
34761
|
}
|
|
34707
34762
|
const infraPath = domainPath;
|
|
34708
|
-
|
|
34763
|
+
let controllerArea = "";
|
|
34764
|
+
if (segments.length >= 3) {
|
|
34765
|
+
controllerArea = path10.join(application, module, section);
|
|
34766
|
+
} else if (segments.length >= 2) {
|
|
34767
|
+
controllerArea = path10.join(application, module);
|
|
34768
|
+
} else if (segments.length === 1) {
|
|
34769
|
+
controllerArea = application;
|
|
34770
|
+
}
|
|
34709
34771
|
return { application, module, section, domainPath, infraPath, controllerArea };
|
|
34710
34772
|
}
|
|
34711
34773
|
async function handleScaffoldExtension(args, config2) {
|
|
@@ -35635,88 +35697,117 @@ GO
|
|
|
35635
35697
|
}
|
|
35636
35698
|
async function scaffoldController(name, options, structure, config2, result, dryRun = false) {
|
|
35637
35699
|
const hierarchy = resolveHierarchy(options?.navRoute);
|
|
35638
|
-
const
|
|
35700
|
+
const namespaceArea = hierarchy.controllerArea.replace(/[\\/]/g, ".");
|
|
35701
|
+
const namespace = options?.namespace || (namespaceArea ? `${config2.conventions.namespaces.api}.Controllers.${namespaceArea}` : `${config2.conventions.namespaces.api}.Controllers`);
|
|
35639
35702
|
const navRoute = options?.navRoute;
|
|
35640
35703
|
const navRouteSuffix = options?.navRouteSuffix;
|
|
35641
|
-
const
|
|
35704
|
+
const customSegment = options?.customSegment;
|
|
35705
|
+
const navRouteParams = [];
|
|
35706
|
+
if (navRoute) {
|
|
35707
|
+
navRouteParams.push(`"${navRoute}"`);
|
|
35708
|
+
if (navRouteSuffix) navRouteParams.push(`Suffix = "${navRouteSuffix}"`);
|
|
35709
|
+
if (customSegment) navRouteParams.push(`CustomSegment = "${customSegment}"`);
|
|
35710
|
+
}
|
|
35711
|
+
const routeAttribute = navRoute ? `[NavRoute(${navRouteParams.join(", ")})]` : `[Route("api/[controller]")]`;
|
|
35642
35712
|
const navRouteUsing = navRoute ? "using SmartStack.Api.Routing;\n" : "";
|
|
35643
|
-
const controllerTemplate = `using
|
|
35713
|
+
const controllerTemplate = `using MediatR;
|
|
35644
35714
|
using Microsoft.AspNetCore.Mvc;
|
|
35645
|
-
using
|
|
35646
|
-
${navRouteUsing}
|
|
35715
|
+
using SmartStack.Api.Authorization;
|
|
35716
|
+
${navRouteUsing}using SmartStack.Application.Common.Authorization;
|
|
35717
|
+
|
|
35647
35718
|
namespace {{namespace}};
|
|
35648
35719
|
|
|
35649
35720
|
/// <summary>
|
|
35650
35721
|
/// API controller for {{name}} operations.
|
|
35651
|
-
/// IMPORTANT: Use [RequirePermission] on each endpoint for RBAC enforcement.
|
|
35652
35722
|
/// </summary>
|
|
35653
35723
|
[ApiController]
|
|
35654
35724
|
{{routeAttribute}}
|
|
35655
|
-
[Authorize]
|
|
35725
|
+
[Microsoft.AspNetCore.Authorization.Authorize]
|
|
35726
|
+
[Produces("application/json")]
|
|
35727
|
+
[Tags("{{namePlural}}")]
|
|
35656
35728
|
public class {{name}}Controller : ControllerBase
|
|
35657
35729
|
{
|
|
35658
|
-
private readonly
|
|
35659
|
-
private readonly ILogger<{{name}}Controller> _logger;
|
|
35730
|
+
private readonly ISender _mediator;
|
|
35660
35731
|
|
|
35661
|
-
public {{name}}Controller(
|
|
35662
|
-
{
|
|
35663
|
-
_service = service;
|
|
35664
|
-
_logger = logger;
|
|
35665
|
-
}
|
|
35732
|
+
public {{name}}Controller(ISender mediator) => _mediator = mediator;
|
|
35666
35733
|
|
|
35667
35734
|
/// <summary>
|
|
35668
35735
|
/// Get all {{nameLower}}s
|
|
35669
35736
|
/// </summary>
|
|
35670
35737
|
[HttpGet]
|
|
35671
|
-
|
|
35672
|
-
|
|
35738
|
+
[RequirePermission(Permissions.{{module}}.{{namePlural}}.View)]
|
|
35739
|
+
[ProducesResponseType(typeof(List<{{name}}ListDto>), StatusCodes.Status200OK)]
|
|
35740
|
+
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
35741
|
+
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
|
35742
|
+
public async Task<ActionResult<List<{{name}}ListDto>>> GetAll(CancellationToken ct)
|
|
35673
35743
|
{
|
|
35674
|
-
|
|
35675
|
-
return Ok(
|
|
35744
|
+
var result = await _mediator.Send(new Get{{namePlural}}Query(), ct);
|
|
35745
|
+
return Ok(result);
|
|
35676
35746
|
}
|
|
35677
35747
|
|
|
35678
35748
|
/// <summary>
|
|
35679
35749
|
/// Get {{nameLower}} by ID
|
|
35680
35750
|
/// </summary>
|
|
35681
35751
|
[HttpGet("{id:guid}")]
|
|
35682
|
-
|
|
35683
|
-
|
|
35752
|
+
[RequirePermission(Permissions.{{module}}.{{namePlural}}.View)]
|
|
35753
|
+
[ProducesResponseType(typeof({{name}}DetailDto), StatusCodes.Status200OK)]
|
|
35754
|
+
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
35755
|
+
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
35756
|
+
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
|
35757
|
+
public async Task<ActionResult<{{name}}DetailDto>> GetById(Guid id, CancellationToken ct)
|
|
35684
35758
|
{
|
|
35685
|
-
|
|
35686
|
-
return NotFound();
|
|
35759
|
+
var result = await _mediator.Send(new Get{{name}}ByIdQuery(id), ct);
|
|
35760
|
+
if (result == null) return NotFound(new { message = "{{name}} not found" });
|
|
35761
|
+
return Ok(result);
|
|
35687
35762
|
}
|
|
35688
35763
|
|
|
35689
35764
|
/// <summary>
|
|
35690
35765
|
/// Create new {{nameLower}}
|
|
35691
35766
|
/// </summary>
|
|
35692
35767
|
[HttpPost]
|
|
35693
|
-
|
|
35694
|
-
|
|
35768
|
+
[RequirePermission(Permissions.{{module}}.{{namePlural}}.Create)]
|
|
35769
|
+
[ProducesResponseType(typeof({{name}}DetailDto), StatusCodes.Status201Created)]
|
|
35770
|
+
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
35771
|
+
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
35772
|
+
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
|
35773
|
+
public async Task<ActionResult<{{name}}DetailDto>> Create(
|
|
35774
|
+
[FromBody] Create{{name}}Request request, CancellationToken ct)
|
|
35695
35775
|
{
|
|
35696
|
-
|
|
35697
|
-
return CreatedAtAction(nameof(GetById), new { id =
|
|
35776
|
+
var result = await _mediator.Send(new Create{{name}}Command(request), ct);
|
|
35777
|
+
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
|
|
35698
35778
|
}
|
|
35699
35779
|
|
|
35700
35780
|
/// <summary>
|
|
35701
35781
|
/// Update {{nameLower}}
|
|
35702
35782
|
/// </summary>
|
|
35703
35783
|
[HttpPut("{id:guid}")]
|
|
35704
|
-
|
|
35705
|
-
|
|
35784
|
+
[RequirePermission(Permissions.{{module}}.{{namePlural}}.Update)]
|
|
35785
|
+
[ProducesResponseType(typeof({{name}}DetailDto), StatusCodes.Status200OK)]
|
|
35786
|
+
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
35787
|
+
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
35788
|
+
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
35789
|
+
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
|
35790
|
+
public async Task<ActionResult<{{name}}DetailDto>> Update(
|
|
35791
|
+
Guid id, [FromBody] Update{{name}}Request request, CancellationToken ct)
|
|
35706
35792
|
{
|
|
35707
|
-
|
|
35708
|
-
return
|
|
35793
|
+
var result = await _mediator.Send(new Update{{name}}Command(id, request), ct);
|
|
35794
|
+
if (result.IsNotFound) return NotFound(new { message = "{{name}} not found" });
|
|
35795
|
+
return Ok(result.Data);
|
|
35709
35796
|
}
|
|
35710
35797
|
|
|
35711
35798
|
/// <summary>
|
|
35712
35799
|
/// Delete {{nameLower}}
|
|
35713
35800
|
/// </summary>
|
|
35714
35801
|
[HttpDelete("{id:guid}")]
|
|
35715
|
-
|
|
35716
|
-
|
|
35802
|
+
[RequirePermission(Permissions.{{module}}.{{namePlural}}.Delete)]
|
|
35803
|
+
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
35804
|
+
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
35805
|
+
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
35806
|
+
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
|
35807
|
+
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
|
35717
35808
|
{
|
|
35718
|
-
|
|
35719
|
-
return NoContent();
|
|
35809
|
+
var success = await _mediator.Send(new Delete{{name}}Command(id), ct);
|
|
35810
|
+
return success ? NoContent() : NotFound(new { message = "{{name}} not found" });
|
|
35720
35811
|
}
|
|
35721
35812
|
}
|
|
35722
35813
|
`;
|
|
@@ -35724,6 +35815,8 @@ public class {{name}}Controller : ControllerBase
|
|
|
35724
35815
|
namespace,
|
|
35725
35816
|
name,
|
|
35726
35817
|
nameLower: name.charAt(0).toLowerCase() + name.slice(1),
|
|
35818
|
+
namePlural: name + "s",
|
|
35819
|
+
module: hierarchy.module || name,
|
|
35727
35820
|
routeAttribute
|
|
35728
35821
|
};
|
|
35729
35822
|
const controllerContent = import_handlebars.default.compile(controllerTemplate)(context);
|
|
@@ -35739,7 +35832,7 @@ public class {{name}}Controller : ControllerBase
|
|
|
35739
35832
|
result.files.push({ path: controllerFilePath, content: controllerContent, type: "created" });
|
|
35740
35833
|
if (navRoute) {
|
|
35741
35834
|
result.instructions.push("Controller created with NavRoute (route resolved from DB at startup).");
|
|
35742
|
-
result.instructions.push(`NavRoute: ${navRoute}${navRouteSuffix ? ` (Suffix: ${navRouteSuffix})` : ""}`);
|
|
35835
|
+
result.instructions.push(`NavRoute: ${navRoute}${navRouteSuffix ? ` (Suffix: ${navRouteSuffix})` : ""}${customSegment ? ` (CustomSegment: ${customSegment})` : ""}`);
|
|
35743
35836
|
result.instructions.push("");
|
|
35744
35837
|
result.instructions.push("NavRoute resolves API routes from Navigation entities in the database.");
|
|
35745
35838
|
result.instructions.push("Ensure the navigation path exists (seed data required):");
|
|
@@ -36818,6 +36911,10 @@ var init_scaffold_extension = __esm({
|
|
|
36818
36911
|
type: "string",
|
|
36819
36912
|
description: 'Optional suffix for NavRoute (e.g., "dashboard" for sub-resources)'
|
|
36820
36913
|
},
|
|
36914
|
+
customSegment: {
|
|
36915
|
+
type: "string",
|
|
36916
|
+
description: 'Optional custom URL segment for NavRoute (e.g., "user/dashboard")'
|
|
36917
|
+
},
|
|
36821
36918
|
withHierarchyFunction: {
|
|
36822
36919
|
type: "boolean",
|
|
36823
36920
|
description: "For entity type with self-reference (ParentId): generate TVF SQL script for hierarchy traversal"
|
|
@@ -57877,11 +57974,36 @@ async function scaffoldRoutes(input, config2) {
|
|
|
57877
57974
|
}
|
|
57878
57975
|
result.files.push({ path: registryFile, content: registryContent, type: "created" });
|
|
57879
57976
|
}
|
|
57880
|
-
const outputFormat = options?.outputFormat ?? "
|
|
57977
|
+
const outputFormat = options?.outputFormat ?? "componentRegistry";
|
|
57881
57978
|
if (outputFormat === "clientRoutes") {
|
|
57882
57979
|
if (!result.warnings) result.warnings = [];
|
|
57883
57980
|
result.warnings.push('outputFormat "clientRoutes" is DEPRECATED. Use "applicationRoutes" instead (same behavior, correct naming).');
|
|
57884
57981
|
}
|
|
57982
|
+
if (outputFormat === "applicationRoutes") {
|
|
57983
|
+
if (!result.warnings) result.warnings = [];
|
|
57984
|
+
result.warnings.push(
|
|
57985
|
+
'outputFormat "applicationRoutes" is DEPRECATED (v3.7+). Use "componentRegistry" for PageRegistry + DynamicRouter pattern. See: DynamicRouter replaces manual App.tsx wiring.'
|
|
57986
|
+
);
|
|
57987
|
+
}
|
|
57988
|
+
if (outputFormat === "componentRegistry") {
|
|
57989
|
+
const pageFiles = await discoverPageFiles(webPath, navRoutes);
|
|
57990
|
+
const registryContent = generateComponentRegistry(navRoutes, pageFiles, webPath);
|
|
57991
|
+
const registryFile = path19.join(
|
|
57992
|
+
options?.outputPath || path19.join(webPath, "src", "extensions"),
|
|
57993
|
+
"componentRegistry.generated.ts"
|
|
57994
|
+
);
|
|
57995
|
+
if (!dryRun) {
|
|
57996
|
+
await ensureDirectory(path19.dirname(registryFile));
|
|
57997
|
+
await writeText(registryFile, registryContent);
|
|
57998
|
+
}
|
|
57999
|
+
result.files.push({ path: registryFile, content: registryContent, type: "created" });
|
|
58000
|
+
result.instructions.push(`Generated PageRegistry.register() calls for ${navRoutes.length} NavRoutes`);
|
|
58001
|
+
result.instructions.push("");
|
|
58002
|
+
result.instructions.push("## Setup (one-time)");
|
|
58003
|
+
result.instructions.push("1. Ensure `main.tsx` imports `./extensions/componentRegistry.generated`");
|
|
58004
|
+
result.instructions.push("2. Ensure navigation seed data has matching `componentKey` values");
|
|
58005
|
+
result.instructions.push("3. DynamicRouter resolves routes automatically \u2014 no App.tsx wiring needed");
|
|
58006
|
+
}
|
|
57885
58007
|
if (outputFormat === "applicationRoutes" || outputFormat === "clientRoutes") {
|
|
57886
58008
|
const pageFiles = await discoverPageFiles(webPath, navRoutes);
|
|
57887
58009
|
const applicationRoutesContent = generateApplicationRoutesConfig(navRoutes, pageFiles, includeGuards);
|
|
@@ -58646,6 +58768,41 @@ function generateApplicationRoutesConfig(routes, pageFiles, includeGuards) {
|
|
|
58646
58768
|
lines.push("");
|
|
58647
58769
|
return lines.join("\n");
|
|
58648
58770
|
}
|
|
58771
|
+
function generateComponentRegistry(navRoutes, pageFiles, _webPath) {
|
|
58772
|
+
const lines = [];
|
|
58773
|
+
lines.push("/**");
|
|
58774
|
+
lines.push(" * Auto-generated by MCP scaffold_routes \u2014 DO NOT EDIT");
|
|
58775
|
+
lines.push(" * Registers all SmartStack pages into PageRegistry for DynamicRouter resolution.");
|
|
58776
|
+
lines.push(" */");
|
|
58777
|
+
lines.push("import { lazy } from 'react';");
|
|
58778
|
+
lines.push("import { PageRegistry } from '@/extensions/PageRegistry';");
|
|
58779
|
+
lines.push("");
|
|
58780
|
+
const routeTree = buildRouteTree(navRoutes);
|
|
58781
|
+
for (const [app, modules] of Object.entries(routeTree)) {
|
|
58782
|
+
lines.push(`// === ${app} ===`);
|
|
58783
|
+
for (const [, moduleRoutes] of Object.entries(modules)) {
|
|
58784
|
+
for (const route of moduleRoutes) {
|
|
58785
|
+
const discovery = pageFiles.get(route.navRoute);
|
|
58786
|
+
if (!discovery) continue;
|
|
58787
|
+
const componentKey = route.navRoute.split(".").map(toKebabCase).join(".");
|
|
58788
|
+
if (discovery.list) {
|
|
58789
|
+
lines.push(`PageRegistry.register('${componentKey}', lazy(() => import('${discovery.list.importPath}').then(m => ({ default: m.${discovery.list.componentName} }))));`);
|
|
58790
|
+
}
|
|
58791
|
+
if (discovery.detail) {
|
|
58792
|
+
lines.push(`PageRegistry.register('${componentKey}.detail', lazy(() => import('${discovery.detail.importPath}').then(m => ({ default: m.${discovery.detail.componentName} }))));`);
|
|
58793
|
+
}
|
|
58794
|
+
if (discovery.create) {
|
|
58795
|
+
lines.push(`PageRegistry.register('${componentKey}.create', lazy(() => import('${discovery.create.importPath}').then(m => ({ default: m.${discovery.create.componentName} }))));`);
|
|
58796
|
+
}
|
|
58797
|
+
if (discovery.edit) {
|
|
58798
|
+
lines.push(`PageRegistry.register('${componentKey}.edit', lazy(() => import('${discovery.edit.importPath}').then(m => ({ default: m.${discovery.edit.componentName} }))));`);
|
|
58799
|
+
}
|
|
58800
|
+
}
|
|
58801
|
+
}
|
|
58802
|
+
lines.push("");
|
|
58803
|
+
}
|
|
58804
|
+
return lines.join("\n");
|
|
58805
|
+
}
|
|
58649
58806
|
function getLayoutName(_application) {
|
|
58650
58807
|
return "AppLayout";
|
|
58651
58808
|
}
|
|
@@ -58754,7 +58911,7 @@ and generates corresponding frontend routing infrastructure.`,
|
|
|
58754
58911
|
includeLayouts: { type: "boolean", default: true },
|
|
58755
58912
|
includeGuards: { type: "boolean", default: true },
|
|
58756
58913
|
generateRegistry: { type: "boolean", default: true },
|
|
58757
|
-
outputFormat: { type: "string", enum: ["standalone", "applicationRoutes", "clientRoutes"], default: "
|
|
58914
|
+
outputFormat: { type: "string", enum: ["standalone", "applicationRoutes", "clientRoutes", "componentRegistry"], default: "componentRegistry", description: "componentRegistry (v3.7+): PageRegistry.register() calls for DynamicRouter. applicationRoutes (deprecated): RouteObject[] arrays for App.tsx. standalone: createBrowserRouter(). clientRoutes: deprecated alias for applicationRoutes." },
|
|
58758
58915
|
dryRun: { type: "boolean", default: false }
|
|
58759
58916
|
}
|
|
58760
58917
|
}
|
|
@@ -58809,17 +58966,26 @@ async function validateFrontendRoutes2(input, config2) {
|
|
|
58809
58966
|
if (scope === "all" || scope === "api-clients") {
|
|
58810
58967
|
await validateApiClients(webPath, backendRoutes, result);
|
|
58811
58968
|
}
|
|
58812
|
-
|
|
58969
|
+
const componentRegistryPath = path20.join(webPath, "src", "extensions", "componentRegistry.generated.ts");
|
|
58970
|
+
const usesDynamicRouter = await fileExists(componentRegistryPath);
|
|
58971
|
+
if ((scope === "all" || scope === "routes") && !usesDynamicRouter) {
|
|
58813
58972
|
await validateRoutes(webPath, backendRoutes, result);
|
|
58814
58973
|
}
|
|
58815
|
-
if (scope === "all" || scope === "routes") {
|
|
58974
|
+
if ((scope === "all" || scope === "routes") && !usesDynamicRouter) {
|
|
58816
58975
|
await validateAppWiring(webPath, backendRoutes, result);
|
|
58817
58976
|
}
|
|
58818
|
-
if (scope === "all" || scope === "routes") {
|
|
58977
|
+
if ((scope === "all" || scope === "routes") && !usesDynamicRouter) {
|
|
58819
58978
|
await validateCrudRoutes(webPath, backendRoutes, result);
|
|
58820
58979
|
}
|
|
58980
|
+
if (scope === "all" || scope === "routes") {
|
|
58981
|
+
await validateComponentRegistry(webPath, backendRoutes, result);
|
|
58982
|
+
}
|
|
58821
58983
|
generateRecommendations2(result);
|
|
58822
|
-
|
|
58984
|
+
if (usesDynamicRouter) {
|
|
58985
|
+
result.valid = result.apiClients.issues.filter((i) => i.severity === "error").length === 0 && result.registry.exists && (!result.componentRegistry || result.componentRegistry.missingKeys.length === 0);
|
|
58986
|
+
} else {
|
|
58987
|
+
result.valid = result.apiClients.issues.filter((i) => i.severity === "error").length === 0 && result.routes.missing.length === 0 && result.registry.exists && result.appWiring.issues.length === 0;
|
|
58988
|
+
}
|
|
58823
58989
|
return result;
|
|
58824
58990
|
}
|
|
58825
58991
|
async function discoverBackendNavRoutes(structure) {
|
|
@@ -59084,6 +59250,13 @@ async function validateAppWiring(webPath, backendRoutes, result) {
|
|
|
59084
59250
|
const hasRoutesImport = appContent.includes("from './routes") || appContent.includes("from '../routes");
|
|
59085
59251
|
const hasInlineRoutes = appContent.includes("<Route ");
|
|
59086
59252
|
result.appWiring.routesImported = hasApplicationRoutesImport || hasRoutesImport || hasInlineRoutes;
|
|
59253
|
+
const hasDynamicRouter = appContent.includes("DynamicRouter") || appContent.includes("<DynamicRouter");
|
|
59254
|
+
const hasComponentRegistry = appContent.includes("componentRegistry.generated");
|
|
59255
|
+
if (hasDynamicRouter || hasComponentRegistry) {
|
|
59256
|
+
result.appWiring.routesImported = true;
|
|
59257
|
+
result.appWiring.issues = [];
|
|
59258
|
+
return;
|
|
59259
|
+
}
|
|
59087
59260
|
if (!result.appWiring.routesImported) {
|
|
59088
59261
|
result.appWiring.issues.push("App.tsx does not import any route configuration");
|
|
59089
59262
|
return;
|
|
@@ -59163,6 +59336,39 @@ async function validateCrudRoutes(webPath, backendRoutes, result) {
|
|
|
59163
59336
|
}
|
|
59164
59337
|
}
|
|
59165
59338
|
}
|
|
59339
|
+
async function validateComponentRegistry(webPath, backendRoutes, result) {
|
|
59340
|
+
const registryPath = path20.join(webPath, "src", "extensions", "componentRegistry.generated.ts");
|
|
59341
|
+
if (!await fileExists(registryPath)) {
|
|
59342
|
+
result.componentRegistry = { exists: false, registeredKeys: [], missingKeys: [] };
|
|
59343
|
+
result.recommendations.push('Run `scaffold_routes outputFormat="componentRegistry"` to generate componentRegistry.generated.ts');
|
|
59344
|
+
return;
|
|
59345
|
+
}
|
|
59346
|
+
const content = await readText(registryPath);
|
|
59347
|
+
const registerCalls = content.matchAll(/PageRegistry\.register\s*\(\s*['"]([^'"]+)['"]/g);
|
|
59348
|
+
const registeredKeys = /* @__PURE__ */ new Set();
|
|
59349
|
+
for (const match2 of registerCalls) {
|
|
59350
|
+
registeredKeys.add(match2[1]);
|
|
59351
|
+
}
|
|
59352
|
+
const missingKeys = [];
|
|
59353
|
+
for (const route of backendRoutes) {
|
|
59354
|
+
const componentKey = route.navRoute.split(".").map(toKebabCase).join(".");
|
|
59355
|
+
if (!registeredKeys.has(componentKey)) {
|
|
59356
|
+
missingKeys.push(componentKey);
|
|
59357
|
+
}
|
|
59358
|
+
}
|
|
59359
|
+
result.componentRegistry = {
|
|
59360
|
+
exists: true,
|
|
59361
|
+
registeredKeys: [...registeredKeys],
|
|
59362
|
+
missingKeys
|
|
59363
|
+
};
|
|
59364
|
+
const mainPath = path20.join(webPath, "src", "main.tsx");
|
|
59365
|
+
if (await fileExists(mainPath)) {
|
|
59366
|
+
const mainContent = await readText(mainPath);
|
|
59367
|
+
if (!mainContent.includes("componentRegistry.generated")) {
|
|
59368
|
+
result.recommendations.push("main.tsx does not import componentRegistry.generated \u2014 pages will not be registered");
|
|
59369
|
+
}
|
|
59370
|
+
}
|
|
59371
|
+
}
|
|
59166
59372
|
function generateRecommendations2(result) {
|
|
59167
59373
|
if (!result.registry.exists) {
|
|
59168
59374
|
result.recommendations.push('Run `scaffold_routes source="controllers"` to generate route registry');
|
|
@@ -59185,7 +59391,11 @@ function generateRecommendations2(result) {
|
|
|
59185
59391
|
if (result.routes.orphaned.length > 0) {
|
|
59186
59392
|
result.recommendations.push(`${result.routes.orphaned.length} frontend routes have no backend NavRoute`);
|
|
59187
59393
|
}
|
|
59188
|
-
if (result.
|
|
59394
|
+
if (result.componentRegistry && !result.componentRegistry.exists) {
|
|
59395
|
+
result.recommendations.push('No componentRegistry.generated.ts found. Run `scaffold_routes outputFormat="componentRegistry"` to generate it.');
|
|
59396
|
+
} else if (result.componentRegistry && result.componentRegistry.missingKeys.length > 0) {
|
|
59397
|
+
result.recommendations.push(`${result.componentRegistry.missingKeys.length} backend routes missing from componentRegistry. Run \`scaffold_routes\` to regenerate.`);
|
|
59398
|
+
} else if (result.appWiring && !result.appWiring.exists) {
|
|
59189
59399
|
result.recommendations.push("No App.tsx found. Create App.tsx with route configuration.");
|
|
59190
59400
|
} else if (result.appWiring && result.appWiring.issues.length > 0) {
|
|
59191
59401
|
const unwired = result.appWiring.issues.filter((i) => i.includes("not wired")).length;
|
|
@@ -59278,6 +59488,23 @@ function formatResult6(result, _input) {
|
|
|
59278
59488
|
lines.push("");
|
|
59279
59489
|
}
|
|
59280
59490
|
}
|
|
59491
|
+
if (result.componentRegistry) {
|
|
59492
|
+
lines.push("## Component Registry");
|
|
59493
|
+
lines.push("");
|
|
59494
|
+
lines.push(`| Check | Status |`);
|
|
59495
|
+
lines.push(`|-------|--------|`);
|
|
59496
|
+
lines.push(`| Registry Exists | ${result.componentRegistry.exists ? "Yes" : "No"} |`);
|
|
59497
|
+
lines.push(`| Registered Keys | ${result.componentRegistry.registeredKeys.length} |`);
|
|
59498
|
+
lines.push(`| Missing Keys | ${result.componentRegistry.missingKeys.length} |`);
|
|
59499
|
+
lines.push("");
|
|
59500
|
+
if (result.componentRegistry.missingKeys.length > 0) {
|
|
59501
|
+
lines.push("### Missing PageRegistry Entries");
|
|
59502
|
+
for (const key of result.componentRegistry.missingKeys) {
|
|
59503
|
+
lines.push(`- \`${key}\``);
|
|
59504
|
+
}
|
|
59505
|
+
lines.push("");
|
|
59506
|
+
}
|
|
59507
|
+
}
|
|
59281
59508
|
lines.push("## Recommendations");
|
|
59282
59509
|
lines.push("");
|
|
59283
59510
|
for (const rec of result.recommendations) {
|