1
1
import type { ImageContentData } from '@onlook/models/actions' ;
2
2
import { DefaultSettings } from '@onlook/models/constants' ;
3
- import { promises as fs , readFileSync } from 'fs' ;
3
+ import { promises as fs } from 'fs' ;
4
4
import mime from 'mime-lite' ;
5
5
import path from 'path' ;
6
+ import { detectRouterType } from '../pages' ;
7
+
8
+ const SUPPORTED_IMAGE_EXTENSIONS = [ '.jpg' , '.jpeg' , '.png' , '.gif' , '.webp' , '.svg' , '.ico' ] ;
9
+ const MAX_FILENAME_LENGTH = 255 ;
10
+ const VALID_FILENAME_REGEX = / ^ [ a - z A - Z 0 - 9 - _ . ] + $ / ;
11
+
12
+ async function getImageFolderPath ( projectRoot : string , folder ?: string ) : Promise < string > {
13
+ if ( folder ) {
14
+ return path . join ( projectRoot , folder ) ;
15
+ }
16
+
17
+ const routerType = await detectRouterType ( projectRoot ) ;
18
+ return routerType ?. basePath
19
+ ? routerType . basePath
20
+ : path . join ( projectRoot , DefaultSettings . IMAGE_FOLDER ) ;
21
+ }
22
+
23
+ // Helper function to validate and process image file
24
+ async function processImageFile ( filePath : string , folder : string ) : Promise < ImageContentData > {
25
+ const image = await fs . readFile ( filePath , { encoding : 'base64' } ) ;
26
+ const mimeType = mime . getType ( filePath ) || 'application/octet-stream' ;
27
+
28
+ return {
29
+ fileName : path . basename ( filePath ) ,
30
+ content : `data:${ mimeType } ;base64,${ image } ` ,
31
+ mimeType,
32
+ folder,
33
+ } ;
34
+ }
6
35
7
36
async function scanImagesDirectory ( projectRoot : string ) : Promise < ImageContentData [ ] > {
8
- const imagesPath = path . join ( projectRoot , DefaultSettings . IMAGE_FOLDER ) ;
9
37
const images : ImageContentData [ ] = [ ] ;
10
38
39
+ const publicImagesPath = path . join ( projectRoot , DefaultSettings . IMAGE_FOLDER ) ;
11
40
try {
12
- const entries = await fs . readdir ( imagesPath , { withFileTypes : true } ) ;
13
-
14
- for ( const entry of entries ) {
15
- if ( entry . isFile ( ) ) {
16
- const extension = path . extname ( entry . name ) . toLowerCase ( ) ;
17
- // Common image extensions
18
- if (
19
- [ '.jpg' , '.jpeg' , '.png' , '.gif' , '.webp' , '.svg' , '.ico' ] . includes ( extension )
20
- ) {
21
- const imagePath = path . join ( imagesPath , entry . name ) ;
22
- const image = readFileSync ( imagePath , { encoding : 'base64' } ) ;
23
- const mimeType = mime . getType ( imagePath ) || 'application/octet-stream' ;
24
- images . push ( {
25
- fileName : entry . name ,
26
- content : `data:${ mimeType } ;base64,${ image } ` ,
27
- mimeType,
28
- } ) ;
29
- }
41
+ const publicEntries = await fs . readdir ( publicImagesPath , { withFileTypes : true } ) ;
42
+ for ( const entry of publicEntries ) {
43
+ if (
44
+ entry . isFile ( ) &&
45
+ SUPPORTED_IMAGE_EXTENSIONS . includes ( path . extname ( entry . name ) . toLowerCase ( ) )
46
+ ) {
47
+ const imagePath = path . join ( publicImagesPath , entry . name ) ;
48
+ images . push ( await processImageFile ( imagePath , DefaultSettings . IMAGE_FOLDER ) ) ;
30
49
}
31
50
}
51
+ } catch ( error ) {
52
+ console . error ( 'Error scanning public images directory:' , error ) ;
53
+ }
32
54
33
- return images ;
55
+ // Scan app directory images
56
+ const appDir = path . join ( projectRoot , 'app' ) ;
57
+ try {
58
+ const appImages = await findImagesInDirectory ( appDir ) ;
59
+ for ( const imagePath of appImages ) {
60
+ images . push ( await processImageFile ( imagePath , 'app' ) ) ;
61
+ }
34
62
} catch ( error ) {
35
- console . error ( 'Error scanning images directory:' , error ) ;
36
- return [ ] ;
63
+ console . error ( 'Error scanning app directory images:' , error ) ;
37
64
}
65
+
66
+ return images ;
67
+ }
68
+
69
+ async function findImagesInDirectory ( dirPath : string ) : Promise < string [ ] > {
70
+ const imageFiles : string [ ] = [ ] ;
71
+ const entries = await fs . readdir ( dirPath , { withFileTypes : true } ) ;
72
+
73
+ for ( const entry of entries ) {
74
+ const fullPath = path . join ( dirPath , entry . name ) ;
75
+
76
+ if ( entry . isDirectory ( ) && ! entry . name . startsWith ( '.' ) && entry . name !== 'node_modules' ) {
77
+ imageFiles . push ( ...( await findImagesInDirectory ( fullPath ) ) ) ;
78
+ } else if (
79
+ entry . isFile ( ) &&
80
+ SUPPORTED_IMAGE_EXTENSIONS . includes ( path . extname ( entry . name ) . toLowerCase ( ) )
81
+ ) {
82
+ imageFiles . push ( fullPath ) ;
83
+ }
84
+ }
85
+
86
+ return imageFiles ;
38
87
}
39
88
40
89
export async function scanNextJsImages ( projectRoot : string ) : Promise < ImageContentData [ ] > {
@@ -72,16 +121,19 @@ async function getUniqueFileName(imageFolder: string, fileName: string): Promise
72
121
}
73
122
74
123
export async function saveImageToProject (
75
- projectFolder : string ,
76
- content : string ,
77
- fileName : string ,
124
+ projectRoot : string ,
125
+ image : ImageContentData ,
78
126
) : Promise < string > {
79
127
try {
80
- const imageFolder = path . join ( projectFolder , DefaultSettings . IMAGE_FOLDER ) ;
81
- const uniqueFileName = await getUniqueFileName ( imageFolder , fileName ) ;
128
+ const imageFolder = await getImageFolderPath ( projectRoot , image . folder ) ;
129
+ const uniqueFileName = await getUniqueFileName ( imageFolder , image . fileName ) ;
82
130
const imagePath = path . join ( imageFolder , uniqueFileName ) ;
83
131
84
- const buffer = Buffer . from ( content , 'base64' ) ;
132
+ if ( ! image . content ) {
133
+ throw new Error ( 'Can not save image with empty content' ) ;
134
+ }
135
+
136
+ const buffer = Buffer . from ( image . content . replace ( / ^ d a t a : [ ^ , ] + , / , '' ) , 'base64' ) ;
85
137
await fs . writeFile ( imagePath , buffer ) ;
86
138
return imagePath ;
87
139
} catch ( error ) {
@@ -92,11 +144,11 @@ export async function saveImageToProject(
92
144
93
145
export async function deleteImageFromProject (
94
146
projectRoot : string ,
95
- imageName : string ,
147
+ image : ImageContentData ,
96
148
) : Promise < string > {
97
149
try {
98
- const imageFolder = path . join ( projectRoot , DefaultSettings . IMAGE_FOLDER ) ;
99
- const imagePath = path . join ( imageFolder , imageName ) ;
150
+ const imageFolder = await getImageFolderPath ( projectRoot , image . folder ) ;
151
+ const imagePath = path . join ( imageFolder , image . fileName ) ;
100
152
await fs . unlink ( imagePath ) ;
101
153
return imagePath ;
102
154
} catch ( error ) {
@@ -107,32 +159,28 @@ export async function deleteImageFromProject(
107
159
108
160
export async function renameImageInProject (
109
161
projectRoot : string ,
110
- imageName : string ,
162
+ image : ImageContentData ,
111
163
newName : string ,
112
164
) : Promise < string > {
113
- if ( ! imageName || ! newName ) {
165
+ if ( ! image . fileName || ! newName ) {
114
166
throw new Error ( 'Image name and new name are required' ) ;
115
167
}
116
168
117
- const imageFolder = path . join ( projectRoot , DefaultSettings . IMAGE_FOLDER ) ;
118
- const oldImagePath = path . join ( imageFolder , imageName ) ;
169
+ const imageFolder = await getImageFolderPath ( projectRoot , image . folder ) ;
170
+ const oldImagePath = path . join ( imageFolder , image . fileName ) ;
119
171
const newImagePath = path . join ( imageFolder , newName ) ;
120
172
121
173
try {
122
174
await validateRename ( oldImagePath , newImagePath ) ;
123
175
await fs . rename ( oldImagePath , newImagePath ) ;
124
-
125
- await updateImageReferences ( projectRoot , imageName , newName ) ;
176
+ await updateImageReferences ( projectRoot , image . fileName , newName ) ;
126
177
return newImagePath ;
127
178
} catch ( error ) {
128
179
console . error ( 'Error renaming image:' , error ) ;
129
180
throw error ;
130
181
}
131
182
}
132
183
133
- const MAX_FILENAME_LENGTH = 255 ;
134
- const VALID_FILENAME_REGEX = / ^ [ a - z A - Z 0 - 9 - _ . ] + $ / ;
135
-
136
184
async function validateRename ( oldImagePath : string , newImagePath : string ) : Promise < void > {
137
185
try {
138
186
await fs . access ( oldImagePath ) ;
0 commit comments