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