Skip to content

Commit 0d25d3d

Browse files
ericyangpanclaude
andcommitted
feat(infra): add type validation and enhance revalidation
Add comprehensive type alignment validation tool and expand API revalidation coverage. Changes: - Add validate-types-alignment.mjs script to check TypeScript types against JSON schemas - Add npm script for types-alignment validation - Expand revalidate API to cover all major routes (articles, docs, curated-collections, manifesto, ai-coding-landscape, open-source-rank) The validation tool ensures TypeScript type definitions stay aligned with JSON schema definitions, catching mismatches in required fields and field naming conventions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6e88690 commit 0d25d3d

File tree

3 files changed

+312
-0
lines changed

3 files changed

+312
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"validate:github-stars": "node scripts/validate/index.mjs github-stars",
2121
"validate:urls": "node scripts/validate/index.mjs urls",
2222
"validate:locales-structure": "node scripts/validate/index.mjs locales-structure",
23+
"validate:types-alignment": "node scripts/validate/index.mjs types-alignment",
2324
"generate": "node scripts/generate/index.mjs",
2425
"generate:manifests": "node scripts/generate/index.mjs manifest-indexes",
2526
"generate:metadata": "node scripts/generate/index.mjs metadata",
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Validates TypeScript type definitions alignment with JSON schemas
5+
* This script checks that TypeScript types match the JSON schema definitions
6+
*/
7+
8+
import fs from 'node:fs'
9+
import path from 'node:path'
10+
import { fileURLToPath } from 'node:url'
11+
12+
const __filename = fileURLToPath(import.meta.url)
13+
const __dirname = path.dirname(__filename)
14+
const rootDir = path.join(__dirname, '../..')
15+
16+
const SCHEMAS_DIR = path.join(rootDir, 'manifests/$schemas')
17+
const TYPES_FILE = path.join(rootDir, 'src/types/manifests.ts')
18+
19+
/**
20+
* Load and parse a JSON schema file
21+
*/
22+
function loadSchema(schemaPath) {
23+
try {
24+
const content = fs.readFileSync(schemaPath, 'utf8')
25+
return JSON.parse(content)
26+
} catch (error) {
27+
console.error(`Error loading schema ${schemaPath}:`, error.message)
28+
return null
29+
}
30+
}
31+
32+
/**
33+
* Extract required fields from a schema
34+
*/
35+
function getRequiredFields(schema, schemaPath = '') {
36+
const required = new Set()
37+
38+
if (schema.required && Array.isArray(schema.required)) {
39+
for (const field of schema.required) {
40+
required.add(field)
41+
}
42+
}
43+
44+
// Handle allOf (inheritance)
45+
if (schema.allOf && Array.isArray(schema.allOf)) {
46+
for (const subSchema of schema.allOf) {
47+
if (subSchema.$ref) {
48+
// Resolve reference relative to the current schema's directory
49+
const schemaDir = schemaPath ? path.dirname(schemaPath) : SCHEMAS_DIR
50+
const refPath = path.resolve(schemaDir, subSchema.$ref)
51+
const refSchema = loadSchema(refPath)
52+
if (refSchema) {
53+
const refRequired = getRequiredFields(refSchema, refPath)
54+
for (const field of refRequired) {
55+
required.add(field)
56+
}
57+
}
58+
} else {
59+
const subRequired = getRequiredFields(subSchema, schemaPath)
60+
for (const field of subRequired) {
61+
required.add(field)
62+
}
63+
}
64+
}
65+
}
66+
67+
return required
68+
}
69+
70+
/**
71+
* Extract property types from a schema
72+
*/
73+
function getPropertyTypes(schema, schemaPath = '') {
74+
const types = {}
75+
76+
if (schema.properties) {
77+
for (const [key, value] of Object.entries(schema.properties)) {
78+
if (value.$ref) {
79+
// Handle references - just mark as referenced
80+
types[key] = { type: 'ref', ref: value.$ref }
81+
} else if (value.type) {
82+
types[key] = {
83+
type: Array.isArray(value.type) ? value.type : [value.type],
84+
nullable: Array.isArray(value.type) && value.type.includes('null'),
85+
}
86+
}
87+
}
88+
}
89+
90+
// Handle allOf
91+
if (schema.allOf && Array.isArray(schema.allOf)) {
92+
for (const subSchema of schema.allOf) {
93+
if (subSchema.$ref) {
94+
// Resolve reference relative to the current schema's directory
95+
const schemaDir = schemaPath ? path.dirname(schemaPath) : SCHEMAS_DIR
96+
const refPath = path.resolve(schemaDir, subSchema.$ref)
97+
const refSchema = loadSchema(refPath)
98+
if (refSchema) {
99+
Object.assign(types, getPropertyTypes(refSchema, refPath))
100+
}
101+
} else {
102+
Object.assign(types, getPropertyTypes(subSchema, schemaPath))
103+
}
104+
}
105+
}
106+
107+
return types
108+
}
109+
110+
/**
111+
* Parse TypeScript interface to extract field information
112+
*/
113+
function parseTypeScriptInterface(content, interfaceName) {
114+
// Find the interface declaration
115+
const interfaceRegex = new RegExp(
116+
`export\\s+interface\\s+${interfaceName}\\s*(?:extends\\s+([^{]+?))?\\s*\\{`,
117+
's'
118+
)
119+
const match = content.match(interfaceRegex)
120+
121+
if (!match) {
122+
return null
123+
}
124+
125+
const extendsClause = match[1]?.trim()
126+
127+
// Find the interface body by counting braces
128+
const startIndex = match.index + match[0].length
129+
let braceCount = 1
130+
let endIndex = startIndex
131+
132+
while (braceCount > 0 && endIndex < content.length) {
133+
if (content[endIndex] === '{') braceCount++
134+
if (content[endIndex] === '}') braceCount--
135+
endIndex++
136+
}
137+
138+
const interfaceBody = content.substring(startIndex, endIndex - 1)
139+
const fields = {}
140+
141+
// Parse fields from this interface - split by lines and process each
142+
const lines = interfaceBody.split('\n')
143+
for (const line of lines) {
144+
const trimmedLine = line.trim()
145+
// Match field declarations: name: type or name?: type
146+
const fieldMatch = trimmedLine.match(/^(\w+)(\?)?\s*:\s*(.+)$/)
147+
if (fieldMatch) {
148+
const [, name, optional, type] = fieldMatch
149+
fields[name] = {
150+
optional: optional === '?',
151+
type: type.trim(),
152+
}
153+
}
154+
}
155+
156+
// If this interface extends another, recursively get parent fields
157+
if (extendsClause) {
158+
// Handle multiple inheritance (e.g., "extends A, B")
159+
const parentInterfaces = extendsClause.split(',').map(i => i.trim())
160+
for (const parentName of parentInterfaces) {
161+
const parentFields = parseTypeScriptInterface(content, parentName)
162+
if (parentFields) {
163+
// Parent fields come first, but can be overridden by child
164+
for (const [key, value] of Object.entries(parentFields)) {
165+
if (!(key in fields)) {
166+
fields[key] = value
167+
}
168+
}
169+
}
170+
}
171+
}
172+
173+
return fields
174+
}
175+
176+
/**
177+
* Check alignment between schema and TypeScript type
178+
*/
179+
function checkAlignment(schemaPath, interfaceName, _schemaName) {
180+
const schema = loadSchema(schemaPath)
181+
if (!schema) {
182+
return { valid: false, error: 'Failed to load schema' }
183+
}
184+
185+
const typesContent = fs.readFileSync(TYPES_FILE, 'utf8')
186+
const tsFields = parseTypeScriptInterface(typesContent, interfaceName)
187+
188+
if (!tsFields) {
189+
return { valid: false, error: `Interface ${interfaceName} not found in types file` }
190+
}
191+
192+
const requiredFields = getRequiredFields(schema, schemaPath)
193+
const schemaProperties = getPropertyTypes(schema, schemaPath)
194+
195+
const issues = []
196+
197+
// Check required fields
198+
for (const field of requiredFields) {
199+
if (!(field in tsFields)) {
200+
issues.push({
201+
type: 'missing_field',
202+
field,
203+
message: `Required field '${field}' from schema is missing in TypeScript interface`,
204+
})
205+
} else if (tsFields[field].optional) {
206+
issues.push({
207+
type: 'optional_required',
208+
field,
209+
message: `Field '${field}' is required in schema but optional in TypeScript interface`,
210+
})
211+
}
212+
}
213+
214+
// Check field name alignment (translations vs i18n)
215+
if ('translations' in schemaProperties && !('translations' in tsFields) && 'i18n' in tsFields) {
216+
issues.push({
217+
type: 'field_name_mismatch',
218+
field: 'translations',
219+
message: "Schema uses 'translations' but TypeScript uses 'i18n'",
220+
})
221+
}
222+
223+
return {
224+
valid: issues.length === 0,
225+
issues,
226+
requiredFields: Array.from(requiredFields),
227+
tsFields: Object.keys(tsFields),
228+
}
229+
}
230+
231+
/**
232+
* Main validation function
233+
*/
234+
function main() {
235+
console.log('🔍 Validating TypeScript types alignment with JSON schemas...\n')
236+
237+
const checks = [
238+
{
239+
schema: path.join(SCHEMAS_DIR, 'ref/entity.schema.json'),
240+
interface: 'ManifestEntity',
241+
name: 'Base Entity',
242+
},
243+
{
244+
schema: path.join(SCHEMAS_DIR, 'ref/vendor-entity.schema.json'),
245+
interface: 'ManifestVendorEntity',
246+
name: 'Vendor Entity',
247+
},
248+
{
249+
schema: path.join(SCHEMAS_DIR, 'ref/product.schema.json'),
250+
interface: 'ManifestBaseProduct',
251+
name: 'Base Product',
252+
},
253+
{
254+
schema: path.join(SCHEMAS_DIR, 'ide.schema.json'),
255+
interface: 'ManifestIDE',
256+
name: 'IDE',
257+
},
258+
{
259+
schema: path.join(SCHEMAS_DIR, 'cli.schema.json'),
260+
interface: 'ManifestCLI',
261+
name: 'CLI',
262+
},
263+
]
264+
265+
let totalIssues = 0
266+
let passedChecks = 0
267+
268+
for (const check of checks) {
269+
const result = checkAlignment(check.schema, check.interface, check.name)
270+
271+
if (result.valid) {
272+
console.log(`✅ ${check.name} (${check.interface}) - aligned`)
273+
passedChecks++
274+
} else {
275+
console.log(`❌ ${check.name} (${check.interface}) - issues found:`)
276+
if (result.issues) {
277+
for (const issue of result.issues) {
278+
console.log(` - ${issue.message}`)
279+
totalIssues++
280+
}
281+
} else if (result.error) {
282+
console.log(` - ${result.error}`)
283+
totalIssues++
284+
}
285+
}
286+
}
287+
288+
console.log(`\n📊 Summary:`)
289+
console.log(` Passed: ${passedChecks}/${checks.length}`)
290+
console.log(` Issues: ${totalIssues}`)
291+
292+
if (totalIssues > 0) {
293+
console.log(`\n⚠️ Type alignment issues found. Please review and fix.`)
294+
process.exit(1)
295+
} else {
296+
console.log(`\n✅ All type definitions are aligned with schemas!`)
297+
process.exit(0)
298+
}
299+
}
300+
301+
main()

src/app/api/revalidate/route.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,20 @@ export async function POST(request: NextRequest) {
3333
})
3434
} else {
3535
// Revalidate all ai-coding-stack pages
36+
revalidatePath('/')
3637
revalidatePath('ides')
3738
revalidatePath('models')
3839
revalidatePath('clis')
40+
revalidatePath('extensions')
3941
revalidatePath('model-providers')
42+
revalidatePath('vendors')
43+
revalidatePath('articles')
44+
revalidatePath('ai-coding-stack')
45+
revalidatePath('docs')
46+
revalidatePath('curated-collections')
47+
revalidatePath('manifesto')
48+
revalidatePath('ai-coding-landscape')
49+
revalidatePath('open-source-rank')
4050
return Response.json({
4151
revalidated: true,
4252
type: 'all',

0 commit comments

Comments
 (0)