diff --git a/biome.json b/biome.json
index 42c6ae21..fd3046c6 100644
--- a/biome.json
+++ b/biome.json
@@ -48,6 +48,8 @@
"website/**",
"!**/wwwroot",
"!modules/*/src/*/types.ts",
+ "!packages/SimpleModule.Client/src/routes.ts",
+ "!template/SimpleModule.Host/ClientApp/routes.ts",
"!test-projects"
]
}
diff --git a/cli/SimpleModule.Cli/Commands/New/NewProjectCommand.cs b/cli/SimpleModule.Cli/Commands/New/NewProjectCommand.cs
index e7d85cb9..82e81da9 100644
--- a/cli/SimpleModule.Cli/Commands/New/NewProjectCommand.cs
+++ b/cli/SimpleModule.Cli/Commands/New/NewProjectCommand.cs
@@ -133,6 +133,18 @@ string frameworkVersion
ProjectTemplates.NugetConfig(solution?.RootPath)
);
+ // ── Type-generation scripts ───────────────────────────
+ var scriptsDir = Path.Combine(rootDir, "scripts");
+ Directory.CreateDirectory(scriptsDir);
+ File.WriteAllText(
+ Path.Combine(scriptsDir, "extract-ts-types.mjs"),
+ ProjectTemplates.ExtractTsTypesScript()
+ );
+ File.WriteAllText(
+ Path.Combine(scriptsDir, "extract-routes.mjs"),
+ ProjectTemplates.ExtractRoutesScript()
+ );
+
// ── Host project ──────────────────────────────────────
File.WriteAllText(
Path.Combine(hostDir, $"{projectName}.Host.csproj"),
@@ -290,6 +302,8 @@ string rootDir
Plan(Path.Combine(rootDir, "tsconfig.json"));
Plan(Path.Combine(rootDir, ".editorconfig"));
Plan(Path.Combine(rootDir, "nuget.config"));
+ Plan(Path.Combine(rootDir, "scripts", "extract-ts-types.mjs"));
+ Plan(Path.Combine(rootDir, "scripts", "extract-routes.mjs"));
// Host project files
Plan(Path.Combine(hostDir, $"{projectName}.Host.csproj"));
diff --git a/cli/SimpleModule.Cli/SimpleModule.Cli.csproj b/cli/SimpleModule.Cli/SimpleModule.Cli.csproj
index bc3e3020..e718efb8 100644
--- a/cli/SimpleModule.Cli/SimpleModule.Cli.csproj
+++ b/cli/SimpleModule.Cli/SimpleModule.Cli.csproj
@@ -62,5 +62,13 @@
Include="..\..\template\SimpleModule.Host\Properties\launchSettings.json"
LogicalName="Templates.Host.Properties.launchSettings.json"
/>
+
+
diff --git a/cli/SimpleModule.Cli/Templates/ProjectTemplates.cs b/cli/SimpleModule.Cli/Templates/ProjectTemplates.cs
index 298dc9bc..21ad6f48 100644
--- a/cli/SimpleModule.Cli/Templates/ProjectTemplates.cs
+++ b/cli/SimpleModule.Cli/Templates/ProjectTemplates.cs
@@ -251,11 +251,23 @@ public string BiomeJson()
var content = File.ReadAllText(path);
- // Replace file includes: monorepo paths → project paths
- content = content.Replace(
- "\"modules/**\", \"packages/**\", \"template/**\", \"tests/**\"",
- "\"src/**\", \"tests/**\"",
- StringComparison.Ordinal
+ // The monorepo's files.includes references top-level directories (modules/**,
+ // packages/**, template/**) that don't exist in scaffolded projects. Swap the
+ // whole array; leave the rest of biome.json verbatim.
+ var scaffoldedIncludes =
+ "\"includes\": [\n"
+ + " \"src/**\",\n"
+ + " \"tests/**\",\n"
+ + " \"!**/wwwroot\",\n"
+ + " \"!src/modules/*/src/*/types.ts\",\n"
+ + " \"!src/*.Host/ClientApp/routes.ts\"\n"
+ + " ]";
+
+ content = Regex.Replace(
+ content,
+ "\"includes\"\\s*:\\s*\\[[^\\]]*\\]",
+ scaffoldedIncludes,
+ RegexOptions.Singleline
);
return content;
@@ -716,7 +728,9 @@ string frameworkVersion
"check": "biome check .",
"check:fix": "biome check --write .",
"build": "cross-env VITE_MODE=prod npm run build --workspaces --if-present",
- "build:dev": "cross-env VITE_MODE=dev npm run build --workspaces --if-present"
+ "build:dev": "cross-env VITE_MODE=dev npm run build --workspaces --if-present",
+ "generate:types": "dotnet build src/{{projectName}}.Host && node scripts/extract-ts-types.mjs src/{{projectName}}.Host/obj/Debug/net10.0/generated/SimpleModule.Generator/SimpleModule.Generator.ModuleDiscovererGenerator src/modules",
+ "generate:routes": "dotnet build src/{{projectName}}.Host && node scripts/extract-routes.mjs src/{{projectName}}.Host/obj/Debug/net10.0/generated/SimpleModule.Generator/SimpleModule.Generator.ModuleDiscovererGenerator src/{{projectName}}.Host/ClientApp/routes.ts"
},
"devDependencies": {
"@biomejs/biome": "^2.4.10",
@@ -743,6 +757,12 @@ string frameworkVersion
""";
}
+ public static string ExtractTsTypesScript() =>
+ EmbeddedResourceReader.ReadTemplate("Templates.scripts.extract-ts-types.mjs");
+
+ public static string ExtractRoutesScript() =>
+ EmbeddedResourceReader.ReadTemplate("Templates.scripts.extract-routes.mjs");
+
private static string FallbackBiomeJson() =>
"""
{
@@ -777,7 +797,13 @@ private static string FallbackBiomeJson() =>
}
},
"files": {
- "includes": ["src/**", "tests/**", "!**/wwwroot"]
+ "includes": [
+ "src/**",
+ "tests/**",
+ "!**/wwwroot",
+ "!src/modules/*/src/*/types.ts",
+ "!src/*.Host/ClientApp/routes.ts"
+ ]
}
}
""";
diff --git a/packages/SimpleModule.Client/src/routes.ts b/packages/SimpleModule.Client/src/routes.ts
index 95f7b7dd..eb54e841 100644
--- a/packages/SimpleModule.Client/src/routes.ts
+++ b/packages/SimpleModule.Client/src/routes.ts
@@ -88,19 +88,16 @@ export const routes = {
},
tenants: {
api: {
- deleteTenantFeature: (id: string | number, flagName: string | number) =>
- `/api/tenants/${id}/features/${flagName}`,
+ deleteTenantFeature: (id: string | number, flagName: string | number) => `/api/tenants/${id}/features/${flagName}`,
getTenantFeatures: (id: string | number) => `/api/tenants/${id}/features`,
- setTenantFeature: (id: string | number, flagName: string | number) =>
- `/api/tenants/${id}/features/${flagName}`,
+ setTenantFeature: (id: string | number, flagName: string | number) => `/api/tenants/${id}/features/${flagName}`,
addHost: (id: string | number) => `/api/tenants/${id}/hosts`,
changeStatus: (id: string | number) => `/api/tenants/${id}/status`,
create: () => '/api/tenants' as const,
delete: (id: string | number) => `/api/tenants/${id}`,
getAll: () => '/api/tenants' as const,
getById: (id: string | number) => `/api/tenants/${id}`,
- removeHost: (id: string | number, hostId: string | number) =>
- `/api/tenants/${id}/hosts/${hostId}`,
+ removeHost: (id: string | number, hostId: string | number) => `/api/tenants/${id}/hosts/${hostId}`,
update: (id: string | number) => `/api/tenants/${id}`,
},
views: {
@@ -170,8 +167,7 @@ export const routes = {
manageIndex: () => '/Identity/Account/Manage' as const,
personalData: () => '/Identity/Account/Manage/PersonalData' as const,
removePhoneNumber: () => '/Identity/Account/Manage/RemovePhoneNumber' as const,
- sendPhoneVerificationCode: () =>
- '/Identity/Account/Manage/SendPhoneVerificationCode' as const,
+ sendPhoneVerificationCode: () => '/Identity/Account/Manage/SendPhoneVerificationCode' as const,
setPassword: () => '/Identity/Account/Manage/SetPassword' as const,
},
},
@@ -216,8 +212,7 @@ export const routes = {
userinfo: () => '/connect/userinfo' as const,
activeSessions: () => '/Identity/Account/Manage/ActiveSessions' as const,
revokeOtherSessions: () => '/Identity/Account/Manage/ActiveSessions/revoke-others' as const,
- revokeSession: (tokenId: string | number) =>
- `/Identity/Account/Manage/ActiveSessions/${tokenId}/revoke`,
+ revokeSession: (tokenId: string | number) => `/Identity/Account/Manage/ActiveSessions/${tokenId}/revoke`,
},
views: {
clientsCreate: () => '/openiddict/clients/create' as const,
@@ -228,8 +223,7 @@ export const routes = {
admin: {
api: {
adminRoles: () => '/admin/roles' as const,
- adminSessions: (id: string | number, tokenId: string | number) =>
- `/admin/users/${id}/sessions/${tokenId}`,
+ adminSessions: (id: string | number, tokenId: string | number) => `/admin/users/${id}/sessions/${tokenId}`,
adminUsers: () => '/admin/users' as const,
},
views: {
@@ -243,3 +237,4 @@ export const routes = {
},
},
} as const;
+
diff --git a/scripts/extract-routes.mjs b/scripts/extract-routes.mjs
index cea3b0c7..276dbf14 100644
--- a/scripts/extract-routes.mjs
+++ b/scripts/extract-routes.mjs
@@ -2,9 +2,22 @@
// Extracts TypeScript route definitions from TypeScriptRoutes.g.cs
// Usage: node scripts/extract-routes.mjs
-import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
+import {
+ existsSync,
+ readFileSync,
+ writeFileSync,
+ mkdirSync,
+} from 'fs';
import { resolve, dirname, join } from 'path';
+function writeIfChanged(path, contents) {
+ if (existsSync(path) && readFileSync(path, 'utf-8') === contents) {
+ return false;
+ }
+ writeFileSync(path, contents);
+ return true;
+}
+
const generatedDir = process.argv[2];
const outputPath =
process.argv[3] ||
@@ -34,10 +47,9 @@ if (!tsMatch) {
const tsContent = tsMatch[1];
const outPath = resolve(outputPath);
+const next = `// Auto-generated from endpoint Route constants \u2014 do not edit\n${tsContent}`;
mkdirSync(dirname(outPath), { recursive: true });
-writeFileSync(
- outPath,
- `// Auto-generated from endpoint Route constants \u2014 do not edit\n${tsContent}`,
-);
-console.log(`Wrote routes to ${outPath}`);
+if (writeIfChanged(outPath, next)) {
+ console.log(`Wrote routes to ${outPath}`);
+}
diff --git a/scripts/extract-ts-types.mjs b/scripts/extract-ts-types.mjs
index fd2c5184..644f0a32 100644
--- a/scripts/extract-ts-types.mjs
+++ b/scripts/extract-ts-types.mjs
@@ -2,9 +2,23 @@
// Extracts TypeScript interfaces from per-module DtoTypeScript_*.g.cs files
// Usage: node scripts/extract-ts-types.mjs
-import { readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
+import {
+ readdirSync,
+ readFileSync,
+ writeFileSync,
+ mkdirSync,
+ existsSync,
+} from 'fs';
import { resolve, join } from 'path';
+function writeIfChanged(path, contents) {
+ if (existsSync(path) && readFileSync(path, 'utf-8') === contents) {
+ return false;
+ }
+ writeFileSync(path, contents);
+ return true;
+}
+
const generatedDir = process.argv[2];
const modulesDir = process.argv[3] || 'modules';
@@ -52,9 +66,8 @@ for (const file of files) {
mkdirSync(resolve(modulesDir, moduleName, 'src', projectDir), {
recursive: true,
});
- writeFileSync(
- outPath,
- `// Auto-generated from [Dto] types \u2014 do not edit\n${tsContent}`,
- );
- console.log(`Wrote ${moduleName} types to ${outPath}`);
+ const next = `// Auto-generated from [Dto] types \u2014 do not edit\n${tsContent}`;
+ if (writeIfChanged(outPath, next)) {
+ console.log(`Wrote ${moduleName} types to ${outPath}`);
+ }
}
diff --git a/template/SimpleModule.Host/ClientApp/routes.ts b/template/SimpleModule.Host/ClientApp/routes.ts
index f3ade0f2..eb54e841 100644
--- a/template/SimpleModule.Host/ClientApp/routes.ts
+++ b/template/SimpleModule.Host/ClientApp/routes.ts
@@ -48,60 +48,6 @@ export const routes = {
browse: () => '/files' as const,
},
},
- marketplace: {
- api: {
- getById: (id: string | number) => `/api/marketplace/${id}`,
- search: () => '/api/marketplace' as const,
- },
- views: {
- browse: () => '/marketplace' as const,
- detail: (id: string | number) => `/marketplace/${id}`,
- },
- },
- pageBuilder: {
- api: {
- create: () => '/api/pagebuilder' as const,
- delete: (id: string | number) => `/api/pagebuilder/${id}`,
- getAll: () => '/api/pagebuilder' as const,
- getById: (id: string | number) => `/api/pagebuilder/${id}`,
- permanentDelete: (id: string | number) => `/api/pagebuilder/${id}/permanent`,
- publish: (id: string | number) => `/api/pagebuilder/${id}/publish`,
- restore: (id: string | number) => `/api/pagebuilder/${id}/restore`,
- trash: () => '/api/pagebuilder/trash' as const,
- unpublish: (id: string | number) => `/api/pagebuilder/${id}/unpublish`,
- updateContent: (id: string | number) => `/api/pagebuilder/${id}/content`,
- update: (id: string | number) => `/api/pagebuilder/${id}`,
- addTagToPage: (id: string | number) => `/api/pagebuilder/${id}/tags`,
- getAllTags: () => '/api/pagebuilder/tags' as const,
- removeTagFromPage: (id: string | number, tagId: string | number) =>
- `/api/pagebuilder/${id}/tags/${tagId}`,
- createTemplate: () => '/api/pagebuilder/templates' as const,
- deleteTemplate: (id: string | number) => `/api/pagebuilder/templates/${id}`,
- getAllTemplates: () => '/api/pagebuilder/templates' as const,
- },
- views: {
- editor: () => '/pages/new' as const,
- manage: () => '/pages/manage' as const,
- pagesList: () => '/pages' as const,
- viewerDraft: (slug: string | number) => `/pages/view/${slug}/draft`,
- viewer: (slug: string | number) => `/pages/view/${slug}`,
- },
- },
- products: {
- api: {
- create: () => '/api/products' as const,
- delete: (id: string | number) => `/api/products/${id}`,
- getAll: () => '/api/products' as const,
- getById: (id: string | number) => `/api/products/${id}`,
- update: (id: string | number) => `/api/products/${id}`,
- },
- views: {
- browse: () => '/products' as const,
- create: () => '/products/create' as const,
- edit: (id: string | number) => `/products/${id}/edit`,
- manage: () => '/products/manage' as const,
- },
- },
rateLimiting: {
api: {
create: () => '/api/rate-limiting' as const,
@@ -142,19 +88,16 @@ export const routes = {
},
tenants: {
api: {
- deleteTenantFeature: (id: string | number, flagName: string | number) =>
- `/api/tenants/${id}/features/${flagName}`,
+ deleteTenantFeature: (id: string | number, flagName: string | number) => `/api/tenants/${id}/features/${flagName}`,
getTenantFeatures: (id: string | number) => `/api/tenants/${id}/features`,
- setTenantFeature: (id: string | number, flagName: string | number) =>
- `/api/tenants/${id}/features/${flagName}`,
+ setTenantFeature: (id: string | number, flagName: string | number) => `/api/tenants/${id}/features/${flagName}`,
addHost: (id: string | number) => `/api/tenants/${id}/hosts`,
changeStatus: (id: string | number) => `/api/tenants/${id}/status`,
create: () => '/api/tenants' as const,
delete: (id: string | number) => `/api/tenants/${id}`,
getAll: () => '/api/tenants' as const,
getById: (id: string | number) => `/api/tenants/${id}`,
- removeHost: (id: string | number, hostId: string | number) =>
- `/api/tenants/${id}/hosts/${hostId}`,
+ removeHost: (id: string | number, hostId: string | number) => `/api/tenants/${id}/hosts/${hostId}`,
update: (id: string | number) => `/api/tenants/${id}`,
},
views: {
@@ -211,13 +154,20 @@ export const routes = {
resetAuthenticator: () => '/Identity/Account/Manage/ResetAuthenticator' as const,
resetPasswordConfirmation: () => '/Identity/Account/ResetPasswordConfirmation' as const,
resetPassword: () => '/Identity/Account/ResetPassword' as const,
+ sendUnlockEmailConfirmation: () => '/Identity/Account/SendUnlockEmailConfirmation' as const,
+ sendUnlockEmail: () => '/Identity/Account/SendUnlockEmail' as const,
+ signOutEverywhere: () => '/Identity/Account/Manage/SignOutEverywhere' as const,
twoFactorAuthentication: () => '/Identity/Account/Manage/TwoFactorAuthentication' as const,
+ unlockAccount: () => '/Identity/Account/UnlockAccount' as const,
changePassword: () => '/Identity/Account/Manage/ChangePassword' as const,
+ confirmPhoneNumber: () => '/Identity/Account/Manage/ConfirmPhoneNumber' as const,
deletePersonalData: () => '/Identity/Account/Manage/DeletePersonalData' as const,
email: () => '/Identity/Account/Manage/Email' as const,
externalLogins: () => '/Identity/Account/Manage/ExternalLogins' as const,
manageIndex: () => '/Identity/Account/Manage' as const,
personalData: () => '/Identity/Account/Manage/PersonalData' as const,
+ removePhoneNumber: () => '/Identity/Account/Manage/RemovePhoneNumber' as const,
+ sendPhoneVerificationCode: () => '/Identity/Account/Manage/SendPhoneVerificationCode' as const,
setPassword: () => '/Identity/Account/Manage/SetPassword' as const,
},
},
@@ -238,29 +188,31 @@ export const routes = {
dashboard: () => '/email/dashboard' as const,
editTemplate: (id: string | number) => `/email/templates/${id}/edit`,
history: () => '/email/history' as const,
+ settings: () => '/email/settings' as const,
templates: () => '/email/templates' as const,
},
},
- orders: {
+ notifications: {
api: {
- create: () => '/api/orders' as const,
- delete: (id: string | number) => `/api/orders/${id}`,
- getAll: () => '/api/orders' as const,
- getById: (id: string | number) => `/api/orders/${id}`,
- update: (id: string | number) => `/api/orders/${id}`,
+ listNotifications: () => '/api/notifications' as const,
+ markAllRead: () => '/api/notifications/read-all' as const,
+ markRead: (id: string | number) => `/api/notifications/${id}/read`,
+ unreadCount: () => '/api/notifications/unread-count' as const,
},
views: {
- create: () => '/orders/create' as const,
- edit: (id: string | number) => `/orders/${id}/edit`,
- list: () => '/orders' as const,
+ inbox: () => '/notifications' as const,
},
},
openIddict: {
api: {
authorization: () => '/connect/authorize' as const,
logout: () => '/connect/endsession' as const,
+ oAuthCallback: () => '/oauth-callback' as const,
token: () => '/connect/token' as const,
userinfo: () => '/connect/userinfo' as const,
+ activeSessions: () => '/Identity/Account/Manage/ActiveSessions' as const,
+ revokeOtherSessions: () => '/Identity/Account/Manage/ActiveSessions/revoke-others' as const,
+ revokeSession: (tokenId: string | number) => `/Identity/Account/Manage/ActiveSessions/${tokenId}/revoke`,
},
views: {
clientsCreate: () => '/openiddict/clients/create' as const,
@@ -271,8 +223,7 @@ export const routes = {
admin: {
api: {
adminRoles: () => '/admin/roles' as const,
- adminSessions: (id: string | number, tokenId: string | number) =>
- `/admin/users/${id}/sessions/${tokenId}`,
+ adminSessions: (id: string | number, tokenId: string | number) => `/admin/users/${id}/sessions/${tokenId}`,
adminUsers: () => '/admin/users' as const,
},
views: {
@@ -286,3 +237,4 @@ export const routes = {
},
},
} as const;
+