shakapacker 6.2.1 → 6.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -4
  3. data/CONTRIBUTING.md +2 -2
  4. data/Gemfile +2 -1
  5. data/Gemfile.lock +84 -74
  6. data/README.md +172 -65
  7. data/docs/react.md +262 -0
  8. data/docs/sprockets.md +10 -0
  9. data/docs/v6_upgrade.md +1 -1
  10. data/lib/install/config/webpacker.yml +14 -0
  11. data/lib/tasks/webpacker/clean.rake +1 -3
  12. data/lib/tasks/webpacker/clobber.rake +1 -3
  13. data/lib/tasks/webpacker/compile.rake +3 -14
  14. data/lib/tasks/webpacker.rake +0 -1
  15. data/lib/webpacker/base_strategy.rb +24 -0
  16. data/lib/webpacker/compiler.rb +4 -67
  17. data/lib/webpacker/compiler_strategy.rb +20 -0
  18. data/lib/webpacker/configuration.rb +13 -0
  19. data/lib/webpacker/digest_strategy.rb +59 -0
  20. data/lib/webpacker/helper.rb +26 -1
  21. data/lib/webpacker/instance.rb +4 -0
  22. data/lib/webpacker/manifest.rb +2 -2
  23. data/lib/webpacker/mtime_strategy.rb +40 -0
  24. data/lib/webpacker/version.rb +1 -1
  25. data/package/babel/preset.js +0 -1
  26. data/package/environments/__tests__/base.js +30 -3
  27. data/package/environments/base.js +8 -1
  28. data/package/rules/file.js +2 -2
  29. data/package.json +13 -12
  30. data/test/compiler_strategy_test.rb +27 -0
  31. data/test/compiler_test.rb +26 -34
  32. data/test/configuration_test.rb +24 -4
  33. data/test/digest_strategy_test.rb +33 -0
  34. data/test/helper_test.rb +22 -0
  35. data/test/manifest_test.rb +3 -3
  36. data/test/mtime_strategy_test.rb +42 -0
  37. data/test/rake_tasks_test.rb +0 -34
  38. data/test/test_app/app/packs/entrypoints/generated/something.js +2 -0
  39. data/test/test_app/config/webpacker.yml +1 -0
  40. data/test/test_app/config/webpacker_nested_entries.yml +83 -0
  41. data/test/test_app/config/webpacker_no_precompile.yml +7 -0
  42. data/yarn.lock +917 -884
  43. metadata +21 -5
  44. data/lib/tasks/webpacker/yarn_install.rake +0 -18
  45. data/lib/tasks/yarn.rake +0 -38
