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