webpacker 3.6.0 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/.node-version +1 -0
  3. data/.travis.yml +12 -4
  4. data/CHANGELOG.md +240 -28
  5. data/CONTRIBUTING.md +33 -0
  6. data/Gemfile +1 -1
  7. data/Gemfile.lock +78 -71
  8. data/MIT-LICENSE +1 -1
  9. data/README.md +75 -16
  10. data/docs/assets.md +7 -4
  11. data/docs/css.md +47 -14
  12. data/docs/deployment.md +11 -0
  13. data/docs/engines.md +155 -0
  14. data/docs/es6.md +5 -5
  15. data/docs/testing.md +1 -1
  16. data/docs/troubleshooting.md +4 -4
  17. data/docs/v4-upgrade.md +132 -0
  18. data/docs/webpack.md +56 -3
  19. data/docs/yarn.md +12 -1
  20. data/gemfiles/Gemfile-rails-edge +1 -1
  21. data/gemfiles/Gemfile-rails.4.2.x +2 -2
  22. data/gemfiles/Gemfile-rails.5.0.x +2 -2
  23. data/gemfiles/Gemfile-rails.5.1.x +2 -2
  24. data/gemfiles/Gemfile-rails.5.2.x +10 -0
  25. data/lib/install/bin/webpack +5 -1
  26. data/lib/install/bin/webpack-dev-server +5 -1
  27. data/lib/install/coffee.rb +2 -2
  28. data/lib/install/config/.browserslistrc +1 -0
  29. data/lib/install/config/babel.config.js +70 -0
  30. data/lib/install/config/postcss.config.js +12 -0
  31. data/lib/install/config/webpacker.yml +26 -0
  32. data/lib/install/elm.rb +2 -2
  33. data/lib/install/erb.rb +2 -2
  34. data/lib/install/examples/react/babel.config.js +83 -0
  35. data/lib/install/examples/stimulus/application.js +1 -6
  36. data/lib/install/examples/stimulus/controllers/index.js +9 -0
  37. data/lib/install/examples/typescript/tsconfig.json +4 -0
  38. data/lib/install/examples/vue/hello_vue.js +6 -4
  39. data/lib/install/javascript/packs/application.js +8 -0
  40. data/lib/install/loaders/typescript.js +8 -3
  41. data/lib/install/loaders/vue.js +1 -8
  42. data/lib/install/react.rb +6 -19
  43. data/lib/install/template.rb +29 -30
  44. data/lib/install/typescript.rb +4 -4
  45. data/lib/install/vue.rb +14 -5
  46. data/lib/tasks/installers.rake +4 -2
  47. data/lib/tasks/webpacker/binstubs.rake +3 -2
  48. data/lib/tasks/webpacker/compile.rake +10 -5
  49. data/lib/tasks/webpacker/install.rake +3 -2
  50. data/lib/tasks/webpacker/verify_install.rake +1 -4
  51. data/lib/tasks/webpacker/yarn_install.rake +1 -1
  52. data/lib/webpacker/commands.rb +0 -1
  53. data/lib/webpacker/compiler.rb +17 -13
  54. data/lib/webpacker/configuration.rb +13 -5
  55. data/lib/webpacker/dev_server.rb +7 -10
  56. data/lib/webpacker/dev_server_proxy.rb +13 -6
  57. data/lib/webpacker/dev_server_runner.rb +13 -6
  58. data/lib/webpacker/helper.rb +78 -20
  59. data/lib/webpacker/manifest.rb +64 -21
  60. data/lib/webpacker/railtie.rb +16 -7
  61. data/lib/webpacker/runner.rb +3 -3
  62. data/lib/webpacker/version.rb +1 -1
  63. data/lib/webpacker/webpack_runner.rb +14 -3
  64. data/package.json +46 -34
  65. data/package/__tests__/config.js +37 -3
  66. data/package/__tests__/dev_server.js +15 -0
  67. data/package/__tests__/production.js +2 -2
  68. data/package/__tests__/staging.js +3 -3
  69. data/package/__tests__/test.js +2 -1
  70. data/package/config.js +21 -9
  71. data/package/config_types/config_list.js +5 -6
  72. data/package/dev_server.js +3 -1
  73. data/package/environments/__tests__/base.js +7 -5
  74. data/package/environments/base.js +84 -31
  75. data/package/environments/development.js +6 -2
  76. data/package/environments/production.js +46 -36
  77. data/package/rules/babel.js +10 -4
  78. data/package/rules/file.js +8 -3
  79. data/package/rules/index.js +7 -2
  80. data/package/rules/node_modules.js +23 -0
  81. data/package/utils/__tests__/get_style_rule.js +20 -0
  82. data/package/utils/deep_merge.js +5 -6
  83. data/package/utils/get_style_rule.js +29 -42
  84. data/package/utils/helpers.js +18 -6
  85. data/package/utils/objectify.js +1 -2
  86. data/test/compiler_test.rb +15 -3
  87. data/test/configuration_test.rb +9 -0
  88. data/test/dev_server_runner_test.rb +51 -0
  89. data/test/helper_test.rb +48 -5
  90. data/test/manifest_test.rb +14 -0
  91. data/test/rake_tasks_test.rb +34 -0
  92. data/test/test_app/config.ru +5 -0
  93. data/test/test_app/config/application.rb +1 -0
  94. data/test/test_app/config/webpack/development.js +0 -0
  95. data/test/test_app/config/webpacker.yml +20 -0
  96. data/test/test_app/config/webpacker_public_root.yml +19 -0
  97. data/test/test_app/package.json +13 -0
  98. data/test/test_app/public/packs/manifest.json +22 -1
  99. data/test/test_app/yarn.lock +11 -0
  100. data/test/test_helper.rb +1 -3
  101. data/test/webpack_runner_test.rb +51 -0
  102. data/yarn.lock +4077 -2816
  103. metadata +28 -6
  104. data/lib/install/config/.babelrc +0 -18
  105. data/lib/install/config/.postcssrc.yml +0 -3
  106. data/lib/install/examples/react/.babelrc +0 -6
