Skip to content

Commit b263c19

Browse files
ericyangpanclaude
andcommitted
build: enhance scripts with biome formatting and improved output
- Add automatic Biome formatting to generated files - Implement alphabetical sorting for imports and exports - Improve code formatting with single quotes and consistent spacing - Enhance string formatting with proper escaping - Add better error handling for formatting failures 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 1393f76 commit b263c19

File tree

2 files changed

+151
-47
lines changed

2 files changed

+151
-47
lines changed

scripts/generate-manifest-indexes.mjs

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* Usage: node scripts/generate-manifest-indexes.mjs
1010
*/
1111

12+
import { execSync } from 'node:child_process'
1213
import fs from 'node:fs'
1314
import path from 'node:path'
1415
import { fileURLToPath } from 'node:url'
@@ -67,31 +68,27 @@ function generateIndexFile(typeName) {
6768
return
6869
}
6970

70-
// Generate import statements
71-
const imports = files
72-
.map(file => {
73-
const id = file.replace('.json', '')
74-
const varName = toPascalCase(id)
75-
const relativePath = `../../../manifests/${typeName}/${file}`
76-
return `import ${varName} from '${relativePath}'`
77-
})
78-
.join('\n')
71+
// Generate import statements - sort alphabetically by variable name
72+
const importData = files.map(file => {
73+
const id = file.replace('.json', '')
74+
const varName = toPascalCase(id)
75+
const relativePath = `../../../manifests/${typeName}/${file}`
76+
return { varName, statement: `import ${varName} from '${relativePath}'` }
77+
})
7978

80-
// Generate array export
81-
const arrayItems = files
82-
.map(file => {
83-
const id = file.replace('.json', '')
84-
return toPascalCase(id)
85-
})
86-
.join(',\n ')
79+
// Sort imports alphabetically by variable name
80+
importData.sort((a, b) => a.varName.localeCompare(b.varName))
81+
const imports = importData.map(item => item.statement).join('\n')
82+
83+
// Generate array export using sorted variable names
84+
const arrayItems = importData.map(item => item.varName).join(',\n ')
8785

8886
// Generate type name (singular)
8987
const typeSingular = typeName.replace(/s$/, '')
9088
const TypeName = toPascalCase(typeSingular)
9189

92-
// Get the first file to extract type
93-
const firstFile = files[0].replace('.json', '')
94-
const firstVarName = toPascalCase(firstFile)
90+
// Get the first file (alphabetically) to extract type
91+
const firstVarName = importData[0].varName
9592

