Skip to content

Commit b247866

Browse files
Ajit Pratap Singhclaude
authored andcommitted
fix(website-v2): address review - CSP headers, typed props, error boundary, skeletons
- Add Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, and Referrer-Policy headers in next.config.ts - Replace all `any` types in playground components with proper interfaces (AstTab, FormatTab, LintTab, AnalyzeTab, Playground) - Add WasmErrorBoundary React error boundary for graceful WASM failures - Add Skeleton and DocsSkeleton loading UI components - Add loading.tsx for docs and blog routes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5fd706a commit b247866

11 files changed

Lines changed: 175 additions & 20 deletions

File tree

website/next.config.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
import type { NextConfig } from 'next';
22

33
const nextConfig: NextConfig = {
4+
async headers() {
5+
return [
6+
{
7+
source: '/(.*)',
8+
headers: [
9+
{
10+
key: 'Content-Security-Policy',
11+
value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' https://img.shields.io https://goreportcard.com https://*.shields.io data:; connect-src 'self'; worker-src 'self' blob:",
12+
},
13+
{
14+
key: 'X-Frame-Options',
15+
value: 'DENY',
16+
},
17+
{
18+
key: 'X-Content-Type-Options',
19+
value: 'nosniff',
20+
},
21+
{
22+
key: 'Referrer-Policy',
23+
value: 'strict-origin-when-cross-origin',
24+
},
25+
],
26+
},
27+
];
28+
},
429
async redirects() {
530
return [
631
{ source: '/docs/getting_started', destination: '/docs/getting-started', permanent: true },

website/src/app/blog/loading.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Skeleton } from '@/components/ui/Skeleton';
2+
export default function Loading() {
3+
return (
4+
<div className="max-w-4xl mx-auto px-6 py-20 space-y-6">
5+
<Skeleton className="h-10 w-64" />
6+
<Skeleton className="h-5 w-96" />
7+
<div className="space-y-4 pt-8">
8+
{[1,2,3,4,5].map(i => <Skeleton key={i} className="h-20 w-full" />)}
9+
</div>
10+
</div>
11+
);
12+
}

website/src/app/docs/loading.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { DocsSkeleton } from '@/components/ui/Skeleton';
2+
export default function Loading() { return <DocsSkeleton />; }
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
'use client';
22
import dynamic from 'next/dynamic';
3+
import { WasmErrorBoundary } from '@/components/playground/WasmErrorBoundary';
34

45
const Playground = dynamic(
56
() => import('@/components/playground/Playground'),
67
{ ssr: false }
78
);
89

910
export default function PlaygroundLoader() {
10-
return <Playground />;
11+
return (
12+
<WasmErrorBoundary>
13+
<Playground />
14+
</WasmErrorBoundary>
15+
);
1116
}

website/src/components/playground/AnalyzeTab.tsx

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,42 @@
11
'use client';
22

3+
interface SecurityAnalysis {
4+
critical?: number;
5+
high?: number;
6+
medium?: number;
7+
low?: number;
8+
}
9+
10+
interface OptimizationResult {
11+
score?: number;
12+
}
13+
14+
interface ComplexityResult {
15+
level?: string;
16+
rating?: string;
17+
}
18+
19+
interface Suggestion {
20+
message?: string;
21+
description?: string;
22+
text?: string;
23+
}
24+
25+
export interface AnalysisData {
26+
error?: string;
27+
security?: SecurityAnalysis;
28+
security_analysis?: SecurityAnalysis;
29+
optimization?: number | OptimizationResult;
30+
optimization_score?: number;
31+
performance?: OptimizationResult;
32+
query_complexity?: string | ComplexityResult;
33+
complexity?: string | ComplexityResult;
34+
suggestions?: (string | Suggestion)[];
35+
recommendations?: (string | Suggestion)[];
36+
}
37+
338
interface AnalyzeTabProps {
4-
data: any;
39+
data: AnalysisData | null;
540
}
641

742
function scoreColor(score: number): string {
@@ -117,7 +152,7 @@ export default function AnalyzeTab({ data }: AnalyzeTabProps) {
117152
<div>
118153
<h3 className="text-sm font-medium text-slate-300 mb-3">Suggestions</h3>
119154
<div className="space-y-2">
120-
{allSuggestions.map((s: any, i: number) => {
155+
{allSuggestions.map((s: string | Suggestion, i: number) => {
121156
const text = typeof s === "string" ? s : s.message || s.description || s.text || JSON.stringify(s);
122157
return (
123158
<div

website/src/components/playground/AstTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ function AstNode({ data, depth = 0, label }: AstNodeProps) {
138138
}
139139

140140
interface AstTabProps {
141-
data: any;
141+
data: (Record<string, unknown> & { error?: string }) | null;
142142
}
143143

144144
export default function AstTab({ data }: AstTabProps) {

website/src/components/playground/FormatTab.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import SqlEditor from "./SqlEditor";
33

44
interface FormatTabProps {
5-
data: any;
5+
data: string | { result?: string; formatted?: string; error?: string } | null;
66
}
77

88
export default function FormatTab({ data }: FormatTabProps) {
@@ -14,7 +14,7 @@ export default function FormatTab({ data }: FormatTabProps) {
1414
);
1515
}
1616

17-
if (data.error) {
17+
if (typeof data !== "string" && data.error) {
1818
return (
1919
<div className="p-4">
2020
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4">

website/src/components/playground/LintTab.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,24 @@
11
'use client';
22

3+
export interface LintViolation {
4+
severity?: string;
5+
level?: string;
6+
rule?: string;
7+
code?: string;
8+
rule_code?: string;
9+
line?: number;
10+
column?: number;
11+
location?: { line: number; column: number };
12+
message?: string;
13+
description?: string;
14+
msg?: string;
15+
suggestion?: string;
16+
fix?: string;
17+
hint?: string;
18+
}
19+
320
interface LintTabProps {
4-
data: any;
21+
data: { error?: string; violations?: LintViolation[]; results?: LintViolation[] } | LintViolation[] | null;
522
}
623

724
function SeverityBadge({ severity }: { severity: string }) {
@@ -29,7 +46,7 @@ export default function LintTab({ data }: LintTabProps) {
2946
);
3047
}
3148

32-
if (data.error) {
49+
if (!Array.isArray(data) && data.error) {
3350
return (
3451
<div className="p-4">
3552
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4">
@@ -72,7 +89,7 @@ export default function LintTab({ data }: LintTabProps) {
7289
<div className="text-sm text-slate-400 mb-2">
7390
{violations.length} violation{violations.length !== 1 ? "s" : ""} found
7491
</div>
75-
{violations.map((v: any, i: number) => (
92+
{violations.map((v: LintViolation, i: number) => (
7693
<div
7794
key={i}
7895
className="bg-slate-800/50 border border-slate-700 rounded-lg p-3 space-y-2"

website/src/components/playground/Playground.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { useWasm } from "./WasmLoader";
55
import SqlEditor from "./SqlEditor";
66
import AstTab from "./AstTab";
77
import FormatTab from "./FormatTab";
8-
import LintTab from "./LintTab";
8+
import LintTab, { type LintViolation } from "./LintTab";
9+
import type { AnalysisData } from "./AnalyzeTab";
910
const AnalyzeTab = React.lazy(() => import("./AnalyzeTab"));
1011

1112
const DEFAULT_SQL = `SELECT u.id, u.name, COUNT(o.id) AS order_count
@@ -36,10 +37,10 @@ const TABS: { id: TabId; label: string }[] = [
3637
];
3738

3839
interface Results {
39-
ast: any;
40-
format: any;
41-
lint: any;
42-
analyze: any;
40+
ast: (Record<string, unknown> & { error?: string }) | null;
41+
format: string | { result?: string; formatted?: string; error?: string } | null;
42+
lint: { error?: string; violations?: LintViolation[]; results?: LintViolation[] } | LintViolation[] | null;
43+
analyze: AnalysisData | null;
4344
}
4445

4546
export default function Playground() {
@@ -67,18 +68,19 @@ export default function Playground() {
6768
const safeCall = (fn: () => unknown) => {
6869
try {
6970
return fn();
70-
} catch (e: any) {
71-
return { error: e.message || String(e) };
71+
} catch (e: unknown) {
72+
const message = e instanceof Error ? e.message : String(e);
73+
return { error: message };
7274
}
7375
};
7476

7577
Promise.resolve().then(() => {
7678
if (runId !== runIdRef.current) return;
7779

78-
const astResult = safeCall(() => api.parse(query, dial));
79-
const formatResult = safeCall(() => api.format(query, dial));
80-
const lintResult = safeCall(() => api.lint(query, dial));
81-
const analyzeResult = safeCall(() => api.analyze(query, dial));
80+
const astResult = safeCall(() => api.parse(query, dial)) as Results['ast'];
81+
const formatResult = safeCall(() => api.format(query, dial)) as Results['format'];
82+
const lintResult = safeCall(() => api.lint(query, dial)) as Results['lint'];
83+
const analyzeResult = safeCall(() => api.analyze(query, dial)) as Results['analyze'];
8284

8385
if (runId === runIdRef.current) {
8486
setResults({
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use client';
2+
import { Component, ReactNode } from 'react';
3+
4+
interface Props { children: ReactNode; }
5+
interface State { hasError: boolean; error: Error | null; }
6+
7+
export class WasmErrorBoundary extends Component<Props, State> {
8+
state: State = { hasError: false, error: null };
9+
10+
static getDerivedStateFromError(error: Error): State {
11+
return { hasError: true, error };
12+
}
13+
14+
render() {
15+
if (this.state.hasError) {
16+
return (
17+
<div className="flex items-center justify-center h-full bg-primary">
18+
<div className="text-center space-y-4 max-w-md">
19+
<div className="w-12 h-12 rounded-full bg-red-500/20 flex items-center justify-center mx-auto">
20+
<svg className="w-6 h-6 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
21+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
22+
</svg>
23+
</div>
24+
<p className="text-red-400 font-medium">Something went wrong</p>
25+
<p className="text-zinc-500 text-sm">{this.state.error?.message}</p>
26+
<button
27+
onClick={() => this.setState({ hasError: false, error: null })}
28+
className="px-4 py-2 bg-white/[0.06] border border-white/[0.1] rounded-lg text-sm text-zinc-300 hover:bg-white/[0.1] transition-colors"
29+
>
30+
Try Again
31+
</button>
32+
</div>
33+
</div>
34+
);
35+
}
36+
return this.props.children;
37+
}
38+
}

0 commit comments

Comments
 (0)