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

Support React on Rails by merging Webpacker Lite #601

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/.bundle
/pkg
/test/test_app/log
/test/test_app/tmp
node_modules
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ foreman start
```

By default, `webpack-dev-server` listens on `0.0.0.0` that means listening
on all IP addresses available on your system: LAN IP address, localhost, 127.0.0.1 etc.
on all IP addresses available on your system: LAN IP address, localhost, 127.0.0.1 etc.
However, we use `localhost` as default hostname for serving assets in browser
and if you want to change that, for example on cloud9 you can do so
by changing host set in `config/webpacker.yml`.
Expand Down Expand Up @@ -360,8 +360,14 @@ development:
host: 0.0.0.0
port: 8080
https: false
hmr: false
```

If you have hmr turned to `true`, then the `stylesheet_pack_tag` generates no output, as you will want
to configure your styles to be inlined in your JavaScript for hot reloading. During production and testing, the
`stylesheet_pack_tag` will create the appropriate HTML tags.


#### Resolved Paths

If you are adding webpacker to an existing app that has most of the assets inside
Expand Down Expand Up @@ -445,6 +451,16 @@ You can checkout these links on this subject:
- https://webpack.js.org/configuration/dev-server/#devserver-hot
- https://webpack.js.org/guides/hmr-react/

If you are using hot reloading, you should either set the `dev_server/hmr` key to TRUE or set the
ENV value `WEBPACKER_HMR=TRUE`. That way, your stylesheet pack tag will do **nothing** because you
need your styles inlined in your JavaScript for hot reloading to work properly.

### Live Reloading or Static Reloading
Live Reloading is having your assets for development provided by the webpack-dev-server.

You can override the the presence of the `dev_server` setup by setting ENV value: `WEBPACKER_DEV_SERVER=FALSE`.

You might do this if you are switching back and forth between statically compiling files during development and trying to get hot reloading to work.

## Linking Styles, Images and Fonts

Expand Down Expand Up @@ -593,9 +609,9 @@ and

#### React

