Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for HMR #5013

Merged
merged 11 commits into from
Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ group :development, :test do
gem 'rubocop-rspec', require: false
gem 'rubocop-performance', require: false
gem 'factory_bot_rails' # Factory for creating ActiveRecord objects in tests
gem 'rack-proxy', '~> 0.7.2'
end

group :test do
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,8 @@ GEM
rack (>= 1.2.0)
rack-protection (2.2.0)
rack
rack-proxy (0.7.2)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.1.4.7)
Expand Down Expand Up @@ -636,6 +638,7 @@ DEPENDENCIES
puma
rack-cors
rack-mini-profiler
rack-proxy (~> 0.7.2)
rails (= 6.1.4.7)
rails-controller-testing
rails-erd
Expand Down
22 changes: 22 additions & 0 deletions app/assets/javascripts/components/Main.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import routes from './util/routes.jsx';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './util/create_store';

const Main = () => {
return (
<Provider store={store} >
<BrowserRouter>
{routes}
</BrowserRouter>
</Provider>
);
};
export const render = (reactRoot) => {
ReactDOM.render(
<Main/>,
reactRoot
);
};
47 changes: 3 additions & 44 deletions app/assets/javascripts/components/app.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import routes from './util/routes.jsx';

import { Provider } from 'react-redux';
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import reducer from '../reducers';

import Nav from './nav/nav.jsx';
import { render } from './Main';

// The navbar is its own React element, independent of the
// main React Router-based component tree.
Expand All @@ -18,42 +11,8 @@ if (navBar) {
ReactDOM.render((<Nav/>), navBar);
}

// The main `react_root` is only present in some Rails views, corresponding
// to the routes above.
const reactRoot = document.getElementById('react_root');
if (reactRoot) {
// This is basic, minimal state info extracted from the HTML,
// used for initial rendering before React fetches all the specific
// data it needs via API calls.
const currentUserFromHtml = JSON.parse(reactRoot.getAttribute('data-current_user'));
const admins = JSON.parse(reactRoot.getAttribute('data-admins'));
const preloadedState = {
courseCreator: {
defaultCourseType: reactRoot.getAttribute('data-default-course-type'),
courseStringPrefix: reactRoot.getAttribute('data-course-string-prefix'),
courseCreationNotice: reactRoot.getAttribute('data-course-creation-notice'),
useStartAndEndTimes: reactRoot.getAttribute('data-use-start-and-end-times') === 'true'
},
currentUserFromHtml,
admins
};

// This is the Redux store.
// It is accessed from container components via `connect()`.
// Enable Redux DevTools browser extension.
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
preloadedState,
composeEnhancers(applyMiddleware(thunk))
);

// Render the main React app
ReactDOM.render((
<Provider store={store} >
<BrowserRouter>
{routes}
</BrowserRouter>
</Provider>
), reactRoot);
if (reactRoot) {
render(reactRoot);
}
27 changes: 4 additions & 23 deletions app/assets/javascripts/components/common/text_area_input.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import createReactClass from 'create-react-class';
import InputHOC from '../high_order/input_hoc.jsx';

const md = require('../../utils/markdown_it.js').default({ openLinksExternally: true });

const TINY_MCE_SCRIPT = 'tiny-mce-script';

// This is a flexible text input box. It switches between edit and read mode,
// and can either provide a wysiwyg editor or a plain text editor.
const TextAreaInput = createReactClass({
Expand Down Expand Up @@ -36,35 +33,19 @@ const TextAreaInput = createReactClass({
},

componentDidMount() {
// check if tinymce is already loaded
const tinyMCEScript = document.getElementById(TINY_MCE_SCRIPT);
if (tinyMCEScript !== null) {
this.setState({
tinymceLoaded: true
});
return;
}
if (this.props.wysiwyg) {
this.loadTinyMCE();
}
},

loadTinyMCE() {
// dynamically add the script tag
// import() doesn't work because
// the skins are loaded relative to the current url which results in 500
const tinyMCEPath = Features.tinyMceUrl;
if (tinyMCEPath.trim() !== '') { // path is empty when logged out
const script = document.createElement('script');
script.id = TINY_MCE_SCRIPT;
script.onload = () => {
// tinymce is loaded at this point
const user_signed_in = Features.user_signed_in;
if (user_signed_in) {
import('../../tinymce').then(() => {
this.setState({
tinymceLoaded: true
});
};
script.src = tinyMCEPath;
document.head.appendChild(script);
});
}
},

Expand Down
35 changes: 35 additions & 0 deletions app/assets/javascripts/components/util/create_store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createStore, applyMiddleware, compose } from 'redux';
import reducer from '../../reducers';
import thunk from 'redux-thunk';

export const getStore = () => {
const reactRoot = document.getElementById('react_root');
if (!reactRoot) {
return null;
}
const currentUserFromHtml = JSON.parse(reactRoot.getAttribute('data-current_user'));
const admins = JSON.parse(reactRoot.getAttribute('data-admins'));

// This is basic, minimal state info extracted from the HTML,
// used for initial rendering before React fetches all the specific
// data it needs via API calls.
const preloadedState = {
courseCreator: {
defaultCourseType: reactRoot.getAttribute('data-default-course-type'),
courseStringPrefix: reactRoot.getAttribute('data-course-string-prefix'),
courseCreationNotice: reactRoot.getAttribute('data-course-creation-notice'),
useStartAndEndTimes: reactRoot.getAttribute('data-use-start-and-end-times') === 'true'
},
currentUserFromHtml,
admins
};
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
preloadedState,
composeEnhancers(applyMiddleware(thunk))
);
return store;
};

export default getStore();
6 changes: 4 additions & 2 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def logo_favicon_tag
end

def dashboard_stylesheet_tag(filename)
if Features.hot_loading?
if Features.hot_loading? && Rails.env.development?
filename = "#{rtl? ? 'rtl-' : nil}#{filename}"
stylesheet_link_tag "http://localhost:8080/assets/stylesheets/#{filename}.css"
else
Expand All @@ -37,7 +37,9 @@ def hot_javascript_tag(filename)
# :nocov:

def hot_javascript_path(filename)
return "http://localhost:8080/#{filename}.js" if Features.hot_loading?
TheTrio marked this conversation as resolved.
Show resolved Hide resolved
if Features.hot_loading? && Rails.env.development?
return "http://localhost:8080/assets/javascripts/#{filename}.js"
end
fingerprinted('/assets/javascripts/', filename)
end

Expand Down
2 changes: 1 addition & 1 deletion app/views/shared/_head.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
enableGetHelpButton: #{Features.enable_get_help_button? && user_signed_in?},
enableAdvancedFeatures: "#{ENV['ENABLE_ADVANCED_FEATURES']}",
consentBanner: #{user_signed_in? && !Rails.env.test?},
tinyMceUrl: '#{user_signed_in? ? hot_javascript_path("tinymce") : ''}'
user_signed_in: #{user_signed_in?}
}

WikiProjects = document.querySelector('meta[name="data-projects"]').content
Expand Down
5 changes: 5 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ module.exports = {
ignore: [
'i18n/*.js'
]
},
development: {
plugins: [
'react-refresh/babel',
]
}
}
};
4 changes: 4 additions & 0 deletions config/initializers/dev_server.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
require_dependency "#{Rails.root}/lib/dev_server_proxy"
if Features.hot_loading? && Rails.env.development?
Rails.application.config.middleware.use WebpackDevServerProxy, dev_server_host: "localhost:8080"
TheTrio marked this conversation as resolved.
Show resolved Hide resolved
end
16 changes: 16 additions & 0 deletions lib/dev_server_proxy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true
class WebpackDevServerProxy < Rack::Proxy
def initialize(app = nil, opts = {})
super
@dev_server_host = opts[:dev_server_host]
end

