I have been trying to learn how to SSR with React(without using Next.js) I've gotten to the point that I'm seeing the HTML rendered on the page but once I add the css imports to style the component babel start throwing errors (
C:\Users\Frozen\Desktop\ssr stuff\ssr4\src\App.css:1
body {
^
SyntaxError: Unexpected token '{'
)
.babelrc
{
"presets": ["#babel/preset-env", "#babel/preset-react"],
}
webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './build'),
filename: "bundle.js",
publicPath: "/"
},
devServer: {
port: 3010,
static: true,
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ['#babel/preset-env', "#babel/preset-react"]
}
},
},
{
test: /\.(css|scss)$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
publicPath: "/"
})
]
};
server.js
import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import App from './src/App.js';
import path from 'path';
import fs from 'fs';
const app = express();
app.get('/', (req, res) => {
console.log('in /');
fs.readFile(
path.resolve('./build/index.html'),
'utf8',
(err, data) => {
if (err) {
console.log(err);
return res.status(500).send('Internal Server Error');
}
const html = data.replace(
'<div id="root"></div>',
`<div id="root">${ReactDOMServer.renderToString(<App />)}</div>`
)
console.log(html)
return res.send(html);
}
);
});
app.use(express.static(path.resolve(__dirname,'build')));
app.listen(3000, () => {
console.log('server listening on port 3000')
})
I also fail to understand why do I have to put the app.use(static) below the app.get('/') aren't middlewares supposed to go one after another in order top to bottom (if I move the app.use(static) above the app.get('/') where I return the component I don't see the console.log('in /');
index.js
import React from "react";
import ReactDOM from 'react-dom'
import App from "./App";
const appElement = document.getElementById('root');
ReactDOM.hydrate(<App/>, appElement)
I run
npx webpack
npx babel-node server.js
And I get the css error (if I remove the css import I don't get it but I don't have css in the component)
Also is there a way to rebuild and run server.js once I change something ( like when you use create-react-app ) Also any other suggestions on what I can improve in the current setup will be much appreciated.
Install babel-plugin-css-modules-transform npm install --save-dev babel-plugin-css-modules-transform.Then include it in .babelrc "plugins": ["css-modules-transform"]
I know I am struggling in this problem.
I am working on a webpack Universal React App but i got this error message and I have no idea where it come from:
TypeError [ERR_INVALID_ARG_TYPE]: The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received type function ([Function (anonymous)])
at new NodeError (node:internal/errors:372:5)
at Function.from (node:buffer:323:9)
at ServerResponse.send (C:\Dev\isomorphic-react-redux-router-app\node_modules\express\lib\response.js:193:22)
at eval (webpack://isomorphic-react-redux-router-app/./app/serverside/server.js?:17:19)
at Layer.handle [as handle_request] (C:\Dev\isomorphic-react-redux-router-app\node_modules\express\lib\router\layer.js:95:5)
at next (C:\Dev\isomorphic-react-redux-router-app\node_modules\express\lib\router\route.js:144:13)
at Route.dispatch (C:\Dev\isomorphic-react-redux-router-app\node_modules\express\lib\router\route.js:114:3)
at Layer.handle [as handle_request] (C:\Dev\isomorphic-react-redux-router-app\node_modules\express\lib\router\layer.js:95:5)
at C:\Dev\isomorphic-react-redux-router-app\node_modules\express\lib\router\index.js:284:15
at Function.process_params (C:\Dev\isomorphic-react-redux-router-app\node_modules\express\lib\router\index.js:346:12)
I think it is because of the url on the eval argument:
webpack://isomorphic-react-redux-router-app/./app/serverside/server.js?:17:19
Having a relative path in the middle of the url is not good.
Here are my webpack file:
const path = require('path');
require("core-js/stable");
require("regenerator-runtime/runtime");
const nodeExternals = require('webpack-node-externals');
module.exports = [
{
mode: 'development',
stats: {warnings:false},
target:'web',
entry: {
'/bundle-app': ['core-js','regenerator-runtime/runtime','./app/clientside/client.jsx']
},
output: {
path: path.resolve(__dirname, 'build/dev'),
filename: '[name].js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.jpe?g|png$/,
exclude: /node_modules/,
use: ["url-loader", "file-loader"]
},
{
test: /\.(jsx|js)$/,
include: path.resolve(__dirname, '/'),
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
}]
}
]
},
},
{
name: 'server',
mode: 'development',
target: 'node',
stats: {warnings:false},
externals: [nodeExternals()],
entry: {
'/server/bundle-server': ['core-js','regenerator-runtime/runtime','./app/serverside/server.js'],
},
output: {
path: path.resolve(__dirname, 'build/dev'),
filename: '[name].js',
},
plugins: [],
module: {
rules: [
{
test: /\.(jsx|js)$/,
include: path.resolve(__dirname, '/'),
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
}]
},
]
}
}
]
my express server file:
import express from 'express';
import serverRenderer from './renderSSR.js';
import cors from 'cors';
let app = express();
app.use(cors());
app.use(express.urlencoded({extended:false}));
app.use(express.json());
app.get('/', (req, res) => {
res.status(200).send(serverRenderer());
}); // when the user connect to the root of the server we send the page
app.listen(8080, () => console.log("Example app listening on port 8080!"));
import { renderToString } from 'react-dom/server';
import fs from 'fs';
import App from '../src/App.jsx';
export default function serverRenderer() {
return (req, res, next) => {
const html = renderToString( // on rend côté serveur du pur HTML
<StaticRouter location={req.url}>
<App/>
</StaticRouter>
);
// we read the index.html page and we change the div to insert the app content
fs.readFile('../html/index.html', 'utf8', function (err,data) {
if (err) {
return console.log(err);
}
// after succefuly read the html file, we insert the App component content
var htmlApp = data.replace('<div id="app">','<div id="app">' + html);
return htmlApp;
});
};
}
my client file:
// principal programme côté client, est capable de faire du rendue HTML, sera appelé une deuxième par le client.
import * as React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter as Router} from 'react-router-dom';
import App from '../src/App.jsx';
ReactDOM.render(
<Router>
<App/>
</Router>,
document.getElementById('root')
); // on rend le react dans l'element HTML root
and finally my commun App file:
import React from 'react';
import ReactDOM from 'react-dom';
export default function App(props)
{
return (
<p>Hello App!</p>
)
};
Where does the problem come from?
How to fix it?
Thanks in advance for your responses.
The answer from Heiko TheiBen solve my problem.
Thanks for your attention!
I am trying to enable webpack HMR in my express app. It's NOT an SPA app. For the view side, I am using EJS and Vue. I don't have the advantage of vue-cli here so I have to manually configure the vue-loader for the SFCs(.vue files) in the webpack. Also worth mentioning, my workflow is very typical: I have my main client-side resources (scss, js, vue etc) in resources dir. and I wish to bundle them inside my public dir.
My webpack.config.js:
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const webpack = require('webpack');
module.exports = {
mode: 'development',
entry: [
'./resources/css/app.scss',
'./resources/js/app.js',
'webpack-hot-middleware/client'
],
output: {
path: path.resolve(__dirname, 'public/js'),
publicPath: '/',
filename: 'app.js',
hotUpdateChunkFilename: "../.hot/[id].[hash].hot-update.js",
hotUpdateMainFilename: "../.hot/[hash].hot-update.json"
},
module: {
rules: [
{
test: /\.(sa|sc|c)ss$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: process.env.NODE_ENV === 'development'
}
},
'css-loader',
'sass-loader'
],
},
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: '../css/app.css'
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
]
};
My app/index.js file:
import express from 'express';
import routes from './routes';
import path from 'path';
import webpack from 'webpack';
import devMiddleware from 'webpack-dev-middleware';
import hotMiddleware from 'webpack-hot-middleware';
const config = require('../webpack.config');
const compiler = webpack(config);
const app = express();
app.use(express.static('public'));
app.use(devMiddleware(compiler, {
noInfo: true,
publicPath: config.output.publicPath
}));
app.use(hotMiddleware(compiler));
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../resources/views'))
routes(app);
app.listen(4000);
export default app;
The scripts section of my package.json file:
"scripts": {
"start": "nodemon app --exec babel-node -e js",
"watch": "./node_modules/.bin/webpack --mode=development --watch",
"build": "./node_modules/.bin/webpack --mode=production"
}
I am using nodemon to restart server to pick up the changes of server-side code. In one tab I keep npm run start open and in other tab npm run watch.
In my console, I see that HMR connected:
It only picks up the change first time only, and throws some warning like this:
Ignored an update to unaccepted module ./resources/css/app.scss -> 0
And doesn't pick up the subsequent changes. How can I fix this?
Reproduction Repo:
https://bitbucket.org/tanmayd/express-test
Since it's not a SPA and you want to use EJS that will require server side rendering. It's not that easy in your case, first you will need to overwrite the render method and after that you need to add those files generated by webpack.
Based on your repo from your description, https://bitbucket.org/tanmayd/express-test, you were on the right track, but you combined development and production settings in your webpack config.
Since I can't push on your repo, I will list below the files that suffered changes or those that are new.
1. Scripts & Packages
"scripts": {
"start": "cross-env NODE_ENV=development nodemon app --exec babel-node -e js",
"watch": "./node_modules/.bin/webpack --mode=development --watch",
"build": "cross-env NODE_ENV=production ./node_modules/.bin/webpack --mode=production",
"dev": "concurrently --kill-others \"npm run watch\" \"npm run start\"",
"production": "cross-env NODE_ENV=production babel-node ./app/server.js"
},
I installed cross-env(because i'm on windows), cheerio(a nodejs jquery kind of version --- it's not that bad), style-loader (which is a must in development while using webpack).
The scripts:
start - start the development server
build - generate production files
production - start the server using the files generated from "build"
2. webpack.config.js - changed
style-loader was added in the mix so webpack will deliver your css from the bundle (see ./resources/js/app.js - line 1). MiniCssExtractPlugin is meant to be used when you want to extract the styles to a separate file, that is in production.
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const webpack = require('webpack');
// Plugins
let webpackPlugins = [
new VueLoaderPlugin(),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
];
// Entry points
let webpackEntryPoints = [
'./resources/js/app.js',
];
if (process.env.NODE_ENV === 'production') {
webpackPlugins = [
new VueLoaderPlugin()
];
// MiniCssExtractPlugin should be used in production
webpackPlugins.push(
new MiniCssExtractPlugin({
filename: '../css/app.css',
allChunks: true
})
)
}else{
// Development
webpackEntryPoints.push('./resources/css/app.scss');
webpackEntryPoints.push('webpack-hot-middleware/client');
}
module.exports = {
mode: process.env.NODE_ENV === 'development' ? 'development' : 'production',
entry: webpackEntryPoints,
devServer: {
hot: true
},
output: {
path: path.resolve(__dirname, 'public/js'),
filename: 'app.js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.(sa|sc|c)ss$/,
use: [
// use style-loader in development
(process.env.NODE_ENV === 'development' ? 'style-loader' : MiniCssExtractPlugin.loader),
'css-loader',
'sass-loader',
],
},
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: webpackPlugins
};
3. ./resources/js/app.js - changed
The styles are now added on the first line import "../css/app.scss";
4. ./app/middlewares.js - new
Here you will find 2 middlewares, overwriteRenderer and webpackAssets.
overwriteRenderer, must be the first middleware before your routes, it's used in both development and production, in development it will suppress the ending of the request after render and will populate the response(res.body) with the rendered string of your file. In production your views will act as layouts, therefore the files that were generated will be added in head(link) and body(script).
webpackAssets will be used only in development, must be the last middleware, this will add to the res.body the files generated in memory by webpack(app.css & app.js). It's a custom version of the example found here webpack-dev-server-ssr
const cheerio = require('cheerio');
let startupID = new Date().getTime();
exports.overwriteRenderer = function (req, res, next) {
var originalRender = res.render;
res.render = function (view, options, fn) {
originalRender.call(this, view, options, function (err, str) {
if (err) return fn(err, null); // Return the original callback passed on error
if (process.env.NODE_ENV === 'development') {
// Force webpack in insert scripts/styles only on text/html
// Prevent webpack injection on XHR requests
// You can tweak this as you see fit
if (!req.xhr) {
// We need to set this header now because we don't use the original "fn" from above which was setting the headers for us.
res.setHeader('Content-Type', 'text/html');
}
res.body = str; // save the rendered string into res.body, this will be used later to inject the scripts/styles from webpack
next();
} else {
const $ = cheerio.load(str.toString());
if (!req.xhr) {
const baseUrl = req.protocol + '://' + req.headers['host'] + "/";
// We need to set this header now because we don't use the original "fn" from above which was setting the headers for us.
res.setHeader('Content-Type', 'text/html');
$("head").append(`<link rel="stylesheet" href="${baseUrl}css/app.css?${startupID}" />`)
$("body").append(`<script type="text/javascript" src="${baseUrl}js/app.js?${startupID}"></script>`)
}
res.send($.html());
}
});
};
next();
};
exports.webpackAssets = function (req, res) {
let body = (res.body || '').toString();
let h = res.getHeaders();
/**
* Inject scripts only when Content-Type is text/html
*/
if (
body.trim().length &&
h['content-type'] === 'text/html'
) {
const webpackJson = typeof res.locals.webpackStats.toJson().assetsByChunkName === "undefined" ?
res.locals.webpackStats.toJson().children :
[res.locals.webpackStats.toJson()];
webpackJson.forEach(item => {
const assetsByChunkName = item.assetsByChunkName;
const baseUrl = req.protocol + '://' + req.headers['host'] + "/";
const $ = require('cheerio').load(body.toString());
Object.values(assetsByChunkName).forEach(chunk => {
if (typeof chunk === 'string') {
chunk = [chunk];
}
if (typeof chunk === 'object' && chunk.length) {
chunk.forEach(item => {
console.log('File generated by webpack ->', item);
if (item.endsWith('js')) {
$("body").append(`<script type="text/javascript" src="${baseUrl}${item}"></script>`)
}
});
}
body = $.html();
});
});
}
res.end(body.toString());
}
5. ./app/index.js - changed
This file is meant for development. Here i added the middlewares from 4 and added the serverSideRender: true option to devMiddleware so webpack will serve us those assets that are used in 4
import express from 'express';
import routes from './routes';
import path from 'path';
import devMiddleware from 'webpack-dev-middleware';
import hotMiddleware from 'webpack-hot-middleware';
import webpack from 'webpack';
const {webpackAssets, overwriteRenderer} = require('./middlewares');
const config = require('../webpack.config');
const compiler = webpack(config);
const app = express();
app.use(express.static('public'));
app.use(devMiddleware(compiler, {
publicPath: config.output.publicPath,
serverSideRender: true // enable serverSideRender, https://github.com/webpack/webpack-dev-middleware
}));
app.use(hotMiddleware(compiler));
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../resources/views'));
// This new renderer must be loaded before your routes.
app.use(overwriteRenderer); // Local render
routes(app);
// This is a custom version for server-side rendering from here https://github.com/webpack/webpack-dev-middleware
app.use(webpackAssets);
app.listen(4000, '0.0.0.0', function () {
console.log(`Server up on port ${this.address().port}`)
console.log(`Environment: ${process.env.NODE_ENV}`);
});
export default app;
6. ./app/server.js - new
This is the production version. It's mostly a cleanup version of 5, all development tools were removed, and only overwriteRenderer remained.
import express from 'express';
import routes from './routes';
import path from 'path';
const {overwriteRenderer} = require('./middlewares');
const app = express();
app.use(express.static('public'));
app.use(overwriteRenderer); // Live render
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../resources/views'));
routes(app);
app.listen(5000, '0.0.0.0', function() {
if( process.env.NODE_ENV === 'development'){
console.error(`Incorrect environment, "production" expected`);
}
console.log(`Server up on port ${this.address().port}`);
console.log(`Environment: ${process.env.NODE_ENV}`);
});
I had faced similar issue some time ago and was able to solve by combination of xdotool and exec in node. It might help you as well.
Here is the summary:
Have a bash script to reload the browser. The script uses xdotool to get the Chrome window and reload (Script can be used for firefox and other browser also).
Relevant SO question:
How to reload Google Chrome tab from terminal?
In the main file (app/index.js), using exec, run the script (inside app.listen callback). When any changes are made, nodemon will reload causing the script to execute and reloading the browser.
Bash script: reload.sh
BID=$(xdotool search --onlyvisible --class Chrome)
xdotool windowfocus $BID key ctrl+r
app/index.js
...
const exec = require('child_process').exec;
app.listen(4000, () => {
exec('sh script/reload.sh',
(error, stdout, stderr) => {
console.log(stdout);
console.log(stderr);
if (error !== null) {
console.log(`exec error: ${error}`);
}
}
);
});
export default app;
Hope it helps. Revert for any doubts.
Actually, your reproduction has some issues on the declaration which they're not related to your current issue but please observe them:
Do not push the build files to the git server, just send source files.
Set a cleaner on the webpack to clean the public folder on the production build.
Rename the folders and files to a name that just exactly they do.
Install nodemon on your project in the dev dependencies.
And your problem, I changed many things on your reproduction structure and if you have no time to read this answer post just see this repo and get what you want.
Change the app/index.js to the below:
import express from 'express';
import routes from './routes';
import hotServerMiddleware from 'webpack-hot-server-middleware';
import devMiddleware from 'webpack-dev-middleware';
import hotMiddleware from 'webpack-hot-middleware';
import webpack from 'webpack';
const config = require('../webpack.config');
const compiler = webpack(config);
const app = express();
app.use(devMiddleware(compiler, {
watchOptions: {
poll: 100,
ignored: /node_modules/,
},
headers: { 'Access-Control-Allow-Origin': '*' },
hot: true,
quiet: true,
noInfo: true,
writeToDisk: true,
stats: 'minimal',
serverSideRender: true,
publicPath: '/public/'
}));
app.use(hotMiddleware(compiler.compilers.find(compiler => compiler.name === 'client')));
app.use(hotServerMiddleware(compiler));
const PORT = process.env.PORT || 4000;
routes(app);
app.listen(PORT, error => {
if (error) {
return console.error(error);
} else {
console.log(`Development Express server running at http://localhost:${PORT}`);
}
});
export default app;
Install webpack-hot-server-middleware, nodemon and vue-server-renderer in the project and change the start script to have package.json like below:
{
"name": "express-test",
"version": "1.0.0",
"main": "index.js",
"author": "Tanmay Mishu (tanmaymishu#gmail.com)",
"license": "MIT",
"scripts": {
"start": "NODE_ENV=development nodemon app --exec babel-node -e ./app/index.js",
"watch": "./node_modules/.bin/webpack --mode=development --watch",
"build": "./node_modules/.bin/webpack --mode=production",
"dev": "concurrently --kill-others \"npm run watch\" \"npm run start\""
},
"dependencies": {
"body-parser": "^1.19.0",
"csurf": "^1.11.0",
"dotenv": "^8.2.0",
"ejs": "^3.0.1",
"errorhandler": "^1.5.1",
"express": "^4.17.1",
"express-validator": "^6.3.1",
"global": "^4.4.0",
"mongodb": "^3.5.2",
"mongoose": "^5.8.10",
"multer": "^1.4.2",
"node-sass-middleware": "^0.11.0",
"nodemon": "^2.0.2",
"vue": "^2.6.11",
"vue-server-renderer": "^2.6.11"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-stage-0": "^6.24.1",
"concurrently": "^5.1.0",
"css-loader": "^3.4.2",
"mini-css-extract-plugin": "^0.9.0",
"node-sass": "^4.13.1",
"nodemon": "^2.0.2",
"sass-loader": "^8.0.2",
"vue-loader": "^15.8.3",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.11",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-dev-middleware": "^3.7.2",
"webpack-hot-middleware": "^2.25.0",
"webpack-hot-server-middleware": "^0.6.0"
}
}
Change the entire your webpack configuration file to below:
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const webpack = require('webpack');
module.exports = [
{
name: 'client',
target: 'web',
mode: 'development',
entry: [
'webpack-hot-middleware/client?reload=true',
'./resources/js/app.js',
],
devServer: {
hot: true
},
output: {
path: path.resolve(__dirname, 'public'),
filename: 'client.js',
publicPath: '/',
},
module: {
rules: [
{
test: /\.(sa|sc|c)ss$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: process.env.NODE_ENV === 'development'
}
},
'css-loader',
'sass-loader'
],
},
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: 'app.css'
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
]
},
{
name: 'server',
target: 'node',
mode: 'development',
entry: [
'./resources/js/appServer.js',
],
devServer: {
hot: true
},
output: {
path: path.resolve(__dirname, 'public'),
filename: 'server.js',
publicPath: '/',
libraryTarget: 'commonjs2',
},
module: {
rules: [
{
test: /\.(sa|sc|c)ss$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: process.env.NODE_ENV === 'development'
}
},
'css-loader',
'sass-loader'
],
},
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: 'app.css'
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
]
}
];
Add a file that name is htmlRenderer.js inside the resources folder:
export default html => `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Tanmay Mishu</title>
<link rel="stylesheet" href="/app.css">
</head>
<body>
<div id="app">${html}</div>
<script src="/client.js"></script>
</body>
</html>`;
Add a new file that name is appServer.js and its codes should be like following:
import Vue from 'vue';
import App from './components/App.vue';
import htmlRenderer from "../htmlRenderer";
const renderer = require('vue-server-renderer').createRenderer()
export default function serverRenderer({clientStats, serverStats}) {
Vue.config.devtools = true;
return (req, res, next) => {
const app = new Vue({
render: h => h(App),
});
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(htmlRenderer(html))
})
};
}
Now, just run yarn start and enjoy server-side rendering alongside hot reload.
I have set up an express application using typescript classes and have run into a strange issue. All tests have been passing, and today when I went to update some of the routes, my tests no longer run. When I run my test script, I get this error message back in the console:
$ mocha -c --reporter spec --compilers ts:ts-node/register ./test/*.test.ts --
timeout 20000
/Users/christiantodd/Development/projects/bby-react-api/src/index.ts:8
const app: Api = new Api();
^
TypeError: Api_1.default is not a constructor
at Object.<anonymous> (/Users/christiantodd/Development/projects/bby-react-api/src/index.ts:8:18)
at Module._compile (module.js:635:30)
at Module.m._compile (/Users/christiantodd/Development/projects/bby-react-api/node_modules/ts-node/src/index.ts:392:23)
at Module._extensions..js (module.js:646:10)
at Object.require.extensions.(anonymous function) [as .ts] (/Users/christiantodd/Development/projects/bby-react-api/node_modules/ts-node/src/index.ts:395:12)
My Api.ts file looks as follows:
import * as bodyParser from 'body-parser';
import * as express from 'express';
import * as expressValidator from 'express-validator';
import * as helmet from 'helmet';
import * as morgan from 'morgan';
import * as passport from 'passport';
import * as compression from 'compression';
/* import all routers */
import BestBuyRouter from './routes/BestBuyRouter';
import UserRouter from './routes/UserRouter';
export default class Api {
/* reference to the express instance */
public express: express.Application;
/* create the express instance and attach app level middleware and routes */
constructor() {
this.express = express();
this.middleware();
this.routes();
}
/* get current environment */
public currentEnv(): string {
return this.express.get('env');
}
/* apply middleware */
private middleware(): void {
this.express.use((req, res, next) => {
/* Don't allow caching. Needed for IE support :/ */
res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
res.header('Pragma', 'no-cache');
res.header('Access-Control-Allow-Origin', '*');
res.header(
'Access-Control-Allow-Methods',
'PUT, GET, POST, DELETE, OPTIONS'
);
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, Authorization, Access-Control-Allow-Credentials'
);
res.header('Access-Control-Allow-Credentials', 'true');
next();
});
this.express.use(compression());
this.express.use(helmet());
this.express.use(morgan('dev'));
this.express.use(bodyParser.json());
this.express.use(bodyParser.urlencoded({ extended: false }));
this.express.use(passport.initialize());
this.express.use(expressValidator());
this.express.use((err, req, res, next) => {
console.error(err);
res.status(err.status || 500).json({
message: err.message,
error: err
});
});
}
/* connect resource routers */
private routes(): void {
/* create an instance of the each of our routers */
const userRouter = new UserRouter();
const bestBuyRouter = new BestBuyRouter();
/* attach all routers to our express app */
this.express.use(userRouter.path, userRouter.router);
this.express.use(bestBuyRouter.path, bestBuyRouter.router);
}
}
and my index.ts:
import Api from './Api';
require('dotenv').config();
const mongoose = require('mongoose');
/* Set mongoose promise to native ES6 promise */
mongoose.Promise = global.Promise;
/* Instantiate our app instance */
const app: Api = new Api();
const connectOptions = {
useMongoClient: true,
keepAlive: true,
reconnectTries: Number.MAX_VALUE
};
/* Get current environment */
export const ENV = app.currentEnv();
let DATABASE_URL;
let PORT;
/* set environment variables */
if (ENV === 'production') {
DATABASE_URL = process.env.MONGODB_URI;
PORT = parseInt(process.env.PORT, 10);
} else {
DATABASE_URL = process.env.TEST_DATABASE_URL;
PORT = 3000;
}
let server;
export const runServer = async (
dbURL: string = DATABASE_URL,
port: number = PORT
) => {
try {
await mongoose.connect(dbURL, connectOptions);
await new Promise((resolve, reject) => {
server = app.express
.listen(port, () => {
console.info(`The ${ENV} server is listening on port ${port} 🤔`);
resolve();
})
.on('error', err => {
mongoose.disconnect();
reject(err);
});
});
} catch (err) {
console.error(err);
}
};
export const closeServer = async () => {
try {
await mongoose.disconnect();
await new Promise((resolve, reject) => {
console.info(`Closing server. Goodbye old friend.`);
server.close(err => (err ? reject(err) : resolve()));
});
} catch (err) {
console.error(err);
}
};
require.main === module && runServer().catch(err => console.error(err));
Lastly, my tsconfig.json
{
"compilerOptions": {
"lib": ["dom", "es7"],
"allowJs": true,
"watch": true,
"noImplicitAny": false,
"removeComments": true,
"sourceMap": false,
"target": "es6",
"module": "commonjs",
"outDir": "./lib",
"types": [
"body-parser",
"mongodb",
"mongoose",
"passport",
"node",
"nodemailer",
"mocha",
"chai",
"express",
"express-validator",
"chai-http"
],
"typeRoots": ["./node_modules/#types"]
},
"compileOnSave": true,
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "**/*.test.ts"]
}
To me it's really strange to get this behavior all of the sudden when this config has worked for me just fine in the past. I can still start my server just fine, but for some reason ts-node doesn't want to compile my *test.ts files for mocha to run my tests. Any idea what this could be?
Debugging "is not a ..." errors
So this is likely NOT going to be the answer but this question was the top result for the error I was debugging and here is a debugging tip.
I was running mocha with the following:
test/mocha.opts
--require ts-node/register
--require source-map-support/register
--watch-extensions ts
But no matter how I imported ./app I could not get classes or functions to work despite tsc compiling fine and node and mocha working fine on the compiled .js files.
import * as app from './app'
console.log({app}); // Pretty print object
As is would happen I had a Heroku project with app.json and app.ts.
The combination of mocha and ts-node were loading the .json file extension as a higher priority instead of the .ts file and Typescript won't allow me to specify a file extension. So this behaviour is different in tsc vs mocha + ts-node.
Bonus Points - Typescript Code Coverage
Unit tests: nyc mocha src/**/*-test.ts
Integration tests: nyc mocha test/**/*.ts
package.json
{
"nyc": {
"extension": [
".ts"
],
"include": [
"src/**/*.ts"
],
"exclude": [
"src/**/*-test.ts",
"test/**/*.ts"
],
"require": [
"ts-node/register"
],
"reporter": [
"text-summary"
],
"sourceMap": true,
"instrument": true,
"all": true
}
}
tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"target":"es2017",
"esModuleInterop":true,
// "strict": true,
"sourceMap": true,
"outDir": "dist",
"baseUrl": ".",
"types":["node"],
"rootDirs":[
"src"
]
},
"include": [
"src/**/*",
"test/**/*"
],
"exclude":[
"**/node_modules/**"
]
}
I am teaching myself to use Nodejs with Redux and Express. I have a tag in my view, and that's it. Very, very basic. I am getting an error in my console that I don't understand:
Uncaught ReferenceError: process is not defined
at Object.<anonymous> (bundle.js:548)
at __webpack_require__ (bundle.js:20)
at Object.defineProperty.value (bundle.js:492)
at __webpack_require__ (bundle.js:20)
at Object.<anonymous> (bundle.js:482)
at __webpack_require__ (bundle.js:20)
at __webpack_exports__.b (bundle.js:63)
at bundle.js:66
Can someone help me figure out what this means?
Here is my server.js file:
"use strict"
var express = require('express');
var app = express();
var path = require('path');
// MIDDLEWARE TO DEFINE FOLDER FOR STATIC FILES
app.use(express.static('public'))
app.get('/', function(req, res){
res.sendFile(path.resolve(__dirname, 'public', 'index.html'))
})
app.listen(3000, function(){
console.log('app is listening');
})
Here is my app.js file:
"use strict"
import {createStore} from 'redux'
// STEP 3 define reducers
const reducer = function(state=0, action){
switch(action.type){
case "INCREMENT":
return state + action.payload;
break;
}
return state
}
// STEP 1 create the store
const store = createStore(reducer);
store.subscribe(function(){
console.log('current state is: ' + store.getState());
})
// STEP 2 create and dispatch actions
store.dispatch({type: "INCREMENT", payload: 1 })
Here is my webpack.config.js file:
var path = require('path');
const webpack = require('webpack');
module.exports = {
entry: ['./src/app.js'],
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'public')
},
target: 'node',
watch: true,
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['react', 'es2015', 'stage-1']
}
}
]
}
}
Try adding this code to the plugin portion of your webpack.config
new webpack.DefinePlugin({
"process.env": {
NODE_ENV: JSON.stringify("production") //or "development"
}
});
You can read about why this might be your issue below. Basically you need to give webpack a NODE_ENV variable.
https://github.com/facebook/react/blob/f7850dd3d78d313a9e7774870e85c32719fbe233/docs/docs/getting-started.md