diff --git a/packages/marketplace_server/.gitignore b/packages/marketplace_server/.gitignore new file mode 100644 index 00000000..80401e47 --- /dev/null +++ b/packages/marketplace_server/.gitignore @@ -0,0 +1 @@ +/static \ No newline at end of file diff --git a/packages/marketplace_server/package.json b/packages/marketplace_server/package.json new file mode 100644 index 00000000..a4fa62ea --- /dev/null +++ b/packages/marketplace_server/package.json @@ -0,0 +1,49 @@ +{ + "name": "@comty/marketplace_server", + "version": "0.45.2", + "main": "dist/index.js", + "scripts": { + "build": "corenode-cli build", + "dev": "cross-env NODE_ENV=development nodemon --ignore dist/ --exec corenode-node ./src/index.js" + }, + "license": "MIT", + "dependencies": { + "7zip-min": "^1.4.4", + "@corenode/utils": "0.28.26", + "@foxify/events": "^2.1.0", + "@octokit/rest": "^19.0.7", + "axios": "^1.2.5", + "bcrypt": "^5.1.0", + "busboy": "^1.6.0", + "connect-mongo": "^4.6.0", + "content-range": "^2.0.2", + "corenode": "0.28.26", + "dotenv": "^16.0.3", + "formidable": "^2.1.1", + "hyper-express": "^6.5.9", + "jsonwebtoken": "^9.0.0", + "linebridge": "0.15.12", + "live-directory": "^3.0.3", + "luxon": "^3.2.1", + "merge-files": "^0.1.2", + "mime-types": "^2.1.35", + "minio": "^7.0.32", + "moment": "^2.29.4", + "moment-timezone": "^0.5.40", + "mongoose": "^6.9.0", + "normalize-url": "^8.0.0", + "p-map": "^6.0.0", + "p-queue": "^7.3.4", + "redis": "^4.6.6", + "sharp": "^0.31.3", + "split-chunk-merge": "^1.0.0", + "sucrase": "^3.32.0", + "uglify-js": "^3.17.4" + }, + "devDependencies": { + "chai": "^4.3.7", + "cross-env": "^7.0.3", + "mocha": "^10.2.0", + "nodemon": "^2.0.15" + } +} diff --git a/packages/marketplace_server/src/api.js b/packages/marketplace_server/src/api.js new file mode 100644 index 00000000..1fe59474 --- /dev/null +++ b/packages/marketplace_server/src/api.js @@ -0,0 +1,119 @@ +import fs from "fs" +import path from "path" + +import DbManager from "@classes/DbManager" +import RedisClient from "@classes/RedisClient" +import StorageClient from "@classes/StorageClient" + +import hyperexpress from "hyper-express" + +import pkg from "../package.json" + +export default class WidgetsAPI { + server = global.server = new hyperexpress.Server() + + listenIp = process.env.HTTP_LISTEN_IP ?? "0.0.0.0" + listenPort = process.env.HTTP_LISTEN_PORT ?? 3040 + + internalRouter = new hyperexpress.Router() + + db = new DbManager() + + redis = global.redis = RedisClient() + + storage = global.storage = StorageClient() + + async __registerControllers() { + let controllersPath = fs.readdirSync(path.resolve(__dirname, "controllers")) + + for await (const controllerPath of controllersPath) { + const controller = require(path.resolve(__dirname, "controllers", controllerPath)).default + + if (!controller) { + console.error(`Controller ${controllerPath} not found.`) + + continue + } + + const handler = controller(new hyperexpress.Router()) + + if (!handler) { + console.error(`Controller ${controllerPath} returning not valid handler.`) + + continue + } + + this.internalRouter.use(handler.path ?? "/", handler.router) + + continue + } + } + + async __registerInternalMiddlewares() { + let middlewaresPath = fs.readdirSync(path.resolve(__dirname, "useMiddlewares")) + + for await (const middlewarePath of middlewaresPath) { + const middleware = require(path.resolve(__dirname, "useMiddlewares", middlewarePath)).default + + if (!middleware) { + console.error(`Middleware ${middlewarePath} not found.`) + + continue + } + + this.server.use(middleware) + } + } + + __registerInternalRoutes() { + this.server.get("/", (req, res) => { + return res.status(200).json({ + name: pkg.name, + version: pkg.version, + routes: this.__getRegisteredRoutes() + }) + }) + + this.server.any("*", (req, res) => { + return res.status(404).json({ + error: "Not found", + }) + }) + } + + __getRegisteredRoutes() { + return this.internalRouter.routes.map((route) => { + return { + method: route.method, + path: route.pattern, + } + }) + } + + async initialize() { + const startHrTime = process.hrtime() + + // initialize clients + await this.db.initialize() + await this.redis.initialize() + await this.storage.initialize() + + // register controllers & middlewares + await this.__registerInternalRoutes() + await this.__registerControllers() + await this.__registerInternalMiddlewares() + + // use internal router + this.server.use(this.internalRouter) + + // start server + await this.server.listen(this.listenPort, this.listenIp) + + // calculate elapsed time + const elapsedHrTime = process.hrtime(startHrTime) + const elapsedTimeInMs = elapsedHrTime[0] * 1000 + elapsedHrTime[1] / 1e6 + + // log server started + console.log(`🚀 Server started ready on \n\t - http://${this.listenIp}:${this.listenPort} \n\t - Tooks ${elapsedTimeInMs}ms`) + } +} \ No newline at end of file diff --git a/packages/marketplace_server/src/classes/DbManager/index.js b/packages/marketplace_server/src/classes/DbManager/index.js new file mode 100755 index 00000000..bdffe441 --- /dev/null +++ b/packages/marketplace_server/src/classes/DbManager/index.js @@ -0,0 +1,58 @@ +import mongoose from "mongoose" + +function getConnectionConfig(obj) { + const { DB_USER, DB_DRIVER, DB_NAME, DB_PWD, DB_HOSTNAME, DB_PORT } = obj + + let auth = [ + DB_DRIVER ?? "mongodb", + "://", + ] + + if (DB_USER && DB_PWD) { + auth.push(`${DB_USER}:${DB_PWD}@`) + } + + auth.push(DB_HOSTNAME ?? "localhost") + auth.push(`:${DB_PORT ?? "27017"}`) + + if (DB_USER) { + auth.push("/?authMechanism=DEFAULT") + } + + auth = auth.join("") + + return [ + auth, + { + dbName: DB_NAME, + useNewUrlParser: true, + useUnifiedTopology: true, + } + ] +} + +export default class DBManager { + initialize = async (config) => { + console.log("🔌 Connecting to DB...") + + const dbConfig = getConnectionConfig(config ?? process.env) + + mongoose.set("strictQuery", false) + + const connection = await mongoose.connect(...dbConfig) + .catch((err) => { + console.log(`❌ Failed to connect to DB, retrying...\n`) + console.log(error) + + // setTimeout(() => { + // this.initialize() + // }, 1000) + + return false + }) + + if (connection) { + console.log(`✅ Connected to DB.`) + } + } +} \ No newline at end of file diff --git a/packages/marketplace_server/src/classes/RedisClient/index.js b/packages/marketplace_server/src/classes/RedisClient/index.js new file mode 100644 index 00000000..c46eff3f --- /dev/null +++ b/packages/marketplace_server/src/classes/RedisClient/index.js @@ -0,0 +1,44 @@ +import { createClient } from "redis" + +function composeURL() { + // support for auth + let url = "redis://" + + if (process.env.REDIS_PASSWORD && process.env.REDIS_USERNAME) { + url += process.env.REDIS_USERNAME + ":" + process.env.REDIS_PASSWORD + "@" + } + + url += process.env.REDIS_HOST ?? "localhost" + + if (process.env.REDIS_PORT) { + url += ":" + process.env.REDIS_PORT + } + + return url +} + +export default () => { + let client = createClient({ + url: composeURL(), + }) + + client.initialize = async () => { + console.log("🔌 Connecting to Redis client...") + + await client.connect() + + return client + } + + // handle when client disconnects unexpectedly to avoid main crash + client.on("error", (error) => { + console.error("❌ Redis client error:", error) + }) + + // handle when client connects + client.on("connect", () => { + console.log("✅ Redis client connected.") + }) + + return client +} \ No newline at end of file diff --git a/packages/marketplace_server/src/classes/StorageClient/index.js b/packages/marketplace_server/src/classes/StorageClient/index.js new file mode 100755 index 00000000..fcd00844 --- /dev/null +++ b/packages/marketplace_server/src/classes/StorageClient/index.js @@ -0,0 +1,94 @@ +const Minio = require("minio") + +export const generateDefaultBucketPolicy = (payload) => { + const { bucketName } = payload + + if (!bucketName) { + throw new Error("bucketName is required") + } + + return { + Version: "2012-10-17", + Statement: [ + { + Action: [ + "s3:GetObject" + ], + Effect: "Allow", + Principal: { + AWS: [ + "*" + ] + }, + Resource: [ + `arn:aws:s3:::${bucketName}/*` + ], + Sid: "" + } + ] + } +} + +export class StorageClient extends Minio.Client { + constructor(options) { + super(options) + + this.defaultBucket = String(options.defaultBucket) + this.defaultRegion = String(options.defaultRegion) + } + + composeRemoteURL = (key) => { + return `${this.protocol}//${this.host}:${this.port}/${this.defaultBucket}/${key}` + } + + setDefaultBucketPolicy = async (bucketName) => { + const policy = generateDefaultBucketPolicy({ bucketName }) + + return this.setBucketPolicy(bucketName, JSON.stringify(policy)) + } + + initialize = async () => { + console.log("🔌 Checking if storage client have default bucket...") + + // check connection with s3 + const bucketExists = await this.bucketExists(this.defaultBucket).catch(() => { + return false + }) + + if (!bucketExists) { + console.warn("🪣 Default bucket not exists! Creating new bucket...") + + await this.makeBucket(this.defaultBucket, "s3") + + // set default bucket policy + await this.setDefaultBucketPolicy(this.defaultBucket) + } + + // check if default bucket policy exists + const bucketPolicy = await this.getBucketPolicy(this.defaultBucket).catch(() => { + return null + }) + + if (!bucketPolicy) { + // set default bucket policy + await this.setDefaultBucketPolicy(this.defaultBucket) + } + + console.log("✅ Storage client is ready.") + } +} + +export const createStorageClientInstance = (options) => { + return new StorageClient({ + ...options, + endPoint: process.env.S3_ENDPOINT, + port: Number(process.env.S3_PORT), + useSSL: toBoolean(process.env.S3_USE_SSL), + accessKey: process.env.S3_ACCESS_KEY, + secretKey: process.env.S3_SECRET_KEY, + defaultBucket: process.env.S3_BUCKET, + defaultRegion: process.env.S3_REGION, + }) +} + +export default createStorageClientInstance \ No newline at end of file diff --git a/packages/marketplace_server/src/controllers/static/index.js b/packages/marketplace_server/src/controllers/static/index.js new file mode 100644 index 00000000..00aed44b --- /dev/null +++ b/packages/marketplace_server/src/controllers/static/index.js @@ -0,0 +1,72 @@ +import path from "path" +import fs from "fs" +import LiveDirectory from "live-directory" + +function serveStaticFiles(req, res, live_dir) { + const path = req.path.replace("/static", "") + + const asset = live_dir.get(path) + + if (!asset) { + return res.status(404).send("Not Found") + } + + if (asset.cached) { + return res.send(asset.content) + } else { + const readable = asset.stream() + + return readable.pipe(res) + } +} + +async function serveRemoteStatic(req, res) { + global.storage.getObject(process.env.S3_BUCKET, req.path, (err, dataStream) => { + if (err) { + console.log(err) + return res.status(404).send("Not Found") + } + + // on end of stream, dispath res.on("finish") + dataStream.on("end", () => { + res.emit("finish") + return res.end() + }) + + return dataStream.pipe(res) + }) +} + +async function syncFolder(dir) { + const files = await fs.promises.readdir(dir) + + for await (const file of files) { + const filePath = path.resolve(dir, file) + + const stat = fs.statSync(filePath) + + if (stat.isDirectory()) { + await syncFolder(filePath) + } else { + const fileContent = await fs.promises.readFile(filePath) + + await global.storage.putObject(process.env.S3_BUCKET, filePath.replace(process.cwd(), ""), fileContent) + } + } +} + +export default (router) => { + const StaticDirectory = new LiveDirectory(path.resolve(process.cwd(), "static"), { + static: true + }) + + //const static_dir = path.resolve(process.cwd(), "static") + //syncFolder(static_dir) + + router.get("*", (req, res) => serveRemoteStatic(req, res, StaticDirectory)) + + return { + path: "/static/", + router, + } +} \ No newline at end of file diff --git a/packages/marketplace_server/src/controllers/widgets/index.js b/packages/marketplace_server/src/controllers/widgets/index.js new file mode 100644 index 00000000..774cb4c9 --- /dev/null +++ b/packages/marketplace_server/src/controllers/widgets/index.js @@ -0,0 +1,14 @@ +import path from "path" +import createRoutesFromDirectory from "@utils/createRoutesFromDirectory" + +export default (router) => { + // create a file based router + const routesPath = path.resolve(__dirname, "routes") + + router = createRoutesFromDirectory("routes", routesPath, router) + + return { + path: "/widgets", + router, + } +} \ No newline at end of file diff --git a/packages/marketplace_server/src/controllers/widgets/routes/get/:widgetId.js b/packages/marketplace_server/src/controllers/widgets/routes/get/:widgetId.js new file mode 100644 index 00000000..63f735d4 --- /dev/null +++ b/packages/marketplace_server/src/controllers/widgets/routes/get/:widgetId.js @@ -0,0 +1,168 @@ +import { Widget } from "@models" + +import { transform } from "sucrase" +import UglifyJS from "uglify-js" + +import resolveUrl from "@utils/resolveUrl" +import replaceImportsWithRemoteURL from "@utils/replaceImportsWithRemoteURL" + +async function compileWidgetCode(widgetCode, manifest, rootURL) { + if (!widgetCode) { + throw new Error("Widget code not defined.") + } + + if (!manifest) { + throw new Error("Manifest not defined.") + } + + if (!rootURL) { + throw new Error("Root URL not defined.") + } + + let renderComponentName = null + let cssFiles = [] + + // inject react with cdn + widgetCode = `import React from "https://cdn.skypack.dev/react@17?dts" \n${widgetCode}` + + widgetCode = await replaceImportsWithRemoteURL(widgetCode, resolveUrl(rootURL, manifest.entryFile)) + + // remove css imports and append to manifest (Only its used in the entry file) + widgetCode = widgetCode.replace(/import\s+["'](.*)\.css["']/g, (match, p1) => { + cssFiles.push(resolveUrl(rootURL, `${p1}.css`)) + + return "" + }) + + // transform jsx to js + widgetCode = await transform(widgetCode, { + transforms: ["jsx"], + //jsxRuntime: "automatic", + //production: true, + }).code + + // search export default and get the name of the function/const/class + const exportDefaultRegex = /export\s+default\s+(?:function|const|class)\s+([a-zA-Z0-9]+)/g + + const exportDefaultMatch = exportDefaultRegex.exec(widgetCode) + + if (exportDefaultMatch) { + renderComponentName = exportDefaultMatch[1] + } + + // remove export default keywords + widgetCode = widgetCode.replace("export default", "") + + + let manifestProcessed = { + ...manifest, + } + + manifestProcessed.cssFiles = cssFiles + manifestProcessed.entryFile = resolveUrl(rootURL, manifest.entryFile) + manifestProcessed.iconUrl = resolveUrl(rootURL, manifest.iconUrl) + + let result = ` + ${widgetCode} + + export default { + manifest: ${JSON.stringify(manifestProcessed)}, + renderComponent: ${renderComponentName}, + } + ` + + // minify code + result = UglifyJS.minify(result, { + compress: true, + mangle: true, + }).code + + return result +} + +export default async (req, res) => { + const widgetId = req.params.widgetId + + const useCache = req.query["use-cache"] ? toBoolean(req.query["use-cache"]) : true + + //console.log(`📦 Getting widget code [${widgetId}], using cache ${useCache}`) + + // try to find Widget + const widget = await Widget.findOne({ + _id: widgetId, + }).catch(() => { + return null + }) + + if (!widget) { + return res.status(404).json({ + error: "Widget not found.", + }) + } + + if (!widget.manifest.entryFile) { + return res.status(404).json({ + error: "Widget entry file not defined", + }) + } + + let widgetCode = null + + // get origin from request url + const origin = req.protocol + "://" + req.get("host") + const remotePath = `/static/${widgetId}/src` + + const remoteEntyFilePath = resolveUrl(remotePath, widget.manifest.entryFile) + + if (useCache) { + widgetCode = await global.redis.get(`widget:${widgetId}`) + } + + if (!widgetCode) { + try { + widgetCode = await new Promise(async (resolve, reject) => { + await global.storage.getObject(process.env.S3_BUCKET, remoteEntyFilePath, (error, dataStream) => { + if (error) { + return false + } + + let data = "" + + dataStream.on("data", (chunk) => { + data += chunk + }) + + dataStream.on("end", () => { + resolve(data) + }) + }) + }) + } catch (error) { + return res.status(500).json({ + error: error.message, + }) + } + + try { + console.log("🔌 Compiling widget code...") + + widgetCode = await compileWidgetCode(widgetCode, widget.manifest, resolveUrl(origin, remotePath)) + + await global.redis.set(`widget:${widgetId}`, widgetCode) + } catch (error) { + return res.status(500).json({ + message: "Unable to transform widget code.", + error: error.message, + }) + } + } + + if (!widgetCode) { + return res.status(404).json({ + error: "Widget not found.", + }) + } + + res.setHeader("Content-Type", "application/javascript") + return res.status(200).send(widgetCode) +} \ No newline at end of file diff --git a/packages/marketplace_server/src/controllers/widgets/routes/get/:widgetId/manifest.js b/packages/marketplace_server/src/controllers/widgets/routes/get/:widgetId/manifest.js new file mode 100644 index 00000000..27aec7ad --- /dev/null +++ b/packages/marketplace_server/src/controllers/widgets/routes/get/:widgetId/manifest.js @@ -0,0 +1,23 @@ +import { Widget } from "@models" + +export default async (req, res) => { + const widget_id = req.params.widgetId + + const widget = await Widget.findOne({ + _id: widget_id, + }).catch(() => { + return false + }) + + if (!widget) { + return res.status(404).json({ + error: "Widget not found", + }) + } + + return res.status(200).json({ + ...widget.manifest, + user_id: widget.user_id, + _id: widget_id, + }) +} \ No newline at end of file diff --git a/packages/marketplace_server/src/controllers/widgets/routes/get/index.js b/packages/marketplace_server/src/controllers/widgets/routes/get/index.js new file mode 100644 index 00000000..99d7b477 --- /dev/null +++ b/packages/marketplace_server/src/controllers/widgets/routes/get/index.js @@ -0,0 +1,41 @@ +import { Widget } from "@models" + +export default async (req, res) => { + let { limit = 20, offset = 0, keywords } = req.query + + keywords = JSON.parse(keywords ?? "{}") + + // remove empty keywords + Object.keys(keywords).forEach((key) => { + if (keywords[key] === "") { + delete keywords[key] + } + }) + + console.log("Searching with keywords:", keywords) + + const query = { + public: true, + } + + // inclide keywords for search in manifest + Object.keys(keywords).forEach((key) => { + query[`manifest.${key}`] = { + $regex: keywords[key], + $options: "i", + } + }) + + let widgets = await Widget.find(query) + .limit(Number(limit)) + .skip(Number(offset)) + + widgets = widgets.map((widget) => { + widget.manifest._id = widget._id + widget.manifest.user_id = widget.user_id + + return widget + }) + + return res.json(widgets) +} \ No newline at end of file diff --git a/packages/marketplace_server/src/controllers/widgets/routes/get/user/:user_id.js b/packages/marketplace_server/src/controllers/widgets/routes/get/user/:user_id.js new file mode 100644 index 00000000..c6c38d41 --- /dev/null +++ b/packages/marketplace_server/src/controllers/widgets/routes/get/user/:user_id.js @@ -0,0 +1,15 @@ +import { Widget } from "@models" + +export default async (req, res) => { + const { user_id } = req.params + const { limit = 20, offset = 0 } = req.query + + const widgets = await Widget.find({ + user_id, + public: true, + }) + .limit(Number(limit)) + .skip(Number(offset)) + + return res.json(widgets) +} \ No newline at end of file diff --git a/packages/marketplace_server/src/controllers/widgets/routes/put/widget/:widgetId.js b/packages/marketplace_server/src/controllers/widgets/routes/put/widget/:widgetId.js new file mode 100644 index 00000000..8b9d611a --- /dev/null +++ b/packages/marketplace_server/src/controllers/widgets/routes/put/widget/:widgetId.js @@ -0,0 +1,5 @@ +export default (req, res) => { + return res.status(200).json({ + test: "Testing endpoints by route files" + }) +} \ No newline at end of file diff --git a/packages/marketplace_server/src/index.js b/packages/marketplace_server/src/index.js new file mode 100644 index 00000000..acb48ba9 --- /dev/null +++ b/packages/marketplace_server/src/index.js @@ -0,0 +1,56 @@ +require("dotenv").config() +import { webcrypto as crypto } from "crypto" +import path from "path" +import { registerBaseAliases } from "linebridge/dist/server" + +registerBaseAliases(undefined, { + "@services": path.resolve(__dirname, "services"), +}) + +// patches +const { Buffer } = require("buffer") + +global.b64Decode = (data) => { + return Buffer.from(data, "base64").toString("utf-8") +} +global.b64Encode = (data) => { + return Buffer.from(data, "utf-8").toString("base64") +} + +global.nanoid = (t = 21) => crypto.getRandomValues(new Uint8Array(t)).reduce(((t, e) => t += (e &= 63) < 36 ? e.toString(36) : e < 62 ? (e - 26).toString(36).toUpperCase() : e > 62 ? "-" : "_"), ""); + +Array.prototype.updateFromObjectKeys = function (obj) { + this.forEach((value, index) => { + if (obj[value] !== undefined) { + this[index] = obj[value] + } + }) + + return this +} + +global.toBoolean = (value) => { + if (typeof value === "boolean") { + return value + } + + if (typeof value === "string") { + return value.toLowerCase() === "true" + } + + return false +} + +global.isProduction = process.env.NODE_ENV === "production" + +import API from "./api" + +async function main() { + const mainAPI = new API() + + await mainAPI.initialize() +} + +main().catch((error) => { + console.error(`🆘 [FATAL ERROR] >`, error) +}) \ No newline at end of file diff --git a/packages/marketplace_server/src/models/index.js b/packages/marketplace_server/src/models/index.js new file mode 100755 index 00000000..e172c9be --- /dev/null +++ b/packages/marketplace_server/src/models/index.js @@ -0,0 +1,19 @@ +import mongoose, { Schema } from "mongoose" +import fs from "fs" +import path from "path" + +function generateModels() { + let models = {} + + const dirs = fs.readdirSync(__dirname).filter(file => file !== "index.js") + + dirs.forEach((file) => { + const model = require(path.join(__dirname, file)).default + + models[model.name] = mongoose.model(model.name, new Schema(model.schema), model.collection) + }) + + return models +} + +module.exports = generateModels() \ No newline at end of file diff --git a/packages/marketplace_server/src/models/widget/index.js b/packages/marketplace_server/src/models/widget/index.js new file mode 100644 index 00000000..283d8caf --- /dev/null +++ b/packages/marketplace_server/src/models/widget/index.js @@ -0,0 +1,24 @@ +export default { + name: "Widget", + collection: "widgets", + schema: { + manifest: { + type: Object, + required: true, + }, + user_id: { + type: String, + required: true, + }, + public: { + type: Boolean, + default: true, + }, + created_at: { + type: Date, + }, + updated_at: { + type: Date, + }, + } +} \ No newline at end of file diff --git a/packages/marketplace_server/src/useMiddlewares/useCors/index.js b/packages/marketplace_server/src/useMiddlewares/useCors/index.js new file mode 100644 index 00000000..7ebac0fc --- /dev/null +++ b/packages/marketplace_server/src/useMiddlewares/useCors/index.js @@ -0,0 +1,8 @@ +import cors from "cors" + +export default cors({ + origin: "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"], + preflightContinue: false, + optionsSuccessStatus: 204, +}) \ No newline at end of file diff --git a/packages/marketplace_server/src/useMiddlewares/useLogger/index.js b/packages/marketplace_server/src/useMiddlewares/useLogger/index.js new file mode 100644 index 00000000..5e9c64f4 --- /dev/null +++ b/packages/marketplace_server/src/useMiddlewares/useLogger/index.js @@ -0,0 +1,14 @@ +export default (req, res, next) => { + const startHrTime = process.hrtime() + + res.on("finish", () => { + const elapsedHrTime = process.hrtime(startHrTime) + const elapsedTimeInMs = elapsedHrTime[0] * 1000 + elapsedHrTime[1] / 1e6 + + res._responseTimeMs = elapsedTimeInMs + + console.log(`${req.method} ${res._status_code ?? res.statusCode ?? 200} ${req.url} ${elapsedTimeInMs}ms`) + }) + + next() +} \ No newline at end of file diff --git a/packages/marketplace_server/src/utils/createRoutesFromDirectory/index.js b/packages/marketplace_server/src/utils/createRoutesFromDirectory/index.js new file mode 100644 index 00000000..ab604981 --- /dev/null +++ b/packages/marketplace_server/src/utils/createRoutesFromDirectory/index.js @@ -0,0 +1,45 @@ +import fs from "fs" + +function createRoutesFromDirectory(startFrom, directoryPath, router) { + const files = fs.readdirSync(directoryPath) + + files.forEach((file) => { + const filePath = `${directoryPath}/${file}` + + const stat = fs.statSync(filePath) + + if (stat.isDirectory()) { + createRoutesFromDirectory(startFrom, filePath, router) + } else if (file.endsWith(".js")) { + let splitedFilePath = filePath.split("/") + + // slice the startFrom path + splitedFilePath = splitedFilePath.slice(splitedFilePath.indexOf(startFrom) + 1) + + const method = splitedFilePath[0] + + let route = splitedFilePath.slice(1, splitedFilePath.length).join("/") + + route = route.replace(".jsx", "") + route = route.replace(".js", "") + route = route.replace(".ts", "") + route = route.replace(".tsx", "") + + if (route === "index") { + route = "/" + } else { + route = `/${route}` + } + + let handler = require(filePath) + + handler = handler.default || handler + + router[method](route, handler) + } + }) + + return router +} + +export default createRoutesFromDirectory \ No newline at end of file diff --git a/packages/marketplace_server/src/utils/replaceImportsWithRemoteURL/index.js b/packages/marketplace_server/src/utils/replaceImportsWithRemoteURL/index.js new file mode 100644 index 00000000..bc08db37 --- /dev/null +++ b/packages/marketplace_server/src/utils/replaceImportsWithRemoteURL/index.js @@ -0,0 +1,18 @@ +import resolveUrl from "@utils/resolveUrl" + +export default (code, rootURL) => { + const importRegex = /import\s+(?:(?:([\w*\s{},]*)\s+from\s+)?["']([^"']*)["']|["']([^"']*)["'])/g + + // replaces all imports with absolute paths + const absoluteImportCode = code.replace(importRegex, (match, p1, p2) => { + let resolved = resolveUrl(rootURL, p2) + + if (!p1) { + return `import "${resolved}"` + } + + return `import ${p1} from "${resolved}"` + }) + + return absoluteImportCode +} \ No newline at end of file diff --git a/packages/marketplace_server/src/utils/resolveUrl/index.js b/packages/marketplace_server/src/utils/resolveUrl/index.js new file mode 100644 index 00000000..0afb2a90 --- /dev/null +++ b/packages/marketplace_server/src/utils/resolveUrl/index.js @@ -0,0 +1,11 @@ +export default (from, to) => { + const resolvedUrl = new URL(to, new URL(from, "resolve://")) + + if (resolvedUrl.protocol === "resolve:") { + const { pathname, search, hash } = resolvedUrl + + return pathname + search + hash + } + + return resolvedUrl.toString() +} \ No newline at end of file