I am requesting help setting up the compilation and dev environment for a typescript library. The library should work when consumed by a web app framework and when consumed by a script tag. I am currently using Webpack as a dev server so I can debug and TSC to build (cjs + esm). The issue that prompted this post was having to constantly switch my API strings between http://localhost:8080 to https://production.com. What tools or changes do I need in order to build dev and prod variables into my compilation?
Here is what I'm doing so far:
package.json fragment
"main": "./lib/cjs/index.js",
"module": "./lib/esm/index.js",
"files": [
"lib/**/*",
"README.md"
],
"scripts": {
"build:esm": "tsc -p tsconfig.json --outDir lib/esm --module ES2020 --sourceMap false",
"build:cjs": "tsc -p tsconfig.json --outdir lib/cjs --module commonjs --sourceMap false",
"clean:build": "rimraf lib",
"clean:serve": "rimraf dist",
"build": "rimraf lib && npm run build:esm && npm run build:cjs",
"serve": "rimraf dist && webpack-dev-server"
}
webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const SRC = path.resolve(__dirname, 'src')
const ENTRTY = path.resolve(__dirname, 'src', 'debug.ts')
const DIST = path.resolve(__dirname, 'dist')
module.exports = {
mode: 'development',
context: SRC,
entry: ENTRTY,
output: {
path: DIST,
filename: 'index.js',
},
devtool: 'source-map',
devServer: {
contentBase: DIST,
writeToDisk: true,
host: '0.0.0.0',
port: 8080,
https: true,
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin()
],
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
include: [SRC]
}
]
}
}
My toolchain does not currently allow me to do do this:
import Axios from 'axios'
import SocketIO from 'socket.io-client'
export const axios = Axios.create({
baseURL: process.env.SERVER_HTTP_URL, //<-- can't do env-vars with tsc build
withCredentials: true
})
typescript cant not do that, but gulp can do it
const replace = require('gulp-replace');
const { src, dest } = require('gulp');
exports.default = function() {
return src(['*.js'], {base: './'})
.pipe(replace('__XXXX__', 'some variables'))
.pipe(dest('./'));
}
I believe my webpack config may be the culprit or my npm run dev script, but I'm not sure what the issue is. When I run my app in dev mode I'm getting error messages like:
Uncaught TypeError: this.props.contacts.map is not a function
at ContactList.renderList (ContactList.jsx:46)
at ContactList.render (ContactList.jsx:61)
at finishClassComponent (react-dom.development.js:14695)
at updateClassComponent (react-dom.development.js:14650)
at beginWork (react-dom.development.js:15598)
at performUnitOfWork (react-dom.development.js:19266)
at workLoop (react-dom.development.js:19306)
at HTMLUnknownElement.callCallback (react-dom.development.js:149)
at Object.invokeGuardedCallbackDev (react-dom.development.js:199)
at invokeGuardedCallback (react-dom.development.js:256)
But my ContactList.jsx is only 36 lines long.
My webpack config:
module.exports = {
mode: 'development',
entry: './client/index.js',
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['#babel/preset-react']
}
}
}
]
},
resolve: {
extensions: ['*', '.js', '.jsx']
},
output: {
path: __dirname + '/dist',
publicPath: '/',
filename: 'bundle.js'
},
devServer: {
contentBase: './dist'
}
};
scripts:
"scripts": {
"start": "npx webpack --mode=development && node server/server.js",
"build": "webpack --mode=development",
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack-dev-server --config ./webpack.config.js --mode development"
}
One other important detail, I think, is that I'm serving my front-end app via an express server. It is that front-end application I'm trying to debug. Can I package it in a dev mode or something like this?
I'm serving an html and bundle.js file. The HTML just references the endpoint where I'm serving my react application.
index.html:
<!DOCTYPE html>
<html>
<head>
<title>The Minimal React Webpack Babel Setup</title>
</head>
<body>
<div id="app"></div>
<script src="./dist/bundle.js"></script>
</body>
</html>
My app is here:
https://github.com/int-a/contacts/tree/split-first-last-name
Try to add devtool in webpack config ,like 'devtool:"#source-map"'
Short Summary
Is combining htmlwebpackplugin functionality with webpack-dev-middleware impossible because of dev-middleware's reliance on files in memory? Screenshots of script outputs at bottom of this post. Because I've chosen to implement cache-hashed filenames in my production config, I can't seem to use dev-middleware anymore.
My Setup
I have 2 main configurations for my webpack instance. One for development (with hot reload) and one for production. I utilize webpack-merge to create a common.config that I'll eventually extract commonalities between the two configurations (for now it's fairly blank). In terms of app setup I have an API run separately in Python.
The problem
On my production config I'm using splitchunks for vendor/bundle splitting as well as some minimizations. It works perfectly fine. However, when I'm trying to run my development environment, although it's creating the the appropriate bundles for development [i.e. without the hashing] according to the terminal, the index.html file is unable to be found (likely because webpack-dev-middleware looks for things in memory). As a result, I can't see my development environment and I can't see any of the hot reload changes? Previously I would generate all my bundle files with npm run build:production and then use NPM start. I imagine dev-middleware would overlay it's in-memory version of the bundle.js changes over my file on disk, but now that I'm using hashed filenames on prod I can't really do that anymore?
Package.json scripts
"scripts": {
"clean": "rimraf dist/*.{js,css,eot,woff,woff2,svg,ttf,jpg,map,json}",
"build":
"webpack -p --progress --verbose --colors --display-error-details --config webpack/common.config.js",
"build:production": "npm run clean && npm run build",
"flow": "flow",
"lint": "eslint src",
"start": "nodemon bin/server.js",
The relevant parts of server.js
(function initWebpack() {
const webpack = require('webpack');
const webpackConfig = require('./webpack/common.config');
const compiler = webpack(webpackConfig);
app.use(
require('webpack-dev-middleware')(compiler, {
noInfo: true,
publicPath: webpackConfig.output.publicPath,
}),
);
app.use(
require('webpack-hot-middleware')(compiler, {
log: console.log,
path: '/__webpack_hmr',
heartbeat: 10 * 1000,
}),
);
app.use(express.static(path.join(__dirname, '/')));
})();
app.get(/.*/, (req, res) => {
res.sendFile(path.join(__dirname, '/dist/index.html'));
});
common.config.js
const path = require('path');
const merge = require('webpack-merge');
// const HtmlWebpackPlugin = require('html-webpack-plugin');
const development = require('./dev.config');
const production = require('./prod.config');
const TARGET = process.env.npm_lifecycle_event;
const PATHS = {
app: path.join(__dirname, '../src'),
build: path.join(__dirname, '../dist'),
nodeModulesDir: path.join(__dirname, 'node_modules'),
indexFile: path.join(__dirname, './src/index'),
};
process.env.BABEL_ENV = TARGET;
const common = {
entry: [PATHS.app],
output: {
path: PATHS.build,
},
};
if (TARGET === 'start' || !TARGET) {
module.exports = merge(development, common);
}
if (TARGET === 'build' || !TARGET) {
module.exports = merge(production, common);
}
dev.config.js
const webpack = require('webpack');
const path = require('path');
const fs = require('fs');
require('babel-polyfill').default;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const PATHS = {
app: path.join(__dirname, '../src'),
};
module.exports = {
devtool: 'cheap-module-eval-source-map',
entry: ['webpack-hot-middleware/client', './src/index'],
mode: 'development',
output: {
publicPath: '/dist/',
},
resolve: {
extensions: ['.jsx', '.js', '.json', '.scss', '.less'],
modules: ['node_modules', PATHS.app],
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.css$/,
exclude: /node_modules/,
use: [
{
loader: 'style-loader',
},
{
loader: 'css-loader',
},
'postcss-loader',
],
},
{
test: /\.(sa|sc|c)ss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
'sass-loader',
],
},
{
test: /\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
{
loader: 'less-loader',
options: {
//addlater
},
},
],
},
{
test: /bootstrap-sass\/assets\/javascripts\//,
use: [
{
loader: 'imports-loader',
options: {
jQuery: 'jquery',
},
},
],
},
{
test: require.resolve('jquery'),
use: [
{
loader: 'expose-loader',
options: '$',
},
{
loader: 'expose-loader',
options: 'jQuery',
},
],
},
{
test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 50000,
mimetype: 'application/font-woff',
},
},
],
},
],
},
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"development"',
},
__DEVELOPMENT__: true,
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.ProvidePlugin({
jQuery: 'jquery',
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index_template.html',
}),
],
};
Here's an example of what my index.html file looks like after using npm run build:production. As you can see, the in-memory version of index.html probably can't work with this anymore with the hashed filenames?
<link href="/dist/vendor.a85f.css" rel="stylesheet"><link href="/dist/main.4d1e.css" rel="stylesheet"></head>
<body>
<div id="root"></div>
<script type="text/javascript" src="/dist/manifest.81a7.js"></script><script type="text/javascript" src="/dist/vendor.99aa.js"></script><script type="text/javascript" src="/dist/main.6eb4.js"></script></body>
Other notes:
On latest version of webpack 4.
My production version works fine
Any help much appreciated.
UPDATE:
I've swapped out rimraf dist for clean webpack plugin and moved it to my common.config. That way on each build it's doing the clean before generating index.html. However, I've noticed that when I use npm start, while the output in terminal is showing that files are emitted....I can't find them anywhere? After investigated webpack-dev-middleware, it seems they store things in memory. This is probably the core problem. How can I tie htmlwebpack plugin together with something like dev-middleware if it's in memory or perhaps I need to maintain a separate index.html file? I'm guessing the reason why this flow worked previously was because I had static names for bundle.js for both prod and dev so the in-memory version had no problem. now that the names are hashed from the prod version...it doesn't know what to do?
I recently changed the name of my root directory (where package.json and webpack.config.js sits) and now webpack-dev-server is not updating anytime I change my files.
Here's my webpack config:
var debug = process.env.NODE_ENV !== "production";
var webpack = require('webpack');
var path = require('path');
module.exports = {
context: path.join(__dirname, "src"),
devtool: debug ? "inline-sourcemap" : null,
entry: "./js/init.js",
module: {
loaders: [
{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel-loader',
query: {
presets: ['react', 'es2015', 'stage-0'],
plugins: ['react-html-attrs', 'transform-class-properties', 'transform-decorators-legacy'],
}
}
]
},
output: {
path: __dirname + "/src/",
filename: "app.js"
},
plugins: debug ? [] : [
new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
],
devServer: {
port: 3000,
hot: true,
historyApiFallback: {
index: 'index.html'
}
}
};
And my directory looks like this (Client React is the folder that had its name changed):
Let me clarify that this worked fine before, so I really have no idea why this isn't working now.
Edit: Scripts in package.json
"scripts": {
"dev": "./node_modules/.bin/webpack-dev-server --content-base src --inline --hot",
"build": "webpack"
},
You have to remove the brackets. Probably because they are not properly escaped by the watch module that webpack uses (watchpack) or the part that does the final watching in the System itself. I recommend you don't use any special characters inside directory- or filenames because of such bugs.
So I am using webpack-dev-server and its live reload ability. I am on a windows machine. When I change a js file it seems to be reloading the browser but it doesn't rebuild the bundle. Here is my webpack config file
var webpack = require("webpack");
var path = require('path');
module.exports = {
entry: ['./app/thirdparty', "./app/app.js"],
output: {
filename: "./build/bundle.js",
publicPath: "/assets/"
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
module: {
loaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"
}]
},
resolve: {
extensions: ['', '.js', '.es6']
},
include: path.join(__dirname, 'app')
}
I've tried to run it with
webpack-dev-server
and
webpack-dev-server --hot
But the bundle is not rebuilt
So I solved my own question.
I had a file looking like :
output: {
filename: "./build/bundle.js",
publicPath: "/assets/"
}
I changed it to
output : {
path: path.resolve('build/js'),
publicPath: "/public/js/",
filename : "bundle.js",
}
Which means it will create a bundle.js that will end up in /build/js/bundle.js
BUT it needs to be referred to in index.html as public/js/bundle.js because of how the publicPath is specified. Also running
webpack-dev-server --inline
Made everything work. Its obvious once you understand web pack I guess...