I'm new to developing with React/Typescript, Express and Webpack so pardon me if this question was already answered.
I created a React Client App using Webpack following all three parts of this tutorial and a separate Express Api/Server to send JSON data to my components. In development, the client runs on localhost:3001 using WebpackDevServer and the express server runs on localhost:3000. For production, I wanted them to run on the same port so I tried to follow this blog post to serve the static files built for production on the express server.
When I tried to run my production script, thats when I got the error "Cannot use import statement outside a module". I tried the answer for this stack overflow question and added "type": "module" to my package.json but that just made the "require()" in my webpack config files unknown. I'm not sure if my issue is from my webpack config files or if its from my package.json scripts or something else but I'd appreciate some help on getting a working production build. I've added a minimal version of my code below. Thank you.
My Project structure
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Surfer Visualization</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
src/client/components/header.tsx
import * as React from 'react'
import Navbar from 'react-bootstrap/Navbar';
import Nav from 'react-bootstrap/Nav';
import {LinkContainer} from 'react-router-bootstrap';
class Header extends React.Component {
render() {
return (
<Navbar bg="dark" variant="dark" fixed="top">
<LinkContainer to="/">
<Navbar.Brand>Test UI</Navbar.Brand>
</LinkContainer>
<Nav className="mr-auto">
<LinkContainer to="/">
<Nav.Link>Maps</Nav.Link>
</LinkContainer>
</Nav>
</Navbar>
);
}
}
export default Header;
src/client/components/maps.tsx
import * as React from 'react';
import Jumbotron from 'react-bootstrap/Jumbotron';
import Container from 'react-bootstrap/Container';
import Accordion from 'react-bootstrap/Accordion';
import Card from 'react-bootstrap/Card';
import ListGroup from 'react-bootstrap/ListGroup';
import axios from 'axios';
import "../styles/default_layout.css";
type MyAccord = {
first_name: string,
last_name: string,
email: string,
city: string
}
function tagToClass(tag: string) {
let tagClass = 'accord-tag-';
tagClass += tag;
return tagClass;
}
function Accord(props: MyAccord) {
return (
<Accordion className={tagToClass(props.last_name)}>
<Card>
<Accordion.Toggle as={Card.Header} eventKey="0">
Surfer: {props.first_name}
</Accordion.Toggle>
<Accordion.Collapse eventKey="0">
<Card.Body>
<ListGroup variant="flush">
<ListGroup.Item>Surfer Name: {props.first_name} {props.last_name}</ListGroup.Item>
<ListGroup.Item>Surfer Email: {props.email}</ListGroup.Item>
<ListGroup.Item>Surfer City: {props.city}</ListGroup.Item>
</ListGroup>
</Card.Body>
</Accordion.Collapse>
</Card>
</Accordion>
)
}
const accordCreate = (item: MyAccord) => {
return <Accord
key={item.first_name}
first_name={item.first_name}
last_name={item.last_name}
email={item.email}
city={item.city} />;
}
class Maps extends React.Component<any, any> {
constructor(props: any) {
super(props);
this.state = {
isLoading: true,
maps: []
}
}
componentDidMount() {
fetch('/api/maps')
.then( response => response.json())
.then(data => {
this.setState({
isLoading: false,
maps: data.surfers
});
});
}
render() {
return (
<div className="default-page">
<Container className="p-3">
<div className="default-jumbotron">
<Jumbotron>
<h1 className="imaps-jumbo-title">Test Surfers</h1>
</Jumbotron>
</div>
<div className="imaps-section">
<section>
<h2 className="default-title-accord">Current Surfers</h2>
<div className="imaps-accord">
{
this.state.isLoading ? 'loading...' : (
<div>
{this.state.maps.map(accordCreate)}
</div>
)
}
</div>
</section>
</div>
</Container>
</div>
);
}
}
export default Maps;
src/client/styles/default_layout.css
.default-page {
margin: 75px 20px 20px 20px;
padding: 10px;
}
.default-jumbotron {
text-align: center;
}
.default-title-accord {
text-align: center;
}
src/client/app.tsx
import * as React from 'react';
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Header from './components/header';
import Maps from './components/maps';
class App extends React.Component {
render() {
return (
<Router>
<Header />
<Switch>
<Route exact path="/" component={Maps}/>
</Switch>
</Router>
);
}
}
export default App;
src/client/index.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './app';
import 'bootstrap/dist/css/bootstrap.min.css';
ReactDOM.render(
<App />,
document.getElementById('root')
);
src/server/index.ts
import express from "express";
import fs from "fs";
import path from "path";
import cors from "cors";
let corsOptions: object = {
origin: 'http://localhost:3001'
}
const server = express();
server.use(cors(corsOptions));
server.use("/", express.static(path.join(__dirname, "../../build")));
const filePath = path.join(__dirname, "testData.json");
server.get("/api/maps", (req, res) => {
fs.readFile(filePath, 'utf-8', function(error, content) {
var data = JSON.parse(content);
res.send(data);
});
});
server.listen(3000, () => {
console.log(`Server running on http://localhost:3000`);
});
src/server/testData.json
{
"surfers": [
{
"first_name": "Adel",
"last_name": "Tease",
"email": "atease0@youtu.be",
"city": "Chengxiang"
},
{
"first_name": "Griff",
"last_name": "Kelley",
"email": "gkelley1@seesaa.net",
"city": "Xiaba"
},
{
"first_name": "Arne",
"last_name": "Rolstone",
"email": "arolstone2@histats.com",
"city": "Muhoroni"
},
{
"first_name": "Gale",
"last_name": "Chatten",
"email": "gchatten3@cloudflare.com",
"city": "Wolofeo"
},
{
"first_name": "Alane",
"last_name": "Lent",
"email": "alent4@google.nl",
"city": "Al Ḩarajah"
},
{
"first_name": "Myrta",
"last_name": "Tongs",
"email": "mtongs5@toplist.cz",
"city": "El Guamo"
}
]
}
package.json
{
"name": "test-ui",
"version": "1.0.0",
"description": "Test UI",
"main": "src/server/index.ts",
"scripts": {
"dev:client": "webpack serve --config webpack.dev.js",
"dev:server": "tsnd --respawn --transpile-only src/server/index.ts ",
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
"prod:build": "webpack --config webpack.prod.js",
"prod:start": "npm run prod:build && node src/server/index.ts"
},
"devDependencies": {
"@babel/core": "^7.14.6",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.14.5",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/react": "^17.0.14",
"@types/react-dom": "^17.0.9",
"@types/react-router-bootstrap": "^0.24.5",
"@types/react-router-dom": "^5.1.8",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"concurrently": "^6.2.1",
"cors": "^2.8.5",
"css-loader": "^5.2.6",
"css-minimizer-webpack-plugin": "^3.0.2",
"html-webpack-plugin": "^5.3.2",
"mini-css-extract-plugin": "^2.1.0",
"terser-webpack-plugin": "^5.1.4",
"ts-node": "^10.2.1",
"ts-node-dev": "^1.1.8",
"typescript": "^4.3.5",
"webpack": "^5.43.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.8.0"
},
"dependencies": {
"bootstrap": "^4.6.0",
"react": "^17.0.2",
"react-bootstrap": "^1.6.1",
"react-dom": "^17.0.2",
"react-router-bootstrap": "^0.25.0",
"react-router-dom": "^5.2.0"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"jsx": "react",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
webpack.common.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const TerserJSPlugin = require("terser-webpack-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
entry: "./src/client/index.tsx",
output: {
path: __dirname + "/build",
publicPath: "/",
},
optimization: {
minimize: true,
minimizer: [new TerserJSPlugin({}), new CssMinimizerPlugin({})],
},
resolve: {
extensions: [".ts", ".tsx", ".jsx", ".js"],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash].css",
chunkFilename: "static/css/[id].[contenthash].chunk.css",
})
],
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
loader: require.resolve("babel-loader"),
exclude: /node_modules/,
// Options for the plugin
options: {
presets: [
require.resolve("@babel/preset-react"),
require.resolve("@babel/preset-typescript"),
],
},
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
}
webpack.prod.js
const {merge} = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
devtool: false,
output: {
filename: "static/js/[name].[contenthash].js",
chunkFilename: "static/js/[name].[contenthash].chunk.js",
}
});
webpack.dev.js
const {merge} = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
devtool: 'source-map',
output: {
filename: "static/js/bundle.js",
chunkFilename: "static/js/[name].chunk.js",
},
devServer: {
port: "3001",
open: true,
proxy: {
'/api': 'http://localhost:3000'
},
historyApiFallback: true,
}
});