@@ -17,9 +17,10 @@ describe('Test environment', () => {
17
17
 
18
18
  const { environment } = require('../index')
19
19
  const config = environment.toWebpackConfig()
20
-
20
+
21
21
  expect(config.output.path).toEqual(resolve('public', 'packs-test'))
22
22
  expect(config.output.publicPath).toEqual('/packs-test/')
23
+ expect(config.devServer).toEqual(undefined)
23
24
  })
24
25
  })
25
26
  })
@@ -2,7 +2,7 @@ const { resolve } = require('path')
2
2
  const { safeLoad } = require('js-yaml')
3
3
  const { readFileSync } = require('fs')
4
4
  const deepMerge = require('./utils/deep_merge')
5
- const { isArray } = require('./utils/helpers')
5
+ const { isArray, ensureTrailingSlash } = require('./utils/helpers')
6
6
  const { railsEnv } = require('./env')
7
7
 
8
8
  const defaultConfigPath = require.resolve('../lib/install/config/webpacker.yml')
@@ -17,17 +17,29 @@ const defaults = getDefaultConfig()
17
17
  const app = safeLoad(readFileSync(configPath), 'utf8')[railsEnv]
18
18
 
19
19
  if (isArray(app.extensions) && app.extensions.length) delete defaults.extensions
20
+ if (isArray(app.static_assets_extensions) && app.static_assets_extensions.length) {
21
+ delete defaults.static_assets_extensions
22
+ }
20
23
 
21
24
  const config = deepMerge(defaults, app)
