diff --git a/packages/sui-ssr/README.md b/packages/sui-ssr/README.md index e2cffdb56..cf13d8e7f 100644 --- a/packages/sui-ssr/README.md +++ b/packages/sui-ssr/README.md @@ -13,25 +13,40 @@ SSR can be tought to configure and maintain. SSR handles that for you providing: npm install @s-ui/ssr --save ``` +## Development + +Starts a development server. + +``` +Usage: sui-ssr-dev [options] + +Options: + -L, --link-all [monorepo] Link all packages inside of monorepo multipackage + -l, --link-package [package] Replace each occurrence of this package with an absolute path to this folder (default: []) + -h, --help display help for command + +Examples: + + $ sui-ssr dev + $ sui-ssr dev --link-package ./domain --link-all ./components +``` + ## Build Generate a static version of the server w/out dependencies in the server folder. ``` - Usage: sui-ssr-build [options] - - Options: +Usage: sui-ssr-build [options] - -C, --clean Remove build folder before create a new one - -V, --verbose Verbose output - -h, --help output usage information - Description: +Options: - Build a production ready ssr server + -C, --clean Remove build folder before create a new one + -V, --verbose Verbose output + -h, --help output usage information - Examples: +Examples: - $ sui-ssr build + $ sui-ssr build ``` ## Archive @@ -41,21 +56,21 @@ Create a zip file with all assets needed to run the server in any infra. It will, over parameter, make that the express server run over a username and password in a htpasswd way. ``` - Usage: sui-ssr-archive [options] +Usage: sui-ssr-archive [options] - Options: +Options: - -C, --clean Remove previous zip - -R, --docker-registry Custom registry to be used as a proxy or instead of the Docker Hub registry - -E, --entry-point Relative path to an entry point script to replace the current one -> https://bit.ly/3e4wT8C - -h, --help Output usage information - -A, --auth Will build the express definition under authentication htpassword like. - -O, --outputFileName A string that will be used to set the name of the output filename. Keep in mind that the outputFilename will have the next suffix -sui-ssr.zip + -C, --clean Remove previous zip + -R, --docker-registry Custom registry to be used as a proxy or instead of the Docker Hub registry + -E, --entry-point Relative path to an entry point script to replace the current one -> https://bit.ly/3e4wT8C + -h, --help Output usage information + -A, --auth Will build the express definition under authentication htpassword like. + -O, --outputFileName A string that will be used to set the name of the output filename. Keep in mind that the outputFilename will have the next suffix -sui-ssr.zip - Examples: +Examples: - $ sui-ssr archive - $ sui-ssr archive --outputFileName=myFile // output: myFile-sui-ssr.zip + $ sui-ssr archive + $ sui-ssr archive --outputFileName=myFile // output: myFile-sui-ssr.zip ``` ### IMPORTANT!! diff --git a/packages/sui-ssr/bin/sui-ssr-dev.js b/packages/sui-ssr/bin/sui-ssr-dev.js new file mode 100644 index 000000000..84edd8e39 --- /dev/null +++ b/packages/sui-ssr/bin/sui-ssr-dev.js @@ -0,0 +1,160 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ + +const {WEBPACK_PORT = 8080} = process.env +process.env.CDN = `http://localhost:${WEBPACK_PORT}/` + +const program = require('commander') +const {exec} = require('child_process') +const path = require('path') +const fs = require('fs') +const express = require('express') +const webpack = require('webpack') +const webpackDevMiddleware = require('webpack-dev-middleware') +const webpackHotMiddleware = require('webpack-hot-middleware') +const nodemon = require('nodemon') +const clientConfig = require('@s-ui/bundler/webpack.config.server.dev.js') +const linkLoaderConfigBuilder = require('@s-ui/bundler/loaders/linkLoaderConfigBuilder.js') + +const serverConfigFactory = require('../compiler/server.js') + +const TMP_PATH = '.sui' +const SERVER_OUTPUT_PATH = path.join(process.cwd(), `${TMP_PATH}/server`) +const STATICS_PATH = path.join(process.cwd(), './statics') +const STATICS_OUTPUT_PATH = path.join(process.cwd(), `${TMP_PATH}/statics`) + +program + .option('-L, --link-all [monorepo]', 'Link all packages inside of monorepo multipackage') + .option( + '-l, --link-package [package]', + 'Replace each occurrence of this package with an absolute path to this folder', + (v, m) => { + m.push(v) + return m + }, + [] + ) + .parse(process.argv) + +const compile = (name, compiler) => { + return new Promise((resolve, reject) => { + compiler.hooks.compile.tap(name, () => { + console.time(`[${name}] Compiling`) + }) + compiler.hooks.done.tap(name, stats => { + console.timeEnd(`[${name}] Compiling`) + if (!stats.hasErrors()) { + return resolve() + } + return reject(new Error(`Failed to compile ${name}`)) + }) + }) +} + +const linkStatics = () => { + return new Promise((resolve, reject) => + fs.symlink(STATICS_PATH, STATICS_OUTPUT_PATH, 'dir', err => { + if (err) { + if (err.code === 'EEXIST') { + return resolve() + } + + return reject(err) + } + + resolve() + }) + ) +} + +const initMSW = () => { + return exec(`npx msw init ${STATICS_PATH}`) +} + +const start = ({packagesToLink, linkAll}) => { + const app = express() + const clientCompiler = webpack( + linkLoaderConfigBuilder({ + config: require('@s-ui/bundler/webpack.config.server.dev.js'), + linkAll, + packagesToLink + }) + ) + const serverCompiler = webpack( + linkLoaderConfigBuilder({ + config: serverConfigFactory({outputPath: SERVER_OUTPUT_PATH}), + linkAll, + packagesToLink + }) + ) + const watchOptions = { + ignored: /node_modules/, + stats: clientConfig.stats + } + + app.use((_req, res, next) => { + res.header('Access-Control-Allow-Origin', '*') + return next() + }) + + app.use( + webpackDevMiddleware(clientCompiler, { + publicPath: clientConfig.output.publicPath, + stats: clientConfig.stats, + writeToDisk: true + }) + ) + app.use(webpackHotMiddleware(clientCompiler)) + + app.listen(WEBPACK_PORT) + + serverCompiler.watch(watchOptions, (error, stats) => { + if (!error && !stats.hasErrors()) { + return + } + + if (error) { + console.log(error, 'error') + } + + if (stats.hasErrors()) { + const info = stats.toJson() + const errors = info.errors + + console.log(errors) + } + }) + + if (!fs.existsSync(TMP_PATH)) { + fs.mkdirSync(TMP_PATH) + } + + Promise.all([linkStatics(), initMSW(), compile('client', clientCompiler), compile('server', serverCompiler)]) + .then(() => { + const script = nodemon({ + script: `${SERVER_OUTPUT_PATH}/index.js`, + watch: [SERVER_OUTPUT_PATH], + delay: 200 + }) + + script.on('restart', () => { + console.log('Server side app has been restarted.', 'warning') + }) + + script.on('quit', () => { + console.log('Process ended') + process.exit() + }) + + script.on('error', () => { + console.log('An error occured. Exiting', 'error') + process.exit(1) + }) + }) + .catch(error => { + console.log('error', error) + }) +} +const opts = program.opts() + +start({packagesToLink: opts.linkPackage, linkAll: opts.linkAll}) diff --git a/packages/sui-ssr/bin/sui-ssr.js b/packages/sui-ssr/bin/sui-ssr.js index d758e3ebc..e0a4d0e8f 100755 --- a/packages/sui-ssr/bin/sui-ssr.js +++ b/packages/sui-ssr/bin/sui-ssr.js @@ -9,5 +9,6 @@ program.version(version, ' --version') program.command('build', 'Build a ssr server').alias('b') program.command('archive', 'Create a server.zip file').alias('a') program.command('release', 'Release new version of the server').alias('r') +program.command('dev', 'Start a ssr server in development mode').alias('d') program.parse(process.argv) diff --git a/packages/sui-ssr/package.json b/packages/sui-ssr/package.json index bc3d30c76..44b408fb6 100644 --- a/packages/sui-ssr/package.json +++ b/packages/sui-ssr/package.json @@ -37,6 +37,9 @@ "mime": "1.6.0", "noop-console": "0.8.0", "parse5": "6.0.1", - "ua-parser-js": "0.7.33" + "ua-parser-js": "0.7.33", + "webpack-dev-middleware": "6.1.1", + "webpack-hot-middleware": "2.25.4", + "nodemon": "3.0.1" } } diff --git a/packages/sui-ssr/server/utils/factory.js b/packages/sui-ssr/server/utils/factory.js index ea2048288..1d53e8cd0 100644 --- a/packages/sui-ssr/server/utils/factory.js +++ b/packages/sui-ssr/server/utils/factory.js @@ -1,5 +1,6 @@ const DEFAULT_SITE_HEADER = 'X-Serve-Site' const DEFAULT_PUBLIC_FOLDER = 'public' +const DEFAULT_DEV_PUBLIC_FOLDER = '.sui/public' const DEFAULT_MULTI_SITE_KEY = 'default' const EXPRESS_STATIC_CONFIG = {index: false} @@ -28,8 +29,15 @@ export default ({path, fs, config: ssrConf = {}, assetsManifest}) => { } const publicFolder = req => { + if (process.env.NODE_ENV !== 'production') { + return DEFAULT_DEV_PUBLIC_FOLDER + } + const site = siteByHost(req) - if (!site) return DEFAULT_PUBLIC_FOLDER + + if (!site) { + return DEFAULT_PUBLIC_FOLDER + } return multiSitePublicFolder(site) }