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