22
- config.outputPath = resolve('public', config.public_output_path)
23
-
24
- let publicPath = `/${config.public_output_path}/`
25
- // Add prefix to publicPath.
26
- if (process.env.RAILS_RELATIVE_URL_ROOT) {
27
- publicPath = `/${process.env.RAILS_RELATIVE_URL_ROOT}${publicPath}`
25
+ config.outputPath = resolve(config.public_root_path, config.public_output_path)
26
+
27
+ // Ensure that the publicPath includes our asset host so dynamic imports
28
+ // (code-splitting chunks and static assets) load from the CDN instead of a relative path.
29
+ const getPublicPath = () => {
30
+ const rootUrl = process.env.WEBPACKER_ASSET_HOST || '/'
31
+ let packPath = `${config.public_output_path}/`
32
+ // Add relative root prefix to pack path.
33
+ if (process.env.RAILS_RELATIVE_URL_ROOT) {
34
+ let relativeRoot = process.env.RAILS_RELATIVE_URL_ROOT
35
+ relativeRoot = relativeRoot.startsWith('/') ? relativeRoot.substr(1) : relativeRoot
36
+ packPath = `${ensureTrailingSlash(relativeRoot)}${packPath}`
37
+ }
38
+
39
+ return ensureTrailingSlash(rootUrl) + packPath
28
40
  }
29
41
 
30
- // Remove extra slashes.
31
- config.publicPath = publicPath.replace(/(^\/|[^:]\/)\/+/g, '$1')
42
+ config.publicPath = getPublicPath()
43
+ config.publicPathWithoutCDN = `/${config.public_output_path}/`
32
44
 
33
45
  module.exports = config
@@ -38,12 +38,11 @@ class ConfigList extends Array {
38
38
  }
39
39
 
40
40
  getIndex(key, shouldThrow = false) {
41
- const index = this.findIndex(entry =>
42
- (
43
- entry === key ||
44
- entry.key === key ||
45
- (entry.constructor && entry.constructor.name === key)
46
- ))
41
+ const index = this.findIndex(entry => (
42
+ entry === key
43
+ || entry.key === key
44
+ || (entry.constructor && entry.constructor.name === key)
45
+ ))
47
46
 
48
47
  if (shouldThrow && index < 0) throw new Error(`Item ${key} not found`)
49
48
  return index
@@ -9,8 +9,10 @@ const fetch = (key) => {
9
9
  const devServerConfig = config.dev_server
10
10
 
11
11
  if (devServerConfig) {
12
+ const envPrefix = config.dev_server.env_prefix || 'WEBPACKER_DEV_SERVER'
13
+
12
14
  Object.keys(devServerConfig).forEach((key) => {
13
- const envValue = fetch(`WEBPACKER_DEV_SERVER_${key.toUpperCase().replace(/_/g, '')}`)
15
+ const envValue = fetch(`${envPrefix}_${key.toUpperCase().replace(/_/g, '')}`)
14
16
  if (envValue !== undefined) devServerConfig[key] = envValue
15
17
  })
16
18
  }
@@ -24,13 +24,15 @@ describe('Environment', () => {
24
24
 
25
25
  test('should return entry', () => {
26
26
  const config = environment.toWebpackConfig()
27
- expect(config.entry.application).toEqual(resolve('app', 'javascript', 'packs', 'application.js'))
27
+ expect(config.entry.application).toEqual(
28
+ resolve('app', 'javascript', 'packs', 'application.js')
29
+ )
28
30
  })
29
31
 
30
32
  test('should return output', () => {
31
33
  const config = environment.toWebpackConfig()
32
- expect(config.output.filename).toEqual('[name]-[chunkhash].js')
33
- expect(config.output.chunkFilename).toEqual('[name]-[chunkhash].chunk.js')
34
+ expect(config.output.filename).toEqual('js/[name]-[chunkhash].js')
35
+ expect(config.output.chunkFilename).toEqual('js/[name]-[chunkhash].chunk.js')
34
36
  })
35
37
 
36
38
  test('should return default loader rules for each file in config/loaders', () => {
@@ -38,8 +40,8 @@ describe('Environment', () => {
38
40
  const defaultRules = Object.keys(rules)
39
41
  const configRules = config.module.rules
40
42
 
41
- expect(defaultRules.length).toBeGreaterThan(1)
42
- expect(configRules.length).toEqual(defaultRules.length)
43
+ expect(defaultRules.length).toEqual(7)
44
+ expect(configRules.length).toEqual(8)
43
45
  })
44
46
 
45
47
  test('should return default plugins', () => {
@@ -8,9 +8,13 @@ const { sync } = require('glob')
8
8
  const extname = require('path-complete-extname')
9
9
 
10
10
  const webpack = require('webpack')
11
- const ExtractTextPlugin = require('extract-text-webpack-plugin')
12
- const ManifestPlugin = require('webpack-manifest-plugin')
11
+ const MiniCssExtractPlugin = require('mini-css-extract-plugin')
12
+ const WebpackAssetsManifest = require('webpack-assets-manifest')
13
13
  const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
14
+ const PnpWebpackPlugin = require('pnp-webpack-plugin')
15
+
16
+ const { isNotObject, prettyPrint } = require('../utils/helpers')
17
+ const deepMerge = require('../utils/deep_merge')
14
18
 
15
19
  const { ConfigList, ConfigObject } = require('../config_types')
16
20
  const rules = require('../rules')
@@ -24,10 +28,27 @@ const getLoaderList = () => {
24
28
 
25
29
  const getPluginList = () => {
26
30
  const result = new ConfigList()
27
- result.append('Environment', new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(process.env))))
31
+ result.append(
32
+ 'Environment',
33
+ new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(process.env)))
34
+ )
28
35
  result.append('CaseSensitivePaths', new CaseSensitivePathsPlugin())
29
- result.append('ExtractText', new ExtractTextPlugin('[name]-[contenthash].css'))
30
- result.append('Manifest', new ManifestPlugin({ publicPath: config.publicPath, writeToFileEmit: true }))
36
+ result.append(
37
+ 'MiniCssExtract',
38
+ new MiniCssExtractPlugin({
39
+ filename: 'css/[name]-[contenthash:8].css',
40
+ chunkFilename: 'css/[name]-[contenthash:8].chunk.css'
41
+ })
42
+ )
43
+ result.append(
44
+ 'Manifest',
45
+ new WebpackAssetsManifest({
46
+ integrity: false,
47
+ entrypoints: true,
48
+ writeToDisk: true,
49
+ publicPath: config.publicPathWithoutCDN
50
+ })
51
+ )
31
52
  return result
32
53
  }
33
54
 
@@ -59,31 +80,34 @@ const getModulePaths = () => {
59
80
  return result
60
81
  }
61
82
 
62
- const getBaseConfig = () =>
63
- new ConfigObject({
64
- output: {
65
- filename: '[name]-[chunkhash].js',
66
- chunkFilename: '[name]-[chunkhash].chunk.js',
67
- path: config.outputPath,
68
- publicPath: config.publicPath
69
- },
70
-
71
- resolve: {
72
- extensions: config.extensions
73
- },
74
-
75
- resolveLoader: {
76
- modules: ['node_modules']
77
- },
78
-
79
- node: {
80
- dgram: 'empty',
81
- fs: 'empty',
82
- net: 'empty',
83
- tls: 'empty',
84
- child_process: 'empty'
85
- }
86
- })
83
+ const getBaseConfig = () => new ConfigObject({
84
+ mode: 'production',
85
+ output: {
86
+ filename: 'js/[name]-[chunkhash].js',
87
+ chunkFilename: 'js/[name]-[chunkhash].chunk.js',
88
+ hotUpdateChunkFilename: 'js/[id]-[hash].hot-update.js',
89
+ path: config.outputPath,
90
+ publicPath: config.publicPath
91
+ },
92
+
93
+ resolve: {
94
+ extensions: config.extensions,
95
+ plugins: [PnpWebpackPlugin]
96
+ },
97
+
98
+ resolveLoader: {
99
+ modules: ['node_modules'],
100
+ plugins: [PnpWebpackPlugin.moduleLoader(module)]
101
+ },
102
+
103
+ node: {
104
+ dgram: 'empty',
105
+ fs: 'empty',
106
+ net: 'empty',
107
+ tls: 'empty',
108
+ child_process: 'empty'
109
+ }
110
+ })
87
111
 
88
112
  module.exports = class Base {
89
113
  constructor() {
@@ -94,13 +118,42 @@ module.exports = class Base {
94
118
  this.resolvedModules = getModulePaths()
95
119
  }
96
120
 
121
+ splitChunks(callback = null) {
122
+ let appConfig = {}
123
+ const defaultConfig = {
124
+ optimization: {
125
+ // Split vendor and common chunks
126
+ // https://twitter.com/wSokra/status/969633336732905474
127
+ splitChunks: {
128
+ chunks: 'all',
129
+ name: false
130
+ },
131
+ // Separate runtime chunk to enable long term caching
132
+ // https://twitter.com/wSokra/status/969679223278505985
133
+ runtimeChunk: true
134
+ }
135
+ }
136
+
137
+ if (callback) {
138
+ appConfig = callback(defaultConfig)
139
+ if (isNotObject(appConfig)) {
140
+ throw new Error(`
141
+ ${prettyPrint(appConfig)} is not a valid splitChunks configuration.
142
+ See https://webpack.js.org/plugins/split-chunks-plugin/#configuration
143
+ `)
144
+ }
145
+ }
146
+
147
+ return this.config.merge(deepMerge(defaultConfig, appConfig))
148
+ }
149
+
97
150
  toWebpackConfig() {
98
151
  return this.config.merge({
99
152
  entry: this.entry.toObject(),
100
153
 
101
154
  module: {
102
155
  strictExportPresence: true,
103
- rules: this.loaders.values()
156
+ rules: [{ parser: { requireEnsure: false } }, ...this.loaders.values()]
104
157
  },
105
158
 
106
159
  plugins: this.plugins.values(),
@@ -9,11 +9,12 @@ module.exports = class extends Base {
9
9
 
10
10
  if (devServer.hmr) {
11
11
  this.plugins.append('HotModuleReplacement', new webpack.HotModuleReplacementPlugin())
12
- this.plugins.append('NamedModules', new webpack.NamedModulesPlugin())
13
12
  this.config.output.filename = '[name]-[hash].js'
14
13
  }
15
14
 
16
15
  this.config.merge({
16
+ mode: 'development',
17
+ cache: true,
17
18
  devtool: 'cheap-module-source-map',
18
19
  output: {
19
20
  pathinfo: true
@@ -38,7 +39,10 @@ module.exports = class extends Base {
38
39
  headers: devServer.headers,
39
40
  overlay: devServer.overlay,
40
41
  stats: {
41
- errorDetails: true
42
+ entrypoints: false,
43
+ errorDetails: false,
44
+ modules: false,
45
+ moduleTrace: false
42
46
  },
43
47
  watchOptions: devServer.watch_options
44
48
  }
@@ -1,57 +1,67 @@
1
- const webpack = require('webpack')
1
+ const TerserPlugin = require('terser-webpack-plugin')
2
2
  const CompressionPlugin = require('compression-webpack-plugin')
3
- const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
4
3
  const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
4
+ const safePostCssParser = require('postcss-safe-parser')
5
5
  const Base = require('./base')
6
6
 
7
7
  module.exports = class extends Base {
8
8
  constructor() {
9
9
  super()
10
10
 
11
- this.plugins.append('ModuleConcatenation', new webpack.optimize.ModuleConcatenationPlugin())
12
- this.plugins.append('OptimizeCSSAssets', new OptimizeCSSAssetsPlugin())
13
-
14
11
  this.plugins.append(
15
- 'UglifyJs',
16
- new UglifyJsPlugin({
17
- parallel: true,
12
+ 'Compression',
13
+ new CompressionPlugin({
14
+ filename: '[path].gz[query]',
15
+ algorithm: 'gzip',
18
16
  cache: true,
19
- sourceMap: true,
20
- uglifyOptions: {
21
- parse: {
22
- // Let uglify-js parse ecma 8 code but always output
23
- // ES5 compliant code for older browsers
24
- ecma: 8
25
- },
26
- compress: {
27
- ecma: 5,
28
- warnings: false,
29
- comparisons: false
30
- },
31
- mangle: {
32
- safari10: true
33
- },
34
- output: {
35
- ecma: 5,
36
- comments: false,
37
- ascii_only: true
38
- }
39
- }
17
+ test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/
40
18
  })
41
19
  )
42
20
 
43
21
  this.plugins.append(
44
- 'Compression',
45
- new CompressionPlugin({
46
- asset: '[path].gz[query]',
47
- algorithm: 'gzip',
48
- test: /\.(js|css|html|json|ico|svg|eot|otf|ttf)$/
22
+ 'OptimizeCSSAssets',
23
+ new OptimizeCSSAssetsPlugin({
24
+ parser: safePostCssParser,
25
+ map: {
26
+ inline: false,
27
+ annotation: true
28
+ }
49
29
  })
50
30
  )
51
31
 
52
32
  this.config.merge({
53
- devtool: 'nosources-source-map',
54
- stats: 'normal'
33
+ devtool: 'source-map',
34
+ stats: 'normal',
35
+ bail: true,
36
+ optimization: {
37
+ minimizer: [
38
+ new TerserPlugin({
39
+ parallel: true,
40
+ cache: true,
41
+ sourceMap: true,
42
+ terserOptions: {
43
+ parse: {
44
+ // Let terser parse ecma 8 code but always output
45
+ // ES5 compliant code for older browsers
46
+ ecma: 8
47
+ },
48
+ compress: {
49
+ ecma: 5,
50
+ warnings: false,
51
+ comparisons: false
52
+ },
53
+ mangle: {
54
+ safari10: true
55
+ },
56
+ output: {
57
+ ecma: 5,
58
+ comments: false,
59
+ ascii_only: true
60
+ }
61
+ }
62
+ })
63
+ ]
64
+ }
55
65
  })
56
66
  }
57
67
  }
@@ -1,14 +1,20 @@
1
- const { join } = require('path')
2
- const { cache_path: cachePath } = require('../config')
1
+ const { join, resolve } = require('path')
2
+ const { cache_path: cachePath, source_path: sourcePath, resolved_paths: resolvedPaths } = require('../config')
3
+ const { nodeEnv } = require('../env')
3
4
 
5
+ // Process application Javascript code with Babel.
6
+ // Uses application .babelrc to apply any transformations
4
7
  module.exports = {
5
- test: /\.(js|jsx)?(\.erb)?$/,
8
+ test: /\.(js|jsx|mjs)?(\.erb)?$/,
9
+ include: [sourcePath, ...resolvedPaths].map(p => resolve(p)),
6
10
  exclude: /node_modules/,
7
11
  use: [
8
12
  {
9
13
  loader: 'babel-loader',
10
14
  options: {
11
- cacheDirectory: join(cachePath, 'babel-loader')
15
+ cacheDirectory: join(cachePath, 'babel-loader-node-modules'),
16
+ cacheCompression: nodeEnv === 'production',
17
+ compact: nodeEnv === 'production'
12
18
  }
13
19
  }
14
20
  ]
@@ -1,13 +1,18 @@
1
1
  const { join } = require('path')
2
- const { source_path: sourcePath } = require('../config')
2
+ const { source_path: sourcePath, static_assets_extensions: fileExtensions } = require('../config')
3
3
 
4
4
  module.exports = {
5
- test: /\.(jpg|jpeg|png|gif|tiff|ico|svg|eot|otf|ttf|woff|woff2)$/i,
5
+ test: new RegExp(`(${fileExtensions.join('|')})$`, 'i'),
6
6
  use: [
7
7
  {
8
8
  loader: 'file-loader',
9
9
  options: {
10
- name: '[path][name]-[hash].[ext]',
10
+ name(file) {
11
+ if (file.includes(sourcePath)) {
12
+ return 'media/[path][name]-[hash].[ext]'
13
+ }
14
+ return 'media/[folder]/[name]-[hash:8].[ext]'
15
+ },
11
16
  context: join(sourcePath)
12
17
  }
13
18
  }