diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 000000000..62eae71b7 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,124 @@ +AllCops: + TargetRubyVersion: 2.2 + # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop + # to ignore them, so only the ones explicitly set in this file are enabled. + DisabledByDefault: true + Exclude: + - 'lib/install/templates/**' + - 'vendor/**/*' + - 'node_modules/**/*' + +# Prefer &&/|| over and/or. +Style/AndOr: + Enabled: true + +# Do not use braces for hash literals when they are the last argument of a +# method call. +Style/BracesAroundHashParameters: + Enabled: true + +# Align `when` with `case`. +Style/CaseIndentation: + Enabled: true + +# Align comments with method definitions. +Style/CommentIndentation: + Enabled: true + +# No extra empty lines. +Style/EmptyLines: + Enabled: true + +# In a regular class definition, no empty lines around the body. +Style/EmptyLinesAroundClassBody: + Enabled: true + +# In a regular method definition, no empty lines around the body. +Style/EmptyLinesAroundMethodBody: + Enabled: true + +# In a regular module definition, no empty lines around the body. +Style/EmptyLinesAroundModuleBody: + Enabled: true + +# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. +Style/HashSyntax: + Enabled: true + +# Method definitions after `private` or `protected` isolated calls need one +# extra level of indentation. +Style/IndentationConsistency: + Enabled: true + EnforcedStyle: rails + +# Two spaces, no tabs (for indentation). +Style/IndentationWidth: + Enabled: true + +Style/SpaceAfterColon: + Enabled: true + +Style/SpaceAfterComma: + Enabled: true + +Style/SpaceAroundEqualsInParameterDefault: + Enabled: true + +Style/SpaceAroundKeyword: + Enabled: true + +Style/SpaceAroundOperators: + Enabled: true + +Style/SpaceBeforeFirstArg: + Enabled: true + +# Defining a method with parameters needs parentheses. +Style/MethodDefParentheses: + Enabled: true + +# Use `foo {}` not `foo{}`. +Style/SpaceBeforeBlockBraces: + Enabled: true + +# Use `foo { bar }` not `foo {bar}`. +Style/SpaceInsideBlockBraces: + Enabled: true + +# Use `{ a: 1 }` not `{a:1}`. +Style/SpaceInsideHashLiteralBraces: + Enabled: true + +Style/SpaceInsideParens: + Enabled: true + +# Check quotes usage according to lint rule below. +Style/StringLiterals: + Enabled: true + EnforcedStyle: double_quotes + +# Detect hard tabs, no hard tabs. +Style/Tab: + Enabled: true + +# Blank lines should not have any spaces. +Style/TrailingBlankLines: + Enabled: true + +# No trailing whitespace. +Style/TrailingWhitespace: + Enabled: true + +# Use quotes for string literals when they are enough. +Style/UnneededPercentQ: + Enabled: true + +# Align `end` with the matching keyword or starting expression except for +# assignments, where it should be aligned with the LHS. +Lint/EndAlignment: + Enabled: true + EnforcedStyleAlignWith: variable + +# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. +Lint/RequireParentheses: + Enabled: true diff --git a/.travis.yml b/.travis.yml index 44f316aac..13211a640 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ cache: yarn: true install: + - gem install rubocop - nvm install node - node -v - npm i -g yarn @@ -20,3 +21,4 @@ install: script: - yarn lint + - rubocop diff --git a/Gemfile b/Gemfile index fa75df156..b4e2a20bb 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ -source 'https://rubygems.org' +source "https://rubygems.org" gemspec diff --git a/README.md b/README.md index 6c3f859cc..8c922b00b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Webpacker is currently compatible with Rails 4.2+, but there's no guarantee it w in the future. You can either make use of Webpacker during setup of a new application with `--webpack` -or you can add the gem and run `bin/rails webpacker:install` in an existing application. +or you can add the gem and run `./bin/rails webpacker:install` in an existing application. As the rubygems version isn't promised to be kept up to date until the release of Rails 5.1, you may want to include the gem directly from GitHub: @@ -23,6 +23,8 @@ As the rubygems version isn't promised to be kept up to date until the release o gem 'webpacker', github: 'rails/webpacker' ``` +You can also see a list of available commands by running `./bin/rails webpacker` + ## Binstubs Webpacker ships with three binstubs: `./bin/webpack`, `./bin/webpack-watcher` and `./bin/webpack-dev-server`. @@ -40,9 +42,7 @@ If you'd rather not have to run the two processes separately by hand, you can us Alternatively, you can run `./bin/webpack-dev-server`. This will launch a [Webpack Dev Server](https://webpack.github.io/docs/webpack-dev-server.html) listening on http://localhost:8080/ -serving your pack files. It will recompile your files as you make changes. You also need to set -`config.x.webpacker[:dev_server_host]` in your `config/environments/development.rb` to tell Webpacker to load -your packs from the Webpack Dev Server. This setup allows you to leverage advanced Webpack features, such +serving your pack files. This setup allows you to leverage advanced Webpack features, such as [Hot Module Replacement](https://webpack.github.io/docs/hot-module-replacement-with-webpack.html). @@ -59,11 +59,6 @@ as Webpack calls it). Let's say you're building a calendar. Your structure could look like this: -```erb -<%# app/views/layout/application.html.erb %> -<%= javascript_pack_tag 'calendar' %> -``` - ```js // app/javascript/packs/calendar.js require('calendar') @@ -72,19 +67,108 @@ require('calendar') ``` app/javascript/calendar/index.js // gets loaded by require('calendar') app/javascript/calendar/components/grid.jsx +app/javascript/calendar/styles/grid.sass app/javascript/calendar/models/month.js ``` -But it could also look a million other ways. The only convention that Webpacker enforces is the -one where entry points are automatically configured by the files in `app/javascript/packs`. +```erb +<%# app/views/layout/application.html.erb %> +<%= javascript_pack_tag 'calendar' %> +<%= stylesheet_pack_tag 'calendar' %> +``` + +But it could also look a million other ways. + +## Advanced Configuration + +By default, webpacker offers simple conventions for where the webpack configs, javascript app files and compiled webpack bundles will go in your rails app, +but all these options are configurable from `config/webpack/paths.yml` file. + +```yml +# config/webpack/paths.yml +source: app/javascript +entry: packs +output: public +config: config/webpack +node_modules: node_modules +``` + +*Note:* Behind the scenes, webpacker will use same `entry` directory name inside `output` +directory to emit bundles. For ex, `public/packs` + +Similary, you can also control and configure `webpack-dev-server` settings from +`config/webpack/development.server.yml` file + +```yml +# config/webpack/development.server.yml +enabled: true +host: localhost +port: 8080 +``` + +By default, `webpack-dev-server` uses `output` option specified in +`paths.yml` as `contentBase`. + +**Note:** Don't forget to disable `webpack-dev-server` incase you are using +`./bin/webpack-watcher` to serve assets in development mode otherwise +you will get 404 for assets because the helper tag will use webpack-dev-server url +to serve assets instead of public directory. + +## Linking to static assets + +Static assets like images, fonts and stylesheets support is enabled out-of-box so, you can link them into your javascript app code and have them compiled automatically. + +```js +// React component example +// app/javascripts/packs/hello_react.jsx +import React from 'react' +import ReactDOM from 'react-dom' +import helloIcon from '../hello_react/images/icon.png' +import './hello-react.sass' + +const Hello = props => ( +
+ hello-icon +

Hello {props.name}!

