diff --git a/MIT-LICENSE b/MIT-LICENSE new file mode 100644 index 000000000..9fded7b4e --- /dev/null +++ b/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2016 David Heinemeier Hansson, Basecamp + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 000000000..9b2de095c --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Webpacker + +Webpacker makes it easy to use the JavaScript preprocessor and bundler Webpack +to manage application-like JavaScript in Rails. It coexists with the asset pipeline, +as the purpose is only to use Webpack for app-like JavaScript, not images, css, or +even JavaScript Sprinkles. + +It's designed to work with Rails 5.1+ and makes use of the Yarn dependency management +that's been made default from that version forward. You can either make use of Webpacker +during setup of a new application with --webpack or you can uncomment the gem and run +`bin/rails webpacker:install` in an existing application. + +When Webpacker has been installed... + +FIXME: Write the rest... + +## License +Webpacker is released under the [MIT License](http://www.opensource.org/licenses/MIT). diff --git a/lib/install/bin/webpack-watcher.tt b/lib/install/bin/webpack-watcher.tt new file mode 100644 index 000000000..ce6e91c67 --- /dev/null +++ b/lib/install/bin/webpack-watcher.tt @@ -0,0 +1,7 @@ +<%= shebang %> + +BIN_PATH = File.expand_path('.', __dir__) + +Dir.chdir(BIN_PATH) do + exec "./webpack --watch --progress --color #{ARGV.join(" ")}" +end diff --git a/lib/install/bin/webpack.tt b/lib/install/bin/webpack.tt new file mode 100644 index 000000000..c087a589d --- /dev/null +++ b/lib/install/bin/webpack.tt @@ -0,0 +1,15 @@ +<%= shebang %> + +RAILS_ENV = ENV['RAILS_ENV'] || 'development' +WEBPACK_ENV = ENV['WEBPACK_ENV'] || RAILS_ENV + +APP_PATH = File.expand_path('../', __dir__) +VENDOR_PATH = File.expand_path('../vendor', __dir__) + +SET_NODE_PATH = "NODE_PATH=#{VENDOR_PATH}/node_modules" +WEBPACK_BIN = "./node_modules/webpack/bin/webpack.js" +WEBPACK_CONFIG = "#{APP_PATH}/config/webpack/#{WEBPACK_ENV}.js" + +Dir.chdir(VENDOR_PATH) do + exec "#{SET_NODE_PATH} #{WEBPACK_BIN} --config #{WEBPACK_CONFIG} #{ARGV.join(" ")}" +end diff --git a/lib/install/config/development.js b/lib/install/config/development.js new file mode 100644 index 000000000..0188b9213 --- /dev/null +++ b/lib/install/config/development.js @@ -0,0 +1,14 @@ +// Note: You must restart bin/webpack-watcher for changes to take effect + +var path = require('path') +var webpack = require('webpack') +var _ = require('lodash') + +var config = module.exports = require('./shared.js') + +config = _.merge(config, { + debug: true, + displayErrorDetails: true, + outputPathinfo: true, + devtool: 'sourcemap', +}) diff --git a/lib/install/config/production.js b/lib/install/config/production.js new file mode 100644 index 000000000..aaeb8d6d8 --- /dev/null +++ b/lib/install/config/production.js @@ -0,0 +1,14 @@ +// Note: You must restart bin/webpack-watcher for changes to take effect + +var path = require('path') +var webpack = require('webpack') +var _ = require('lodash') + +var config = module.exports = require('./shared.js'); + +config.output = _.merge(config.output, { filename: '[name]-[hash].js' }) + +config.plugins.push( + new webpack.optimize.UglifyJsPlugin(), + new webpack.optimize.OccurenceOrderPlugin() +) diff --git a/lib/install/config/shared.js b/lib/install/config/shared.js new file mode 100644 index 000000000..d14925675 --- /dev/null +++ b/lib/install/config/shared.js @@ -0,0 +1,31 @@ +// Note: You must restart bin/webpack-watcher for changes to take effect + +var path = require('path') +var glob = require('glob') +var _ = require('lodash') + +module.exports = { + entry: _.keyBy(glob.sync('../app/javascript/packs/*.js'), function(entry) { return path.basename(entry, '.js') }), + + output: { filename: '[name].js', path: '../public/packs' }, + + module: { + loaders: [ + { test: /\.coffee$/, loader: "coffee-loader" } + ] + }, + + plugins: [], + + resolve: { + extensions: [ '', '.js', '.coffee' ], + root: [ + path.resolve('../app/javascript'), + path.resolve('../vendor/node_modules') + ] + }, + + resolveLoader: { + modulesDirectories: [ path.resolve('../vendor/node_modules') ] + } +} diff --git a/lib/install/javascript/packs/application.js b/lib/install/javascript/packs/application.js new file mode 100644 index 000000000..56b1f64d1 --- /dev/null +++ b/lib/install/javascript/packs/application.js @@ -0,0 +1 @@ +// var React = require('react') diff --git a/lib/install/template.rb b/lib/install/template.rb new file mode 100644 index 000000000..47e2d977b --- /dev/null +++ b/lib/install/template.rb @@ -0,0 +1,15 @@ +INSTALL_PATH = File.dirname(__FILE__) + +directory "#{INSTALL_PATH}/javascript", 'app/javascript' + +directory "#{INSTALL_PATH}/bin", 'bin' +chmod 'bin', 0755 & ~File.umask, verbose: false + +directory "#{INSTALL_PATH}/config", 'config/webpack' + +run './bin/yarn add webpack lodash' + +environment \ + "# Make javascript_pack_tag lookup digest hash to enable long-term caching\n" + + " config.x.webpacker[:digesting] = true\n", + env: 'production' diff --git a/lib/tasks/webpacker.rake b/lib/tasks/webpacker.rake new file mode 100644 index 000000000..93e78784b --- /dev/null +++ b/lib/tasks/webpacker.rake @@ -0,0 +1,22 @@ +PACKS_PATH = Rails.root.join('public/packs') +PACK_DIGESTS_PATH = PACKS_PATH.join('digests.json') + +WEBPACKER_APP_TEMPLATE_PATH = File.expand_path('../install/template.rb', File.dirname(__FILE__)) + +namespace :webpacker do + desc "compile javascript packs using webpack for production with digests" + task :compile do + webpack_digests_json = JSON.parse(`WEBPACK_ENV=production ./bin/webpack --json`)['assetsByChunkName'].to_json + + FileUtils.mkdir_p(PACKS_PATH) + File.open(PACK_DIGESTS_PATH, 'w+') { |file| file.write webpack_digests_json } + + puts "Compiled digests for all packs in #{PACK_DIGESTS_PATH}: " + puts webpack_digests_json + end + + desc "install webpacker in this application" + task :install do + exec "./bin/rails app:template LOCATION=#{WEBPACKER_APP_TEMPLATE_PATH}" + end +end diff --git a/lib/webpacker.rb b/lib/webpacker.rb new file mode 100644 index 000000000..463f2ee7b --- /dev/null +++ b/lib/webpacker.rb @@ -0,0 +1,4 @@ +module Webpacker +end + +require 'webpacker/railtie' if defined?(Rails) diff --git a/lib/webpacker/digests.rb b/lib/webpacker/digests.rb new file mode 100644 index 000000000..0b1519cb9 --- /dev/null +++ b/lib/webpacker/digests.rb @@ -0,0 +1,36 @@ +class Webpacker::Digests + class_attribute :instance + + class << self + def load(path) + self.instance = new(path) + end + + def lookup(name) + if instance + instance.lookup(name) + else + raise "Webpacker::Digests.load(path) must be called first" + end + end + end + + def initialize(path) + @path = path + load + end + + def lookup(name) + @digests[name.to_s] + end + + private + def load + begin + @digests = JSON.parse(File.read(@path)) + rescue Errno::ENOENT + Rails.logger.error \ + "Missing digests file at #{@path}! You must first compile the packs via rails webpacker:compile" + end + end +end diff --git a/lib/webpacker/helper.rb b/lib/webpacker/helper.rb new file mode 100644 index 000000000..dbabd2296 --- /dev/null +++ b/lib/webpacker/helper.rb @@ -0,0 +1,7 @@ +require 'webpacker/source' + +module Webpacker::Helper + def javascript_pack_tag(name, **options) + tag.script src: Webpacker::Source.new(name).path, **options + end +end diff --git a/lib/webpacker/railtie.rb b/lib/webpacker/railtie.rb new file mode 100644 index 000000000..1b9b2d1c6 --- /dev/null +++ b/lib/webpacker/railtie.rb @@ -0,0 +1,18 @@ +require 'rails/railtie' + +require 'webpacker/helper' +require 'webpacker/digests' + +class Webpacker::Engine < ::Rails::Engine + initializer :webpacker do + ActiveSupport.on_load :action_controller do + ActionController::Base.helper Webpacker::Helper + end + + if Rails.configuration.x.webpacker[:digesting] + Webpacker::Digests.load \ + Rails.application.config.x.webpacker[:digests_path] || + Rails.root.join('public/packs/digests.json') + end + end +end diff --git a/lib/webpacker/source.rb b/lib/webpacker/source.rb new file mode 100644 index 000000000..3c3ca6e13 --- /dev/null +++ b/lib/webpacker/source.rb @@ -0,0 +1,28 @@ +class Webpacker::Source + def initialize(name) + @name = name + end + + def path + if digesting? + "/packs/#{digested_filename}" + else + "/packs/#{filename}" + end + end + + private + attr_accessor :name + + def digesting? + Rails.configuration.x.webpacker[:digesting] + end + + def digested_filename + Webpacker::Digests.lookup(name) + end + + def filename + "#{name}.js" + end +end diff --git a/webpacker.gemspec b/webpacker.gemspec new file mode 100644 index 000000000..e62a5eb04 --- /dev/null +++ b/webpacker.gemspec @@ -0,0 +1,17 @@ +Gem::Specification.new do |s| + s.name = 'webpacker' + s.version = '0.1' + s.authors = 'David Heinemeier Hansson' + s.email = 'david@basecamp.com' + s.summary = 'Use Webpack to manage app-like JavaScript modules in Rails' + s.homepage = 'https://github.com/rails/webpacker' + s.license = 'MIT' + + s.required_ruby_version = '>= 1.9.3' + + s.add_dependency 'activesupport', '>= 3.0.0', '< 5.1' + s.add_dependency 'multi_json', '~> 1.2' + + s.files = `git ls-files`.split("\n") + s.test_files = `git ls-files -- test/*`.split("\n") +end