diff --git a/package.json b/package.json index 690a9ac..d39639c 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "@types/react-router-dom": "^5.3.3", "@types/react-simple-maps": "^3.0.0", "@types/styled-components": "^5.1.26", - "aws-serverless-express": "^3.4.0", "axios": "^1.4.0", "cheerio": "^1.0.0-rc.12", "chrome-aws-lambda": "^10.1.0", diff --git a/server.js b/server.js index 7aab723..bb1a386 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,4 @@ const express = require('express'); -const awsServerlessExpress = require('aws-serverless-express'); const fs = require('fs'); const path = require('path'); const cors = require('cors'); @@ -8,87 +7,89 @@ require('dotenv').config(); const app = express(); +const port = process.env.PORT || 3000; // The port to run the server on const API_DIR = '/api'; // Name of the dir containing the lambda functions const dirPath = path.join(__dirname, API_DIR); // Path to the lambda functions dir const guiPath = path.join(__dirname, 'build'); +const handlers = {}; // Will store list of API endpoints +process.env.WC_SERVER = 'true'; // Tells middleware to return in non-lambda mode // Enable CORS app.use(cors({ origin: process.env.API_CORS_ORIGIN || '*', })); -// Execute the lambda function -const executeHandler = async (handler, req) => { - return new Promise((resolve, reject) => { - const callback = (err, response) => err ? reject(err) : resolve(response); - const promise = handler(req, {}, callback); - - if (promise && typeof promise.then === 'function') { - promise.then(resolve).catch(reject); - } - }); -}; - -// Array of all the lambda function file names -const fileNames = fs.readdirSync(dirPath, { withFileTypes: true }) +// Read and register each API function as an Express routes +fs.readdirSync(dirPath, { withFileTypes: true }) .filter(dirent => dirent.isFile() && dirent.name.endsWith('.js')) - .map(dirent => dirent.name); + .forEach(dirent => { + const routeName = dirent.name.split('.')[0]; + const route = `${API_DIR}/${routeName}`; + const handler = require(path.join(dirPath, dirent.name)); + handlers[route] = handler; -const handlers = {}; + app.get(route, async (req, res) => { + try { + await handler(req, res); + } catch (err) { + res.status(500).json({ error: err.message }); + } + }); + }); -fileNames.forEach(file => { - const routeName = file.split('.')[0]; - const route = `${API_DIR}/${routeName}`; - const handler = require(path.join(dirPath, file)).handler; - - handlers[route] = handler; + // Create a single API endpoint to execute all lambda functions + app.get('/api', async (req, res) => { + const results = {}; + const { url } = req.query; + const maxExecutionTime = process.env.API_TIMEOUT_LIMIT || 20000; - app.get(route, async (req, res) => { - try { - const { statusCode = 200, body = '' } = await executeHandler(handler, req); - res.status(statusCode).json(JSON.parse(body)); - } catch (err) { - res.status(500).json({ error: err.message }); - } + const executeHandler = async (handler, req, res) => { + return new Promise(async (resolve, reject) => { + try { + const mockRes = { + status: (statusCode) => mockRes, + json: (body) => resolve({ body }), + }; + await handler({ ...req, query: { url } }, mockRes); + } catch (err) { + reject(err); + } + }); + }; + + const timeout = (ms, jobName = null) => { + return new Promise((_, reject) => { + setTimeout(() => { + reject(new Error( + `Timed out after ${ms/1000} seconds${jobName ? `, when executing ${jobName}` : ''}` + )); + }, ms); + }); + }; + + const handlerPromises = Object.entries(handlers).map(async ([route, handler]) => { + const routeName = route.replace(`${API_DIR}/`, ''); + + try { + const result = await Promise.race([ + executeHandler(handler, req, res), + timeout(maxExecutionTime, routeName) + ]); + results[routeName] = result.body; + } catch (err) { + results[routeName] = { error: err.message }; + } + }); + + await Promise.all(handlerPromises); + res.json(results); }); -}); - -const timeout = (ms, jobName = null) => { - return new Promise((_, reject) => { - setTimeout(() => { - reject(new Error(`Timed out after the ${ms/1000} second limit${jobName ? `, when executing the ${jobName} task` : ''}`)); - }, ms); - }); -} - -app.get('/api', async (req, res) => { - const results = {}; - const { url } = req.query; - const maxExecutionTime = process.env.API_TIMEOUT_LIMIT || 15000; - - const handlerPromises = Object.entries(handlers).map(async ([route, handler]) => { - const routeName = route.replace(`${API_DIR}/`, ''); - - try { - const result = await Promise.race([ - executeHandler(handler, { query: { url } }), - timeout(maxExecutionTime, routeName) - ]); - results[routeName] = JSON.parse((result || {}).body); - - } catch (err) { - results[routeName] = { error: err.message }; - } - }); - - await Promise.all(handlerPromises); - res.json(results); -}); + // Handle SPA routing app.use(historyApiFallback({ rewrites: [ - { from: /^\/api\/.*$/, to: function(context) { return context.parsedUrl.path; } }, + { from: /^\/api\/.*$/, to: (context) => context.parsedUrl.path }, ] })); @@ -111,12 +112,25 @@ if (process.env.DISABLE_GUI && process.env.DISABLE_GUI !== 'false') { app.use(express.static(guiPath)); } -// Create serverless express server -const port = process.env.PORT || 3000; -const server = awsServerlessExpress.createServer(app).listen(port, () => { - console.log(`Server is running on port ${port}`); +// Print nice welcome message to user +const printMessage = () => { + console.log( + `\x1b[36m\n` + + ' __ __ _ ___ _ _ \n' + + ' \\ \\ / /__| |__ ___ / __| |_ ___ __| |__\n' + + ' \\ \\/\\/ / -_) \'_ \\___| (__| \' \\/ -_) _| / /\n' + + ' \\_/\\_/\\___|_.__/ \\___|_||_\\___\\__|_\\_\\\n' + + `\x1b[0m\n`, + `\x1b[1m\x1b[32m🚀 Web-Check is up and running at http://localhost:${port} \x1b[0m\n\n`, + `\x1b[2m\x1b[36m🛟 For documentation and support, visit the GitHub repo: ` + + `https://github.com/lissy93/web-check \n`, + `💖 Found Web-Check useful? Consider sponsoring us on GitHub ` + + `to help fund maintenance & development.\x1b[0m` + ); +}; + +// Create server +app.listen(port, () => { + printMessage(); }); -exports.handler = (event, context) => { - awsServerlessExpress.proxy(server, event, context); -}; diff --git a/yarn.lock b/yarn.lock index 5e9b6bb..4d407f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5793,14 +5793,6 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" -"@vendia/serverless-express@^3.4.0": - version "3.4.0" - resolved "https://registry.npmjs.org/@vendia/serverless-express/-/serverless-express-3.4.0.tgz" - integrity sha512-/UAAbi9qRjUtjRISt5MJ1sfhtrHb26hqQ0nvE5qhMLsAdR5H7ErBwPD8Q/v2OENKm0iWsGwErIZEg7ebUeFDjQ== - dependencies: - binary-case "^1.0.0" - type-is "^1.6.16" - "@vercel/nft@^0.22.0": version "0.22.6" resolved "https://registry.npmjs.org/@vercel/nft/-/nft-0.22.6.tgz" @@ -6612,15 +6604,6 @@ avvio@^8.2.0: debug "^4.0.0" fastq "^1.6.1" -aws-serverless-express@^3.4.0: - version "3.4.0" - resolved "https://registry.npmjs.org/aws-serverless-express/-/aws-serverless-express-3.4.0.tgz" - integrity sha512-YG9ZjAOI9OpwqDDWzkRc3kKJYJuR7gTMjLa3kAWopO17myoprxskCUyCEee+RKe34tcR4UNrVtgAwW5yDe74bw== - dependencies: - "@vendia/serverless-express" "^3.4.0" - binary-case "^1.0.0" - type-is "^1.6.16" - axe-core@^4.6.2: version "4.7.2" resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz" @@ -6866,11 +6849,6 @@ big.js@^5.2.2: resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== -binary-case@^1.0.0: - version "1.1.4" - resolved "https://registry.npmjs.org/binary-case/-/binary-case-1.1.4.tgz" - integrity sha512-9Kq8m6NZTAgy05Ryuh7U3Qc4/ujLQU1AZ5vMw4cr3igTdi5itZC6kCNrRr2X8NzPiDn2oUIFTfa71DKMnue/Zg== - binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" @@ -18272,7 +18250,7 @@ type-fest@^3.0.0: resolved "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz" integrity sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g== -type-is@^1.6.16, type-is@~1.6.18: +type-is@~1.6.18: version "1.6.18" resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==