1
- import type { RegistryItem } from '@/src/registry/schema'
1
+ import type { RegistryItem , registryItemFileSchema } from '@/src/registry/schema'
2
2
import type { Config } from '@/src/utils/get-config'
3
+ import type { ProjectInfo } from '@/src/utils/get-project-info'
4
+ import type { z } from 'zod'
3
5
import { existsSync , promises as fs } from 'node:fs'
4
6
import { tmpdir } from 'node:os'
5
7
import {
6
8
getRegistryBaseColor ,
7
- getRegistryItemFileTargetPath ,
8
9
} from '@/src/registry/api'
9
10
import { getProjectInfo } from '@/src/utils/get-project-info'
10
11
import { highlighter } from '@/src/utils/highlighter'
@@ -15,20 +16,6 @@ import path, { basename, dirname } from 'pathe'
15
16
// import { transformIcons } from '@/src/utils/transformers/transform-icons'
16
17
import prompts from 'prompts'
17
18
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
-
32
19
export async function updateFiles (
33
20
files : RegistryItem [ 'files' ] ,
34
21
config : Config ,
@@ -41,7 +28,11 @@ export async function updateFiles(
41
28
} ,
42
29
) {
43
30
if ( ! files ?. length ) {
44
- return
31
+ return {
32
+ filesCreated : [ ] ,
33
+ filesUpdated : [ ] ,
34
+ filesSkipped : [ ] ,
35
+ }
45
36
}
46
37
options = {
47
38
overwrite : false ,
@@ -61,8 +52,8 @@ export async function updateFiles(
61
52
62
53
const filesCreated = [ ]
63
54
const filesUpdated = [ ]
64
- const folderSkipped = new Map < string , boolean > ( )
65
55
const filesSkipped = [ ]
56
+ const folderSkipped = new Map < string , boolean > ( )
66
57
67
58
let tempRoot = ''
68
59
if ( ! config . typescript ) {
@@ -99,21 +90,48 @@ export async function updateFiles(
99
90
continue
100
91
}
101
92
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
+ } )
105
100
106
- if ( file . target ) {
107
- filePath = resolveTargetDir ( projectInfo , config , file . target )
108
- targetDir = path . dirname ( filePath )
101
+ if ( ! filePath ) {
102
+ continue
109
103
}
110
104
105
+ const fileName = basename ( file . path )
106
+ const targetDir = path . dirname ( filePath )
107
+
111
108
if ( ! config . typescript ) {
112
109
filePath = filePath . replace ( / \. t s ? $ / , match => '.js' )
113
110
}
114
-
115
111
const existingFile = existsSync ( filePath )
116
112
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
+
117
135
// Check for existing folder in UI component only
118
136
if ( file . type === 'registry:ui' ) {
119
137
const folderName = basename ( dirname ( filePath ) )
@@ -143,6 +161,9 @@ export async function updateFiles(
143
161
else {
144
162
if ( existingFile && ! options . overwrite ) {
145
163
filesCreatedSpinner . stop ( )
164
+ if ( options . rootSpinner ) {
165
+ options . rootSpinner . stop ( )
166
+ }
146
167
const { overwrite } = await prompts ( {
147
168
type : 'confirm' ,
148
169
name : 'overwrite' ,
@@ -154,9 +175,15 @@ export async function updateFiles(
154
175
155
176
if ( ! overwrite ) {
156
177
filesSkipped . push ( path . relative ( config . resolvedPaths . cwd , filePath ) )
178
+ if ( options . rootSpinner ) {
179
+ options . rootSpinner . start ( )
180
+ }
157
181
continue
158
182
}
159
183
filesCreatedSpinner ?. start ( )
184
+ if ( options . rootSpinner ) {
185
+ options . rootSpinner . start ( )
186
+ }
160
187
}
161
188
}
162
189
@@ -165,26 +192,12 @@ export async function updateFiles(
165
192
await fs . mkdir ( targetDir , { recursive : true } )
166
193
}
167
194
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
-
177
195
await fs . writeFile ( filePath , content , 'utf-8' )
178
196
existingFile
179
197
? filesUpdated . push ( path . relative ( config . resolvedPaths . cwd , filePath ) )
180
198
: filesCreated . push ( path . relative ( config . resolvedPaths . cwd , filePath ) )
181
199
}
182
200
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
-
188
201
const hasUpdatedFiles = filesCreated . length || filesUpdated . length
189
202
if ( ! hasUpdatedFiles && ! filesSkipped . length ) {
190
203
filesCreatedSpinner ?. info ( 'No files updated.' )
@@ -226,7 +239,7 @@ export async function updateFiles(
226
239
spinner (
227
240
`Skipped ${ filesSkipped . length } ${
228
241
filesUpdated . length === 1 ? 'file' : 'files'
229
- } :`,
242
+ } : (files might be identical, use --overwrite to overwrite) `,
230
243
{
231
244
silent : options . silent ,
232
245
} ,
@@ -241,4 +254,132 @@ export async function updateFiles(
241
254
if ( ! options . silent ) {
242
255
logger . break ( )
243
256
}
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 ( )
244
385
}
0 commit comments