@@ -10,11 +10,29 @@ import { DirEntryType, FileEntryType, FileType } from '@srcbook/shared';
1010import { FileContent } from '../ai/app-parser.mjs' ;
1111import type { Plan } from '../ai/plan-parser.mjs' ;
1212import archiver from 'archiver' ;
13+ import { wss } from '../index.mjs' ;
1314
1415export function pathToApp ( id : string ) {
1516 return Path . join ( APPS_DIR , id ) ;
1617}
1718
19+ export function broadcastFileUpdated ( app : DBAppType , file : FileType ) {
20+ wss . broadcast ( `app:${ app . externalId } ` , 'file:updated' , { file } ) ;
21+ }
22+
23+ // Use this rather than fs.writeFile to ensure we notify the client that the file has been updated.
24+ export async function writeFile ( app : DBAppType , file : FileType ) {
25+ // Guard against absolute / relative path issues for safety
26+ let path = file . path ;
27+ if ( ! path . startsWith ( pathToApp ( app . externalId ) ) ) {
28+ path = Path . join ( pathToApp ( app . externalId ) , file . path ) ;
29+ }
30+ const dirPath = Path . dirname ( path ) ;
31+ await fs . mkdir ( dirPath , { recursive : true } ) ;
32+ await fs . writeFile ( path , file . source , 'utf-8' ) ;
33+ broadcastFileUpdated ( app , file ) ;
34+ }
35+
1836function pathToTemplate ( template : string ) {
1937 return Path . resolve ( fileURLToPath ( import . meta. url ) , '..' , 'templates' , template ) ;
2038}
@@ -24,14 +42,16 @@ export function deleteViteApp(id: string) {
2442}
2543
2644export async function applyPlan ( app : DBAppType , plan : Plan ) {
27- const appPath = pathToApp ( app . externalId ) ;
2845 try {
2946 for ( const item of plan . actions ) {
3047 if ( item . type === 'file' ) {
31- const filePath = Path . join ( appPath , item . path ) ;
32- const dirPath = Path . dirname ( filePath ) ;
33- await fs . mkdir ( dirPath , { recursive : true } ) ;
34- await fs . writeFile ( filePath , item . modified ) ;
48+ const basename = Path . basename ( item . path ) ;
49+ await writeFile ( app , {
50+ path : item . path ,
51+ name : basename ,
52+ source : item . modified ,
53+ binary : isBinary ( basename ) ,
54+ } ) ;
3555 }
3656 }
3757 } catch ( e ) {
@@ -42,27 +62,16 @@ export async function applyPlan(app: DBAppType, plan: Plan) {
4262
4363export async function createAppFromProject ( app : DBAppType , project : Project ) {
4464 const appPath = pathToApp ( app . externalId ) ;
45-
4665 await fs . mkdir ( appPath , { recursive : true } ) ;
4766
4867 for ( const item of project . items ) {
4968 if ( item . type === 'file' ) {
50- const filePath = Path . join ( appPath , item . filename ) ;
51- const dirPath = Path . dirname ( filePath ) ;
52-
53- // Create nested directories if they don't exist
54- try {
55- await fs . stat ( dirPath ) ;
56- } catch ( error ) {
57- if ( ( error as NodeJS . ErrnoException ) . code === 'ENOENT' ) {
58- await fs . mkdir ( dirPath , { recursive : true } ) ;
59- } else {
60- throw error ;
61- }
62- }
63-
64- // Write the file content
65- await fs . writeFile ( filePath , item . content ) ;
69+ await writeFile ( app , {
70+ path : item . filename ,
71+ name : Path . basename ( item . filename ) ,
72+ source : item . content ,
73+ binary : isBinary ( Path . basename ( item . filename ) ) ,
74+ } ) ;
6675 } else if ( item . type === 'command' ) {
6776 // For now, we'll just log the commands
6877 // TODO: execute the commands in the right order.
@@ -106,7 +115,12 @@ async function scaffold(app: DBAppType, destDir: string) {
106115 const targetPath = Path . join ( destDir , file ) ;
107116 return content === undefined
108117 ? copy ( Path . join ( templateDir , file ) , targetPath )
109- : fs . writeFile ( targetPath , content , 'utf-8' ) ;
118+ : writeFile ( app , {
119+ path : targetPath ,
120+ name : Path . basename ( targetPath ) ,
121+ source : content ,
122+ binary : isBinary ( Path . basename ( targetPath ) ) ,
123+ } ) ;
110124 }
111125
112126 const templateDir = pathToTemplate ( template ) ;
@@ -135,9 +149,8 @@ async function scaffold(app: DBAppType, destDir: string) {
135149 ] ) ;
136150}
137151
138- export function fileUpdated ( app : DBAppType , file : FileType ) {
139- const path = Path . join ( pathToApp ( app . externalId ) , file . path ) ;
140- return fs . writeFile ( path , file . source , 'utf-8' ) ;
152+ export async function fileUpdated ( app : DBAppType , file : FileType ) {
153+ return writeFile ( app , file ) ;
141154}
142155
143156async function copy ( src : string , dest : string ) {
@@ -244,15 +257,15 @@ export async function createFile(
244257 basename : string ,
245258 source : string ,
246259) : Promise < FileEntryType > {
247- const projectDir = Path . join ( APPS_DIR , app . externalId ) ;
248- const filePath = Path . join ( projectDir , dirname , basename ) ;
260+ const filePath = Path . join ( dirname , basename ) ;
249261
250- // Create intermediate directories if they don't exist
251- await fs . mkdir ( Path . dirname ( filePath ) , { recursive : true } ) ;
252-
253- await fs . writeFile ( filePath , source , 'utf-8' ) ;
254- const relativePath = Path . relative ( projectDir , filePath ) ;
255- return { ...getPathInfo ( relativePath ) , type : 'file' as const } ;
262+ await writeFile ( app , {
263+ path : filePath ,
264+ name : basename ,
265+ source,
266+ binary : isBinary ( basename ) ,
267+ } ) ;
268+ return { ...getPathInfo ( filePath ) , type : 'file' as const } ;
256269}
257270
258271export function deleteFile ( app : DBAppType , path : string ) {
@@ -336,7 +349,8 @@ async function getFlatFiles(dir: string, basePath: string = ''): Promise<FileCon
336349 const fullPath = Path . join ( dir , entry . name ) ;
337350
338351 if ( entry . isDirectory ( ) ) {
339- if ( entry . name !== 'node_modules' ) {
352+ // TODO better ignore list mechanism. Should use a glob
353+ if ( ! [ '.git' , 'node_modules' ] . includes ( entry . name ) ) {
340354 files = files . concat ( await getFlatFiles ( fullPath , relativePath ) ) ;
341355 }
342356 } else if ( entry . isFile ( ) && entry . name !== 'package-lock.json' ) {
0 commit comments