Skip to content

Commit

Permalink
feat: Fix/improve hot reloading 🚀, support SCSS exports again, and ad…
Browse files Browse the repository at this point in the history
…d local alias overrides to webpack.dev-stage.config.js (#191)

* fix: support js imports of scss exports again

* feat: fix hot module replacement, add fast refresh webpack plugin

* fix: add local aliases to webpack.dev-stage.config.js
  • Loading branch information
adamstankiewicz authored Jul 19, 2021
1 parent c3187db commit 33cab17
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 74 deletions.
67 changes: 67 additions & 0 deletions config/getLocalAliases.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const path = require('path');
const fs = require('fs');

/*
This function reads in a 'module.config.js' file if it exists and uses its contents to define
a set of webpack resolve.alias aliases for doing local development of application dependencies.
It reads the package.json file of the dependency to determine if it has any peer dependencies, and
then forces those peer dependencies to be resolved with the application's version. Primarily, this
is useful for making sure there's only one version of those dependencies loaded at once, which is a
problem with both react and react-intl.
The module.config.js file should have the form:
{
localModules: [
{ moduleName: 'nameOfPackage', dir: '../path/to/repo', dist: '/path/to/dist/in/repo' },
... others...
],
}
Some working examples, as of the time of this writing:
{ moduleName: '@edx/paragon/scss', dir: '../paragon', dist: 'scss' }
{ moduleName: '@edx/paragon', dir: '../paragon', dist: 'dist' }
{ moduleName: '@edx/frontend-platform', dir: '../frontend-platform', dist: 'dist' }
*/
function getLocalAliases() {
const aliases = {};

try {
const moduleConfigPath = path.resolve(process.cwd(), 'module.config.js');
if (!fs.existsSync(moduleConfigPath)) {
console.log('No local module configuration file found. This is fine.');
return aliases;
}
// eslint-disable-next-line import/no-dynamic-require, global-require
const { localModules } = require(moduleConfigPath);

let allPeerDependencies = [];
const excludedPeerPackages = [];
if (localModules.length > 0) {
console.info('Resolving modules from local directories via module.config.js.');
}
localModules.forEach(({ moduleName, dir, dist = '' }) => {
console.info(`Using local version of ${moduleName} from ${dir}/${dist}.`);
// eslint-disable-next-line import/no-dynamic-require, global-require
const { peerDependencies = {}, name } = require(path.resolve(process.cwd(), dir, 'package.json'));
allPeerDependencies = allPeerDependencies.concat(Object.keys(peerDependencies));
aliases[moduleName] = path.resolve(process.cwd(), dir, dist);
excludedPeerPackages.push(name);
});

allPeerDependencies = allPeerDependencies.filter((dep) => !excludedPeerPackages.includes(dep));

allPeerDependencies.forEach((dep) => {
aliases[dep] = path.resolve(process.cwd(), 'node_modules', dep);
});
} catch (e) {
console.error(e);
console.error('Error in module.config.js parsing. module.config.js will be ignored.');
return {};
}
return aliases;
}

module.exports = getLocalAliases;
18 changes: 13 additions & 5 deletions config/webpack.dev-stage.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ const path = require('path');
const PostCssAutoprefixerPlugin = require('autoprefixer');
const PostCssRTLCSS = require('postcss-rtlcss');
const { HotModuleReplacementPlugin } = require('webpack');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

const commonConfig = require('./webpack.common.config.js');
const presets = require('../lib/presets');
const resolvePrivateEnvConfig = require('../lib/resolvePrivateEnvConfig');
const getLocalAliases = require('./getLocalAliases');

// Add process env vars. Currently used only for setting the
// server port and the publicPath
Expand All @@ -25,19 +27,18 @@ dotenv.config({
// in temporary modifications to .env.development.
resolvePrivateEnvConfig('.env.private');

const aliases = getLocalAliases();
const PUBLIC_PATH = process.env.PUBLIC_PATH || '/';

module.exports = merge(commonConfig, {
mode: 'development',
devtool: 'eval-source-map',
entry: {
// enable react's custom hot dev client so we get errors reported in the browser
hot: require.resolve('react-dev-utils/webpackHotDevClient'),
app: path.resolve(process.cwd(), 'src/index'),
},
output: {
publicPath: PUBLIC_PATH,
},
resolve: {
alias: aliases,
},
module: {
// Specify file-by-file rules to Webpack. Some file-types need a particular kind of loader.
rules: [
Expand All @@ -54,6 +55,9 @@ module.exports = merge(commonConfig, {
// from the cache to avoid needing to run the expensive recompilation process
// on each run.
cacheDirectory: true,
plugins: [
require.resolve('react-refresh/babel'),
],
},
},
},
Expand All @@ -68,6 +72,9 @@ module.exports = merge(commonConfig, {
loader: 'css-loader', // translates CSS into CommonJS
options: {
sourceMap: true,
modules: {
compileType: 'icss',
},
},
},
{
Expand Down Expand Up @@ -156,6 +163,7 @@ module.exports = merge(commonConfig, {
// the HotModuleReplacementPlugin has to be specified in the Webpack configuration
// https://webpack.js.org/configuration/dev-server/#devserver-hot
new HotModuleReplacementPlugin(),
new ReactRefreshWebpackPlugin(),
],
// This configures webpack-dev-server which serves bundles from memory and provides live
// reloading.
Expand Down
78 changes: 9 additions & 69 deletions config/webpack.dev.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
const { merge } = require('webpack-merge');
const Dotenv = require('dotenv-webpack');
const dotenv = require('dotenv');
const fs = require('fs');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
const PostCssAutoprefixerPlugin = require('autoprefixer');
const PostCssRTLCSS = require('postcss-rtlcss');
const { HotModuleReplacementPlugin } = require('webpack');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

const commonConfig = require('./webpack.common.config.js');
const presets = require('../lib/presets');
const resolvePrivateEnvConfig = require('../lib/resolvePrivateEnvConfig');
const getLocalAliases = require('./getLocalAliases');

// Add process env vars. Currently used only for setting the
// server port and the publicPath
Expand All @@ -26,80 +27,12 @@ dotenv.config({
// in temporary modifications to .env.development.
resolvePrivateEnvConfig('.env.private');

/*
This function reads in a 'module.config.js' file if it exists and uses its contents to define
a set of webpack resolve.alias aliases for doing local development of application dependencies.
It reads the package.json file of the dependency to determine if it has any peer dependencies, and
then forces those peer dependencies to be resolved with the application's version. Primarily, this
is useful for making sure there's only one version of those dependencies loaded at once, which is a
problem with both react and react-intl.
The module.config.js file should have the form:
{
localModules: [
{ moduleName: 'nameOfPackage', dir: '../path/to/repo', dist: '/path/to/dist/in/repo' },
... others...
],
}
Some working examples, as of the time of this writing:
{ moduleName: '@edx/paragon/scss', dir: '../paragon', dist: 'scss' }
{ moduleName: '@edx/paragon', dir: '../paragon', dist: 'dist' }
{ moduleName: '@edx/frontend-platform', dir: '../frontend-platform', dist: 'dist' }
*/
function getLocalAliases() {
const aliases = {};

try {
const moduleConfigPath = path.resolve(process.cwd(), 'module.config.js');
if (!fs.existsSync(moduleConfigPath)) {
console.log('No local module configuration file found. This is fine.');
return aliases;
}
// eslint-disable-next-line import/no-dynamic-require, global-require
const { localModules } = require(moduleConfigPath);

let allPeerDependencies = [];
const excludedPeerPackages = [];
if (localModules.length > 0) {
console.info('Resolving modules from local directories via module.config.js.');
}
localModules.forEach(({ moduleName, dir, dist = '' }) => {
console.info(`Using local version of ${moduleName} from ${dir}/${dist}.`);
// eslint-disable-next-line import/no-dynamic-require, global-require
const { peerDependencies = {}, name } = require(path.resolve(process.cwd(), dir, 'package.json'));
allPeerDependencies = allPeerDependencies.concat(Object.keys(peerDependencies));
aliases[moduleName] = path.resolve(process.cwd(), dir, dist);
excludedPeerPackages.push(name);
});

allPeerDependencies = allPeerDependencies.filter((dep) => !excludedPeerPackages.includes(dep));

allPeerDependencies.forEach((dep) => {
aliases[dep] = path.resolve(process.cwd(), 'node_modules', dep);
});
} catch (e) {
console.error(e);
console.error('Error in module.config.js parsing. module.config.js will be ignored.');
return {};
}
return aliases;
}

const aliases = getLocalAliases();
const PUBLIC_PATH = process.env.PUBLIC_PATH || '/';

module.exports = merge(commonConfig, {
mode: 'development',
devtool: 'eval-source-map',
entry: {
// enable react's custom hot dev client so we get errors reported in the browser
hot: require.resolve('react-dev-utils/webpackHotDevClient'),
app: path.resolve(process.cwd(), 'src/index'),
},
output: {
publicPath: PUBLIC_PATH,
},
Expand All @@ -122,6 +55,9 @@ module.exports = merge(commonConfig, {
// from the cache to avoid needing to run the expensive recompilation process
// on each run.
cacheDirectory: true,
plugins: [
require.resolve('react-refresh/babel'),
],
},
},
},
Expand All @@ -136,6 +72,9 @@ module.exports = merge(commonConfig, {
loader: 'css-loader', // translates CSS into CommonJS
options: {
sourceMap: true,
modules: {
compileType: 'icss',
},
},
},
{
Expand Down Expand Up @@ -224,6 +163,7 @@ module.exports = merge(commonConfig, {
// the HotModuleReplacementPlugin has to be specified in the Webpack configuration
// https://webpack.js.org/configuration/dev-server/#devserver-hot
new HotModuleReplacementPlugin(),
new ReactRefreshWebpackPlugin(),
],
// This configures webpack-dev-server which serves bundles from memory and provides live
// reloading.
Expand Down
3 changes: 3 additions & 0 deletions config/webpack.prod.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ module.exports = merge(commonConfig, {
loader: 'css-loader', // translates CSS into CommonJS
options: {
sourceMap: true,
modules: {
compileType: 'icss',
},
},
},
{
Expand Down
49 changes: 49 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@babel/preset-env": "7.10.4",
"@babel/preset-react": "7.10.4",
"@edx/eslint-config": "1.1.6",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.0-rc.2",
"@svgr/webpack": "5.5.0",
"autoprefixer": "10.2.6",
"babel-eslint": "10.1.0",
Expand Down Expand Up @@ -64,6 +65,7 @@
"postcss-loader": "6.1.1",
"postcss-rtlcss": "3.3.4",
"react-dev-utils": "11.0.4",
"react-refresh": "0.10.0",
"resolve-url-loader": "5.0.0-beta.1",
"sass": "1.26.11",
"sass-loader": "10.0.5",
Expand Down

0 comments on commit 33cab17

Please sign in to comment.