def perform_request(env)
TheTrio marked this conversation as resolved.
Show resolved Hide resolved
if env['PATH_INFO'].start_with?('/assets/') # Specify asset paths to proxy
env['HTTP_HOST'] = @dev_server_host
super
else
@app.call(env)
end
end
end
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@
"lint-non-build": "eslint 'test/**/*.{jsx,js}' '*.js'",
"start": "node prepare.js && webpack serve --env development --client-overlay --client-progress --progress",
"start:no-lint": "node prepare.js && webpack serve --env development --env DISABLE_ESLINT --client-overlay --client-progress --progress",
"hot": "node prepare.js && webpack serve --env development --env memory --client-overlay --client-progress --progress",
"hot:no-lint": "node prepare.js && webpack serve --env development --env DISABLE_ESLINT --env memory --client-overlay --client-progress --progress",
"coverage": "node prepare.js && webpack build --env development --env coverage --progress --node-env test",
"build": "node prepare.js && webpack build --env production --node-env production --progress",
"analyze": "webpack --env production --env stats --profile --json > stats.json && webpack-bundle-analyzer stats.json public/assets/javascripts"
Expand All @@ -127,9 +129,11 @@
"devDependencies": {
"@babel/eslint-parser": "^7.17.0",
"@babel/plugin-transform-runtime": "^7.9.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
"babel-plugin-root-import": "^6.5.0",
"eslint-import-resolver-babel-plugin-root-import": "^1.1.1",
"eslint-webpack-plugin": "^3.1.1",
"react-refresh": "^0.14.0",
"stylus-native-loader": "^1.1.2",
"webpack-cli": "^4.9.0"
},
Expand Down
19 changes: 13 additions & 6 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');
const MomentLocalesPlugin = require('moment-locales-webpack-plugin');
const config = require('./config');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

const jsSource = `./${config.sourcePath}/${config.jsDirectory}`;
const cssSource = `./${config.sourcePath}/${config.cssDirectory}`;
Expand All @@ -32,7 +33,6 @@ module.exports = (env) => {
survey_results: [`${jsSource}/surveys/survey-results.jsx`],
campaigns: [`${jsSource}/campaigns.js`],
charts: [`${jsSource}/charts.js`],
tinymce: [`${jsSource}/tinymce.js`],
embed_course_stats: [`${jsSource}/embed_course_stats.js`],
accordian: [`${jsSource}/accordian.js`],

Expand Down Expand Up @@ -119,17 +119,20 @@ module.exports = (env) => {
}
return file;
}
})
}),
(env.development && !env.coverage) && new ReactRefreshWebpackPlugin({ overlay: {
sockPort: 8080
} })
].filter(Boolean),

optimization: {
splitChunks: {
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]((?!(chart)).*)[\\/]/,
chunks: chunk => !/tinymce/.test(chunk.name),
test: /[\\/]node_modules[\\/]((?!(chart|tinymce)).*)[\\/]/,
chunks: 'all',
name: 'vendors'
}
},
}
},
minimizer: [
Expand All @@ -145,7 +148,11 @@ module.exports = (env) => {
stats: env.stats ? 'normal' : 'minimal',
};

if (env.development) {
if (env.development && env.memory) {
output.devServer = {
hot: true
};
} else if (env.development) {
output.devServer = {
devMiddleware: {
publicPath: path.join(__dirname, '/public'),
Expand Down