You may consider using [react-rails](https://github.com/reactjs/react-rails) or
[webpacker-react](https://github.com/renchap/webpacker-react) for more advanced react integration.
However here is how you can do it yourself:
If you need more advanced React-integration, like server rendering, redux, or react-router, see [shakacode/react_on_rails](https://github.com/shakacode/react_on_rails), [react-rails](https://github.com/reactjs/react-rails), and [webpacker-react](https://github.com/renchap/webpacker-react).

If you're not concerned with view helpers to pass props or server rendering, can do it yourself:

```erb
<%# views/layouts/application.html.erb %>
Expand Down
9 changes: 8 additions & 1 deletion lib/install/config/webpack/development.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Note: You must restart bin/webpack-dev-server for changes to take effect

const webpack = require('webpack')
const merge = require('webpack-merge')
const sharedConfig = require('./shared.js')
const { settings, output } = require('./configuration.js')
Expand All @@ -16,6 +17,7 @@ module.exports = merge(sharedConfig, {
https: settings.dev_server.https,
host: settings.dev_server.host,
port: settings.dev_server.port,
hot: settings.dev_server.hmr,
contentBase: output.path,
publicPath: output.publicPath,
compress: true,
Expand All @@ -27,5 +29,10 @@ module.exports = merge(sharedConfig, {
stats: {
errorDetails: true
}
}
},

plugins: settings.dev_server.hmr ? [
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin()
] : []
})
8 changes: 2 additions & 6 deletions lib/install/config/webpack/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ module.exports = {

output: {
filename: '[name].js',
path: output.path,
publicPath: output.publicPath
path: output.path
},

module: {
Expand All @@ -38,10 +37,7 @@ module.exports = {
plugins: [
new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))),
new ExtractTextPlugin(env.NODE_ENV === 'production' ? '[name]-[contenthash].css' : '[name].css'),
new ManifestPlugin({
publicPath: output.publicPath,
writeToFileEmit: true
})
new ManifestPlugin({ writeToFileEmit: true })
],

resolve: {
Expand Down
1 change: 1 addition & 0 deletions lib/install/config/webpacker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ development:
host: localhost
port: 8080
https: false
hmr: false

test:
<<: *default
Expand Down
1 change: 1 addition & 0 deletions lib/webpacker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def env
require "webpacker/logger"
require "webpacker/env"
require "webpacker/configuration"
require "webpacker/dev_server"
require "webpacker/manifest"
require "webpacker/compiler"
require "webpacker/railtie" if defined?(Rails)
4 changes: 2 additions & 2 deletions lib/webpacker/compiler.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
require "open3"
require "webpacker/env"
require "webpacker/configuration"

module Webpacker::Compiler
extend self
Expand All @@ -13,6 +11,8 @@ module Webpacker::Compiler
mattr_accessor(:watched_paths) { [] }

def compile
return if Webpacker::DevServer.running?

if stale?
cache_source_timestamp
run_webpack
Expand Down
8 changes: 6 additions & 2 deletions lib/webpacker/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ def entry_path
source_path.join(fetch(:source_entry_path))
end

def public_output_path
fetch(:public_output_path)
end

def output_path
public_path.join(fetch(:public_output_path))
public_path.join(public_output_path)
end

def manifest_path
Expand Down Expand Up @@ -61,7 +65,7 @@ def defaults

private
def load
return super unless File.exist?(@path)
return Webpacker::Configuration.defaults unless File.exist?(@path)
HashWithIndifferentAccess.new(YAML.load(File.read(@path))[Webpacker.env])
end
end
47 changes: 47 additions & 0 deletions lib/webpacker/dev_server.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
module Webpacker::DevServer
extend self

def running?
socket = Socket.tcp(host, port, connect_timeout: 1)
socket.close
true

rescue Errno::ECONNREFUSED, NoMethodError
false
end

def hmr?
case ENV["WEBPACKER_HMR"]
when /true/i then true
when /false/i then false
else fetch(:hmr)
end

rescue NoMethodError
false
end

def host
fetch(:host)
end

def port
fetch(:port)
end

def https?
fetch(:https)
end

def protocol
https? ? "https" : "http"
end

def base_url
"#{protocol}://#{host}:#{port}"
end

def fetch(key)
Webpacker::Configuration.data[:dev_server][key] || Webpacker::Configuration.defaults[:dev_server][key]
end
end
21 changes: 19 additions & 2 deletions lib/webpacker/file_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,29 @@ class NotFoundError < StandardError; end
class FileLoaderError < StandardError; end

class_attribute :instance
attr_accessor :data
attr_accessor :data, :mtime, :path

class << self
def load(path = file_path)
self.instance = new(path)
if instance.nil? || !production_env? || !file_cached?(path)
self.instance = new(path)
end
end

def file_path
raise FileLoaderError.new("Subclass of Webpacker::FileLoader should override this method")
end

private
def file_cached?(path)
File.exist?(path) && self.instance.mtime == File.mtime(path)
end

# Prefer the NODE_ENV to the rails env.
def production_env?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Webpacker::Env already does this - https://github.com/shakacode/webpacker/blob/4cf3fcab5c03203216be7ab0b7f161b591908174/lib/webpacker/env.rb#L19

so if we just call Webpacker.env.production? it would be same thing as this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gauravtiwari are you sure that would work since you're creating a circular dependency amongst the files. I don't think so but if you're sure, I'll try.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, totally missed 👍

(ENV["NODE_ENV"].presence || Rails.env) == "production"
end

protected
def ensure_loaded_instance(klass)
raise Webpacker::FileLoader::FileLoaderError.new("#{klass.name}#load must be called first") unless instance
Expand All @@ -20,6 +36,7 @@ def ensure_loaded_instance(klass)
private
def initialize(path)
@path = path
@mtime = File.exist?(path) ? File.mtime(path) : nil
@data = load
end

Expand Down
15 changes: 12 additions & 3 deletions lib/webpacker/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ module Webpacker::Helper
# In production mode:
# <%= asset_pack_path 'calendar.css' %> # => "/packs/calendar-1016838bab065ae1e122.css"
def asset_pack_path(name, **options)
asset_path(Webpacker::Manifest.lookup(name), **options)
asset_path(Webpacker::Manifest.pack_path(name), **options)
end

# Creates a script tag that references the named pack file, as compiled by Webpack per the entries list
# in config/webpack/shared.js. By default, this list is auto-generated to match everything in
# app/javascript/packs/*.js. In production mode, the digested reference is automatically looked up.
Expand Down Expand Up @@ -41,13 +42,21 @@ def javascript_pack_tag(*names, **options)
# # In production mode:
# <%= stylesheet_pack_tag 'calendar', 'data-turbolinks-track': 'reload' %> # =>
# <link rel="stylesheet" media="screen" href="/packs/calendar-1016838bab065ae1e122.css" data-turbolinks-track="reload" />
# # In development mode with hot-reloading
# <%= stylesheet_pack_tag('main') %> <% # Default is false for enabled_when_hot_loading%>
# # No output
#
def stylesheet_pack_tag(*names, **options)
stylesheet_link_tag(*sources_from_pack_manifest(names, type: :stylesheet), **options)
if Webpacker::DevServer.hmr?
""
else
stylesheet_link_tag(*sources_from_pack_manifest(names, type: :stylesheet), **options)
end
end

private
def sources_from_pack_manifest(names, type:)
names.map { |name| Webpacker::Manifest.lookup(pack_name_with_extension(name, type: type)) }
names.map { |name| Webpacker::Manifest.pack_path(pack_name_with_extension(name, type: type)) }
end

def pack_name_with_extension(name, type:)
Expand Down
77 changes: 68 additions & 9 deletions lib/webpacker/manifest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,90 @@ def file_path
Webpacker::Configuration.manifest_path
end

def lookup(name)
# Converts the "name" (aka the pack or bundle name) to to the full path for use in a browser.
def pack_path(name)
relative_pack_path = "#{Webpacker::Configuration.public_output_path}/#{lookup(name)}"

if Webpacker::DevServer.running?
"#{Webpacker::DevServer.base_url}/#{relative_pack_path}"
else
relative_pack_path.starts_with?("/") ? relative_pack_path : "/#{relative_pack_path}"
end
end

# Converts the "name" (aka the pack or bundle name) to the possibly hashed name per a manifest.
#
# If Configuration.compile? then compilation is invoked the file is missing.
#
# Options
# throw_if_missing: default is true. If false, then nill is returned if the file is missing.
def lookup(name, throw_if_missing: true)
instance.confirm_manifest_exists

if Webpacker::Configuration.compile?
compile_and_find!(name)
compile_and_find!(name, throw_if_missing: throw_if_missing)
else
find!(name)
# Since load checks a `mtime` on the manifest for a non-production env before loading,
# we should always call this before a call to `find!` since one may be using
# `webpack -w` and a file may have been added to the manifest since Rails first started.
load
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not going to want to do a file stat in production.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dhh There's no file stat in production. The code checks the Rails.env. Does that meet your concern here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgive the drive-by comment, but framework code should not be directly checking the environment: we have a config subsystem for that. People often have a production-alike staging environment, for example.

(More locally-relevant, this should likely use the file watcher to avoid the stat [where possible] even in development.)

raise Webpacker::FileLoader::FileLoaderError.new("Webpacker::Manifest.load must be called first") unless instance

return find!(name, throw_if_missing: throw_if_missing)
end
end

# Why does this method exist? Testing? It's not in the README
def lookup_path(name)
Rails.root.join(File.join(Webpacker::Configuration.public_path, lookup(name)))
Rails.root.join(File.join(Webpacker::Configuration.output_path, lookup(name)))
end

private
def find!(name)
def missing_file_from_manifest_error(bundle_name)
msg = <<-MSG
Webpacker can't find #{bundle_name} in your manifest at #{file_path}. Possible causes:
1. You are hot reloading.
2. You want to set Configuration.compile to true for your environment.
3. Webpack has not re-run to reflect updates.
4. You have misconfigured Webpacker's config/webpacker.yml file.
5. Your Webpack configuration is not creating a manifest.
Your manifest contains:
#{instance.data.to_json}
MSG
raise(Webpacker::FileLoader::NotFoundError.new(msg))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is great 👍 🍰

end

def missing_manifest_file_error(path_object)
msg = <<-MSG
Webpacker can't find the manifest file: #{path_object}
Possible causes:
1. You have not invoked webpack.
2. You have misconfigured Webpacker's config/webpacker_.yml file.
3. Your Webpack configuration is not creating a manifest.
MSG
raise(Webpacker::FileLoader::NotFoundError.new(msg))
end

def find!(name, throw_if_missing: true)
ensure_loaded_instance(self)
instance.data[name.to_s] ||
raise(Webpacker::FileLoader::NotFoundError.new("Can't find #{name} in #{file_path}. Is webpack still compiling?"))
value = instance.data[name.to_s]
return value if value.present?

if throw_if_missing
missing_file_from_manifest_error(name)
end
end

def compile_and_find!(name)
def compile_and_find!(name, throw_if_missing: true)
Webpacker.logger.tagged("Webpacker") { Webpacker.compile }
find!(name)
find!(name, throw_if_missing: throw_if_missing)
end
end

def confirm_manifest_exists
raise missing_manifest_file_error(@path) unless File.exist?(@path)
end

private
def load
return super unless File.exist?(@path)
Expand Down
4 changes: 2 additions & 2 deletions test/compiler_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def test_watched_paths
end

def test_freshness
assert Webpacker::Compiler.stale?
assert !Webpacker::Compiler.fresh?
refute Webpacker::Compiler.stale?
refute !Webpacker::Compiler.fresh?
end
end
Loading