├── .gitignore ├── src ├── template.js ├── index.js └── bundler.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /src/template.js: -------------------------------------------------------------------------------- 1 | module.exports = (code) => { 2 | return ` 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 |
15 |
16 | 23 | 24 | 25 | `; 26 | }; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node src/index.js", 8 | "dev": "nodemon src/index.js", 9 | "generate:bundle": "NODE_ENV=development browserify -p tinyify -r react -r react-dom -o public/bundle.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@babel/core": "^7.10.2", 16 | "@babel/preset-env": "^7.10.2", 17 | "@babel/preset-react": "^7.10.1", 18 | "browserify": "^16.5.1", 19 | "cors": "^2.8.5", 20 | "express": "^4.17.1", 21 | "md5": "^2.2.1", 22 | "node-cache": "^5.1.1", 23 | "react": "^16.13.1", 24 | "react-dom": "^16.13.1", 25 | "strip-ansi": "^6.0.0", 26 | "tinyify": "^2.5.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const NodeCache = require('node-cache'); 4 | const stripAnsi = require('strip-ansi'); 5 | 6 | const md5 = require('md5'); 7 | const template = require('./template'); 8 | const bundler = require('./bundler'); 9 | 10 | const cache = new NodeCache({ 11 | stdTTL: 60, 12 | useClones: false, 13 | checkperiod: 15, 14 | }); 15 | 16 | const app = express(); 17 | 18 | app.use(cors()); 19 | app.use(express.static('public')); 20 | app.use(express.json()); 21 | 22 | app.post('/', async (req, res) => { 23 | const { code: rawCode } = req.body; 24 | if (!rawCode || typeof rawCode !== 'string') { 25 | return res.status(400).send({ error: 'You must enter some code' }); 26 | } 27 | 28 | const key = md5(rawCode); 29 | if (cache.has(key)) { 30 | return res.send({ key }); 31 | } 32 | 33 | try { 34 | const code = await bundler(rawCode); 35 | cache.set(key, code); 36 | res.send({ key }); 37 | } catch (err) { 38 | return res.status(400).send({ 39 | error: stripAnsi(err.message), 40 | }); 41 | } 42 | }); 43 | 44 | app.get('/:key', (req, res) => { 45 | const { key } = req.params; 46 | const code = cache.get(key); 47 | 48 | res.send(template(code)); 49 | }); 50 | 51 | const PORT = process.env.PORT || 4000; 52 | app.listen(PORT, () => { 53 | console.log(`Listening on ${PORT}`); 54 | }); 55 | -------------------------------------------------------------------------------- /src/bundler.js: -------------------------------------------------------------------------------- 1 | const { Readable } = require('stream'); 2 | const browserify = require('browserify'); 3 | const babel = require('@babel/core'); 4 | 5 | const CODE_LENGTH_LIMIT = 10000; 6 | 7 | module.exports = (rawCode) => { 8 | const code = ` 9 | ${injectReact(rawCode)} 10 | ${injectReactDOM(rawCode)} 11 | 12 | ${rawCode} 13 | (() => { 14 | const root = document.querySelector('#root'); 15 | if (!root.innerHTML && typeof App !== 'undefined') { 16 | ReactDOM.render(React.createElement(App), root); 17 | } else if (!root.innerHTML && typeof App === 'undefined') { 18 | root.innerHTML = 'Found nothing to render. Either call ReactDOM.render yourself or make sure your component is called "App"'; 19 | } 20 | })() 21 | `; 22 | 23 | const result = babel.transform(code, { 24 | presets: ['@babel/preset-react', '@babel/preset-env'], 25 | }); 26 | 27 | if (result.code.length > CODE_LENGTH_LIMIT) { 28 | throw new Error('Too much code'); 29 | } 30 | 31 | const b = browserify(); 32 | b.add(Readable.from(result.code)); 33 | b.external('react'); 34 | b.external('react-dom'); 35 | 36 | return streamToString(b.bundle()); 37 | }; 38 | 39 | function streamToString(stream) { 40 | const chunks = []; 41 | return new Promise((resolve, reject) => { 42 | stream.on('data', (chunk) => chunks.push(chunk)); 43 | stream.on('error', reject); 44 | stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); 45 | }); 46 | } 47 | 48 | function injectReact(rawCode) { 49 | if (rawCode.includes('import React ') || rawCode.includes('import React,')) { 50 | return ''; 51 | } 52 | 53 | return "import React from 'react';"; 54 | } 55 | 56 | function injectReactDOM(rawCode) { 57 | if (rawCode.includes('import ReactDOM ')) { 58 | return ''; 59 | } 60 | 61 | return "import ReactDOM from 'react-dom';"; 62 | } 63 | --------------------------------------------------------------------------------