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