Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,16 @@ jobs:
run: npm ci

- name: Lint and format check
run: npm run check
run: npx biome check .

- name: Validate pages registry
run: npm run validate-pages

- name: Validate i18n keys
run: npm run validate:i18n

- name: TypeScript type check
run: npm run typecheck

build:
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions modules/AuditLogs/src/SimpleModule.AuditLogs/Pages/Browse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -415,8 +415,8 @@ export default function Browse({ result, filters }: Props) {
<div className="flex flex-col items-center gap-2 sm:flex-row sm:justify-between">
<span className="text-sm text-text-muted">
{t(AuditLogsKeys.Browse.Showing, {
start: startItem,
end: endItem,
start: String(startItem),
end: String(endItem),
total: result.totalCount.toLocaleString(),
})}
</span>
Expand Down
6 changes: 3 additions & 3 deletions modules/AuditLogs/src/SimpleModule.AuditLogs/Pages/Detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,15 +173,15 @@ export default function Detail({ entry, correlated }: Props) {
<TooltipProvider>
<PageShell
className="space-y-4 sm:space-y-6"
title={t(AuditLogsKeys.Detail.Title, { id: entry.id })}
title={t(AuditLogsKeys.Detail.Title, { id: String(entry.id) })}
actions={
<Button variant="secondary" onClick={() => router.get('/audit-logs/browse')}>
{t(AuditLogsKeys.Detail.BackToBrowse)}
</Button>
}
breadcrumbs={[
{ label: t(AuditLogsKeys.Detail.BreadcrumbAuditLogs), href: '/audit-logs/browse' },
{ label: t(AuditLogsKeys.Detail.BreadcrumbEntry, { id: entry.id }) },
{ label: t(AuditLogsKeys.Detail.BreadcrumbEntry, { id: String(entry.id) }) },
]}
>
{/* Overview Card */}
Expand Down Expand Up @@ -382,7 +382,7 @@ export default function Detail({ entry, correlated }: Props) {
<CardTitle>
{t(AuditLogsKeys.Detail.CorrelatedTitle)}
<span className="ml-2 text-sm font-normal text-text-muted">
{t(AuditLogsKeys.Detail.CorrelatedRelated, { count: correlated.length })}
{t(AuditLogsKeys.Detail.CorrelatedRelated, { count: String(correlated.length) })}
</span>
</CardTitle>
</CardHeader>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export default function Detail({ job }: Props) {
<Badge variant={stateVariant[job.state] ?? 'default'}>{job.state}</Badge>
{job.retryCount > 0 && (
<span className="text-sm text-text-muted">
{t(BackgroundJobsKeys.Detail.RetryCount, { count: job.retryCount })}
{t(BackgroundJobsKeys.Detail.RetryCount, { count: String(job.retryCount) })}
</span>
)}
</div>
Expand Down Expand Up @@ -138,7 +138,7 @@ export default function Detail({ job }: Props) {
<Card className="mt-4 sm:mt-6">
<CardHeader>
<CardTitle>
{t(BackgroundJobsKeys.Detail.LogsCard, { count: job.logs.length })}
{t(BackgroundJobsKeys.Detail.LogsCard, { count: String(job.logs.length) })}
</CardTitle>
</CardHeader>
<CardContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function List({ jobs }: Props) {
return (
<DataGridPage
title={t(BackgroundJobsKeys.List.Title)}
description={t(BackgroundJobsKeys.List.TotalCount, { count: jobs.totalCount })}
description={t(BackgroundJobsKeys.List.TotalCount, { count: String(jobs.totalCount) })}
data={jobs.items}
emptyTitle={t(BackgroundJobsKeys.List.EmptyTitle)}
emptyDescription={t(BackgroundJobsKeys.List.EmptyDescription)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default function Recurring({ jobs }: Props) {
<>
<DataGridPage
title={t(BackgroundJobsKeys.Recurring.Title)}
description={t(BackgroundJobsKeys.Recurring.Description, { count: jobs.length })}
description={t(BackgroundJobsKeys.Recurring.Description, { count: String(jobs.length) })}
data={jobs}
emptyTitle={t(BackgroundJobsKeys.Recurring.EmptyTitle)}
emptyDescription={t(BackgroundJobsKeys.Recurring.EmptyDescription)}
Expand Down
6 changes: 3 additions & 3 deletions modules/Email/src/SimpleModule.Email/Pages/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ interface Props {
stats: EmailStats;
}

function failureRateVariant(rate: number): 'default' | 'secondary' | 'destructive' {
function failureRateVariant(rate: number): 'default' | 'warning' | 'danger' {
if (rate < 5) return 'default';
if (rate <= 15) return 'secondary';
return 'destructive';
if (rate <= 15) return 'warning';
return 'danger';
}

export default function Dashboard({ stats }: Props) {
Expand Down
10 changes: 5 additions & 5 deletions modules/Email/src/SimpleModule.Email/Pages/History.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@ import type { EmailMessage } from '../types';

type EmailStatus = 'Queued' | 'Sending' | 'Sent' | 'Failed' | 'Retrying';

function statusVariant(status: EmailStatus): 'default' | 'secondary' | 'destructive' | 'outline' {
function statusVariant(status: EmailStatus): 'default' | 'success' | 'danger' | 'warning' | 'info' {
switch (status) {
case 'Sent':
return 'default';
return 'success';
case 'Failed':
return 'destructive';
return 'danger';
case 'Sending':
case 'Retrying':
return 'secondary';
return 'warning';
default:
return 'outline';
return 'default';
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"paths": {
"@/*": ["./*"]
}
}
},
"include": ["**/*", "../../../../global.d.ts"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export default function Manage({ products }: Props) {
<DialogHeader>
<DialogTitle>{t(ProductsKeys.Manage.DeleteDialog.Title)}</DialogTitle>
<DialogDescription>
{t(ProductsKeys.Manage.DeleteDialog.Confirm, { name: deleteTarget?.name })}
{t(ProductsKeys.Manage.DeleteDialog.Confirm, { name: deleteTarget?.name ?? '' })}
</DialogDescription>
</DialogHeader>
<DialogFooter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ const API_BASE = '/api/rate-limiting';

function PolicyTypeBadge({ type }: { type: string }) {
const variant =
type === 'TokenBucket' ? 'secondary' : type === 'SlidingWindow' ? 'outline' : 'default';
type === 'TokenBucket' ? 'info' : type === 'SlidingWindow' ? 'warning' : 'default';
return <Badge variant={variant}>{type}</Badge>;
}

function TargetBadge({ target }: { target: string }) {
return <Badge variant="outline">{target}</Badge>;
return <Badge variant="info">{target}</Badge>;
}

export default function Admin({ rules, activePolicies }: AdminProps) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@ export default function MenuManager({ menuItems: initial, availablePages }: Menu
{t(SettingsKeys.MenuManager.CardTreeTitle)}
</CardTitle>
{totalItems > 0 && (
<Badge>{t(SettingsKeys.MenuManager.ItemsCount, { count: totalItems })}</Badge>
<Badge>
{t(SettingsKeys.MenuManager.ItemsCount, { count: String(totalItems) })}
</Badge>
)}
</div>
<div className="flex gap-1.5">
Expand Down
2 changes: 1 addition & 1 deletion modules/Tenants/src/SimpleModule.Tenants/Pages/Browse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function Browse({ tenants }: { tenants: BrowseTenant[] }) {
tenant.hostCount !== 1
? TenantsKeys.Browse.HostCount_other
: TenantsKeys.Browse.HostCount_one,
{ count: tenant.hostCount },
{ count: String(tenant.hostCount) },
)}
</span>
<span className={`text-sm font-medium ${statusColors[tenant.status]}`}>
Expand Down
4 changes: 2 additions & 2 deletions modules/Tenants/src/SimpleModule.Tenants/Pages/Manage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function Manage({ tenants }: { tenants: Tenant[] }) {
<>
<DataGridPage
title={t(TenantsKeys.Manage.Title)}
description={t(TenantsKeys.Manage.Description, { count: tenants.length })}
description={t(TenantsKeys.Manage.Description, { count: String(tenants.length) })}
actions={
<Button onClick={() => router.get('/tenants/create')}>
{t(TenantsKeys.Manage.CreateButton)}
Expand Down Expand Up @@ -108,7 +108,7 @@ export default function Manage({ tenants }: { tenants: Tenant[] }) {
<DialogHeader>
<DialogTitle>{t(TenantsKeys.Manage.DeleteDialog.Title)}</DialogTitle>
<DialogDescription>
{t(TenantsKeys.Manage.DeleteDialog.Confirm, { name: deleteTarget?.name })}
{t(TenantsKeys.Manage.DeleteDialog.Confirm, { name: deleteTarget?.name ?? '' })}
</DialogDescription>
</DialogHeader>
<DialogFooter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default function DeletePersonalData({ requirePassword, errors }: Props) {
/>
</Field>
)}
<Button type="submit" variant="destructive" className="w-full">
<Button type="submit" variant="danger" className="w-full">
Delete data and close my account
</Button>
</FieldGroup>
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"dev": "node tools/dev-orchestrator.mjs",
"lint": "biome lint .",
"format": "biome format --write .",
"check": "biome check . && npm run validate-pages && npm run validate:i18n",
"check": "biome check . && npm run validate-pages && npm run validate:i18n && npm run typecheck",
"typecheck": "node tools/typecheck.mjs",
"check:fix": "biome check --write . && npm run validate-pages",
"validate-pages": "node template/SimpleModule.Host/ClientApp/validate-pages.mjs",
"generate:types": "dotnet build template/SimpleModule.Host && node tools/extract-ts-types.mjs template/SimpleModule.Host/obj/Debug/net10.0/generated/SimpleModule.Generator/SimpleModule.Generator.ModuleDiscovererGenerator modules",
Expand Down
2 changes: 1 addition & 1 deletion packages/SimpleModule.UI/components/layouts/app-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ function AdminGroup({
}

export function AppLayout({ children }: { children: React.ReactNode }) {
const { props } = usePage<SharedProps>();
const { props } = usePage<SharedProps & Record<string, unknown>>();
const { auth, menus, csrfToken } = props;
const pathname = typeof window !== 'undefined' ? window.location.pathname : '/';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { PublicLayout } from './public-layout';
import type { SharedProps } from './types';

function AutoLayout({ children }: { children: React.ReactNode }) {
const { props } = usePage<SharedProps>();
const { props } = usePage<SharedProps & Record<string, unknown>>();
const { auth } = props;

if (auth?.isAuthenticated) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ function MobileOverlay({
}

export function PublicLayout({ children }: { children: React.ReactNode }) {
const { props } = usePage<SharedProps>();
const { props } = usePage<SharedProps & Record<string, unknown>>();
const { publicMenu = [] } = props;
const [mobileOpen, setMobileOpen] = React.useState(false);
const closeMobile = React.useCallback(() => setMobileOpen(false), []);
Expand Down
100 changes: 100 additions & 0 deletions tools/typecheck.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env node

/**
* typecheck.mjs
*
* Runs `tsc --noEmit` in every module and package that has a tsconfig.json.
* Each project is checked independently so @/* path aliases resolve correctly.
* All checks run in parallel for speed.
*
* Exit codes:
* 0 = All projects pass type checking
* 1 = Type errors found
*/

import { spawn } from 'node:child_process';
import { createRequire } from 'node:module';
import fs from 'node:fs';
import path from 'node:path';

const projectRoot = path.resolve(import.meta.dirname, '..');
const modulesDir = path.join(projectRoot, 'modules');
const packagesDir = path.join(projectRoot, 'packages');

const require = createRequire(import.meta.url);
const tscBin = require.resolve('typescript/bin/tsc');

function findProjects(baseDir, layout) {
const dirs = [];
if (!fs.existsSync(baseDir)) return dirs;

for (const entry of fs.readdirSync(baseDir)) {
const full = path.join(baseDir, entry);
if (!fs.statSync(full).isDirectory()) continue;

if (layout === 'flat') {
// packages/Foo — check directly
if (fs.existsSync(path.join(full, 'tsconfig.json'))) {
dirs.push(full);
}
} else {
// modules/Foo/src/Bar — recurse into src/*
const srcDir = path.join(full, 'src');
if (!fs.existsSync(srcDir)) continue;
for (const sub of fs.readdirSync(srcDir)) {
const subFull = path.join(srcDir, sub);
if (
fs.statSync(subFull).isDirectory() &&
fs.existsSync(path.join(subFull, 'tsconfig.json'))
) {
dirs.push(subFull);
}
}
}
}
return dirs;
}

function checkProject(dir) {
return new Promise((resolve) => {
const proc = spawn(process.execPath, [tscBin, '--noEmit'], {
cwd: dir,
stdio: 'pipe',
});
let output = '';
proc.stdout.on('data', (d) => (output += d));
proc.stderr.on('data', (d) => (output += d));
proc.on('close', (code) => resolve({ dir, code, output }));
});
}

const projects = [
...findProjects(modulesDir, 'nested'),
...findProjects(packagesDir, 'flat'),
];

const results = await Promise.all(projects.map(checkProject));

const failures = [];
for (const { dir, code, output } of results) {
const label = path.relative(projectRoot, dir);
if (code === 0) {
console.log(` \u2713 ${label}`);
} else {
failures.push({ label, output });
console.log(` \u2717 ${label}`);
}
}

if (failures.length > 0) {
console.log('\n--- Type errors ---\n');
for (const { label, output } of failures) {
console.log(`${label}:`);
console.log(output);
}
}

console.log(
`\nTypecheck: ${projects.length - failures.length}/${projects.length} passed`,
);
process.exit(failures.length > 0 ? 1 : 0);
Loading