+
+) +``` + +under the hood webpack uses [extract-text-webpack-plugin](https://github.com/webpack-contrib/extract-text-webpack-plugin) plugin to extract all the referenced styles and compile it into a separate `[pack_name].css` bundle so that within your view you can use the `stylesheet_pack_tag` helper, +```erb +<%= stylesheet_pack_tag 'hello_react' %> +``` + +## Getting asset path + +Webpacker provides `asset_pack_path` helper to get the path of any given asset that's been compiled by webpack. + +**For ex,** if you want to create a `` or `` +for an asset used in your pack code you can reference them like this in your view, + +```erb +<%= asset_pack_path 'hello_react.css' %> +<% # => "/packs/hello_react.css" %> + +<% # => %> +``` ## Deployment -To compile all the packs during deployment, you can use the `rails webpacker:compile` command. This -will invoke the production configuration, which includes digesting. The `javascript_pack_tag` helper -method will automatically insert the correct digest when run in production mode. Just like the asset -pipeline does it. +Webpacker hooks up a new `webpacker:compile` task to `assets:precompile`, which gets run whenever you run `assets:precompile`. The `javascript_pack_tag` and `stylesheet_pack_tag` helper method will automatically insert the correct HTML tag for compiled pack. Just like the asset pipeline does it. By default the output will look like this in different environments, + +```html + + + + + + + + + +``` ## Linking to sprockets assets @@ -102,25 +186,42 @@ This is enabled by the `rails-erb-loader` loader rule in `config/webpack/shared. ## Ready for React -To use Webpacker with React, just create a new app with `rails new myapp --webpack=react` (or run `rails webpacker:install:react` on a Rails 5.1 app already setup with webpack), and all the relevant dependencies +To use Webpacker with React, just create a new app with `rails new myapp --webpack=react` (or run `rails webpacker:install:react` on a Rails app already setup with webpacker), and all the relevant dependencies will be added via yarn and changes to the configuration files made. Now you can create JSX files and have them properly compiled automatically. ## Ready for Angular with TypeScript -To use Webpacker with Angular, just create a new app with `rails new myapp --webpack=angular` (or run `rails webpacker:install:angular` on a Rails 5.1 app already setup with webpack). TypeScript support and the Angular core libraries will be added via yarn and changes to the configuration files made. An example component written in TypeScript is also added to your project in `app/javascript` so that you can experiment Angular right away. +To use Webpacker with Angular, just create a new app with `rails new myapp --webpack=angular` (or run `rails webpacker:install:angular` on a Rails app already setup with webpacker). TypeScript support and the Angular core libraries will be added via yarn and changes to the configuration files made. An example component written in TypeScript is also added to your project in `app/javascript` so that you can experiment Angular right away. ## Ready for Vue -To use Webpacker with Vue, just create a new app with `rails new myapp --webpack=vue` (or run `rails webpacker:install:vue` on a Rails 5.1 app already setup with webpack). Vue and its supported libraries will be added via yarn and changes to the configuration files made. An example component is also added to your project in `app/javascript` so that you can experiment Vue right away. +To use Webpacker with Vue, just create a new app with `rails new myapp --webpack=vue` (or run `rails webpacker:install:vue` on a Rails app already setup with webpacker). Vue and its supported libraries will be added via yarn and changes to the configuration files made. An example component is also added to your project in `app/javascript` so that you can experiment Vue right away. + +## Troubleshooting + +* If you get this error `ENOENT: no such file or directory - node-sass` on Heroku +or elsewhere during `assets:precompile` or `bundle exec rails webpacker:compile` +then you would need to rebuild node-sass. It's a bit weird error, +basically, it can't find the `node-sass` binary. +An easy solution is to create a postinstall hook - `npm rebuild node-sass` in +`package.json` and that will ensure `node-sass` is rebuild whenever +you install any new modules. +* If you get this error `Can't find hello_react.js in manifest.json` +when loading a view in browser it's because Webpack is still compiling packs. +Webpacker uses a `manifest.json` file to keep track of packs in all environments, +however since this file is generated after packs are compiled by webpack. So, +if you load a view in browser whilst webpack is compiling you will get this error. +Therefore, make sure webpack +(i.e `.bin/webpack-watcher` or `.bin/webpack-dev-sever`) is running and has +completed the compilation successfully before loading a view. ## Wishlist - Improve process for linking to assets compiled by sprockets - shouldn't need to specify ` <% helpers = ActionController::Base.helpers %>` at the beginning of each file - Consider chunking setup -- Consider on-demand compiling with digests when digesting=true ## License Webpacker is released under the [MIT License](https://opensource.org/licenses/MIT). diff --git a/lib/install/angular.rb b/lib/install/angular.rb new file mode 100644 index 000000000..ddf8e00be --- /dev/null +++ b/lib/install/angular.rb @@ -0,0 +1,18 @@ +require "webpacker/configuration" + +puts "Copying angular loader to #{Webpacker::Configuration.config_path}/loaders" +copy_file "#{__dir__}/config/loaders/installers/angular.js", "config/webpack/loaders/angular.js" + +puts "Copying angular example entry file to #{Webpacker::Configuration.entry_path}" +copy_file "#{__dir__}/examples/angular/hello_angular.js", "#{Webpacker::Configuration.entry_path}/hello_angular.js" + +puts "Copying hello_angular app to #{Webpacker::Configuration.source_path}" +directory "#{__dir__}/examples/angular/hello_angular", "#{Webpacker::Configuration.source_path}/hello_angular" + +puts "Copying tsconfig.json to the Rails root directory for typescript" +copy_file "#{__dir__}/examples/angular/tsconfig.json", "tsconfig.json" + +puts "Installing all angular dependencies" +run "./bin/yarn add typescript ts-loader core-js zone.js rxjs @angular/core @angular/common @angular/compiler @angular/platform-browser @angular/platform-browser-dynamic" + +puts "Webpacker now supports angular.js and typescript 🎉" diff --git a/lib/install/bin/webpack-dev-server.tt b/lib/install/bin/webpack-dev-server.tt index f09a430d8..4b21d6551 100644 --- a/lib/install/bin/webpack-dev-server.tt +++ b/lib/install/bin/webpack-dev-server.tt @@ -1,27 +1,33 @@ <%= shebang %> -require 'shellwords' +$stdout.sync = true -ENV['RAILS_ENV'] ||= 'development' -RAILS_ENV = ENV['RAILS_ENV'] +require "shellwords" +require "yaml" -ENV['NODE_ENV'] ||= RAILS_ENV -NODE_ENV = ENV['NODE_ENV'] +ENV["RAILS_ENV"] ||= "development" +RAILS_ENV = ENV["RAILS_ENV"] -APP_PATH = File.expand_path('../', __dir__) -ESCAPED_APP_PATH = APP_PATH.shellescape +ENV["NODE_ENV"] ||= RAILS_ENV +NODE_ENV = ENV["NODE_ENV"] -SET_NODE_PATH = "NODE_PATH=#{ESCAPED_APP_PATH}/node_modules" -WEBPACKER_BIN = "./node_modules/.bin/webpack-dev-server" -WEBPACK_CONFIG = "#{ESCAPED_APP_PATH}/config/webpack/#{NODE_ENV}.js" +APP_PATH = File.expand_path("../", __dir__) +CONFIG_PATH = File.join(APP_PATH, "config/webpack/paths.yml") -# Warn the user if the configuration is not set -RAILS_ENV_CONFIG = File.join("config", "environments", "#{RAILS_ENV}.rb") +begin + paths = YAML.load(File.read(CONFIG_PATH)) -# Look into the environment file for a non-commented variable declaration -unless File.foreach(File.join(APP_PATH, RAILS_ENV_CONFIG)).detect { |line| line.match(/^\s*[^#]*config\.x\.webpacker\[\:dev_server_host\].*=/) } - puts "Warning: if you want to use webpack-dev-server, you need to tell Webpacker to serve asset packs from it. Please set config.x.webpacker[:dev_server_host] in #{RAILS_ENV_CONFIG}.\n\n" + NODE_MODULES_PATH = File.join(APP_PATH.shellescape, paths["node_modules"]) + WEBPACK_CONFIG_PATH = File.join(APP_PATH.shellescape, paths["config"]) + + WEBPACK_BIN = "#{NODE_MODULES_PATH}/.bin/webpack-dev-server" + DEV_SERVER_CONFIG = "#{WEBPACK_CONFIG_PATH}/development.server.js" +rescue Errno::ENOENT, NoMethodError + puts "Configuration not found in config/webpacker/paths.yml." + puts "Please run bundle exec rails webpacker:install to install webpacker" + exit! end Dir.chdir(APP_PATH) do - exec "#{SET_NODE_PATH} #{WEBPACKER_BIN} --config #{WEBPACK_CONFIG} --content-base #{ESCAPED_APP_PATH}/public/packs #{ARGV.join(" ")}" + exec "NODE_PATH=#{NODE_MODULES_PATH} #{WEBPACK_BIN} --progress --color " \ + "--config #{DEV_SERVER_CONFIG}" end diff --git a/lib/install/bin/webpack.tt b/lib/install/bin/webpack.tt index 2835897c8..fcbd427fa 100644 --- a/lib/install/bin/webpack.tt +++ b/lib/install/bin/webpack.tt @@ -1,19 +1,40 @@ <%= shebang %> -require 'shellwords' +$stdout.sync = true -ENV['RAILS_ENV'] ||= 'development' -RAILS_ENV = ENV['RAILS_ENV'] +require "shellwords" +require "yaml" -ENV['NODE_ENV'] ||= RAILS_ENV -NODE_ENV = ENV['NODE_ENV'] +ENV["RAILS_ENV"] ||= "development" +RAILS_ENV = ENV["RAILS_ENV"] -APP_PATH = File.expand_path('../', __dir__) -ESCAPED_APP_PATH = APP_PATH.shellescape +ENV["NODE_ENV"] ||= RAILS_ENV +NODE_ENV = ENV["NODE_ENV"] -SET_NODE_PATH = "NODE_PATH=#{ESCAPED_APP_PATH}/node_modules" -WEBPACK_BIN = "./node_modules/webpack/bin/webpack.js" -WEBPACK_CONFIG = "#{ESCAPED_APP_PATH}/config/webpack/#{NODE_ENV}.js" +APP_PATH = File.expand_path("../", __dir__) +CONFIG_PATH = File.join(APP_PATH, "config/webpack/paths.yml") +DEV_SERVER_CONFIG_PATH = File.join(APP_PATH, "config/webpack/development.server.yml") + +begin + paths = YAML.load(File.read(CONFIG_PATH)) + dev_server = YAML.load(File.read(DEV_SERVER_CONFIG_PATH)) + + NODE_MODULES_PATH = File.join(APP_PATH.shellescape, paths["node_modules"]) + WEBPACK_CONFIG_PATH = File.join(APP_PATH.shellescape, paths["config"]) + + if NODE_ENV == "development" && dev_server["enabled"] + puts "Warning: webpack-dev-server is currently enabled in #{DEV_SERVER_CONFIG_PATH}. " \ + "Disable to serve assets directly from public/packs directory" + end +rescue Errno::ENOENT, NoMethodError + puts "Configuration not found in config/webpack/paths.yml or config/webpack/development.server.yml." + puts "Please run bundle exec rails webpacker:install to install webpacker" + exit! +end + +WEBPACK_BIN = "#{NODE_MODULES_PATH}/.bin/webpack" +WEBPACK_CONFIG = "#{WEBPACK_CONFIG_PATH}/#{NODE_ENV}.js" Dir.chdir(APP_PATH) do - exec "#{SET_NODE_PATH} #{WEBPACK_BIN} --config #{WEBPACK_CONFIG} #{ARGV.join(" ")}" + exec "NODE_PATH=#{NODE_MODULES_PATH} #{WEBPACK_BIN} --config #{WEBPACK_CONFIG}" \ + " #{ARGV.join(" ")}" end diff --git a/lib/install/config/.postcssrc.yml b/lib/install/config/.postcssrc.yml new file mode 100644 index 000000000..bc4f02ab3 --- /dev/null +++ b/lib/install/config/.postcssrc.yml @@ -0,0 +1,4 @@ +plugins: + postcss-smart-import: {} + precss: {} + autoprefixer: {} diff --git a/lib/install/config/loaders/core/assets.js b/lib/install/config/loaders/core/assets.js new file mode 100644 index 000000000..c859daf0b --- /dev/null +++ b/lib/install/config/loaders/core/assets.js @@ -0,0 +1,12 @@ +const { env, publicPath } = require('../configuration.js') + +module.exports = { + test: /\.(jpeg|png|gif|svg|eot|ttf|woff|woff2)$/i, + use: [{ + loader: 'file-loader', + options: { + publicPath, + name: env.NODE_ENV === 'production' ? '[name]-[hash].[ext]' : '[name].[ext]' + } + }] +} diff --git a/lib/install/config/loaders/core/babel.js b/lib/install/config/loaders/core/babel.js new file mode 100644 index 000000000..82481e15e --- /dev/null +++ b/lib/install/config/loaders/core/babel.js @@ -0,0 +1,10 @@ +module.exports = { + test: /\.js(\.erb)?$/, + exclude: /node_modules/, + loader: 'babel-loader', + options: { + presets: [ + ['env', { modules: false }] + ] + } +} diff --git a/lib/install/config/loaders/core/coffee.js b/lib/install/config/loaders/core/coffee.js new file mode 100644 index 000000000..dae874249 --- /dev/null +++ b/lib/install/config/loaders/core/coffee.js @@ -0,0 +1,4 @@ +module.exports = { + test: /\.coffee(\.erb)?$/, + loader: 'coffee-loader' +} diff --git a/lib/install/config/loaders/core/erb.js b/lib/install/config/loaders/core/erb.js new file mode 100644 index 000000000..c1a6cc158 --- /dev/null +++ b/lib/install/config/loaders/core/erb.js @@ -0,0 +1,9 @@ +module.exports = { + test: /\.erb$/, + enforce: 'pre', + exclude: /node_modules/, + loader: 'rails-erb-loader', + options: { + runner: 'DISABLE_SPRING=1 bin/rails runner' + } +} diff --git a/lib/install/config/loaders/core/sass.js b/lib/install/config/loaders/core/sass.js new file mode 100644 index 000000000..faba9d5b0 --- /dev/null +++ b/lib/install/config/loaders/core/sass.js @@ -0,0 +1,9 @@ +const ExtractTextPlugin = require('extract-text-webpack-plugin') + +module.exports = { + test: /\.(scss|sass|css)$/i, + use: ExtractTextPlugin.extract({ + fallback: 'style-loader', + use: ['css-loader', 'postcss-loader', 'sass-loader'] + }) +} diff --git a/lib/install/config/loaders/installers/angular.js b/lib/install/config/loaders/installers/angular.js new file mode 100644 index 000000000..cbb916e33 --- /dev/null +++ b/lib/install/config/loaders/installers/angular.js @@ -0,0 +1,4 @@ +module.exports = { + test: /.ts$/, + loader: 'ts-loader' +} diff --git a/lib/install/config/loaders/installers/react.js b/lib/install/config/loaders/installers/react.js new file mode 100644 index 000000000..065dc4e07 --- /dev/null +++ b/lib/install/config/loaders/installers/react.js @@ -0,0 +1,11 @@ +module.exports = { + test: /\.(js|jsx)?(\.erb)?$/, + exclude: /node_modules/, + loader: 'babel-loader', + options: { + presets: [ + 'react', + ['env', { modules: false }] + ] + } +} diff --git a/lib/install/config/loaders/installers/vue.js b/lib/install/config/loaders/installers/vue.js new file mode 100644 index 000000000..7dbab0409 --- /dev/null +++ b/lib/install/config/loaders/installers/vue.js @@ -0,0 +1,10 @@ +module.exports = { + test: /.vue$/, + loader: 'vue-loader', + options: { + loaders: { + scss: 'vue-style-loader!css-loader!sass-loader', + sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax' + } + } +} diff --git a/lib/install/config/shared.js b/lib/install/config/shared.js deleted file mode 100644 index 9ae6571de..000000000 --- a/lib/install/config/shared.js +++ /dev/null @@ -1,76 +0,0 @@ -// Note: You must restart bin/webpack-watcher for changes to take effect - -const webpack = require('webpack') -const path = require('path') -const process = require('process') -const glob = require('glob') -const extname = require('path-complete-extname') - -let distDir = process.env.WEBPACK_DIST_DIR - -if (distDir === undefined) { - distDir = 'packs' -} - -const extensions = ['.js', '.coffee'] -const extensionGlob = `*{${extensions.join(',')}}*` -const packPaths = glob.sync(path.join('app', 'javascript', 'packs', extensionGlob)) - -const config = { - entry: packPaths.reduce( - (map, entry) => { - const basename = path.basename(entry, extname(entry)) - const localMap = map - localMap[basename] = path.resolve(entry) - return localMap - }, {} - ), - - output: { filename: '[name].js', path: path.resolve('public', distDir) }, - - module: { - rules: [ - { test: /\.coffee(\.erb)?$/, loader: 'coffee-loader' }, - { - test: /\.js(\.erb)?$/, - exclude: /node_modules/, - loader: 'babel-loader', - options: { - presets: [ - ['env', { modules: false }] - ] - } - }, - { - test: /\.erb$/, - enforce: 'pre', - exclude: /node_modules/, - loader: 'rails-erb-loader', - options: { - runner: 'DISABLE_SPRING=1 bin/rails runner' - } - } - ] - }, - - plugins: [ - new webpack.EnvironmentPlugin(Object.keys(process.env)) - ], - - resolve: { - extensions, - modules: [ - path.resolve('app/javascript'), - path.resolve('node_modules') - ] - }, - - resolveLoader: { - modules: [path.resolve('node_modules')] - } -} - -module.exports = { - distDir, - config -} diff --git a/lib/install/config/webpack/configuration.js b/lib/install/config/webpack/configuration.js new file mode 100644 index 000000000..3617eab1c --- /dev/null +++ b/lib/install/config/webpack/configuration.js @@ -0,0 +1,21 @@ +// Common configuration for webpacker loaded from config/webpack/paths.yml + +const { join, resolve } = require('path') +const { env } = require('process') +const { safeLoad } = require('js-yaml') +const { readFileSync } = require('fs') + +const configPath = resolve('config', 'webpack') +const loadersDir = join(__dirname, 'loaders') +const paths = safeLoad(readFileSync(join(configPath, 'paths.yml'), 'utf8')) +const devServer = safeLoad(readFileSync(join(configPath, 'development.server.yml'), 'utf8')) +const publicPath = env.NODE_ENV !== 'production' && devServer.enabled ? + `http://${devServer.host}:${devServer.port}/` : `/${paths.entry}/` + +module.exports = { + devServer, + env, + paths, + loadersDir, + publicPath +} diff --git a/lib/install/config/development.js b/lib/install/config/webpack/development.js similarity index 60% rename from lib/install/config/development.js rename to lib/install/config/webpack/development.js index 24f9d630e..d98ec5b13 100644 --- a/lib/install/config/development.js +++ b/lib/install/config/webpack/development.js @@ -1,11 +1,9 @@ // Note: You must restart bin/webpack-watcher for changes to take effect -const webpack = require('webpack') const merge = require('webpack-merge') - const sharedConfig = require('./shared.js') -module.exports = merge(sharedConfig.config, { +module.exports = merge(sharedConfig, { devtool: 'sourcemap', stats: { @@ -14,11 +12,5 @@ module.exports = merge(sharedConfig.config, { output: { pathinfo: true - }, - - plugins: [ - new webpack.LoaderOptionsPlugin({ - debug: true - }) - ] + } }) diff --git a/lib/install/config/webpack/development.server.js b/lib/install/config/webpack/development.server.js new file mode 100644 index 000000000..fe840c6e4 --- /dev/null +++ b/lib/install/config/webpack/development.server.js @@ -0,0 +1,17 @@ +// Note: You must restart bin/webpack-dev-server for changes to take effect + +const { resolve } = require('path') +const merge = require('webpack-merge') +const devConfig = require('./development.js') +const { devServer, publicPath, paths } = require('./configuration.js') + +module.exports = merge(devConfig, { + devServer: { + host: devServer.host, + port: devServer.port, + compress: true, + historyApiFallback: true, + contentBase: resolve(paths.output, paths.entry), + publicPath + } +}) diff --git a/lib/install/config/webpack/development.server.yml b/lib/install/config/webpack/development.server.yml new file mode 100644 index 000000000..5dbfb7c88 --- /dev/null +++ b/lib/install/config/webpack/development.server.yml @@ -0,0 +1,4 @@ +# Restart webpack-dev-server if you make changes here +enabled: true +host: localhost +port: 8080 diff --git a/lib/install/config/webpack/paths.yml b/lib/install/config/webpack/paths.yml new file mode 100644 index 000000000..818d71fc8 --- /dev/null +++ b/lib/install/config/webpack/paths.yml @@ -0,0 +1,18 @@ +# Restart webpack-watcher or webpack-dev-server if you make changes here +config: config/webpack +entry: packs +output: public +node_modules: node_modules +source: app/javascript +extensions: + - .coffee + - .js + - .jsx + - .ts + - .vue + - .sass + - .css + - .png + - .svg + - .gif + - .jpeg diff --git a/lib/install/config/production.js b/lib/install/config/webpack/production.js similarity index 65% rename from lib/install/config/production.js rename to lib/install/config/webpack/production.js index 83b8f2ee3..82e2e9ff6 100644 --- a/lib/install/config/production.js +++ b/lib/install/config/webpack/production.js @@ -1,23 +1,20 @@ -// Note: You must restart bin/webpack-watcher for changes to take effect +/* eslint global-require: 0 */ +// Note: You must run bin/webpack for changes to take effect const webpack = require('webpack') const merge = require('webpack-merge') const CompressionPlugin = require('compression-webpack-plugin') - const sharedConfig = require('./shared.js') -module.exports = merge(sharedConfig.config, { +module.exports = merge(sharedConfig, { output: { filename: '[name]-[chunkhash].js' }, plugins: [ - new webpack.LoaderOptionsPlugin({ - minimize: true - }), new webpack.optimize.UglifyJsPlugin(), new CompressionPlugin({ asset: '[path].gz[query]', algorithm: 'gzip', - test: /\.js$/ + test: /\.(js|css|svg|eot|ttf|woff|woff2)$/ }) ] }) diff --git a/lib/install/config/webpack/shared.js b/lib/install/config/webpack/shared.js new file mode 100644 index 000000000..39492aa44 --- /dev/null +++ b/lib/install/config/webpack/shared.js @@ -0,0 +1,51 @@ +// Note: You must restart bin/webpack-watcher for changes to take effect +/* eslint global-require: 0 */ +/* eslint import/no-dynamic-require: 0 */ + +const webpack = require('webpack') +const { basename, join, resolve } = require('path') +const { sync } = require('glob') +const { readdirSync } = require('fs') +const ExtractTextPlugin = require('extract-text-webpack-plugin') +const ManifestPlugin = require('webpack-manifest-plugin') +const extname = require('path-complete-extname') +const { env, paths, publicPath, loadersDir } = require('./configuration.js') + +const extensionGlob = `*{${paths.extensions.join(',')}}*` +const packPaths = sync(join(paths.source, paths.entry, extensionGlob)) + +module.exports = { + entry: packPaths.reduce( + (map, entry) => { + const localMap = map + localMap[basename(entry, extname(entry))] = resolve(entry) + return localMap + }, {} + ), + + output: { filename: '[name].js', path: resolve(paths.output, paths.entry) }, + + module: { + rules: readdirSync(loadersDir).map(file => ( + require(join(loadersDir, file)) + )) + }, + + plugins: [ + new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))), + new ExtractTextPlugin(env.NODE_ENV === 'production' ? '[name]-[hash].css' : '[name].css'), + new ManifestPlugin({ fileName: 'manifest.json', publicPath, writeToFileEmit: true }) + ], + + resolve: { + extensions: paths.extensions, + modules: [ + resolve(paths.source), + resolve(paths.node_modules) + ] + }, + + resolveLoader: { + modules: [paths.node_modules] + } +} diff --git a/lib/install/examples/vue/hello_vue.js b/lib/install/examples/vue/hello_vue.js index 5ae2e3605..6141a86ec 100644 --- a/lib/install/examples/vue/hello_vue.js +++ b/lib/install/examples/vue/hello_vue.js @@ -4,7 +4,7 @@ // like app/views/layouts/application.html.erb. // All it does is render
Hello Vue
at the bottom of the page. -import Vue from 'vue' +import Vue from 'vue/dist/vue.esm' import App from './app.vue' document.addEventListener('DOMContentLoaded', () => { diff --git a/lib/install/react.rb b/lib/install/react.rb new file mode 100644 index 000000000..b5f3b737f --- /dev/null +++ b/lib/install/react.rb @@ -0,0 +1,15 @@ +require "webpacker/configuration" + +puts "Copying react loader to #{Webpacker::Configuration.config_path}/loaders" +copy_file "#{__dir__}/config/loaders/installers/react.js", "config/webpack/loaders/react.js" + +puts "Copying .babelrc to app root directory" +copy_file "#{__dir__}/examples/react/.babelrc", ".babelrc" + +puts "Copying react example entry file to #{Webpacker::Configuration.entry_path}" +copy_file "#{__dir__}/examples/react/hello_react.jsx", "#{Webpacker::Configuration.entry_path}/hello_react.jsx" + +puts "Installing all react dependencies" +run "./bin/yarn add react react-dom babel-preset-react" + +puts "Webpacker now supports react.js 🎉" diff --git a/lib/install/template.rb b/lib/install/template.rb index 0f37d6afd..03217f3ec 100644 --- a/lib/install/template.rb +++ b/lib/install/template.rb @@ -1,24 +1,29 @@ -directory "#{__dir__}/javascript", 'app/javascript' +# Install webpacker +puts "Creating javascript app source directory" +directory "#{__dir__}/javascript", "app/javascript" -directory "#{__dir__}/bin", 'bin' -chmod 'bin', 0755 & ~File.umask, verbose: false +puts "Copying binstubs" +directory "#{__dir__}/bin", "bin" +chmod "bin", 0755 & ~File.umask, verbose: false -directory "#{__dir__}/config", 'config/webpack' +puts "Copying webpack core config and loaders" +directory "#{__dir__}/config/webpack", "config/webpack" +directory "#{__dir__}/config/loaders/core", "config/webpack/loaders" +copy_file "#{__dir__}/config/.postcssrc.yml", ".postcssrc.yml" -append_to_file '.gitignore', <<-EOS +append_to_file ".gitignore", <<-EOS /public/packs /node_modules EOS -run './bin/yarn add webpack webpack-merge path-complete-extname babel-loader babel-core babel-preset-env coffee-loader coffee-script compression-webpack-plugin rails-erb-loader glob' -run './bin/yarn add --dev webpack-dev-server' +puts "Installing all JavaScript dependencies" +run "./bin/yarn add webpack webpack-merge js-yaml path-complete-extname " \ +"webpack-manifest-plugin babel-loader coffee-loader coffee-script " \ +"babel-core babel-preset-env compression-webpack-plugin rails-erb-loader glob " \ +"extract-text-webpack-plugin node-sass file-loader sass-loader css-loader style-loader " \ +"postcss-loader autoprefixer postcss-smart-import precss" -environment \ - "# Make javascript_pack_tag lookup digest hash to enable long-term caching\n" + - " config.x.webpacker[:digesting] = true\n", - env: 'production' +puts "Installing dev server for live reloading" +run "./bin/yarn add --dev webpack-dev-server" -environment \ - "# Make javascript_pack_tag load assets from webpack-dev-server.\n" + - " # config.x.webpacker[:dev_server_host] = \"http://localhost:8080\"\n", - env: 'development' +puts "Webpacker successfully installed 🎉 🍰" diff --git a/lib/install/vue.rb b/lib/install/vue.rb new file mode 100644 index 000000000..512c1b998 --- /dev/null +++ b/lib/install/vue.rb @@ -0,0 +1,15 @@ +require "webpacker/configuration" + +puts "Copying vue loader to #{Webpacker::Configuration.config_path}/loaders" +copy_file "#{__dir__}/config/loaders/installers/vue.js", "config/webpack/loaders/vue.js" + +puts "Copying the example entry file to #{Webpacker::Configuration.entry_path}" +copy_file "#{__dir__}/examples/vue/hello_vue.js", "#{Webpacker::Configuration.entry_path}/hello_vue.js" + +puts "Copying vue app file to #{Webpacker::Configuration.entry_path}" +copy_file "#{__dir__}/examples/vue/app.vue", "#{Webpacker::Configuration.entry_path}/app.vue" + +puts "Installing all vue dependencies" +run "./bin/yarn add vue vue-loader vue-template-compiler sass-loader node-sass css-loader" + +puts "Webpacker now supports vue.js 🎉" diff --git a/lib/tasks/installers/angular.rake b/lib/tasks/installers/angular.rake index 3e13d3bb6..9bac3eb6c 100644 --- a/lib/tasks/installers/angular.rake +++ b/lib/tasks/installers/angular.rake @@ -1,46 +1,14 @@ +ANGULAR_TEMPLATE_PATH = File.expand_path("../../install/angular.rb", __dir__) + namespace :webpacker do namespace :install do desc "Install everything needed for Angular" - task :angular do - config_path = Rails.root.join('config/webpack/shared.js') - - config = begin - File.read(config_path) - rescue Errno::ENOENT - puts 'Webpack config not found. Make sure webpacker:install' \ - ' is run successfully before installing angular' - exit! - end - - if config.include?('ts-loader') - puts "The configuration file already has a reference to ts-loader, skipping the test rule..." - else - puts "Adding a loader rule to include ts-loader for .ts files in #{config_path}..." - config.gsub!(/rules:(\s*\[)(\s*\{)/, "rules:\\1\\2 test: /\.ts$/, loader: 'ts-loader' },\\2") - end - - if config =~ /["'].ts["']/ - puts "The configuration file already has a reference to .ts extension, skipping the addition of this extension to the list..." + task angular: ["webpacker:verify_install"] do + if Rails::VERSION::MAJOR >= 5 + exec "./bin/rails app:template LOCATION=#{ANGULAR_TEMPLATE_PATH}" else - puts "Adding '.ts' in loader extensions in #{config_path}..." - config.gsub!(/extensions:(.*')(\s*\])/, "extensions:\\1, '.ts'\\2") + exec "./bin/rake rails:template LOCATION=#{ANGULAR_TEMPLATE_PATH}" end - - File.write config_path, config - - puts "Copying Angular example to app/javascript/packs/hello_angular.js" - FileUtils.copy File.expand_path('../../install/examples/angular/hello_angular.js', __dir__), - Rails.root.join('app/javascript/packs/hello_angular.js') - - puts "Copying Angular Hello app to app/javascript/hello_angular" - FileUtils.copy_entry File.expand_path('../../install/examples/angular/hello_angular', __dir__), - Rails.root.join('app/javascript/hello_angular') - - puts "Copying tsconfig.json to the Rails root directory" - FileUtils.copy File.expand_path('../../install/examples/angular/tsconfig.json', __dir__), - Rails.root.join('tsconfig.json') - - exec './bin/yarn add typescript ts-loader core-js zone.js rxjs @angular/core @angular/common @angular/compiler @angular/platform-browser @angular/platform-browser-dynamic' end end end diff --git a/lib/tasks/installers/react.rake b/lib/tasks/installers/react.rake index 2e10a8899..9b934403d 100644 --- a/lib/tasks/installers/react.rake +++ b/lib/tasks/installers/react.rake @@ -1,42 +1,14 @@ +REACT_TEMPLATE_PATH = File.expand_path("../../install/react.rb", __dir__) + namespace :webpacker do namespace :install do desc "Install everything needed for react" - task :react do - config_path = Rails.root.join('config/webpack/shared.js') - - config = begin - File.read(config_path) - rescue Errno::ENOENT - puts 'Webpack config not found. Make sure webpacker:install is' \ - ' run successfully before installing react' - exit! - end - - if config =~ /presets:\s*\[\s*\[\s*'env'/ - puts "Replacing loader presets to include react in #{config_path}" - config.gsub!(/presets:(\s*\[)(\s*)\[(\s)*'env'/, "presets:\\1\\2'react',\\2[\\3'env'") + task react: ["webpacker:verify_install"] do + if Rails::VERSION::MAJOR >= 5 + exec "./bin/rails app:template LOCATION=#{REACT_TEMPLATE_PATH}" else - puts "Couldn't automatically update loader presets in #{config_path}. Please set presets: [ 'react', [ 'env', { 'modules': false } ] ]." + exec "./bin/rake rails:template LOCATION=#{REACT_TEMPLATE_PATH}" end - - if config.include?("test: /\\.js(\\.erb)?$/") - puts "Replacing loader test to include react in #{config_path}" - config.gsub!("test: /\\.js(\\.erb)?$/", "test: /\\.jsx?(\\.erb)?$/") - else - puts "Couldn't automatically update loader test in #{config_path}. Please set test: /\\.jsx?(\\.erb)?$/." - end - - File.write config_path, config - - puts "Copying .babelrc to project directory" - FileUtils.copy File.expand_path('../../install/examples/react/.babelrc', __dir__), - Rails.root - - puts "Copying react example to app/javascript/packs/hello_react.jsx" - FileUtils.copy File.expand_path('../../install/examples/react/hello_react.jsx', __dir__), - Rails.root.join('app/javascript/packs/hello_react.jsx') - - exec './bin/yarn add react react-dom babel-preset-react' end end end diff --git a/lib/tasks/installers/vue.rake b/lib/tasks/installers/vue.rake index cdac9ce6b..0f612e16d 100644 --- a/lib/tasks/installers/vue.rake +++ b/lib/tasks/installers/vue.rake @@ -1,46 +1,14 @@ +VUE_TEMPLATE_PATH = File.expand_path("../../install/vue.rb", __dir__) + namespace :webpacker do namespace :install do desc "Install everything needed for Vue" - task :vue do - config_path = Rails.root.join('config/webpack/shared.js') - - config = begin - File.read(config_path) - rescue Errno::ENOENT - puts 'Webpack config not found. Make sure webpacker:install' \ - ' is run successfully before installing vue' - exit! - end - - # Module resolution https://webpack.js.org/concepts/module-resolution/ - if config.include?("'vue$':'vue/dist/vue.esm.js'") - puts "Couldn't automatically update module resolution in #{config_path}. Please set resolve { alias:{ 'vue$':'vue/dist/vue.esm.js' } }." - else - config.gsub!(/resolve:(\s*\{)(\s*)extensions/,"resolve:\\1\\2alias: { 'vue$':'vue/dist/vue.esm.js' },\\2extensions") - end - - if config.include?("loader: 'url-loader?mimetype=image/png'") - puts "Couldn't automatically update url-loader in #{config_path}. Please set { test: /\.png$/, loader: 'url-loader?mimetype=image/png' }." + task vue: ["webpacker:verify_install"] do + if Rails::VERSION::MAJOR >= 5 + exec "./bin/rails app:template LOCATION=#{VUE_TEMPLATE_PATH}" else - config.gsub!(/module:(\s*\{)(\s*)rules:(\s*)\[/,"module:\\1\\2rules:\\3[\\2 { test: /\.png$/, loader: 'url-loader?mimetype=image/png'},") + exec "./bin/rake rails:template LOCATION=#{VUE_TEMPLATE_PATH}" end - - if config.include?("loader: 'vue-loader',") - puts "Couldn't automatically update vue-loader in #{config_path}. Please set { test: /.vue$/, loader: 'vue-loader', options: { loaders: { 'scss': 'vue-style-loader!css-loader!sass-loader', 'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax'}}}." - else - config.gsub!(/module:(\s*\{)(\s*)rules:(\s*)\[/,"module:\\1\\2rules:\\3[\\2 {\\2 test: /\.vue$/, loader: 'vue-loader',\\2 options: {\\2 loaders: { 'scss': 'vue-style-loader!css-loader!sass-loader', 'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax'}\\2 }\\2 },") - end - - File.write config_path, config - - puts "Copying the Vue example to app/javascript/packs/vue" - FileUtils.copy File.expand_path('../../install/examples/vue/hello_vue.js', File.dirname(__FILE__)), - Rails.root.join('app/javascript/packs/hello_vue.js') - - FileUtils.copy File.expand_path('../../install/examples/vue/app.vue', File.dirname(__FILE__)), - Rails.root.join('app/javascript/packs/app.vue') - - exec "./bin/yarn add vue vue-loader vue-template-compiler sass-loader node-sass css-loader url-loader axios" end end end diff --git a/lib/tasks/webpacker.rake b/lib/tasks/webpacker.rake index 722bc9523..1b18788d1 100644 --- a/lib/tasks/webpacker.rake +++ b/lib/tasks/webpacker.rake @@ -1,13 +1,15 @@ tasks = { - 'webpacker:install' => 'Installs and setup webpack with yarn', - 'webpacker:compile' => 'Compiles webpack bundles based on environment', - 'webpacker:install:react' => 'Installs and setup example react component', - 'webpacker:install:vue' => 'Installs and setup example vue component', - 'webpacker:install:angular' => 'Installs and setup example angular2 component' + "webpacker:install" => "Installs and setup webpack with yarn", + "webpacker:compile" => "Compiles webpack bundles based on environment", + "webpacker:verify_install" => "Verifies if webpacker is installed", + "webpacker:yarn_install" => "Support for older Rails versions. Install all JavaScript dependencies as specified via Yarn", + "webpacker:install:react" => "Installs and setup example react component", + "webpacker:install:vue" => "Installs and setup example vue component", + "webpacker:install:angular" => "Installs and setup example angular2 component" }.freeze -desc 'Lists available tasks under webpacker' +desc "Lists available tasks under webpacker" task :webpacker do - puts 'Available webpacker tasks are:' + puts "Available webpacker tasks are:" tasks.each { |task, message| puts task.ljust(30) + message } end diff --git a/lib/tasks/webpacker/compile.rake b/lib/tasks/webpacker/compile.rake index e1b26c80e..2428a4f06 100644 --- a/lib/tasks/webpacker/compile.rake +++ b/lib/tasks/webpacker/compile.rake @@ -1,35 +1,29 @@ +require "webpacker/configuration" REGEX_MAP = /\A.*\.map\z/ namespace :webpacker do desc "Compile javascript packs using webpack for production with digests" - task :compile => :environment do - dist_dir = Rails.application.config.x.webpacker[:packs_dist_dir] - result = `WEBPACK_DIST_DIR=#{dist_dir} NODE_ENV=production ./bin/webpack --json` + task compile: ["webpacker:verify_install", :environment] do + puts "Compiling webpacker assets 🎉" + result = `NODE_ENV=production ./bin/webpack` unless $?.success? - puts JSON.parse(result)['errors'] + puts JSON.parse(result)["errors"] exit! $?.exitstatus end - webpack_digests = JSON.parse(result)['assetsByChunkName'].each_with_object({}) do |(chunk, file), h| - h[chunk] = file.is_a?(Array) ? file.find {|f| REGEX_MAP !~ f } : file - end.to_json - - digests_path = Rails.application.config.x.webpacker[:digests_path] - packs_path = Rails.root.join('public', dist_dir) || File.dirname(digests_path) - packs_digests_path = digests_path || Rails.root.join(packs_path, 'digests.json') - - FileUtils.mkdir_p(packs_path) - File.open(packs_digests_path, 'w+') { |file| file.write webpack_digests } - - puts "Compiled digests for all packs in #{packs_digests_path}: " - puts webpack_digests + puts "Compiled digests for all packs in #{Webpacker::Configuration.output_path}: " + puts JSON.parse(File.read(Webpacker::Configuration.manifest_path)) end end # Compile packs after we've compiled all other assets during precompilation -if Rake::Task.task_defined?('assets:precompile') - Rake::Task['assets:precompile'].enhance do - Rake::Task['webpacker:compile'].invoke +if Rake::Task.task_defined?("assets:precompile") + Rake::Task["assets:precompile"].enhance do + unless Rake::Task.task_defined?("yarn:install") + # For Rails < 5.1 + Rake::Task["webpacker:yarn_install"].invoke + end + Rake::Task["webpacker:compile"].invoke end end diff --git a/lib/tasks/webpacker/install.rake b/lib/tasks/webpacker/install.rake index 770070a03..e6bd16e8d 100644 --- a/lib/tasks/webpacker/install.rake +++ b/lib/tasks/webpacker/install.rake @@ -1,4 +1,4 @@ -WEBPACKER_APP_TEMPLATE_PATH = File.expand_path('../../install/template.rb', __dir__) +WEBPACKER_APP_TEMPLATE_PATH = File.expand_path("../../install/template.rb", __dir__) namespace :webpacker do desc "Install webpacker in this application" diff --git a/lib/tasks/webpacker/verify_install.rake b/lib/tasks/webpacker/verify_install.rake new file mode 100644 index 000000000..06aa46f9b --- /dev/null +++ b/lib/tasks/webpacker/verify_install.rake @@ -0,0 +1,16 @@ +require "webpacker/configuration" + +namespace :webpacker do + desc "Verifies if webpacker is installed" + task :verify_install do + if File.exist?(Webpacker::Configuration.file_path) + puts "Webpacker is installed 🎉 🍰" + puts "Using #{Webpacker::Configuration.file_path} file for setting up webpack paths" + else + puts "Configuration config/webpack/paths.yml file not found. \n"\ + "Make sure webpacker:install is run successfully before " \ + "running dependent tasks" + exit! + end + end +end diff --git a/lib/tasks/webpacker/yarn_install.rake b/lib/tasks/webpacker/yarn_install.rake new file mode 100644 index 000000000..0a0b824ec --- /dev/null +++ b/lib/tasks/webpacker/yarn_install.rake @@ -0,0 +1,6 @@ +namespace :webpacker do + desc "Support for older Rails versions. Install all JavaScript dependencies as specified via Yarn" + task :yarn_install do + system("./bin/yarn") + end +end diff --git a/lib/webpacker.rb b/lib/webpacker.rb index 463f2ee7b..7ae894239 100644 --- a/lib/webpacker.rb +++ b/lib/webpacker.rb @@ -1,4 +1,4 @@ module Webpacker end -require 'webpacker/railtie' if defined?(Rails) +require "webpacker/railtie" if defined?(Rails) diff --git a/lib/webpacker/configuration.rb b/lib/webpacker/configuration.rb new file mode 100644 index 000000000..ac96a89e6 --- /dev/null +++ b/lib/webpacker/configuration.rb @@ -0,0 +1,42 @@ +# Loads webpacker configuration from config/webpack/paths.yml +require "webpacker/file_loader" + +class Webpacker::Configuration < Webpacker::FileLoader + class << self + def config_path + Rails.root.join(paths.fetch(:config, "config/webpack")) + end + + def entry_path + Rails.root.join(source_path, paths.fetch(:entry, "packs")) + end + + def file_path + Rails.root.join("config", "webpack", "paths.yml") + end + + def manifest_path + Rails.root.join(output_path, "manifest.json") + end + + def output_path + Rails.root.join(paths.fetch(:output, "public"), paths.fetch(:entry, "packs")) + end + + def paths + load if Rails.env.development? + raise Webpacker::FileLoader::FileLoaderError.new("Webpacker::Configuration.load must be called first") unless instance + instance.data + end + + def source_path + Rails.root.join(paths.fetch(:source, "app/javascript")) + end + end + + private + def load + return super unless File.exist?(@path) + HashWithIndifferentAccess.new(YAML.load(File.read(@path))) + end +end diff --git a/lib/webpacker/digests.rb b/lib/webpacker/digests.rb deleted file mode 100644 index ccf74b833..000000000 --- a/lib/webpacker/digests.rb +++ /dev/null @@ -1,42 +0,0 @@ -# Singleton registry for accessing the digested filenames computed by Webpack in production mode. -# This allows javascript_pack_tag to take a reference to, say, "calendar.js" and turn it into -# "calendar-1016838bab065ae1e314.js". These digested filenames are what enables you to long-term -# cache things in production. -class Webpacker::Digests - class DigestError < StandardError; end - - class_attribute :instance - - class << self - def load(path) - self.instance = new(path) - end - - def lookup(name) - if instance - instance.lookup(name).presence || raise(DigestError.new("Can't find #{name} in #{instance.inspect}")) - else - raise DigestError.new("Webpacker::Digests.load(path) must be called first") - end - end - end - - def initialize(path) - @path = path - @digests = load - end - - def lookup(name) - @digests[name.to_s] - end - - private - def load - if File.exist?(@path) - JSON.parse(File.read(@path)) - else - Rails.logger.info "Didn't find any digests file at #{@path}. You must first compile the packs via rails webpacker:compile" - {} - end - end -end diff --git a/lib/webpacker/file_loader.rb b/lib/webpacker/file_loader.rb new file mode 100644 index 000000000..8d478420b --- /dev/null +++ b/lib/webpacker/file_loader.rb @@ -0,0 +1,24 @@ +# Provides a base singleton-configuration pattern for loading a file, given a path +class Webpacker::FileLoader + class NotFoundError < StandardError; end + class FileLoaderError < StandardError; end + + class_attribute :instance + attr_accessor :data + + class << self + def load(path = file_path) + self.instance = new(path) + end + end + + private + def initialize(path) + @path = path + @data = load + end + + def load + {}.freeze + end +end diff --git a/lib/webpacker/helper.rb b/lib/webpacker/helper.rb index 5875729c0..9660c7d70 100644 --- a/lib/webpacker/helper.rb +++ b/lib/webpacker/helper.rb @@ -1,8 +1,20 @@ -require 'webpacker/source' +require "webpacker/manifest" module Webpacker::Helper + # Computes the full path for a given webpacker asset. + # Return relative path using manifest.json and passes it to asset_url helper + # This will use asset_path internally, so most of their behaviors will be the same. + # Examples: + # + # In development mode: + # <%= asset_pack_path 'calendar.js' %> # => "/packs/calendar.js" + # 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) + 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 + # 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. # # Examples: @@ -15,6 +27,23 @@ module Webpacker::Helper # <%= javascript_pack_tag 'calendar', 'data-turbolinks-track': 'reload' %> # => # def javascript_pack_tag(name, **options) - javascript_include_tag(Webpacker::Source.new(name).path, **options) + javascript_include_tag(Webpacker::Manifest.lookup("#{name}#{compute_asset_extname(name, type: :javascript)}"), **options) + end + + # Creates a link 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. + # + # Examples: + # + # # In development mode: + # <%= stylesheet_pack_tag 'calendar', 'data-turbolinks-track': 'reload' %> # => + # + # + # # In production mode: + # <%= stylesheet_pack_tag 'calendar', 'data-turbolinks-track': 'reload' %> # => + # + def stylesheet_pack_tag(name, **options) + stylesheet_link_tag(Webpacker::Manifest.lookup("#{name}#{compute_asset_extname(name, type: :stylesheet)}"), **options) end end diff --git a/lib/webpacker/manifest.rb b/lib/webpacker/manifest.rb new file mode 100644 index 000000000..eed6924c1 --- /dev/null +++ b/lib/webpacker/manifest.rb @@ -0,0 +1,29 @@ +# Singleton registry for accessing the packs path using generated manifest. +# This allows javascript_pack_tag, stylesheet_pack_tag, asset_pack_path to take a reference to, +# say, "calendar.js" or "calendar.css" and turn it into "/packs/calendar.js" or +# "/packs/calendar.css" in development. In production mode, it returns compiles +# files, # "/packs/calendar-1016838bab065ae1e314.js" and +# "/packs/calendar-1016838bab065ae1e314.css" for long-term caching + +require "webpacker/file_loader" +require "webpacker/configuration" + +class Webpacker::Manifest < Webpacker::FileLoader + class << self + def file_path + Webpacker::Configuration.manifest_path + end + + def lookup(name) + load if Rails.env.development? + raise Webpacker::FileLoader::FileLoaderError.new("Webpacker::Manifest.load must be called first") unless instance + instance.data[name.to_s] || raise(Webpacker::FileLoader::NotFoundError.new("Can't find #{name} in #{file_path}. Is webpack still compiling?")) + end + end + + private + def load + return super unless File.exist?(@path) + JSON.parse(File.read(@path)) + end +end diff --git a/lib/webpacker/railtie.rb b/lib/webpacker/railtie.rb index 2b096a25b..f710a0264 100644 --- a/lib/webpacker/railtie.rb +++ b/lib/webpacker/railtie.rb @@ -1,7 +1,6 @@ -require 'rails/railtie' +require "rails/railtie" -require 'webpacker/helper' -require 'webpacker/digests' +require "webpacker/helper" class Webpacker::Engine < ::Rails::Engine initializer :webpacker do |app| @@ -9,18 +8,9 @@ class Webpacker::Engine < ::Rails::Engine ActionController::Base.helper Webpacker::Helper end - app.config.x.webpacker[:packs_dist_dir] ||= 'packs' - app.config.x.webpacker[:packs_dist_path] ||= \ - "/#{app.config.x.webpacker[:packs_dist_dir]}" - - if app.config.x.webpacker[:digesting] - app.config.x.webpacker[:digests_path] ||= \ - Rails.root.join('public', - app.config.x.webpacker[:packs_dist_dir], - 'digests.json') - - Webpacker::Digests.load \ - app.config.x.webpacker[:digests_path] - end + # Loads webpacker config data from config/webpack/paths.yml + Webpacker::Configuration.load + # Loads manifest data from public/packs/manifest.json + Webpacker::Manifest.load end end diff --git a/lib/webpacker/source.rb b/lib/webpacker/source.rb deleted file mode 100644 index 18ab4de1c..000000000 --- a/lib/webpacker/source.rb +++ /dev/null @@ -1,38 +0,0 @@ -# Translates a logical reference for a pack source into the final path needed in the HTML. -# This translation takes into account whether digesting is configured to happen, which it -# is by default in the production environment (as set via -# `Rails.configuration.x.webpacker[:digesting] = true`). -class Webpacker::Source - def initialize(name) - @name = name - end - - def path - if config[:dev_server_host].present? - "#{config[:dev_server_host]}/#{filename}" - elsif config[:digesting] - File.join(dist_path, digested_filename) - else - File.join(dist_path, filename) - end - end - - private - attr_accessor :name - - def config - Rails.configuration.x.webpacker - end - - def digested_filename - Webpacker::Digests.lookup(name) - end - - def dist_path - config[:packs_dist_path] - end - - def filename - "#{name}.js" - end -end diff --git a/lib/webpacker/version.rb b/lib/webpacker/version.rb new file mode 100644 index 000000000..24b5532d5 --- /dev/null +++ b/lib/webpacker/version.rb @@ -0,0 +1,3 @@ +module Webpacker + VERSION = "1.0".freeze +end diff --git a/webpacker.gemspec b/webpacker.gemspec index 66f05570d..2c6a9b49b 100644 --- a/webpacker.gemspec +++ b/webpacker.gemspec @@ -1,19 +1,22 @@ +$:.push File.expand_path("../lib", __FILE__) +require "webpacker/version" + Gem::Specification.new do |s| - s.name = 'webpacker' - s.version = '1.0' - 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.name = "webpacker" + s.version = Webpacker::VERSION + 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.required_ruby_version = ">= 1.9.3" - s.add_dependency 'activesupport', '>= 4.2' - s.add_dependency 'multi_json', '~> 1.2' - s.add_dependency 'railties', '>= 4.2' + s.add_dependency "activesupport", ">= 4.2" + s.add_dependency "multi_json", "~> 1.2" + s.add_dependency "railties", ">= 4.2" - s.add_development_dependency 'bundler', '~> 1.12' + s.add_development_dependency "bundler", "~> 1.12" s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- test/*`.split("\n")