website refactor
This commit is contained in:
1097
apps/website/tests/guardrails/ArchitectureGuardrails.ts
Normal file
1097
apps/website/tests/guardrails/ArchitectureGuardrails.ts
Normal file
File diff suppressed because it is too large
Load Diff
25
apps/website/tests/guardrails/GuardrailViolation.ts
Normal file
25
apps/website/tests/guardrails/GuardrailViolation.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Guardrail violation representation
|
||||
*/
|
||||
|
||||
export class GuardrailViolation {
|
||||
constructor(
|
||||
public readonly ruleName: string,
|
||||
public readonly filePath: string,
|
||||
public readonly lineNumber: number,
|
||||
public readonly description: string,
|
||||
) {}
|
||||
|
||||
toString(): string {
|
||||
return `${this.filePath}:${this.lineNumber} - ${this.ruleName}: ${this.description}`;
|
||||
}
|
||||
|
||||
toJSON(): object {
|
||||
return {
|
||||
rule: this.ruleName,
|
||||
file: this.filePath,
|
||||
line: this.lineNumber,
|
||||
description: this.description,
|
||||
};
|
||||
}
|
||||
}
|
||||
235
apps/website/tests/guardrails/allowed-violations.ts
Normal file
235
apps/website/tests/guardrails/allowed-violations.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Allowlist for architecture guardrail violations
|
||||
*
|
||||
* This file contains violations that currently exist in the codebase.
|
||||
* In future slices, these should be shrunk to zero.
|
||||
*
|
||||
* Format: Each rule has an array of file paths that are allowed to violate it.
|
||||
*/
|
||||
|
||||
export interface GuardrailAllowlist {
|
||||
[ruleName: string]: string[];
|
||||
}
|
||||
|
||||
export const ALLOWED_VIOLATIONS: GuardrailAllowlist = {
|
||||
// Rule 1: ContainerManager usage in server page queries
|
||||
'no-container-manager-in-server': [
|
||||
'apps/website/lib/page-queries/page-queries/ProfilePageQuery.ts',
|
||||
'apps/website/lib/page-queries/page-queries/TeamDetailPageQuery.ts',
|
||||
'apps/website/lib/page-queries/page-queries/TeamsPageQuery.ts',
|
||||
],
|
||||
|
||||
// Rule 2: PageDataFetcher.fetch() usage in server page queries
|
||||
'no-page-data-fetcher-fetch-in-server': [],
|
||||
|
||||
// Rule 3: ViewModels imported in forbidden paths
|
||||
'no-view-models-in-server': [
|
||||
'apps/website/lib/page-queries/page-queries/ProfilePageQuery.ts',
|
||||
'apps/website/lib/page-queries/page-queries/TeamDetailPageQuery.ts',
|
||||
'apps/website/lib/page-queries/page-queries/TeamsPageQuery.ts',
|
||||
'apps/website/app/leaderboards/drivers/page.tsx',
|
||||
'apps/website/app/leaderboards/page.tsx',
|
||||
'apps/website/app/leagues/[id]/page.tsx',
|
||||
'apps/website/app/leagues/[id]/rulebook/page.tsx',
|
||||
'apps/website/app/leagues/[id]/schedule/page.tsx',
|
||||
'apps/website/app/leagues/[id]/standings/page.tsx',
|
||||
'apps/website/app/leagues/[id]/stewarding/protests/[protestId]/page.tsx',
|
||||
'apps/website/app/profile/page.tsx',
|
||||
'apps/website/app/races/[id]/page.tsx',
|
||||
'apps/website/app/races/[id]/results/page.tsx',
|
||||
'apps/website/app/races/[id]/stewarding/page.tsx',
|
||||
'apps/website/app/sponsor/leagues/page.tsx',
|
||||
'apps/website/app/teams/leaderboard/page.tsx',
|
||||
'apps/website/lib/services/analytics/DashboardService.ts',
|
||||
'apps/website/lib/services/auth/SessionService.ts',
|
||||
'apps/website/lib/services/drivers/DriverService.ts',
|
||||
'apps/website/lib/services/landing/LandingService.test.ts',
|
||||
'apps/website/lib/services/landing/LandingService.ts',
|
||||
'apps/website/lib/services/leagues/LeagueMembershipService.ts',
|
||||
'apps/website/lib/services/leagues/LeagueSettingsService.test.ts',
|
||||
'apps/website/lib/services/leagues/LeagueSettingsService.ts',
|
||||
'apps/website/lib/services/leagues/LeagueWalletService.test.ts',
|
||||
'apps/website/lib/services/media/AvatarService.ts',
|
||||
'apps/website/lib/services/media/MediaService.ts',
|
||||
'apps/website/lib/services/onboarding/OnboardingService.ts',
|
||||
'apps/website/lib/services/payments/MembershipFeeService.ts',
|
||||
'apps/website/lib/services/payments/PaymentService.ts',
|
||||
'apps/website/lib/services/payments/WalletService.ts',
|
||||
'apps/website/lib/services/teams/TeamJoinService.ts',
|
||||
'apps/website/lib/services/teams/TeamService.ts',
|
||||
],
|
||||
|
||||
// Rule 4: Templates importing view-models or display-objects
|
||||
'no-view-models-in-templates': [
|
||||
'apps/website/templates/DriverProfileTemplate.tsx',
|
||||
'apps/website/templates/DriverRankingsTemplate.tsx',
|
||||
'apps/website/templates/DriversTemplate.tsx',
|
||||
'apps/website/templates/LeaderboardsTemplate.tsx',
|
||||
'apps/website/templates/LeagueAdminScheduleTemplate.tsx',
|
||||
'apps/website/templates/LeagueDetailTemplate.tsx',
|
||||
'apps/website/templates/LeagueRulebookTemplate.tsx',
|
||||
'apps/website/templates/LeagueScheduleTemplate.tsx',
|
||||
'apps/website/templates/LeagueStandingsTemplate.tsx',
|
||||
'apps/website/templates/LeaguesTemplate.tsx',
|
||||
'apps/website/templates/TeamLeaderboardTemplate.tsx',
|
||||
],
|
||||
|
||||
// Rule 5: Intl.* or toLocale* in presentation paths
|
||||
'no-intl-in-presentation': [
|
||||
'apps/website/app/leagues/[id]/stewarding/protests/[protestId]/page.tsx',
|
||||
'apps/website/app/profile/liveries/page.tsx',
|
||||
'apps/website/app/profile/page.tsx',
|
||||
'apps/website/app/sponsor/campaigns/page.tsx',
|
||||
'apps/website/templates/DriverProfileTemplate.tsx',
|
||||
'apps/website/templates/DriverRankingsTemplate.tsx',
|
||||
'apps/website/templates/DriversTemplate.tsx',
|
||||
'apps/website/templates/LeagueDetailTemplate.tsx',
|
||||
'apps/website/templates/LeagueScheduleTemplate.tsx',
|
||||
'apps/website/templates/RaceDetailTemplate.tsx',
|
||||
'apps/website/templates/RaceResultsTemplate.tsx',
|
||||
'apps/website/templates/RaceStewardingTemplate.tsx',
|
||||
'apps/website/templates/RacesAllTemplate.tsx',
|
||||
'apps/website/templates/RacesTemplate.tsx',
|
||||
'apps/website/templates/SponsorLeagueDetailTemplate.tsx',
|
||||
'apps/website/templates/TeamDetailTemplate.tsx',
|
||||
'apps/website/templates/TeamLeaderboardTemplate.tsx',
|
||||
'apps/website/lib/view-models/view-models/ActivityItemViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/AdminUserViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/AnalyticsMetricsViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/BillingViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/LeagueDetailViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/LeagueJoinRequestViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/LeagueMemberViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/LeagueStatsViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/MembershipFeeViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/PaymentViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/PrizeViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/ProtestViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/RaceDetailViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/RaceListItemViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/RaceStatsViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/RaceViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/RenewalAlertViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/SponsorDashboardViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/SponsorSponsorshipsViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/SponsorshipDetailViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/SponsorshipPricingViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/SponsorshipRequestViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/SponsorshipViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/TeamJoinRequestViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/TeamMemberViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/UpcomingRaceCardViewModel.ts',
|
||||
'apps/website/lib/view-models/view-models/WalletTransactionViewModel.ts',
|
||||
'apps/website/lib/display-objects/DashboardDisplay.ts',
|
||||
'apps/website/lib/display-objects/ProfileDisplay.ts',
|
||||
'apps/website/components/DriverTopThreePodium.tsx',
|
||||
'apps/website/components/achievements/AchievementCard.tsx',
|
||||
'apps/website/components/dashboard/UpcomingRaceItem.tsx',
|
||||
'apps/website/components/dev/sections/APIStatusSection.tsx',
|
||||
'apps/website/components/dev/sections/ReplaySection.tsx',
|
||||
'apps/website/components/drivers/CareerHighlights.tsx',
|
||||
'apps/website/components/drivers/FeaturedDriverCard.tsx',
|
||||
'apps/website/components/drivers/HeroSection.tsx',
|
||||
'apps/website/components/drivers/LeaderboardPreview.tsx',
|
||||
'apps/website/components/errors/DevErrorPanel.tsx',
|
||||
'apps/website/components/errors/EnhancedErrorBoundary.tsx',
|
||||
'apps/website/components/errors/ErrorAnalyticsDashboard.tsx',
|
||||
'apps/website/components/leaderboards/DriverLeaderboardPreview.tsx',
|
||||
'apps/website/components/leagues/LeagueActivityFeed.tsx',
|
||||
'apps/website/components/leagues/LeagueMembers.tsx',
|
||||
'apps/website/components/leagues/LeagueReviewSummary.tsx',
|
||||
'apps/website/components/leagues/LeagueSchedule.tsx',
|
||||
'apps/website/components/leagues/PenaltyHistoryList.tsx',
|
||||
'apps/website/components/leagues/PendingProtestsList.tsx',
|
||||
'apps/website/components/leagues/QuickPenaltyModal.tsx',
|
||||
'apps/website/components/leagues/ReadonlyLeagueInfo.tsx',
|
||||
'apps/website/components/leagues/ReviewProtestModal.tsx',
|
||||
'apps/website/components/notifications/ModalNotification.tsx',
|
||||
'apps/website/components/notifications/NotificationCenter.tsx',
|
||||
'apps/website/components/profile/LiveryCard.tsx',
|
||||
'apps/website/components/races/LatestResultsSidebar.tsx',
|
||||
'apps/website/components/races/RaceResultCard.tsx',
|
||||
'apps/website/components/races/RaceResultsHeader.tsx',
|
||||
'apps/website/components/races/UpcomingRacesSidebar.tsx',
|
||||
'apps/website/components/sponsors/MetricCard.tsx',
|
||||
'apps/website/components/sponsors/PendingSponsorshipRequests.tsx',
|
||||
'apps/website/components/sponsors/SponsorInsightsCard.tsx',
|
||||
'apps/website/components/sponsors/SponsorshipCategoryCard.tsx',
|
||||
'apps/website/components/teams/TeamAdmin.tsx',
|
||||
'apps/website/components/teams/TeamCard.tsx',
|
||||
'apps/website/components/teams/TeamLeaderboardPreview.tsx',
|
||||
'apps/website/components/teams/TeamMembershipCard.tsx',
|
||||
'apps/website/components/teams/TeamRoster.tsx',
|
||||
'apps/website/lib/utilities/time.ts',
|
||||
// Additional Intl violations in test files (old location)
|
||||
'apps/website/lib/view-models/ActivityItemViewModel.test.ts',
|
||||
'apps/website/lib/view-models/AnalyticsMetricsViewModel.test.ts',
|
||||
'apps/website/lib/view-models/BillingViewModel.test.ts',
|
||||
'apps/website/lib/view-models/LeagueDetailViewModel.test.ts',
|
||||
'apps/website/lib/view-models/LeagueStatsViewModel.test.ts',
|
||||
'apps/website/lib/view-models/RaceStatsViewModel.test.ts',
|
||||
'apps/website/lib/view-models/SponsorDashboardViewModel.test.ts',
|
||||
'apps/website/lib/view-models/SponsorSponsorshipsViewModel.test.ts',
|
||||
'apps/website/lib/view-models/SponsorshipPricingViewModel.test.ts',
|
||||
'apps/website/lib/view-models/SponsorshipViewModel.test.ts',
|
||||
],
|
||||
|
||||
// Rule 6: Client-side fetch with write methods
|
||||
'no-client-write-fetch': [
|
||||
'apps/website/app/sponsor/signup/page.tsx',
|
||||
],
|
||||
|
||||
// Rule 7: *Template.tsx files under app/
|
||||
'no-templates-in-app': [],
|
||||
|
||||
// Rule 8: 'as any' usage - ZERO TOLERANCE
|
||||
// Hard fail - no allowlist entries allowed
|
||||
'no-as-any': [],
|
||||
|
||||
// New Rule 1: RSC boundary - additional checks
|
||||
'no-presenters-in-server': [],
|
||||
'no-sorting-filtering-in-server': [],
|
||||
'no-display-objects-in-server': [],
|
||||
'no-unsafe-services-in-server': [],
|
||||
'no-di-in-server': [],
|
||||
'no-local-helpers-in-server': [],
|
||||
'no-object-construction-in-server': [],
|
||||
'no-container-manager-calls-in-server': [],
|
||||
|
||||
// New Rule 2: Template purity - additional checks
|
||||
'no-state-hooks-in-templates': [],
|
||||
'no-computations-in-templates': [],
|
||||
'no-restricted-imports-in-templates': [],
|
||||
'no-invalid-template-signature': [],
|
||||
'no-template-helper-exports': [],
|
||||
'invalid-template-filename': [],
|
||||
|
||||
// New Rule 3: Display Object guardrails
|
||||
'no-io-in-display-objects': [],
|
||||
'no-non-class-display-exports': [],
|
||||
|
||||
// New Rule 4: Page Query guardrails
|
||||
'no-null-returns-in-page-queries': [],
|
||||
'invalid-page-query-filename': [],
|
||||
|
||||
// New Rule 5: Services guardrails
|
||||
'no-service-state': [],
|
||||
'no-blockers-in-services': [],
|
||||
'no-dto-variable-name': [],
|
||||
|
||||
// New Rule 6: Client-only guardrails
|
||||
'no-use-client-directive': [],
|
||||
'no-viewmodel-imports-from-server': [],
|
||||
'no-http-in-presenters': [],
|
||||
|
||||
// New Rule 7: Write boundary guardrails
|
||||
'no-server-action-imports-from-client': [],
|
||||
'no-server-action-viewmodel-returns': [],
|
||||
|
||||
// New Rule 10: Generated DTO isolation
|
||||
'no-generated-dto-in-ui': [],
|
||||
'no-types-in-templates': [],
|
||||
|
||||
// New Rule 11: Filename rules
|
||||
'invalid-app-filename': [],
|
||||
};
|
||||
142
apps/website/tests/guardrails/architecture-guardrails.test.ts
Normal file
142
apps/website/tests/guardrails/architecture-guardrails.test.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ArchitectureGuardrails } from './ArchitectureGuardrails';
|
||||
|
||||
/**
|
||||
* Architecture Guardrail Tests
|
||||
*
|
||||
* These tests enforce the architectural contract for the website.
|
||||
* They use an allowlist to permit existing violations while preventing new ones.
|
||||
*
|
||||
* The goal is to shrink the allowlist slice-by-slice until zero violations remain.
|
||||
*/
|
||||
describe('Architecture Guardrails', () => {
|
||||
const guardrails = new ArchitectureGuardrails();
|
||||
|
||||
it('should detect all violations in the codebase', () => {
|
||||
const allViolations = guardrails.scan();
|
||||
|
||||
// This test documents the current state
|
||||
// It will always pass but shows what violations exist
|
||||
console.log(`\n📊 Total violations found: ${allViolations.length}`);
|
||||
|
||||
if (allViolations.length > 0) {
|
||||
console.log('\n📋 Violations by rule:');
|
||||
const byRule = allViolations.reduce((acc, v) => {
|
||||
acc[v.ruleName] = (acc[v.ruleName] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
Object.entries(byRule).forEach(([rule, count]) => {
|
||||
console.log(` - ${rule}: ${count}`);
|
||||
});
|
||||
}
|
||||
|
||||
// We expect violations to exist initially
|
||||
expect(allViolations.length).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('should have no violations after filtering by allowlist', () => {
|
||||
const filteredViolations = guardrails.getFilteredViolations();
|
||||
|
||||
console.log(`\n🔍 Filtered violations (after allowlist): ${filteredViolations.length}`);
|
||||
|
||||
if (filteredViolations.length > 0) {
|
||||
console.log('\n❌ New violations not in allowlist:');
|
||||
filteredViolations.forEach(v => {
|
||||
console.log(` - ${v.toString()}`);
|
||||
});
|
||||
}
|
||||
|
||||
// This is the main assertion - no new violations allowed
|
||||
expect(filteredViolations.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should not have stale allowlist entries', () => {
|
||||
const staleEntries = guardrails.findStaleAllowlistEntries();
|
||||
|
||||
console.log(`\n🧹 Stale allowlist entries: ${staleEntries.length}`);
|
||||
|
||||
if (staleEntries.length > 0) {
|
||||
console.log('\n⚠️ These allowlist entries no longer match any violations:');
|
||||
staleEntries.forEach(entry => {
|
||||
console.log(` - ${entry}`);
|
||||
});
|
||||
console.log('\n💡 Consider removing them from allowed-violations.ts');
|
||||
}
|
||||
|
||||
// Stale entries should be removed to keep allowlist clean
|
||||
expect(staleEntries.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should enforce: no ContainerManager in server page queries', () => {
|
||||
const violations = guardrails.getFilteredViolations().filter(
|
||||
v => v.ruleName === 'no-container-manager-in-server'
|
||||
);
|
||||
|
||||
expect(violations.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should enforce: no PageDataFetcher.fetch() in server page queries', () => {
|
||||
const violations = guardrails.getFilteredViolations().filter(
|
||||
v => v.ruleName === 'no-page-data-fetcher-fetch-in-server'
|
||||
);
|
||||
|
||||
expect(violations.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should enforce: no view-models imports in server code', () => {
|
||||
const violations = guardrails.getFilteredViolations().filter(
|
||||
v => v.ruleName === 'no-view-models-in-server'
|
||||
);
|
||||
|
||||
expect(violations.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should enforce: no view-models/display-objects in templates', () => {
|
||||
const violations = guardrails.getFilteredViolations().filter(
|
||||
v => v.ruleName === 'no-view-models-in-templates'
|
||||
);
|
||||
|
||||
expect(violations.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should enforce: no Intl.* or toLocale* in presentation paths', () => {
|
||||
const violations = guardrails.getFilteredViolations().filter(
|
||||
v => v.ruleName === 'no-intl-in-presentation'
|
||||
);
|
||||
|
||||
expect(violations.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should enforce: no client-side write fetch', () => {
|
||||
const violations = guardrails.getFilteredViolations().filter(
|
||||
v => v.ruleName === 'no-client-write-fetch'
|
||||
);
|
||||
|
||||
expect(violations.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should enforce: no *Template.tsx under app/', () => {
|
||||
const violations = guardrails.getFilteredViolations().filter(
|
||||
v => v.ruleName === 'no-templates-in-app'
|
||||
);
|
||||
|
||||
expect(violations.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should enforce: no hooks directory in apps/website/', () => {
|
||||
const violations = guardrails.getFilteredViolations().filter(
|
||||
v => v.ruleName === 'no-hooks-directory'
|
||||
);
|
||||
|
||||
expect(violations.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should enforce: no as any usage', () => {
|
||||
const violations = guardrails.getFilteredViolations().filter(
|
||||
v => v.ruleName === 'no-as-any'
|
||||
);
|
||||
|
||||
expect(violations.length).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user