fix(registry): stabilize registry build on Windows (#1848)

This commit is contained in:
Jevin
2026-08-01 01:54:02 +08:00
committed by GitHub
parent e316f1150a
commit fc771a79c4
8 changed files with 133 additions and 97 deletions
+4
View File
@@ -0,0 +1,4 @@
# Keep registry codegen output stable across Windows and POSIX checkouts.
apps/v4/styles/reka-*/** text eol=lf
apps/v4/public/r/**/*.json text eol=lf
apps/v4/registry/**/*.json text eol=lf
+26 -69
View File
@@ -1,8 +1,8 @@
import type { RegistryBase, RegistryContentType } from './registry.config'
import { exec } from 'node:child_process'
import { existsSync, promises as fs } from 'node:fs'
import path, { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { rimraf } from 'rimraf'
import { getAllBlocks } from '@/lib/blocks'
import { registry } from '@/registry/index'
@@ -11,6 +11,7 @@ import { buildIcons } from './build-icons'
import { crawlBlock, crawlChart, crawlComposables, crawlExample, crawlLib, crawlUI } from './crawl-content'
import { buildStyles } from './lib/build-styles'
import { buildStylesRegistry } from './lib/build-styles-registry'
import { runEslintFix, runPrettierWrite, runShadcnVueCli } from './lib/run-formatters'
import { registryConfig } from './registry.config'
async function writeFile(path: string, payload: any) {
@@ -68,7 +69,7 @@ async function buildRegistryContentType(
export const ${contentType.name}: Registry["items"] = ${JSON.stringify(result ?? '', null, 2)}`,
)
exec(`eslint --fix ${outputPath}`)
await runEslintFix([outputPath])
return result
}
@@ -185,20 +186,11 @@ async function buildRegistryJsonFile(base: RegistryBase) {
await fs.mkdir(outputDir, { recursive: true })
const registryJsonPath = path.join(outputDir, 'registry.json')
await fs.writeFile(registryJsonPath, JSON.stringify(fixedRegistry, null, 2))
await new Promise<void>((resolve, reject) => {
exec(`pnpm exec prettier --write "${registryJsonPath}"`, (error) => {
if (error) {
reject(error)
}
else {
resolve()
}
})
})
await writeFile(registryJsonPath, JSON.stringify(fixedRegistry, null, 2))
await runPrettierWrite([registryJsonPath])
const tempRegistryPath = path.join(process.cwd(), `registry-${base.name}.json`)
await fs.writeFile(tempRegistryPath, JSON.stringify(fixedRegistry, null, 2))
await writeFile(tempRegistryPath, JSON.stringify(fixedRegistry, null, 2))
return tempRegistryPath
}
@@ -216,57 +208,22 @@ async function buildRegistry(base: RegistryBase, tempRegistryPath: string) {
// eslint-disable-next-line no-console
console.log(`🏗️ Building registry with CLI for ${base.name}...`)
return new Promise<{ success: boolean, skipped?: boolean, error?: string }>((resolve) => {
// Use local shadcn copy.
const command = `node ../../packages/cli/dist/index.js build ${tempRegistryPath} --output ${base.publicOutputDir}`
// eslint-disable-next-line no-console
console.log(` Command: ${command}`)
const process = exec(command)
// Capture stdout and stderr
let stdout = ''
let stderr = ''
if (process.stdout) {
process.stdout.on('data', (data) => {
stdout += data
// eslint-disable-next-line no-console
console.log(data.toString().trim())
})
try {
await runShadcnVueCli(['build', tempRegistryPath, '--output', base.publicOutputDir!])
return { success: true }
}
catch (err: any) {
console.error(`\n❌ CLI build failed:`, err)
if (err.stderr) {
console.error('Error output:', err.stderr.toString())
}
if (process.stderr) {
process.stderr.on('data', (data) => {
stderr += data
console.error(data.toString().trim())
})
if (err.stdout) {
// eslint-disable-next-line no-console
console.log('Output:', err.stdout.toString())
}
process.on('exit', (code) => {
if (code === 0) {
resolve({ success: true })
}
else {
console.error(`\n❌ CLI build failed with code ${code}`)
if (stderr) {
console.error('Error output:', stderr)
}
if (stdout) {
// eslint-disable-next-line no-console
console.log('Output:', stdout)
}
console.warn('⚠️ Continuing with rest of build...')
resolve({ success: false, error: `Process exited with code ${code}` })
}
})
process.on('error', (err) => {
console.error(`\n❌ Failed to execute CLI command:`, err)
console.warn('⚠️ Continuing with rest of build...')
resolve({ success: false, error: err.message })
})
})
console.warn('⚠️ Continuing with rest of build...')
return { success: false, error: err.message }
}
}
/**
@@ -276,10 +233,10 @@ async function buildPublicIndex() {
rimraf.sync(path.join(process.cwd(), 'public/r/index.json'))
await fs.writeFile(
path.join(process.cwd(), 'public/r/index.json'),
JSON.stringify(ui, null, 2),
`${JSON.stringify(ui, null, 2)}\n`,
)
await exec(`eslint --fix public/r/index.json`)
await runEslintFix(['public/r/index.json'])
}
/**
@@ -297,10 +254,10 @@ async function buildBlocksIndex() {
rimraf.sync(path.join(process.cwd(), 'registry/__blocks__.json'))
await fs.writeFile(
path.join(process.cwd(), 'registry/__blocks__.json'),
JSON.stringify(payload, null, 2),
`${JSON.stringify(payload, null, 2)}\n`,
)
await exec(`eslint --fix registry/__blocks__.json`)
await runEslintFix(['registry/__blocks__.json'])
}
/**
@@ -347,7 +304,7 @@ export const Index: Record<string, Record<string, any>> = {\n`
try {
// Dynamically import the registry file
const registryModule = await import(path.resolve(registryFilePath))
const registryModule = await import(pathToFileURL(path.resolve(registryFilePath)).href)
const items = registryModule[contentType.name] || []
// Add each item to the index
@@ -395,7 +352,7 @@ export const Index: Record<string, Record<string, any>> = {\n`
rimraf.sync(outputPath)
await fs.writeFile(outputPath, index)
exec(`eslint --fix ${outputPath}`)
await runEslintFix([outputPath])
// eslint-disable-next-line no-console
console.log(' ✅ Bases index generated')
+35 -10
View File
@@ -43,6 +43,31 @@ function sanitizeString(input: string): string {
.toLowerCase() // Convert to lowercase
}
function sortDirents<T extends { name: string }>(dirents: T[]) {
return dirents.sort((a, b) => {
if (a.name < b.name)
return -1
if (a.name > b.name)
return 1
return 0
})
}
function sortBlockDirents<T extends { name: string }>(dirents: T[]) {
return dirents.sort((a, b) => {
if (a.name === 'page.vue')
return -1
if (b.name === 'page.vue')
return 1
if (a.name < b.name)
return -1
if (a.name > b.name)
return 1
return 0
})
}
/**
* Extract chart categories from component name
* Example: ChartAreaAxes -> ["chart", "chart-area"]
@@ -66,7 +91,7 @@ function getChartCategories(componentName: string): string[] {
}
export async function crawlUI(rootPath: string) {
const dir = (await readdir(rootPath, { recursive: true, withFileTypes: true })).sort()
const dir = sortDirents(await readdir(rootPath, { recursive: true, withFileTypes: true }))
const uiRegistry: RegistryItem[] = []
@@ -85,7 +110,7 @@ export async function crawlUI(rootPath: string) {
export async function crawlExample(rootPath: string) {
const type = 'registry:example' as const
const dir = (await readdir(rootPath, { withFileTypes: true })).sort()
const dir = sortDirents(await readdir(rootPath, { withFileTypes: true }))
const registry: RegistryItem[] = []
@@ -98,7 +123,7 @@ export async function crawlExample(rootPath: string) {
const folderName = dirent.name
const folderPath = join(rootPath, folderName)
const filesInFolder = await readdir(folderPath, { withFileTypes: true })
const filesInFolder = sortDirents(await readdir(folderPath, { withFileTypes: true }))
const files: any[] = []
const dependencies = new Set<string>()
@@ -146,7 +171,7 @@ export async function crawlExample(rootPath: string) {
export async function crawlBlock(rootPath: string) {
const type = 'registry:block' as const
const dir = (await readdir(rootPath, { withFileTypes: true })).sort()
const dir = sortDirents(await readdir(rootPath, { withFileTypes: true }))
const registry: RegistryItem[] = []
@@ -199,7 +224,7 @@ export async function crawlBlock(rootPath: string) {
export async function crawlChart(rootPath: string) {
const type = 'registry:block' as const
const dir = (await readdir(rootPath, { withFileTypes: true })).sort()
const dir = sortDirents(await readdir(rootPath, { withFileTypes: true }))
const registry: RegistryItem[] = []
@@ -245,7 +270,7 @@ export async function crawlChart(rootPath: string) {
export async function crawlComposables(rootPath: string) {
const type = 'registry:composable' as const
const dir = (await readdir(rootPath, { withFileTypes: true })).sort()
const dir = sortDirents(await readdir(rootPath, { withFileTypes: true }))
const registry: RegistryItem[] = []
@@ -280,7 +305,7 @@ export async function crawlComposables(rootPath: string) {
export async function crawlLib(rootPath: string) {
const type = 'registry:lib' as const
const dir = (await readdir(rootPath, { withFileTypes: true })).sort()
const dir = sortDirents(await readdir(rootPath, { withFileTypes: true }))
const registry: RegistryItem[] = []
@@ -313,9 +338,9 @@ export async function crawlLib(rootPath: string) {
}
async function buildUIRegistry(componentPath: string, componentName: string) {
const dir = (await readdir(componentPath, {
const dir = sortDirents(await readdir(componentPath, {
withFileTypes: true,
})).sort()
}))
const files: RegistryFile[] = []
const dependencies = new Set<string>()
@@ -354,7 +379,7 @@ async function buildUIRegistry(componentPath: string, componentName: string) {
}
async function buildBlockRegistry(blockPath: string, blockName: string) {
const dir = (await readdir(blockPath, { withFileTypes: true, recursive: true })).sort()
const dir = sortBlockDirents(await readdir(blockPath, { withFileTypes: true, recursive: true }))
const files: RegistryFile[] = []
const dependencies = new Set<string>()
+8 -8
View File
@@ -1,13 +1,10 @@
import { exec } from 'node:child_process'
import { promises as fs } from 'node:fs'
import path from 'node:path'
import { promisify } from 'node:util'
import { rimraf } from 'rimraf'
import { lib as rekaLibItems } from '@/registry/bases/reka/lib/_registry'
import { ui as rekaUiItems } from '@/registry/bases/reka/ui/_registry'
import { STYLES } from '@/registry/styles'
const execAsync = promisify(exec)
import { runShadcnVueCli } from './run-formatters'
/**
* Mirrors shadcn-ui's `buildRegistryJsonFile` + `buildRegistry` flow for the
@@ -37,7 +34,6 @@ const SOURCE_BASE = 'reka'
const REGISTRY_NAME = 'shadcn-vue'
const REGISTRY_HOMEPAGE = 'https://shadcn-vue.com'
const PUBLIC_OUTPUT_BASE = 'public/r/styles'
const CLI_RELATIVE_PATH = '../../packages/cli/dist/index.js'
interface RegistryFile {
path: string
@@ -120,14 +116,18 @@ async function publishStyle(styleName: string) {
const tempRegistryPath = path.join(cwd, `registry-${styleDir}.json`)
await fs.writeFile(
tempRegistryPath,
JSON.stringify(registry, null, 2),
`${JSON.stringify(registry, null, 2)}\n`,
'utf8',
)
// 3. Spawn the local shadcn-vue CLI to emit per-component JSONs.
try {
const command = `node ${CLI_RELATIVE_PATH} build ${tempRegistryPath} --output ${PUBLIC_OUTPUT_BASE}/${styleDir}`
const { stderr } = await execAsync(command, { cwd, maxBuffer: 64 * 1024 * 1024 })
const { stderr } = await runShadcnVueCli([
'build',
tempRegistryPath,
'--output',
`${PUBLIC_OUTPUT_BASE}/${styleDir}`,
], cwd)
if (stderr && !stderr.includes('warning') && stderr.trim() !== '') {
// CLI sometimes writes progress to stderr — only flag real errors
const looksLikeError = /error|fail|cannot|enoent/i.test(stderr)
+15 -8
View File
@@ -1,16 +1,13 @@
import { exec } from 'node:child_process'
import { promises as fs } from 'node:fs'
import path from 'node:path'
import { promisify } from 'node:util'
import { rimraf } from 'rimraf'
import { transform } from 'vue-metamorph'
import { STYLES } from '@/registry/styles'
import { createStyleMap } from './create-style-map'
import { makeExpandCnPlugin } from './expand-cn-classes'
import { runEslintFix } from './run-formatters'
import { makeTransformIconsPlugin } from './transform-icons'
const execAsync = promisify(exec)
/**
* Codegen pipeline that mirrors shadcn-ui's build-registry.mts step 6
* ("Copy compiled ui/* from the temporary registries into styles/<style>/ui").
@@ -54,6 +51,17 @@ async function walkFiles(dir: string): Promise<string[]> {
return out
}
async function normalizeLineEndings(files: string[]) {
await Promise.all(files.map(async (file) => {
const content = await fs.readFile(file, 'utf8')
const normalized = content.replace(/\r\n/g, '\n')
if (normalized !== content) {
await fs.writeFile(file, normalized, 'utf8')
}
}))
}
export async function buildStyles() {
const cwd = process.cwd()
const sourceUiAbs = path.resolve(cwd, SOURCE_UI_DIR)
@@ -140,10 +148,7 @@ export async function buildStyles() {
// eslint-disable-next-line no-console
console.log(' 🧹 Running eslint --fix on generated output...')
try {
await execAsync(`eslint --fix "${OUTPUT_BASE_DIR}/${SOURCE_BASE}-*/ui/**/*.{ts,vue}"`, {
cwd,
maxBuffer: 64 * 1024 * 1024,
})
await runEslintFix([`${OUTPUT_BASE_DIR}/${SOURCE_BASE}-*/ui/**/*.{ts,vue}`], cwd)
}
catch (err: any) {
// eslint --fix exits non-zero when there are unfixable warnings — that's
@@ -153,6 +158,8 @@ export async function buildStyles() {
}
}
await normalizeLineEndings(await walkFiles(path.resolve(cwd, OUTPUT_BASE_DIR)))
// eslint-disable-next-line no-console
console.log('🎨 Per-style build complete.\n')
}
+43
View File
@@ -0,0 +1,43 @@
import { execFile } from 'node:child_process'
import { createRequire } from 'node:module'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
const require = createRequire(import.meta.url)
const execFileAsync = promisify(execFile)
const maxBuffer = 64 * 1024 * 1024
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const v4Root = path.resolve(currentDir, '../..')
const repoRoot = path.resolve(v4Root, '../..')
const cliTsconfigPath = path.join(repoRoot, 'packages/cli/tsconfig.json')
const cliEntryPath = path.join(repoRoot, 'packages/cli/src/index.ts')
function resolvePackageBin(packageName: string, binPath: string) {
const packageJsonPath = require.resolve(`${packageName}/package.json`)
return path.join(path.dirname(packageJsonPath), binPath)
}
async function runNodeBin(binPath: string, args: string[], cwd = process.cwd()) {
return execFileAsync(process.execPath, [binPath, ...args], {
cwd,
maxBuffer,
})
}
export function runEslintFix(args: string[], cwd?: string) {
return runNodeBin(resolvePackageBin('eslint', 'bin/eslint.js'), ['--fix', ...args], cwd)
}
export function runPrettierWrite(args: string[], cwd?: string) {
return runNodeBin(resolvePackageBin('prettier', 'bin/prettier.cjs'), ['--write', ...args], cwd)
}
export function runShadcnVueCli(args: string[], cwd?: string) {
return runNodeBin(resolvePackageBin('tsx', 'dist/cli.mjs'), [
'--tsconfig',
cliTsconfigPath,
cliEntryPath,
...args,
], cwd)
}
+1 -1
View File
@@ -82,7 +82,7 @@ export const build = new Command()
// Write the registry item to the output directory.
await fs.writeFile(
path.resolve(resolvePaths.outputDir, `${result.data.name}.json`),
JSON.stringify(result.data, null, 2),
`${JSON.stringify(result.data, null, 2)}\n`,
)
}
+1 -1
View File
@@ -154,7 +154,7 @@ async function buildRegistry(opts: z.infer<typeof buildOptionsSchema>) {
// Write the registry item to the output directory.
await fs.writeFile(
path.resolve(resolvePaths.outputDir, `${result.data.name}.json`),
JSON.stringify(result.data, null, 2),
`${JSON.stringify(result.data, null, 2)}\n`,
)
}