data/docs/react.md ADDED
@@ -0,0 +1,262 @@
1
+ # React Integration
2
+
3
+ These steps describe how to create a Rails/React app, using Shakapacker as the bundler.
4
+
5
+ Before starting, ensure that you have Yarn installed, for example:
6
+
7
+ ```shell
8
+ npm i -g yarn
9
+ ```
10
+
11
+ ## Basic Setup
12
+
13
+ Create a new Rails app as per the [installation instructions in the README](https://github.com/shakacode/shakapacker#installation).
14
+
15
+ Add React, as well as the necessary libraries to enable CSS support in your application:
16
+
17
+ ```shell
18
+ yarn add react react-dom @babel/preset-react
19
+ yarn add css-loader style-loader mini-css-extract-plugin css-minimizer-webpack-plugin
20
+ ```
21
+
22
+ Update the Babel configuration in the `package.json` file:
23
+
24
+ ```diff
25
+ "babel": {
26
+ "presets": [
27
+ "./node_modules/shakapacker/package/babel/preset.js",
28
+ + "@babel/preset-react"
29
+ ]
30
+ },
31
+ ```
32
+
33
+ And that's it. You can now create a React app using `app/javascript/application.js` as your entry point.
34
+
35
+ ## Enabling Hot Module Replacement (HMR)
36
+
37
+ With HMR enabled, Shakapacker will automatically update only that part of the page that changed when it detects changes in your project files. This has the nice advantage of preserving your app’s state.
38
+
39
+ To enable HMR in a React app, proceed as follows:.
40
+
41
+ In `config/webpacker.yml` set `hmr` is set to `true`.
42
+
43
+ Install the [react-refresh](https://www.npmjs.com/package/react-refresh) package, as well as [@pmmmwh/react-refresh-webpack-plugin](https://www.npmjs.com/package/@pmmmwh/react-refresh-webpack-plugin):
44
+
45
+ ```shell
46
+ yarn add --dev react-refresh @pmmmwh/react-refresh-webpack-plugin
47
+ ```
48
+
49
+ Alter `config/webpack/webpack.config.js` like so:
50
+
51
+ ```js
52
+ const { webpackConfig, inliningCss } = require('shakapacker');
53
+ const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
54
+ const isDevelopment = process.env.NODE_ENV !== 'production';
55
+
56
+ if (isDevelopment && inliningCss) {
57
+ webpackConfig.plugins.push(
58
+ new ReactRefreshWebpackPlugin({
59
+ overlay: {
60
+ sockPort: webpackConfig.devServer.port,
61
+ },
62
+ })
63
+ );
64
+ }
65
+
66
+ module.exports = webpackConfig;
67
+ ```
68
+
69
+ This applies the plugin to the webpack configuration.
70
+
71
+ Delete the Babel configuration from `package.json`:
72
+
73
+ ```diff
74
+ - "babel": {
75
+ - "presets": [
76
+ - "./node_modules/shakapacker/package/babel/preset.js",
77
+ - "@babel/preset-react"
78
+ - ]
79
+ - },
80
+ ```
81
+
82
+ Then create a `babel.config.js` file in the root of project and add the following:
83
+
84
+ ```js
85
+ module.exports = function (api) {
86
+ const defaultConfigFunc = require('shakapacker/package/babel/preset.js')
87
+ const resultConfig = defaultConfigFunc(api)
88
+ const isDevelopmentEnv = api.env('development')
89
+ const isProductionEnv = api.env('production')
90
+ const isTestEnv = api.env('test')
91
+
92
+ const changesOnDefault = {
93
+ presets: [
94
+ [
95
+ '@babel/preset-react',
96
+ {
97
+ development: isDevelopmentEnv || isTestEnv,
98
+ useBuiltIns: true
99
+ }
100
+ ]
101
+ ].filter(Boolean),
102
+ plugins: [
103
+ isProductionEnv && ['babel-plugin-transform-react-remove-prop-types',
104
+ {
105
+ removeImport: true
106
+ }
107
+ ],
108
+ process.env.WEBPACK_SERVE && 'react-refresh/babel'
109
+ ].filter(Boolean),
110
+ }
111
+
112
+ resultConfig.presets = [...resultConfig.presets, ...changesOnDefault.presets]
113
+ resultConfig.plugins = [...resultConfig.plugins, ...changesOnDefault.plugins ]
114
+
115
+ return resultConfig
116
+ }
117
+ ```
118
+
119
+ This is taken from the [sample React Babel config](https://github.com/jameshibbard/shakapacker/blob/master/docs/customizing_babel_config.md#react-configuration).
120
+
121
+ HMR for your React app is now enabled. 🚀
122
+
123
+ ## A Basic Demo App
124
+
125
+ To test that all of the above is working, you can follow these instructions to create a basic React app using Shakapacker.
126
+
127
+ 1. Create a new Rails app:
128
+ ```shell
129
+ rails new myapp --skip-javascript
130
+ cd myapp
131
+ bundle add shakapacker --strict
132
+ ./bin/bundle install
133
+ ./bin/rails webpacker:install
134
+ yarn add react react-dom @babel/preset-react
135
+ yarn add css-loader style-loader mini-css-extract-plugin css-minimizer-webpack-plugin
136
+ ```
137
+
138
+ 2. Generate a controller
139
+ ```shell
140
+ rails g controller site index
141
+ echo '<div id="root"></div>' > app/views/site/index.html.erb
142
+ ```
143
+
144
+ 3. Create a CSS file and a React component:
145
+ ```shell
146
+ touch app/javascript/App.css app/javascript/App.js
147
+ ```
148
+
149
+ 4. Edit `app/javascript/application.js` like so:
150
+ ```jsx
151
+ import React from 'react';
152
+ import { createRoot } from 'react-dom/client';
153
+ import HelloMessage from './App';
154
+
155
+ const container = document.getElementById('root');
156
+ const root = createRoot(container);
157
+
158
+ document.addEventListener('DOMContentLoaded', () => {
159
+ root.render(<HelloMessage name="World" />);
160
+ });
161
+ ```
162
+
163
+ 5. Add the following to `app/javascript/App.js`:
164
+ ```jsx
165
+ import React from 'react';
166
+ import 'App.css';
167
+ const HelloMessage = ({ name }) => <h1>Hello, {name}!</h1>;
168
+ export default HelloMessage;
169
+ ```
170
+
171
+ 6. Add the following to `app/javascript/App.css`:
172
+ ```css
173
+ h1 { color: blue; }
174
+ ```
175
+
176
+ 7. Enable HMR in config/webpacker.yml:
177
+ ```shell
178
+ hmr: true
179
+ ```
180
+
181
+ 8. Install the [react-refresh](https://www.npmjs.com/package/react-refresh) package, as well as [@pmmmwh/react-refresh-webpack-plugin](https://www.npmjs.com/package/@pmmmwh/react-refresh-webpack-plugin):
182
+
183
+ ```shell
184
+ yarn add --dev react-refresh @pmmmwh/react-refresh-webpack-plugin
185
+ ```
186
+
187
+ 9. Alter `config/webpack/webpack.config.js` like so:
188
+
189
+ ```js
190
+ const { webpackConfig, inliningCss } = require('shakapacker');
191
+ const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
192
+ const isDevelopment = process.env.NODE_ENV !== 'production';
193
+
194
+ if (isDevelopment && inliningCss) {
195
+ webpackConfig.plugins.push(
196
+ new ReactRefreshWebpackPlugin({
197
+ overlay: {
198
+ sockPort: webpackConfig.devServer.port,
199
+ },
200
+ })
201
+ );
202
+ }
203
+
204
+ module.exports = webpackConfig;
205
+ ```
206
+
207
+ 10. Remove the Babel configuration from `package.json`
208
+ ```diff
209
+ - "babel": {
210
+ - "presets": [
211
+ - "./node_modules/shakapacker/package/babel/preset.js"
212
+ - ]
213
+ - },
214
+ ```
215
+
216
+ 11. Create a `babel.config.js` file in the project root and add the following [sample code](https://github.com/shakacode/shakapacker/blob/master/docs/customizing_babel_config.md#react-configuration):
217
+ ```js
218
+ module.exports = function (api) {
219
+ const defaultConfigFunc = require('shakapacker/package/babel/preset.js')
220
+ const resultConfig = defaultConfigFunc(api)
221
+ const isDevelopmentEnv = api.env('development')
222
+ const isProductionEnv = api.env('production')
223
+ const isTestEnv = api.env('test')
224
+
225
+ const changesOnDefault = {
226
+ presets: [
227
+ [
228
+ '@babel/preset-react',
229
+ {
230
+ development: isDevelopmentEnv || isTestEnv,
231
+ useBuiltIns: true
232
+ }
233
+ ]
234
+ ].filter(Boolean),
235
+ plugins: [
236
+ isProductionEnv && ['babel-plugin-transform-react-remove-prop-types',
237
+ {
238
+ removeImport: true
239
+ }
240
+ ],
241
+ process.env.WEBPACK_SERVE && 'react-refresh/babel'
242
+ ].filter(Boolean),
243
+ }
244
+
245
+ resultConfig.presets = [...resultConfig.presets, ...changesOnDefault.presets]
246
+ resultConfig.plugins = [...resultConfig.plugins, ...changesOnDefault.plugins ]
247
+
248
+ return resultConfig
249
+ }
250
+ ```
251
+
252
+ 9. Start the Rails server and the webpack-dev-server in separate console windows:
253
+ ```shell
254
+ rails s
255
+ ./bin/webpacker-dev-server
256
+ ```
257
+
258
+ 10. Hit: <http://localhost:3000/site/index>
259
+
260
+ 11. Edit either the React component at `app/javascript/App.js` or the CSS file at `app/javascript/App.css` and observe the HMR goodness.
261
+
262
+ Note that HMR will not work if you edit `app/javascript/application.js` and you experience a full refresh with a warning in the console. For more info on this, see here: https://github.com/pmmmwh/react-refresh-webpack-plugin/issues/177
data/docs/sprockets.md ADDED
@@ -0,0 +1,10 @@
1
+ # Sprockets
2
+
3
+ ### Note for Sprockets usage
4
+
5
+ If you are still using Sprockets for some of your assets, you might want to include files from `node_modules` directory in your asset pipeline. This is useful, for example, if you want to reference a stylesheet from a node package in your `.scss` stylesheet.
6
+
7
+ In order to enable this, make sure you add `node_modules` to the asset load path by adding the following in an initializer (for example `config/initializers/assets.rb`)
8
+ ```ruby
9
+ Rails.application.config.assets.paths << Rails.root.join('node_modules')
10
+ ```
data/docs/v6_upgrade.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  There are several substantial changes in Shakapacker v6 that you need to manually account for when coming from Webpacker 5. This guide will help you through it.
4
4
 
5
- [ShakaCode](https://www.shakacode.com) offers suppport for upgrading from webpacker or using Shakapacker. If interested, contact [justin@shakacode.com](mailto:justin@shakacode.com).
5
+ [ShakaCode](https://www.shakacode.com) offers support for upgrading from webpacker or using Shakapacker. If interested, contact [justin@shakacode.com](mailto:justin@shakacode.com).
6
6
 
7
7
  ## Webpacker/Shakapacker has become a slimmer wrapper around Webpack
8
8
 
@@ -2,11 +2,21 @@
2
2
 
3
3
  default: &default
4
4
  source_path: app/javascript
5
+
6
+ # You can have a subdirectory of the source_path, like 'packs' (recommended).
7
+ # Alternatively, you can use '/' to use the whole source_path directory.
5
8
  source_entry_path: /
9
+
10
+ # If nested_entries is true, then we'll pick up subdirectories within the source_entry_path.
11
+ # You cannot set this option to true if you set source_entry_path to '/'
12
+ nested_entries: false
13
+
6
14
  public_root_path: public
7
15
  public_output_path: packs
8
16
  cache_path: tmp/webpacker
9
17
  webpack_compile_output: true
18
+ # See https://github.com/shakacode/shakapacker#deployment
19
+ webpacker_precompile: true
10
20
 
11
21
  # Location for manifest.json, defaults to {public_output_path}/manifest.json if unset
12
22
  # manifest_path: public/packs/manifest.json
@@ -24,9 +34,13 @@ default: &default
24
34
  # Set to true to enable check for matching versions of shakapacker gem and NPM package - will raise an error if there is a mismatch or wildcard versioning is used
25
35
  ensure_consistent_versioning: false
26
36
 
37
+ # Select whether the compiler will use SHA digest ('digest' option) or most most recent modified timestamp ('mtime') to determine freshness
38
+ compiler_strategy: digest
39
+
27
40
  development:
28
41
  <<: *default
29
42
  compile: true
43
+ compiler_strategy: mtime
30
44
 
31
45
  # Reference: https://webpack.js.org/configuration/dev-server/
32
46
  dev_server:
@@ -11,9 +11,7 @@ namespace :webpacker do
11
11
  end
12
12
  end
13
13
 
14
- skip_webpacker_clean = %w(no false n f).include?(ENV["WEBPACKER_PRECOMPILE"])
15
-
16
- unless skip_webpacker_clean
14
+ if Webpacker.config.webpacker_precompile?
17
15
  # Run clean if the assets:clean is run
18
16
  if Rake::Task.task_defined?("assets:clean")
19
17
  Rake::Task["assets:clean"].enhance do
@@ -8,9 +8,7 @@ namespace :webpacker do
8
8
  end
9
9
  end
10
10
 
11
- skip_webpacker_clobber = %w(no false n f).include?(ENV["WEBPACKER_PRECOMPILE"])
12
-
13
- unless skip_webpacker_clobber
11
+ if Webpacker.config.webpacker_precompile?
14
12
  # Run clobber if the assets:clobber is run
15
13
  if Rake::Task.task_defined?("assets:clobber")
16
14
  Rake::Task["assets:clobber"].enhance do
@@ -1,16 +1,8 @@
1
1
  $stdout.sync = true
2
2
 
3
- def yarn_install_available?
4
- rails_major = Rails::VERSION::MAJOR
5
- rails_minor = Rails::VERSION::MINOR
6
-
7
- rails_major > 5 || (rails_major == 5 && rails_minor >= 1)
8
- end
9
-
10
3
  def enhance_assets_precompile
11
4
  # yarn:install was added in Rails 5.1
12
- deps = yarn_install_available? ? [] : ["webpacker:yarn_install"]
13
- Rake::Task["assets:precompile"].enhance(deps) do |task|
5
+ Rake::Task["assets:precompile"].enhance do |task|
14
6
  prefix = task.name.split(/#|assets:precompile/).first
15
7
 
16
8
  Rake::Task["#{prefix}webpacker:compile"].invoke
@@ -33,13 +25,10 @@ namespace :webpacker do
33
25
  end
34
26
  end
35
27
 
36
- # Compile packs after we've compiled all other assets during precompilation
37
- skip_webpacker_precompile = %w(no false n f).include?(ENV["WEBPACKER_PRECOMPILE"])
38
-
39
- unless skip_webpacker_precompile
28
+ if Webpacker.config.webpacker_precompile?
40
29
  if Rake::Task.task_defined?("assets:precompile")
41
30
  enhance_assets_precompile
42
31
  else
43
- Rake::Task.define_task("assets:precompile" => ["webpacker:yarn_install", "webpacker:compile"])
32
+ Rake::Task.define_task("assets:precompile" => ["webpacker:compile"])
44
33
  end
45
34
  end
@@ -9,7 +9,6 @@ tasks = {
9
9
  "webpacker:check_binstubs" => "Verifies that bin/webpacker is present",
10
10
  "webpacker:binstubs" => "Installs Webpacker binstubs in this application",
11
11
  "webpacker:verify_install" => "Verifies if Webpacker is installed",
12
- "webpacker:yarn_install" => "Support for older Rails versions. Install all JavaScript dependencies as specified via Yarn"
13
12
  }.freeze
14
13
 
15
14
  desc "Lists all available tasks in Webpacker"
@@ -0,0 +1,24 @@
1
+ module Webpacker
2
+ class BaseStrategy
3
+ def initialize
4
+ @config = Webpacker.config
5
+ end
6
+
7
+ def after_compile_hook
8
+ nil
9
+ end
10
+
11
+ private
12
+
13
+ attr_reader :config
14
+
15
+ def default_watched_paths
16
+ [
17
+ *config.additional_paths.map { |path| "#{path}{,/**/*}" },
18
+ "#{config.source_path}{,/**/*}",
19
+ "yarn.lock", "package.json",
20
+ "config/webpack{,/**/*}"
21
+ ].freeze
22
+ end
23
+ end
24
+ end
@@ -1,18 +1,13 @@
1
1
  require "open3"
2
- require "digest/sha1"
2
+ require "webpacker/compiler_strategy"
3
3
 
4
4
  class Webpacker::Compiler
5
- # Additional paths that test compiler needs to watch
6
- # Webpacker::Compiler.watched_paths << 'bower_components'
7
- #
8
- # Deprecated. Use additional_paths in the YAML configuration instead.
9
- cattr_accessor(:watched_paths) { [] }
10
-
11
5
  # Additional environment variables that the compiler is being run with
12
6
  # Webpacker::Compiler.env['FRONTEND_API_KEY'] = 'your_secret_key'
13
7
  cattr_accessor(:env) { {} }
14
8
 
15
- delegate :config, :logger, to: :webpacker
9
+ delegate :config, :logger, :strategy, to: :webpacker
10
+ delegate :fresh?, :stale?, :after_compile_hook, to: :strategy
16
11
 
17
12
  def initialize(webpacker)
18
13
  @webpacker = webpacker
@@ -21,11 +16,7 @@ class Webpacker::Compiler
21
16
  def compile
22
17
  if stale?
23
18
  run_webpack.tap do |success|
24
- # We used to only record the digest on success
25
- # However, the output file is still written on error, meaning that the digest should still be updated.
26
- # If it's not, you can end up in a situation where a recompile doesn't take place when it should.
27
- # See https://github.com/rails/webpacker/issues/2113
28
- record_compilation_digest
19
+ after_compile_hook
29
20
  end
30
21
  else
31
22
  logger.debug "Everything's up-to-date. Nothing to do"
@@ -33,50 +24,9 @@ class Webpacker::Compiler
33
24
  end
34
25
  end
35
26
 
36
- # Returns true if all the compiled packs are up to date with the underlying asset files.
37
- def fresh?
38
- last_compilation_digest&.== watched_files_digest
39
- end
40
-
41
- # Returns true if the compiled packs are out of date with the underlying asset files.
42
- def stale?
43
- !fresh?
44
- end
45
-
46
27
  private
47
28
  attr_reader :webpacker
48
29
 
49
- def last_compilation_digest
50
- compilation_digest_path.read if compilation_digest_path.exist? && config.manifest_path.exist?
51
- rescue Errno::ENOENT, Errno::ENOTDIR
52
- end
53
-
54
- def watched_files_digest
55
- if Rails.env.development?
56
- warn <<~MSG.strip
57
- Webpacker::Compiler - Slow setup for development
58
-
59
- Prepare JS assets with either:
60
- 1. Running `bin/webpacker-dev-server`
61
- 2. Set `compile` to false in webpacker.yml and run `bin/webpacker -w`
62
- MSG
63
- end
64
-
65
- warn "Webpacker::Compiler.watched_paths has been deprecated. Set additional_paths in webpacker.yml instead." unless watched_paths.empty?
66
- root_path = Pathname.new(File.expand_path(config.root_path))
67
- expanded_paths = [*default_watched_paths, *watched_paths].map do |path|
68
- root_path.join(path)
69
- end
70
- files = Dir[*expanded_paths].reject { |f| File.directory?(f) }
71
- file_ids = files.sort.map { |f| "#{File.basename(f)}/#{Digest::SHA1.file(f).hexdigest}" }
72
- Digest::SHA1.hexdigest(file_ids.join("/"))
73
- end
74
-
75
- def record_compilation_digest
76
- config.cache_path.mkpath
77
- compilation_digest_path.write(watched_files_digest)
78
- end
79
-
80
30
  def optionalRubyRunner
81
31
  bin_webpack_path = config.root_path.join("bin/webpacker")
82
32
  first_line = File.readlines(bin_webpack_path).first.chomp
@@ -107,19 +57,6 @@ class Webpacker::Compiler
107
57
  status.success?
108
58
  end
109
59
 
110
- def default_watched_paths
111
- [
112
- *config.additional_paths.map { |path| "#{path}/**/*" },
113
- "#{config.source_path}/**/*",
114
- "yarn.lock", "package.json",
115
- "config/webpack/**/*"
116
- ].freeze
117
- end
118
-
119
- def compilation_digest_path
120
- config.cache_path.join("last-compilation-digest-#{webpacker.env}")
121
- end
122
-
123
60
  def webpack_env
124
61
  return env unless defined?(ActionController::Base)
125
62
 
@@ -0,0 +1,20 @@
1
+ require "webpacker/mtime_strategy"
2
+ require "webpacker/digest_strategy"
3
+
4
+ module Webpacker
5
+ class CompilerStrategy
6
+ def self.from_config
7
+ strategy_from_config = Webpacker.config.compiler_strategy
8
+
9
+ case strategy_from_config
10
+ when "mtime"
11
+ Webpacker::MtimeStrategy.new
12
+ when "digest"
13
+ Webpacker::DigestStrategy.new
14
+ else
15
+ raise "Unknown strategy '#{strategy_from_config}'. " \
16
+ "Available options are 'mtime' and 'digest'."
17
+ end
18
+ end
19
+ end
20
+ end
@@ -27,6 +27,15 @@ class Webpacker::Configuration
27
27
  fetch(:ensure_consistent_versioning)
28
28
  end
29
29
 
30
+ def webpacker_precompile?
31
+ # ENV of false takes precedence
32
+ return false if %w(no false n f).include?(ENV["WEBPACKER_PRECOMPILE"])
33
+ return true if %w(yes true t).include?(ENV["WEBPACKER_PRECOMPILE"])
34
+
35
+ return false unless config_path.exist?
36
+ fetch(:webpacker_precompile)
37
+ end
38
+
30
39
  def source_path
31
40
  root_path.join(fetch(:source_path))
32
41
  end
@@ -79,6 +88,10 @@ class Webpacker::Configuration
79
88
  fetch(:webpack_compile_output)
80
89
  end
81
90
 
91
+ def compiler_strategy
92
+ fetch(:compiler_strategy)
93
+ end
94
+
82
95
  def fetch(key)
83
96
  data.fetch(key, defaults[key])
84
97
  end
@@ -0,0 +1,59 @@
1
+ require "digest/sha1"
2
+ require "webpacker/base_strategy"
3
+
4
+ module Webpacker
5
+ class DigestStrategy < BaseStrategy
6
+ # Returns true if all the compiled packs are up to date with the underlying asset files.
7
+ def fresh?
8
+ last_compilation_digest&.== watched_files_digest
9
+ end
10
+
11
+ # Returns true if the compiled packs are out of date with the underlying asset files.
12
+ def stale?
13
+ !fresh?
14
+ end
15
+
16
+ def after_compile_hook
17
+ # We used to only record the digest on success
18
+ # However, the output file is still written on error, meaning that the digest should still be updated.
19
+ # If it's not, you can end up in a situation where a recompile doesn't take place when it should.
20
+ # See https://github.com/rails/webpacker/issues/2113
21
+ record_compilation_digest
22
+ end
23
+
24
+ private
25
+
26
+ def last_compilation_digest
27
+ compilation_digest_path.read if compilation_digest_path.exist? && config.manifest_path.exist?
28
+ rescue Errno::ENOENT, Errno::ENOTDIR
29
+ end
30
+
31
+ def watched_files_digest
32
+ if Rails.env.development?
33
+ warn <<~MSG.strip
34
+ Webpacker::Compiler - Slow setup for development
35
+ Prepare JS assets with either:
36
+ 1. Running `bin/webpacker-dev-server`
37
+ 2. Set `compile` to false in webpacker.yml and run `bin/webpacker -w`
38
+ MSG
39
+ end
40
+
41
+ root_path = Pathname.new(File.expand_path(config.root_path))
42
+ expanded_paths = [*default_watched_paths].map do |path|
43
+ root_path.join(path)
44
+ end
45
+ files = Dir[*expanded_paths].reject { |f| File.directory?(f) }
46
+ file_ids = files.sort.map { |f| "#{File.basename(f)}/#{Digest::SHA1.file(f).hexdigest}" }
47
+ Digest::SHA1.hexdigest(file_ids.join("/"))
48
+ end
49
+
50
+ def record_compilation_digest
51
+ config.cache_path.mkpath
52
+ compilation_digest_path.write(watched_files_digest)
53
+ end
54
+
55
+ def compilation_digest_path
56
+ config.cache_path.join("last-compilation-digest-#{Webpacker.env}")
57
+ end
58
+ end
59
+ end
@@ -101,9 +101,17 @@ module Webpacker::Helper
101
101
  "Please refer to https://github.com/shakacode/shakapacker/blob/master/README.md#usage for the usage guide"
102
102
  end
103
103
 
104
+ append_javascript_pack_tag(*names, defer: defer)
105
+ non_deferred = sources_from_manifest_entrypoints(javascript_pack_tag_queue[:non_deferred], type: :javascript)
106
+ deferred = sources_from_manifest_entrypoints(javascript_pack_tag_queue[:deferred], type: :javascript) - non_deferred
107
+
104
108
  @javascript_pack_tag_loaded = true
105
109
 
106
- javascript_include_tag(*sources_from_manifest_entrypoints(names, type: :javascript), **options.tap { |o| o[:defer] = defer })
110
+ capture do
111
+ concat javascript_include_tag(*deferred, **options.tap { |o| o[:defer] = true })
112
+ concat "\n" if non_deferred.any? && deferred.any?
113
+ concat javascript_include_tag(*non_deferred, **options.tap { |o| o[:defer] = false })
114
+ end
107
115
  end
108
116
 
109
117
  # Creates a link tag, for preloading, that references a given Webpacker asset.
@@ -153,8 +161,25 @@ module Webpacker::Helper
153
161
  stylesheet_link_tag(*sources_from_manifest_entrypoints(names, type: :stylesheet), **options)
154
162
  end
155
163
 
164
+ def append_javascript_pack_tag(*names, defer: true)
165
+ if @javascript_pack_tag_loaded
166
+ raise "You can only call append_javascript_pack_tag before javascript_pack_tag helper. " \
167
+ "Please refer to https://github.com/shakacode/shakapacker/blob/master/README.md#usage for the usage guide"
168
+ end
169
+
170
+ hash_key = defer ? :deferred : :non_deferred
171
+ javascript_pack_tag_queue[hash_key] |= names
172
+ end
173
+
156
174
  private
157
175
 
176
+ def javascript_pack_tag_queue
177
+ @javascript_pack_tag_queue ||= {
178
+ deferred: [],
179
+ non_deferred: []
180
+ }
181
+ end
182
+
158
183
  def sources_from_manifest_entrypoints(names, type:)
159
184
  names.map { |name| current_webpacker_instance.manifest.lookup_pack_with_chunks!(name.to_s, type: type) }.flatten.uniq
160
185
  end
@@ -19,6 +19,10 @@ class Webpacker::Instance
19
19
  )
20
20
  end
21
21
 
22
+ def strategy
23
+ @strategy ||= Webpacker::CompilerStrategy.from_config
24
+ end
25
+
22
26
  def compiler
23
27
  @compiler ||= Webpacker::Compiler.new self
24
28
  end