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; +