Add server

This commit is contained in:
Dan Abramov
2024-07-06 04:46:21 +01:00
parent 21123dab43
commit e212a3f441
9 changed files with 2889 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
# production
/build
.eslintcache
# misc
.DS_Store
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
+74
View File
@@ -0,0 +1,74 @@
/* global Buffer */
import {
getSource as getSourceImpl,
load as reactLoad,
resolve,
transformSource as reactTransformSource,
} from 'react-server-dom-webpack/node-loader'
export {resolve}
import babel from '@babel/core'
const babelOptions = {
babelrc: false,
ignore: [/\/(build|node_modules)\//],
plugins: [
'@babel/plugin-syntax-import-meta',
'@babel/plugin-transform-react-jsx',
],
sourceMaps: process.env.NODE_ENV === 'development' ? 'inline' : false,
}
async function babelLoad(url, context, defaultLoad) {
const result = await defaultLoad(url, context, defaultLoad)
if (result.format === 'module') {
const opt = Object.assign({filename: url}, babelOptions)
const newResult = await babel.transformAsync(result.source, opt)
if (!newResult) {
if (typeof result.source === 'string') {
return result
}
return {
source: Buffer.from(result.source).toString('utf8'),
format: 'module',
}
}
return {source: newResult.code, format: 'module'}
}
return defaultLoad(url, context, defaultLoad)
}
export async function load(url, context, defaultLoad) {
return await reactLoad(url, context, (u, c) => {
return babelLoad(u, c, defaultLoad)
})
}
async function babelTransformSource(source, context, defaultTransformSource) {
const {format} = context
if (format === 'module') {
const opt = Object.assign({filename: context.url}, babelOptions)
const newResult = await babel.transformAsync(source, opt)
if (!newResult) {
if (typeof source === 'string') {
return {source}
}
return {
source: Buffer.from(source).toString('utf8'),
}
}
return {source: newResult.code}
}
return defaultTransformSource(source, context, defaultTransformSource)
}
async function transformSourceImpl(source, context, defaultTransformSource) {
return await reactTransformSource(source, context, (s, c) => {
return babelTransformSource(s, c, defaultTransformSource)
})
}
export const transformSource =
process.version < 'v16' ? transformSourceImpl : undefined
export const getSource = process.version < 'v16' ? getSourceImpl : undefined
+32
View File
@@ -0,0 +1,32 @@
{
"name": "sdui",
"type": "module",
"version": "0.1.0",
"private": true,
"devEngines": {
"node": "20.x || 21.x"
},
"dependencies": {
"@babel/core": "^7.16.0",
"@babel/preset-react": "^7.22.5",
"@babel/register": "^7.24.6",
"babel-preset-react-app": "^10.0.1",
"body-parser": "^1.20.1",
"busboy": "^1.6.0",
"compression": "^1.7.4",
"nodemon": "^2.0.19",
"react": "^19.0.0-rc-f38c22b244-20240704",
"react-dom": "^19.0.0-rc-f38c22b244-20240704",
"react-server-dom-webpack": "^19.0.0-rc-f38c22b244-20240704",
"undici": "^5.20.0"
},
"scripts": {
"dev": "NODE_ENV=development BUILD_PATH=dist nodemon --watch src --watch dist -- --enable-source-maps --experimental-loader ./loader/region.js --conditions=react-server --inspect=127.0.0.1:9229 server/region",
"start": "NODE_ENV=production node --experimental-loader ./loader/region.js --conditions=react-server server/region"
},
"babel": {
"presets": [
"react-app"
]
}
}
+3
View File
@@ -0,0 +1,3 @@
{
"type": "commonjs"
}
+131
View File
@@ -0,0 +1,131 @@
'use strict'
const path = require('path')
const register = require('react-server-dom-webpack/node-register')
register()
const babelRegister = require('@babel/register')
babelRegister({
babelrc: false,
ignore: [
/\/(build|node_modules)\//,
function (file) {
// eslint-disable-next-line no-path-concat
if ((path.dirname(file) + '/').startsWith(__dirname + '/')) {
// Ignore everything in this folder
// because it's a mix of CJS and ESM
// and working with raw code is easier.
return true
}
return false
},
],
presets: ['@babel/preset-react'],
plugins: ['@babel/transform-modules-commonjs'],
sourceMaps: process.env.NODE_ENV === 'development' ? 'inline' : false,
})
if (typeof fetch === 'undefined') {
// Patch fetch for earlier Node versions.
global.fetch = require('undici').fetch
}
const express = require('express')
const bodyParser = require('body-parser')
const busboy = require('busboy')
const app = express()
const compress = require('compression')
app.use(compress())
// Application
const React = require('react')
async function renderApp(res, returnValue, formState) {
const {renderToPipeableStream} = await import(
'react-server-dom-webpack/server'
)
// const m = require('../src/App.js');
const m = await import('../src/App.js')
const moduleMap = {
Text: {
id: 'Text',
},
}
const App = m.default.default || m.default
const root = React.createElement(
React.Fragment,
null,
React.createElement(App),
)
// For client-invoked server actions we refresh the tree and return a return value.
const payload = {root, returnValue, formState}
const {pipe} = renderToPipeableStream(payload, moduleMap)
pipe(res)
}
app.get('/', async function (req, res) {
await renderApp(res, null, null)
})
app.post('/', bodyParser.text(), async function (req, res) {
const {decodeReply, decodeReplyFromBusboy} = await import(
'react-server-dom-webpack/server'
)
const serverReference = req.get('rsc-action')
if (serverReference) {
// This is the client-side case
const [filepath, name] = serverReference.split('#')
const action = (await import(filepath))[name]
// Validate that this is actually a function we intended to expose and
// not the client trying to invoke arbitrary functions. In a real app,
// you'd have a manifest verifying this before even importing it.
if (action.$$typeof !== Symbol.for('react.server.reference')) {
throw new Error('Invalid action')
}
let args
if (req.is('multipart/form-data')) {
// Use busboy to streamingly parse the reply from form-data.
const bb = busboy({headers: req.headers})
const reply = decodeReplyFromBusboy(bb)
req.pipe(bb)
args = await reply
} else {
args = await decodeReply(req.body)
}
const result = action.apply(null, args)
try {
// Wait for any mutations
await result
} catch (x) {
// We handle the error on the client
}
// Refresh the client and return the value
renderApp(res, result, null)
}
})
app.listen(3001, () => {
console.log('Regional Flight Server listening on port 3001...')
})
app.on('error', function (error) {
if (error.syscall !== 'listen') {
throw error
}
switch (error.code) {
case 'EACCES':
console.error('port 3001 requires elevated privileges')
process.exit(1)
break
case 'EADDRINUSE':
console.error('Port 3001 is already in use')
process.exit(1)
break
default:
throw error
}
})
+7
View File
@@ -0,0 +1,7 @@
import * as React from 'react'
import {Text} from './client.js'
export default async function App() {
return <Text>hello</Text>
}
+4
View File
@@ -0,0 +1,4 @@
export const Text = {
$$typeof: Symbol.for('react.client.reference'),
$$id: 'Text',
}
File diff suppressed because it is too large Load Diff