1import fs from 'fs'; 2import path from 'path'; 3 4import { ExpoMiddleware } from './ExpoMiddleware'; 5import { ServerRequest, ServerResponse } from './server.types'; 6 7const debug = require('debug')('expo:start:server:middleware:createFile') as typeof console.log; 8 9export type TouchFileBody = { path: string; contents: string }; 10 11/** 12 * Middleware for creating a file given a `POST` request with 13 * `{ contents: string, path: string }` in the body. 14 */ 15export class CreateFileMiddleware extends ExpoMiddleware { 16 constructor(protected projectRoot: string) { 17 super(projectRoot, ['/_expo/touch']); 18 } 19 20 protected resolvePath(inputPath: string): string { 21 let resolvedPath = path.join(this.projectRoot, inputPath); 22 const extension = path.extname(resolvedPath); 23 if (extension === '.js') { 24 // Automatically convert JS files to TS files when added to a project 25 // with TypeScript. 26 const tsconfigPath = path.join(this.projectRoot, 'tsconfig.json'); 27 if (fs.existsSync(tsconfigPath)) { 28 resolvedPath = resolvedPath.replace(/\.js$/, '.tsx'); 29 } 30 } 31 32 return resolvedPath; 33 } 34 35 protected async parseRawBody(req: ServerRequest): Promise<TouchFileBody> { 36 const rawBody = await new Promise<string>((resolve, reject) => { 37 let body = ''; 38 req.on('data', (chunk) => { 39 body += chunk.toString(); 40 }); 41 req.on('end', () => { 42 resolve(body); 43 }); 44 req.on('error', (err) => { 45 reject(err); 46 }); 47 }); 48 49 const properties = JSON.parse(rawBody); 50 this.assertTouchFileBody(properties); 51 52 return properties; 53 } 54 55 private assertTouchFileBody(body: any): asserts body is TouchFileBody { 56 if (typeof body !== 'object' || body == null) { 57 throw new Error('Expected object'); 58 } 59 if (typeof body.path !== 'string') { 60 throw new Error('Expected "path" in body to be string'); 61 } 62 if (typeof body.contents !== 'string') { 63 throw new Error('Expected "contents" in body to be string'); 64 } 65 } 66 67 async handleRequestAsync(req: ServerRequest, res: ServerResponse): Promise<void> { 68 if (req.method !== 'POST') { 69 res.statusCode = 405; 70 res.end('Method Not Allowed'); 71 return; 72 } 73 74 let properties: TouchFileBody; 75 76 try { 77 properties = await this.parseRawBody(req); 78 } catch (e) { 79 debug('Error parsing request body', e); 80 res.statusCode = 400; 81 res.end('Bad Request'); 82 return; 83 } 84 85 debug(`Requested: %O`, properties); 86 87 const resolvedPath = this.resolvePath(properties.path); 88 89 if (fs.existsSync(resolvedPath)) { 90 res.statusCode = 409; 91 res.end('File already exists.'); 92 return; 93 } 94 95 debug(`Resolved path:`, resolvedPath); 96 97 try { 98 await fs.promises.mkdir(path.dirname(resolvedPath), { recursive: true }); 99 await fs.promises.writeFile(resolvedPath, properties.contents, 'utf8'); 100 } catch (e) { 101 debug('Error writing file', e); 102 res.statusCode = 500; 103 res.end('Error writing file.'); 104 return; 105 } 106 107 debug(`File created`); 108 res.statusCode = 200; 109 res.end('OK'); 110 } 111} 112