Skip to content

Commit 9cf3b48

Browse files
committed
fix(cli): resolve target path for utils is outdated
1 parent a282315 commit 9cf3b48

File tree

2 files changed

+181
-79
lines changed

2 files changed

+181
-79
lines changed

packages/cli/src/registry/api.ts

-39
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import type {
2-
registryItemFileSchema,
3-
} from '@/src/registry/schema'
41
import {
52
iconsSchema,
63
registryBaseColorSchema,
@@ -212,42 +209,6 @@ export async function fetchRegistry(paths: string[]) {
212209
}
213210
}
214211

215-
export function getRegistryItemFileTargetPath(
216-
file: z.infer<typeof registryItemFileSchema>,
217-
config: Config,
218-
override?: string,
219-
) {
220-
if (override) {
221-
return override
222-
}
223-
224-
if (file.type === 'registry:ui') {
225-
// For UI component we place them in folder
226-
const folder = file.path.split('/')[1]
227-
return path.join(config.resolvedPaths.ui, folder)
228-
}
229-
230-
if (file.type === 'registry:lib') {
231-
return config.resolvedPaths.lib
232-
}
233-
234-
if (file.type === 'registry:block' || file.type === 'registry:component') {
235-
return config.resolvedPaths.components
236-
}
237-
238-
if (file.type === 'registry:hook') {
239-
return config.resolvedPaths.composables
240-
}
241-
242-
// TODO: we put this in components for now.
243-
// We should move this to pages as per framework.
244-
if (file.type === 'registry:page') {
245-
return config.resolvedPaths.components
246-
}
247-
248-
return config.resolvedPaths.components
249-
}
250-
251212
export async function registryResolveItemsTree(
252213
names: z.infer<typeof registryItemSchema>['name'][],
253214
config: Config,

packages/cli/src/utils/updaters/update-files.ts

+181-40
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import type { RegistryItem } from '@/src/registry/schema'
1+
import type { RegistryItem, registryItemFileSchema } from '@/src/registry/schema'
22
import type { Config } from '@/src/utils/get-config'
3+
import type { ProjectInfo } from '@/src/utils/get-project-info'
4+
import type { z } from 'zod'
35
import { existsSync, promises as fs } from 'node:fs'
46
import { tmpdir } from 'node:os'
57
import {
68
getRegistryBaseColor,
7-
getRegistryItemFileTargetPath,
89
} from '@/src/registry/api'
910
import { getProjectInfo } from '@/src/utils/get-project-info'
1011
import { highlighter } from '@/src/utils/highlighter'
@@ -15,20 +16,6 @@ import path, { basename, dirname } from 'pathe'
1516
// import { transformIcons } from '@/src/utils/transformers/transform-icons'
1617
import prompts from 'prompts'
1718

18-
export function resolveTargetDir(
19-
projectInfo: Awaited<ReturnType<typeof getProjectInfo>>,
20-
config: Config,
21-
target: string,
22-
) {
23-
if (target.startsWith('~/')) {
24-
return path.join(config.resolvedPaths.cwd, target.replace('~/', ''))
25-
}
26-
return path.join(config.resolvedPaths.cwd, target)
27-
// return projectInfo?.isSrcDir
28-
// ? path.join(config.resolvedPaths.cwd, 'src', target)
29-
// : path.join(config.resolvedPaths.cwd, target)
30-
}
31-
3219
export async function updateFiles(
3320
files: RegistryItem['files'],
3421
config: Config,
@@ -41,7 +28,11 @@ export async function updateFiles(
4128
},
4229
) {
4330
if (!files?.length) {
44-
return
31+
return {
32+
filesCreated: [],
33+
filesUpdated: [],
34+
filesSkipped: [],
35+
}
4536
}
4637
options = {
4738
overwrite: false,
@@ -61,8 +52,8 @@ export async function updateFiles(
6152

6253
const filesCreated = []
6354
const filesUpdated = []
64-
const folderSkipped = new Map<string, boolean>()
6555
const filesSkipped = []
56+
const folderSkipped = new Map<string, boolean>()
6657

6758
let tempRoot = ''
6859
if (!config.typescript) {
@@ -99,21 +90,48 @@ export async function updateFiles(
9990
continue
10091
}
10192

102-
let targetDir = getRegistryItemFileTargetPath(file, config)
103-
const fileName = basename(file.path)
104-
let filePath = path.join(targetDir, fileName)
93+
let filePath = resolveFilePath(file, config, {
94+
framework: projectInfo?.framework.name,
95+
commonRoot: findCommonRoot(
96+
files.map(f => f.path),
97+
file.path,
98+
),
99+
})
105100

106-
if (file.target) {
107-
filePath = resolveTargetDir(projectInfo, config, file.target)
108-
targetDir = path.dirname(filePath)
101+
if (!filePath) {
102+
continue
109103
}
110104

105+
const fileName = basename(file.path)
106+
const targetDir = path.dirname(filePath)
107+
111108
if (!config.typescript) {
112109
filePath = filePath.replace(/\.ts?$/, match => '.js')
113110
}
114-
115111
const existingFile = existsSync(filePath)
116112

113+
// Run our transformers.
114+
const content = await transform({
115+
filename: path.join(tempRoot, 'registry', config.style, file.path),
116+
raw: file.content,
117+
config,
118+
baseColor,
119+
isRemote: options.isRemote,
120+
})
121+
122+
// Skip the file if it already exists and the content is the same.
123+
if (existingFile) {
124+
const existingFileContent = await fs.readFile(filePath, 'utf-8')
125+
const [normalizedExisting, normalizedNew] = await Promise.all([
126+
getNormalizedFileContent(existingFileContent),
127+
getNormalizedFileContent(content),
128+
])
129+
if (normalizedExisting === normalizedNew) {
130+
filesSkipped.push(path.relative(config.resolvedPaths.cwd, filePath))
131+
continue
132+
}
133+
}
134+
117135
// Check for existing folder in UI component only
118136
if (file.type === 'registry:ui') {
119137
const folderName = basename(dirname(filePath))
@@ -143,6 +161,9 @@ export async function updateFiles(
143161
else {
144162
if (existingFile && !options.overwrite) {
145163
filesCreatedSpinner.stop()
164+
if (options.rootSpinner) {
165+
options.rootSpinner.stop()
166+
}
146167
const { overwrite } = await prompts({
147168
type: 'confirm',
148169
name: 'overwrite',
@@ -154,9 +175,15 @@ export async function updateFiles(
154175

155176
if (!overwrite) {
156177
filesSkipped.push(path.relative(config.resolvedPaths.cwd, filePath))
178+
if (options.rootSpinner) {
179+
options.rootSpinner.start()
180+
}
157181
continue
158182
}
159183
filesCreatedSpinner?.start()
184+
if (options.rootSpinner) {
185+
options.rootSpinner.start()
186+
}
160187
}
161188
}
162189

@@ -165,26 +192,12 @@ export async function updateFiles(
165192
await fs.mkdir(targetDir, { recursive: true })
166193
}
167194

168-
// Run our transformers.
169-
const content = await transform({
170-
filename: path.join(tempRoot, 'registry', config.style, file.path),
171-
raw: file.content,
172-
config,
173-
baseColor,
174-
isRemote: options.isRemote,
175-
})
176-
177195
await fs.writeFile(filePath, content, 'utf-8')
178196
existingFile
179197
? filesUpdated.push(path.relative(config.resolvedPaths.cwd, filePath))
180198
: filesCreated.push(path.relative(config.resolvedPaths.cwd, filePath))
181199
}
182200

183-
// Perform clean up if there's tempRoot generated for compiler-sfc to parse
184-
if (tempRoot) {
185-
await fs.rm(tempRoot, { recursive: true })
186-
}
187-
188201
const hasUpdatedFiles = filesCreated.length || filesUpdated.length
189202
if (!hasUpdatedFiles && !filesSkipped.length) {
190203
filesCreatedSpinner?.info('No files updated.')
@@ -226,7 +239,7 @@ export async function updateFiles(
226239
spinner(
227240
`Skipped ${filesSkipped.length} ${
228241
filesUpdated.length === 1 ? 'file' : 'files'
229-
}:`,
242+
}: (files might be identical, use --overwrite to overwrite)`,
230243
{
231244
silent: options.silent,
232245
},
@@ -241,4 +254,132 @@ export async function updateFiles(
241254
if (!options.silent) {
242255
logger.break()
243256
}
257+
258+
return {
259+
filesCreated,
260+
filesUpdated,
261+
filesSkipped,
262+
}
263+
}
264+
265+
export function resolveTargetDir(
266+
projectInfo: Awaited<ReturnType<typeof getProjectInfo>>,
267+
config: Config,
268+
target: string,
269+
) {
270+
if (target.startsWith('~/')) {
271+
return path.join(config.resolvedPaths.cwd, target.replace('~/', ''))
272+
}
273+
return path.join(config.resolvedPaths.cwd, target)
274+
// return projectInfo?.isSrcDir
275+
// ? path.join(config.resolvedPaths.cwd, 'src', target)
276+
// : path.join(config.resolvedPaths.cwd, target)
277+
}
278+
279+
export function resolveFilePath(
280+
file: z.infer<typeof registryItemFileSchema>,
281+
config: Config,
282+
options: {
283+
commonRoot?: string
284+
framework?: ProjectInfo['framework']['name']
285+
},
286+
) {
287+
if (file.target) {
288+
const target = file.target
289+
if (target.startsWith('~/')) {
290+
return path.join(config.resolvedPaths.cwd, target.replace('~/', ''))
291+
}
292+
return path.join(config.resolvedPaths.cwd, target)
293+
}
294+
295+
const targetDir = resolveFileTargetDirectory(file, config)
296+
297+
const relativePath = resolveNestedFilePath(file.path, targetDir)
298+
return path.join(targetDir, relativePath)
299+
}
300+
301+
function resolveFileTargetDirectory(
302+
file: z.infer<typeof registryItemFileSchema>,
303+
config: Config,
304+
) {
305+
if (file.type === 'registry:ui') {
306+
return config.resolvedPaths.ui
307+
}
308+
309+
if (file.type === 'registry:lib') {
310+
return config.resolvedPaths.lib
311+
}
312+
313+
if (file.type === 'registry:block' || file.type === 'registry:component') {
314+
return config.resolvedPaths.components
315+
}
316+
317+
if (file.type === 'registry:hook') {
318+
return config.resolvedPaths.composables
319+
}
320+
321+
return config.resolvedPaths.components
322+
}
323+
324+
export function findCommonRoot(paths: string[], needle: string): string {
325+
// Remove leading slashes for consistent handling
326+
const normalizedPaths = paths.map(p => p.replace(/^\//, ''))
327+
const normalizedNeedle = needle.replace(/^\//, '')
328+
329+
// Get the directory path of the needle by removing the file name
330+
const needleDir = normalizedNeedle.split('/').slice(0, -1).join('/')
331+
332+
// If needle is at root level, return empty string
333+
if (!needleDir) {
334+
return ''
335+
}
336+
337+
// Split the needle directory into segments
338+
const needleSegments = needleDir.split('/')
339+
340+
// Start from the full path and work backwards
341+
for (let i = needleSegments.length; i > 0; i--) {
342+
const testPath = needleSegments.slice(0, i).join('/')
343+
// Check if this is a common root by verifying if any other paths start with it
344+
const hasRelatedPaths = normalizedPaths.some(
345+
path => path !== normalizedNeedle && path.startsWith(`${testPath}/`),
346+
)
347+
if (hasRelatedPaths) {
348+
return `/${testPath}` // Add leading slash back for the result
349+
}
350+
}
351+
352+
// If no common root found with other files, return the parent directory of the needle
353+
return `/${needleDir}` // Add leading slash back for the result
354+
}
355+
356+
export function resolveNestedFilePath(
357+
filePath: string,
358+
targetDir: string,
359+
): string {
360+
// Normalize paths by removing leading/trailing slashes
361+
const normalizedFilePath = filePath.replace(/^\/|\/$/g, '')
362+
const normalizedTargetDir = targetDir.replace(/^\/|\/$/g, '')
363+
364+
// Split paths into segments
365+
const fileSegments = normalizedFilePath.split('/')
366+
const targetSegments = normalizedTargetDir.split('/')
367+
368+
// Find the last matching segment from targetDir in filePath
369+
const lastTargetSegment = targetSegments[targetSegments.length - 1]
370+
const commonDirIndex = fileSegments.findIndex(
371+
segment => segment === lastTargetSegment,
372+
)
373+
374+
if (commonDirIndex === -1) {
375+
// Return just the filename if no common directory is found
376+
return fileSegments[fileSegments.length - 1]
377+
}
378+
379+
// Return everything after the common directory
380+
return fileSegments.slice(commonDirIndex + 1).join('/')
381+
}
382+
383+
export async function getNormalizedFileContent(content: string) {
384+
return content.replace(/\r\n/g, '\n').trim()
244385
}

0 commit comments

Comments
 (0)