server/api.server.js (173 lines of code) (raw):

/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ 'use strict'; const register = require('react-server-dom-webpack/node-register'); register(); const babelRegister = require('@babel/register'); babelRegister({ ignore: [/[\\\/](build|server|node_modules)[\\\/]/], presets: [['react-app', {runtime: 'automatic'}]], plugins: ['@babel/transform-modules-commonjs'], }); const express = require('express'); const compress = require('compression'); const {readFileSync} = require('fs'); const {unlink, writeFile} = require('fs').promises; const {renderToPipeableStream} = require('react-server-dom-webpack/writer'); const path = require('path'); const {Pool} = require('pg'); const React = require('react'); const ReactApp = require('../src/App.server').default; // Don't keep credentials in the source tree in a real app! const pool = new Pool(require('../credentials')); const PORT = process.env.PORT || 4000; const app = express(); app.use(compress()); app.use(express.json()); app .listen(PORT, () => { console.log(`React Notes listening at ${PORT}...`); }) .on('error', function(error) { if (error.syscall !== 'listen') { throw error; } const isPipe = (portOrPipe) => Number.isNaN(portOrPipe); const bind = isPipe(PORT) ? 'Pipe ' + PORT : 'Port ' + PORT; switch (error.code) { case 'EACCES': console.error(bind + ' requires elevated privileges'); process.exit(1); break; case 'EADDRINUSE': console.error(bind + ' is already in use'); process.exit(1); break; default: throw error; } }); function handleErrors(fn) { return async function(req, res, next) { try { return await fn(req, res); } catch (x) { next(x); } }; } app.get( '/', handleErrors(async function(_req, res) { await waitForWebpack(); const html = readFileSync( path.resolve(__dirname, '../build/index.html'), 'utf8' ); // Note: this is sending an empty HTML shell, like a client-side-only app. // However, the intended solution (which isn't built out yet) is to read // from the Server endpoint and turn its response into an HTML stream. res.send(html); }) ); async function renderReactTree(res, props) { await waitForWebpack(); const manifest = readFileSync( path.resolve(__dirname, '../build/react-client-manifest.json'), 'utf8' ); const moduleMap = JSON.parse(manifest); const {pipe} = renderToPipeableStream( React.createElement(ReactApp, props), moduleMap ); pipe(res); } function sendResponse(req, res, redirectToId) { const location = JSON.parse(req.query.location); if (redirectToId) { location.selectedId = redirectToId; } res.set('X-Location', JSON.stringify(location)); renderReactTree(res, { selectedId: location.selectedId, isEditing: location.isEditing, searchText: location.searchText, }); } app.get('/react', function(req, res) { sendResponse(req, res, null); }); const NOTES_PATH = path.resolve(__dirname, '../notes'); app.post( '/notes', handleErrors(async function(req, res) { const now = new Date(); const result = await pool.query( 'insert into notes (title, body, created_at, updated_at) values ($1, $2, $3, $3) returning id', [req.body.title, req.body.body, now] ); const insertedId = result.rows[0].id; await writeFile( path.resolve(NOTES_PATH, `${insertedId}.md`), req.body.body, 'utf8' ); sendResponse(req, res, insertedId); }) ); app.put( '/notes/:id', handleErrors(async function(req, res) { const now = new Date(); const updatedId = Number(req.params.id); await pool.query( 'update notes set title = $1, body = $2, updated_at = $3 where id = $4', [req.body.title, req.body.body, now, updatedId] ); await writeFile( path.resolve(NOTES_PATH, `${updatedId}.md`), req.body.body, 'utf8' ); sendResponse(req, res, null); }) ); app.delete( '/notes/:id', handleErrors(async function(req, res) { await pool.query('delete from notes where id = $1', [req.params.id]); await unlink(path.resolve(NOTES_PATH, `${req.params.id}.md`)); sendResponse(req, res, null); }) ); app.get( '/notes', handleErrors(async function(_req, res) { const {rows} = await pool.query('select * from notes order by id desc'); res.json(rows); }) ); app.get( '/notes/:id', handleErrors(async function(req, res) { const {rows} = await pool.query('select * from notes where id = $1', [ req.params.id, ]); res.json(rows[0]); }) ); app.get('/sleep/:ms', function(req, res) { setTimeout(() => { res.json({ok: true}); }, req.params.ms); }); app.use(express.static('build')); app.use(express.static('public')); async function waitForWebpack() { while (true) { try { readFileSync(path.resolve(__dirname, '../build/index.html')); return; } catch (err) { console.log( 'Could not find webpack build output. Will retry in a second...' ); await new Promise((resolve) => setTimeout(resolve, 1000)); } } }