9693
// Add appropriate type import based on manifest type
9794
const typeImportMap = {
@@ -105,7 +102,7 @@ function generateIndexFile(typeName) {
105102

106103
const manifestType = typeImportMap[typeName]
107104
const typeImport = manifestType
108-
? `\nimport type { ${manifestType} } from '../../types/manifests'`
105+
? `import type { ${manifestType} } from '../../types/manifests'\n`
109106
: ''
110107

111108
const content = `/**
@@ -114,8 +111,8 @@ function generateIndexFile(typeName) {
114111
* Do not edit manually - run the script to regenerate
115112
*/
116113
117-
${imports}${typeImport}
118-
114+
${imports}
115+
${typeImport}
119116
export const ${typeName}Data = [
120117
${arrayItems},
121118
]${manifestType ? ` as unknown as ${manifestType}[]` : ''}
@@ -134,18 +131,33 @@ export default ${typeName}Data
134131
* Generate main index.ts file that exports all manifests
135132
*/
136133
function generateMainIndex() {
137-
const exports = MANIFEST_TYPES.map(typeName => {
138-
return `export { ${typeName}Data } from './${typeName}'
139-
export type { ${toPascalCase(typeName.replace(/s$/, ''))} } from './${typeName}'`
140-
}).join('\n')
134+
// Generate all export statements
135+
const exportStatements = []
136+
for (const typeName of MANIFEST_TYPES) {
137+
const typeSingular = toPascalCase(typeName.replace(/s$/, ''))
138+
exportStatements.push({
139+
type: `export type { ${typeSingular} } from './${typeName}'`,
140+
data: `export { ${typeName}Data } from './${typeName}'`,
141+
})
142+
}
143+
144+
// Sort exports alphabetically by type name and maintain type-before-data order
145+
const sortedExports = exportStatements
146+
.sort((a, b) => {
147+
const typeA = a.type.match(/\{ (\w+) \}/)[1]
148+
const typeB = b.type.match(/\{ (\w+) \}/)[1]
149+
return typeA.localeCompare(typeB)
150+
})
151+
.flatMap(item => [item.type, item.data])
152+
.join('\n')
141153

142154
const content = `/**
143155
* Auto-generated main manifest index
144156
* Generated by scripts/generate-manifest-indexes.mjs
145157
* Do not edit manually - run the script to regenerate
146158
*/
147159
148-
${exports}
160+
${sortedExports}
149161
`
150162

151163
const outputPath = path.join(OUTPUT_DIR, 'index.ts')
@@ -231,6 +243,18 @@ function main() {
231243
console.log(`\n${'='.repeat(60)}`)
232244
console.log('✅ All index files generated successfully!')
233245
console.log('='.repeat(60))
246+
247+
// Run biome formatting on generated files
248+
console.log(`\n🎨 Formatting generated files with Biome...`)
249+
try {
250+
execSync(`npx biome format --write ${OUTPUT_DIR}`, {
251+
cwd: path.join(__dirname, '..'),
252+
stdio: 'inherit',
253+
})
254+
console.log(`✅ Formatting complete`)
255+
} catch (error) {
256+
console.error(`⚠️ Biome formatting failed:`, error.message)
257+
}
234258
}
235259

236260
// Run the script

scripts/generate-metadata.mjs

Lines changed: 100 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* This eliminates runtime fs calls, making it compatible with Cloudflare Edge
66
*/
77

8+
import { execSync } from 'node:child_process'
89
import fs from 'node:fs'
910
import path from 'node:path'
1011
import { fileURLToPath } from 'node:url'
@@ -247,10 +248,14 @@ function generateArticleComponentsCode(articles) {
247248
for (const locale of locales) {
248249
const localeArticles = articles[locale]
249250
const componentEntries = localeArticles
250-
.map(
251-
article =>
252-
` '${article.slug}': () => import('@content/articles/${locale}/${article.slug}.mdx'),`
253-
)
251+
.map(article => {
252+
const importLine = ` '${article.slug}': () => import('@content/articles/${locale}/${article.slug}.mdx'),`
253+
// Only split if line is very long (>100 chars)
254+
if (importLine.length > 100) {
255+
return ` '${article.slug}': () =>\n import('@content/articles/${locale}/${article.slug}.mdx'),`
256+
}
257+
return importLine
258+
})
254259
.join('\n')
255260

256261
if (componentEntries) {
@@ -260,7 +265,10 @@ function generateArticleComponentsCode(articles) {
260265
}
261266
}
262267

263-
return `const articleComponents: Record<string, Record<string, () => Promise<{ default: React.ComponentType }>>> = {\n${componentLines.join('\n')}\n}`
268+
return `const articleComponents: Record<
269+
string,
270+
Record<string, () => Promise<{ default: React.ComponentType }>>
271+
> = {\n${componentLines.join('\n')}\n}`
264272
}
265273

266274
// Generate component imports for docs
@@ -271,7 +279,11 @@ function generateDocComponentsCode(docs) {
271279
for (const locale of locales) {
272280
const localeDocs = docs[locale]
273281
const componentEntries = localeDocs
274-
.map(doc => ` '${doc.slug}': () => import('@content/docs/${locale}/${doc.slug}.mdx'),`)
282+
.map(doc => {
283+
// Use unquoted keys for simple identifiers
284+
const key = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(doc.slug) ? doc.slug : `'${doc.slug}'`
285+
return ` ${key}: () => import('@content/docs/${locale}/${doc.slug}.mdx'),`
286+
})
275287
.join('\n')
276288

277289
if (componentEntries) {
@@ -281,7 +293,10 @@ function generateDocComponentsCode(docs) {
281293
}
282294
}
283295

284-
return `const docComponents: Record<string, Record<string, () => Promise<{ default: React.ComponentType }>>> = {\n${componentLines.join('\n')}\n}`
296+
return `const docComponents: Record<
297+
string,
298+
Record<string, () => Promise<{ default: React.ComponentType }>>
299+
> = {\n${componentLines.join('\n')}\n}`
285300
}
286301

287302
// Generate component import for manifesto (single index.mdx per locale)
@@ -323,22 +338,66 @@ function main() {
323338

324339
const header = `// This file is auto-generated by scripts/generate-metadata.mjs\n// DO NOT EDIT MANUALLY`
325340

341+
// Custom JSON stringify that uses single quotes and unquoted keys
342+
function formatObject(obj, indent = 0) {
343+
const spaces = ' '.repeat(indent)
344+
const innerSpaces = ' '.repeat(indent + 1)
345+
346+
if (Array.isArray(obj)) {
347+
if (obj.length === 0) return '[]'
348+
const items = obj.map(item => `${innerSpaces}${formatObject(item, indent + 1)}`).join(',\n')
349+
return `[\n${items},\n${spaces}]`
350+
}
351+
352+
if (obj !== null && typeof obj === 'object') {
353+
const keys = Object.keys(obj)
354+
if (keys.length === 0) return '{}'
355+
const items = keys
356+
.map(key => {
357+
const quotedKey =
358+
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key) && !key.includes('-') ? key : `'${key}'`
359+
const value = formatObject(obj[key], indent + 1)
360+
// Check if the line would be too long (>80 chars) and split if needed
361+
const fullLine = `${innerSpaces}${quotedKey}: ${value}`
362+
if (fullLine.length > 80 && typeof obj[key] === 'string') {
363+
return `${innerSpaces}${quotedKey}:\n${innerSpaces} ${value}`
364+
}
365+
return fullLine
366+
})
367+
.join(',\n')
368+
return `{\n${items},\n${spaces}}`
369+
}
370+
371+
if (typeof obj === 'string') {
372+
// Escape backslashes first, then escape single quotes, then escape newlines
373+
const escaped = obj
374+
.replace(/\\/g, '\\\\')
375+
.replace(/'/g, "\\'")
376+
.replace(/\n/g, '\\n')
377+
.replace(/\r/g, '\\r')
378+
.replace(/\t/g, '\\t')
379+
return `'${escaped}'`
380+
}
381+
382+
return JSON.stringify(obj)
383+
}
384+
326385
const content = `${header}
327386
328-
import type { ArticleMetadata } from './articles';
329-
import type { DocSection } from './docs';
330-
import type { CollectionSection } from '../collections';
331-
import type { FaqItem } from '../faq';
387+
import type { CollectionSection } from '../collections'
388+
import type { FaqItem } from '../faq'
389+
import type { ArticleMetadata } from './articles'
390+
import type { DocSection } from './docs'
332391
333-
export const articlesMetadata: Record<string, ArticleMetadata[]> = ${JSON.stringify(articles, null, 2)};
392+
export const articlesMetadata: Record<string, ArticleMetadata[]> = ${formatObject(articles)}
334393
335-
export const docsMetadata: Record<string, DocSection[]> = ${JSON.stringify(docs, null, 2)};
394+
export const docsMetadata: Record<string, DocSection[]> = ${formatObject(docs)}
336395
337-
export const collectionsMetadata: Record<string, CollectionSection> = ${JSON.stringify(collections, null, 2)};
396+
export const collectionsMetadata: Record<string, CollectionSection> = ${formatObject(collections)}
338397
339-
export const faqMetadata: Record<string, FaqItem[]> = ${JSON.stringify(faqs, null, 2)};
398+
export const faqMetadata: Record<string, FaqItem[]> = ${formatObject(faqs)}
340399
341-
export const stackCounts: Record<string, number> = ${JSON.stringify(stackCounts, null, 2)};
400+
export const stackCounts: Record<string, number> = ${formatObject(stackCounts)}
342401
`
343402

344403
fs.writeFileSync(outputFile, content, 'utf8')
@@ -348,7 +407,7 @@ export const stackCounts: Record<string, number> = ${JSON.stringify(stackCounts,
348407
const articlesComponentsCode = generateArticleComponentsCode(articles)
349408
const articlesGeneratedContent = `${header}
350409
351-
import { articlesMetadata } from './metadata';
410+
import { articlesMetadata } from './metadata'
352411
353412
export type ArticleMetadata = {
354413
title: string
@@ -375,7 +434,10 @@ export function getArticleBySlug(slug: string, locale: string = 'en'): ArticleMe
375434
${articlesComponentsCode}
376435
377436
// Get a specific article component for a given locale and slug
378-
export async function getArticleComponent(locale: string = 'en', slug: string): Promise<React.ComponentType | null> {
437+
export async function getArticleComponent(
438+
locale: string = 'en',
439+
slug: string
440+
): Promise<React.ComponentType | null> {
379441
const loaders = articleComponents[locale] || articleComponents.en
380442
const loader = loaders?.[slug]
381443
if (!loader) return null
@@ -391,7 +453,7 @@ export async function getArticleComponent(locale: string = 'en', slug: string):
391453
const docsComponentsCode = generateDocComponentsCode(docs)
392454
const docsGeneratedContent = `${header}
393455
394-
import { docsMetadata } from './metadata';
456+
import { docsMetadata } from './metadata'
395457
396458
export type DocSection = {
397459
id: string
@@ -417,7 +479,10 @@ export function getDocBySlug(slug: string, locale: string = 'en'): DocSection |
417479
${docsComponentsCode}
418480
419481
// Get a specific doc component for a given locale and slug
420-
export async function getDocComponent(locale: string = 'en', slug: string): Promise<React.ComponentType | null> {
482+
export async function getDocComponent(
483+
locale: string = 'en',
484+
slug: string
485+
): Promise<React.ComponentType | null> {
421486
const loaders = docComponents[locale] || docComponents.en
422487
const loader = loaders?.[slug]
423488
if (!loader) return null
@@ -441,6 +506,9 @@ export async function getManifestoComponent(locale: string = 'en'): Promise<Reac
441506
${manifestoComponentsCode}
442507
443508
const loader = components[locale] || components.en
509+
if (!loader) {
510+
throw new Error(\`No manifesto loader found for locale: \${locale}\`)
511+
}
444512
const mdxModule = await loader()
445513
return mdxModule.default
446514
}
@@ -478,6 +546,18 @@ ${manifestoComponentsCode}
478546
console.log(` - ${path.relative(rootDir, articlesGeneratedFile)}`)
479547
console.log(` - ${path.relative(rootDir, docsGeneratedFile)}`)
480548
console.log(` - ${path.relative(rootDir, manifestoGeneratedFile)}`)
549+
550+
// Run biome formatting on generated files
551+
console.log(`\n🎨 Formatting generated files with Biome...`)
552+
try {
553+
execSync(`npx biome format --write ${outputDir}`, {
554+
cwd: rootDir,
555+
stdio: 'inherit',
556+
})
557+
console.log(`✅ Formatting complete`)
558+
} catch (error) {
559+
console.error(`⚠️ Biome formatting failed:`, error.message)
560+
}
481561
}
482562

483563
main()

0 commit comments

Comments
 (0)