mirror of
https://github.com/unovue/shadcn-vue.git
synced 2026-09-14 19:59:38 +08:00
fix(registry): stabilize registry build on Windows (#1848)
This commit is contained in:
@@ -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
|
||||
@@ -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')
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user