Add server
This commit is contained in:
@@ -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*
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import {Text} from './client.js'
|
||||
|
||||
export default async function App() {
|
||||
return <Text>hello</Text>
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user