@coko/client
Version:
Client side common code for coko apps
429 lines (366 loc) • 11.8 kB
JavaScript
const path = require('path')
const appRootPath = require('app-root-path')
const startsWith = require('lodash/startsWith')
const range = require('lodash/range')
const has = require('lodash/has')
const colors = require('colors/safe')
const webpack = require('webpack')
const CompressionPlugin = require('compression-webpack-plugin')
const CopyPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const {
NODE_ENV,
CLIENT_APP_ROOT_PATH,
CLIENT_BUILD_FOLDER_PATH,
CLIENT_ENTRY_FILE_PATH,
CLIENT_FAST_REFRESH,
CLIENT_FAVICON_PATH,
CLIENT_PAGE_TITLE,
CLIENT_STATIC_FOLDER_PATH,
CLIENT_PAGES_FOLDER_PATH,
CLIENT_PORT,
CLIENT_UI_FOLDER_PATH,
CLIENT_LANGUAGE,
CLIENT_WS_MIN_TIMEOUT,
CLIENT_WS_TIMEOUT,
SERVER_URL,
WEBSOCKET_SERVER_URL,
YJS_WEBSOCKET_SERVER_URL,
} = process.env
//
/* SET UP VARS */
//
// Environment variables that will only be used in this config
const variablesForWebpackConfig = [
'CLIENT_APP_ROOT_PATH',
'CLIENT_BUILD_FOLDER_PATH',
'CLIENT_ENTRY_FILE_PATH',
'CLIENT_FAST_REFRESH',
'CLIENT_FAVICON_PATH',
'CLIENT_LANGUAGE',
'CLIENT_PAGE_TITLE',
'CLIENT_PAGES_FOLDER_PATH',
'CLIENT_PORT',
'CLIENT_STATIC_FOLDER_PATH',
'CLIENT_UI_FOLDER_PATH',
]
/**
* This object maps variables to their default values
* If a variable is undefined, an error will be thrown
* Use undefined for required variables, and null or an actual value for optional ones
* Server url and websocket url are optional because they might not be provided at build time in production,
* but at runtime with our dynamic url mechanism
*/
const DEFAULT_CLIENT_WS_MIN_TIMEOUT = 5000
const DEFAULT_CLIENT_WS_TIMEOUT = 60000
// Environment variables that will be passed down to the build
const variablesForBuild = {
NODE_ENV: undefined,
SERVER_URL: null,
WEBSOCKET_SERVER_URL: null,
YJS_WEBSOCKET_SERVER_URL: null,
CLIENT_WS_MIN_TIMEOUT: DEFAULT_CLIENT_WS_MIN_TIMEOUT,
CLIENT_WS_TIMEOUT: DEFAULT_CLIENT_WS_TIMEOUT,
}
// Allow custom variables that start with CLIENT_ to pass into the build
const customVariables = Object.keys(process.env)
.filter(k => {
return (
!variablesForWebpackConfig.includes(k) &&
!has(variablesForBuild, k) &&
startsWith(k, 'CLIENT_')
)
})
.reduce((obj, k) => {
const newObj = { ...obj }
newObj[k] = undefined
return newObj
}, {})
const variablesInBuild = { ...variablesForBuild, ...customVariables }
const mode = NODE_ENV === 'production' ? 'production' : 'development'
const isEnvDevelopment = mode === 'development'
const isEnvProduction = mode === 'production'
let appPath
if (CLIENT_APP_ROOT_PATH) {
appPath = path.resolve(CLIENT_APP_ROOT_PATH)
} else {
appPath = path.resolve(appRootPath.toString(), 'app')
}
const uiFolderPath = path.resolve(appPath, CLIENT_UI_FOLDER_PATH || 'ui')
const pagesFolderPath = path.resolve(
appPath,
CLIENT_PAGES_FOLDER_PATH || 'pages',
)
const noopPath = path.resolve(__dirname, 'noop.js')
const staticFolderPath =
CLIENT_STATIC_FOLDER_PATH || path.resolve(appPath, '..', 'static')
const buildFolderPath =
CLIENT_BUILD_FOLDER_PATH || path.resolve(appPath, '..', '_build')
// react's fast-refresh is opt-in for the time being
const fastRefreshValue =
CLIENT_FAST_REFRESH && CLIENT_FAST_REFRESH.toString().toLowerCase()
const useFastRefresh = fastRefreshValue === '1' || fastRefreshValue === 'true'
const templatePath = path.resolve(__dirname, 'index.ejs')
const entryFilePath = CLIENT_ENTRY_FILE_PATH || './start.js'
const devServerPort = CLIENT_PORT || 8080
const faviconPath = CLIENT_FAVICON_PATH
const pageTitle = CLIENT_PAGE_TITLE
const language = CLIENT_LANGUAGE
const defaultLanguage = 'en-US'
const serverUrl = SERVER_URL
const websocketServerUrl = WEBSOCKET_SERVER_URL
const yjsWebsocketServerUrl = YJS_WEBSOCKET_SERVER_URL
const WSLinkMinTimeout = CLIENT_WS_MIN_TIMEOUT || DEFAULT_CLIENT_WS_MIN_TIMEOUT
const WSLinkTimeout = CLIENT_WS_TIMEOUT || DEFAULT_CLIENT_WS_TIMEOUT
// const antVariablesPath = require.resolve(
// path.join(antPath, 'lib/style/themes/default.less'),
// )
// #region log-init
/* eslint-disable no-console */
const symbolGenerator = n => {
const SYMBOL = '/'
let symbols = ''
range(n).forEach(() => (symbols += SYMBOL))
return symbols
}
const cyan = t => console.log(colors.cyan(t))
const logSeparator = () => cyan(symbolGenerator(22))
const logHeader = text => {
logSeparator()
cyan(`${symbolGenerator(2)} ${text.toUpperCase()}\n`)
}
const logStatus = (label, message, newLine) => {
console.log(`${colors.cyan(label)}: ${message}${newLine ? '\n' : ''}`)
}
logHeader('coko client info')
logStatus('Environment', NODE_ENV, true)
logStatus(`App context path is set to`, appPath)
isEnvProduction && logStatus(`Build will be written to`, buildFolderPath)
logStatus(`Static folder path found at`, staticFolderPath)
logStatus(`App entry file will be`, entryFilePath)
logStatus(`UI folder path will be`, uiFolderPath)
logStatus(`Pages folder path will be`, pagesFolderPath)
logStatus(`Favicon path will be`, faviconPath)
logStatus(`Page title set to`, pageTitle)
logStatus(`Language set to`, language || `${defaultLanguage} (default)`)
isEnvDevelopment && logStatus(`Dev server will run at port`, devServerPort)
logStatus(`Server will be requested at`, serverUrl)
logStatus(`Websocket server will be requested at`, websocketServerUrl)
logStatus(`Websocket link min timeout will be `, WSLinkMinTimeout)
logStatus(`Websocket link timeout will be `, WSLinkTimeout)
logStatus(
'yjs websocket server url will be requested at',
yjsWebsocketServerUrl,
)
logStatus(`React fast-refresh is`, useFastRefresh ? 'on' : 'off')
logStatus(
'Custom environment variables detected',
customVariables.length > 0 ? `${customVariables}` : 'none',
true,
)
logSeparator()
console.log('')
/* eslint-enable no-console */
// #endregion log-init
//
/* BASE CONFIG */
//
const webpackConfig = {
context: appPath,
entry: entryFilePath,
name: 'client',
mode,
// TO D0 -- browserlist?
target: 'web',
// TO DO -- bundle analyzer
// TO DO -- code splitting
// TO DO -- hash is deprecated
output: {
chunkFilename: isEnvProduction
? 'js/[name].[hash:8].chunk.js'
: isEnvDevelopment && 'js/[name].chunk.js',
filename: isEnvProduction
? 'js/[name].[hash:8].js'
: isEnvDevelopment && 'js/bundle.js',
// There are also additional JS chunk files if you use code splitting.
// path: contentBase,
path: buildFolderPath,
// publicPath: isEnvProduction ? '/assets/' : '/',
publicPath: '/',
},
//
/* RULES */
//
module: {
rules: [
// Typescript
{ test: /\.tsx?$/, loader: 'ts-loader' },
// TO DELETE?
// mjs files: needed because of apollo client??
{
test: /\.m?js/,
resolve: {
fullySpecified: false,
},
},
// js files
{
test: /\.js$|\.jsx$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: [
// ['@babel/plugin-proposal-decorators', { version: '2023-11' }],
['@babel/plugin-proposal-decorators', { legacy: true }],
'babel-plugin-parameter-decorator',
['@babel/plugin-transform-class-properties', { loose: true }],
[
'@babel/plugin-transform-private-property-in-object',
{ loose: true },
],
['@babel/plugin-transform-private-methods', { loose: true }],
'@babel/plugin-transform-runtime',
'babel-plugin-styled-components',
].filter(Boolean),
env: {
development: {
plugins: [
useFastRefresh && require.resolve('react-refresh/babel'),
].filter(Boolean),
},
// TO DO: install babel-preset-minify?
// production: {
// /* bug requires mangle:false https://github.com/babel/minify/issues/556#issuecomment-339751209 */
// presets: [['minify', { builtIns: false, mangle: false }]],
// },
},
},
},
},
// TO DO -- remove url-loader from dependencies
// Images
{
test: /\.png|\.jpg|\.svg$/,
type: 'asset/resource',
},
// {
// test: /\.png|\.jpg$/,
// loader: 'url-loader',
// // options: {
// // limit: 5000,
// // },
// },
// Fonts
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
// {
// test: /\.woff|\.woff2|\.svg|.eot|\.ttf/,
// loader: 'url-loader',
// options: {
// name: 'static/media/[name].[hash:8].[ext]',
// // fonts break without this line, revisit when webpack dependencies are upgraded
// esModule: false,
// },
// },
// HTML
{ test: /\.html$/, loader: 'html-loader' },
// CSS
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.less$/,
use: [
{
loader: 'style-loader',
},
{
loader: 'css-loader', // translates CSS into CommonJS
},
],
},
],
},
//
/* PLUGINS */
//
plugins: [
// Use the index.ejs template to create the base index.html file of the bundle
new HtmlWebpackPlugin({
favicon: faviconPath,
template: templatePath,
title: pageTitle,
language: language || defaultLanguage,
}),
// DEV-ONLY
// React fast-refresh
isEnvDevelopment &&
useFastRefresh &&
new webpack.HotModuleReplacementPlugin(),
isEnvDevelopment && useFastRefresh && new ReactRefreshWebpackPlugin(),
isEnvDevelopment &&
new BundleAnalyzerPlugin({
openAnalyzer: false,
analyzerHost: '0.0.0.0',
}),
// PROD-ONLY
isEnvProduction &&
new MiniCssExtractPlugin({
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
filename: 'static/css/[name].[contenthash:8].css',
}),
// Make sure environment variables are defined
new webpack.EnvironmentPlugin(variablesInBuild),
// Copy static assets to root of build folder
new CopyPlugin({
patterns: [{ from: staticFolderPath }],
}),
new webpack.optimize.AggressiveMergingPlugin(),
new CompressionPlugin(),
// TO DELETE
// new webpack.NoEmitOnErrorsPlugin(),
// new webpack.optimize.OccurrenceOrderPlugin(),
].filter(Boolean),
// TO DELETE
// resolve: {
// alias: {
// config: clientConfigPath,
// joi: 'joi-browser',
// },
// enforceExtension: false,
// extensions: ['.mjs', '.js', '.jsx', '.json', '.scss'],
// },
resolve: {
alias: {
pages: pagesFolderPath,
ui: uiFolderPath,
},
fallback: {
pages: noopPath,
ui: noopPath,
},
},
}
if (isEnvDevelopment) {
// TO DO -- use any source map in production?
webpackConfig.devtool = 'cheap-module-source-map'
//
/* WEBPACK DEV SERVER */
//
webpackConfig.devServer = {
// handle unknown routes
historyApiFallback: true,
// play nice from a within a docker container
host: '0.0.0.0',
port: devServerPort,
}
}
module.exports = webpackConfig