diff --git a/CHANGELOG.md b/CHANGELOG.md index d0f684879..28270c40f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ **Please note that Webpacker 3.1.0 and 3.1.1 have some serious bugs so please consider using either 3.0.2 or 3.2.0** +## [Unreleased] - xxxxxx + +## Breaking changes + + - Order of rules changed so you might have to change append to prepend, + depending on how you want to process packs [#1823](https://github.com/rails/webpacker/pull/1823) + ```js + environment.loaders.prepend() + ``` + - Separate rule to compile node modules + (fixes cases where ES6 libraries were included in the app code) [#1823](https://github.com/rails/webpacker/pull/1823) + - File loader extensions API [#1823](https://github.com/rails/webpacker/pull/1823) + ```yml + # webpacker.yml + static_assets_extensions: + - .pdf + # etc.. + ``` + +### Added + + - Move `.babelrc` and `.postcssrc` to `.js` variant [#1822](https://github.com/rails/webpacker/pull/1822) + - Use postcss safe parser when optimising css assets [#1822](https://github.com/rails/webpacker/pull/1822) + - Add split chunks api (undocumented) + ```js + const { environment } = require('@rails/webpacker') + // Enable with default config + environment.splitChunks() + // Configure via a callback + environment.splitChunks((config) => Object.assign({}, config, { optimization: { splitChunks: false }})) + ``` + - Allow changing static file extensions using webpacker.yml (undocumented) + ## [4.0.0-pre.3] - 2018-10-01 ### Added diff --git a/lib/install/coffee.rb b/lib/install/coffee.rb index ef8f65fb7..891126e13 100644 --- a/lib/install/coffee.rb +++ b/lib/install/coffee.rb @@ -9,11 +9,11 @@ after: "require('@rails/webpacker')\n" insert_into_file Rails.root.join("config/webpack/environment.js").to_s, - "environment.loaders.append('coffee', coffee)\n", + "environment.loaders.prepend('coffee', coffee)\n", before: "module.exports" say "Updating webpack paths to include .coffee file extension" -insert_into_file Webpacker.config.config_path, "- .coffee\n".indent(4), after: /extensions:\n/ +insert_into_file Webpacker.config.config_path, "- .coffee\n".indent(4), after: /\s+extensions:\n/ say "Copying the example entry file to #{Webpacker.config.source_entry_path}" copy_file "#{__dir__}/examples/coffee/hello_coffee.coffee", diff --git a/lib/install/config/webpacker.yml b/lib/install/config/webpacker.yml index 53dc310b7..b0ff7b496 100644 --- a/lib/install/config/webpacker.yml +++ b/lib/install/config/webpacker.yml @@ -15,7 +15,22 @@ default: &default # Reload manifest.json on all requests so we reload latest compiled packs cache_manifest: false + static_assets_extensions: + - .jpg + - .jpeg + - .png + - .gif + - .tiff + - .ico + - .svg + - .eot + - .otf + - .ttf + - .woff + - .woff2 + extensions: + - .mjs - .js - .sass - .scss diff --git a/lib/install/elm.rb b/lib/install/elm.rb index d809c328b..dfcc2c44b 100644 --- a/lib/install/elm.rb +++ b/lib/install/elm.rb @@ -9,7 +9,7 @@ after: "require('@rails/webpacker')\n" insert_into_file Rails.root.join("config/webpack/environment.js").to_s, - "environment.loaders.append('elm', elm)\n", + "environment.loaders.prepend('elm', elm)\n", before: "module.exports" say "Copying Elm example entry file to #{Webpacker.config.source_entry_path}" @@ -27,7 +27,7 @@ run "yarn run elm make" say "Updating webpack paths to include .elm file extension" -insert_into_file Webpacker.config.config_path, "- .elm\n".indent(4), after: /extensions:\n/ +insert_into_file Webpacker.config.config_path, "- .elm\n".indent(4), after: /\s+extensions:\n/ say "Updating Elm source location" gsub_file "elm.json", /\"\src\"\n/, diff --git a/lib/install/erb.rb b/lib/install/erb.rb index 08c1dfa96..f5a40aabf 100644 --- a/lib/install/erb.rb +++ b/lib/install/erb.rb @@ -9,11 +9,11 @@ after: "require('@rails/webpacker')\n" insert_into_file Rails.root.join("config/webpack/environment.js").to_s, - "environment.loaders.append('erb', erb)\n", + "environment.loaders.prepend('erb', erb)\n", before: "module.exports" say "Updating webpack paths to include .erb file extension" -insert_into_file Webpacker.config.config_path, "- .erb\n".indent(4), after: /extensions:\n/ +insert_into_file Webpacker.config.config_path, "- .erb\n".indent(4), after: /\s+extensions:\n/ say "Copying the example entry file to #{Webpacker.config.source_entry_path}" copy_file "#{__dir__}/examples/erb/hello_erb.js.erb", diff --git a/lib/install/loaders/typescript.js b/lib/install/loaders/typescript.js index 9400260e1..2f5e17909 100644 --- a/lib/install/loaders/typescript.js +++ b/lib/install/loaders/typescript.js @@ -1,6 +1,11 @@ +const PnpWebpackPlugin = require('pnp-webpack-plugin') + module.exports = { test: /\.(ts|tsx)?(\.erb)?$/, - use: [{ - loader: 'ts-loader' - }] + use: [ + { + loader: 'ts-loader', + options: PnpWebpackPlugin.tsLoaderOptions() + } + ] } diff --git a/lib/install/react.rb b/lib/install/react.rb index 4c8478e0a..c08e39717 100644 --- a/lib/install/react.rb +++ b/lib/install/react.rb @@ -10,7 +10,7 @@ copy_file "#{__dir__}/examples/react/hello_react.jsx", "#{Webpacker.config.source_entry_path}/hello_react.jsx" say "Updating webpack paths to include .jsx file extension" -insert_into_file Webpacker.config.config_path, "- .jsx\n".indent(4), after: /extensions:\n/ +insert_into_file Webpacker.config.config_path, "- .jsx\n".indent(4), after: /\s+extensions:\n/ say "Installing all react dependencies" run "yarn add react react-dom @babel/preset-react prop-types babel-plugin-transform-react-remove-prop-types" diff --git a/lib/install/typescript.rb b/lib/install/typescript.rb index cd2391f04..965a014ab 100644 --- a/lib/install/typescript.rb +++ b/lib/install/typescript.rb @@ -24,17 +24,17 @@ after: "require('@rails/webpacker')\n" insert_into_file Rails.root.join("config/webpack/environment.js").to_s, - "environment.loaders.append('typescript', typescript)\n", + "environment.loaders.prepend('typescript', typescript)\n", before: "module.exports" say "Copying tsconfig.json to the Rails root directory for typescript" copy_file "#{__dir__}/examples/#{example_source}/tsconfig.json", "tsconfig.json" say "Updating webpack paths to include .ts file extension" -insert_into_file Webpacker.config.config_path, "- .ts\n".indent(4), after: /extensions:\n/ +insert_into_file Webpacker.config.config_path, "- .ts\n".indent(4), after: /\s+extensions:\n/ say "Updating webpack paths to include .tsx file extension" -insert_into_file Webpacker.config.config_path, "- .tsx\n".indent(4), after: /extensions:\n/ +insert_into_file Webpacker.config.config_path, "- .tsx\n".indent(4), after: /\s+extensions:\n/ say "Copying the example entry file to #{Webpacker.config.source_entry_path}" copy_file "#{__dir__}/examples/typescript/hello_typescript.ts", diff --git a/lib/install/vue.rb b/lib/install/vue.rb index 979e4150f..18a5adb89 100644 --- a/lib/install/vue.rb +++ b/lib/install/vue.rb @@ -9,7 +9,7 @@ after: "require('@rails/webpacker')\n" insert_into_file Rails.root.join("config/webpack/environment.js").to_s, - "environment.plugins.append('VueLoaderPlugin', new VueLoaderPlugin())\n", + "environment.plugins.prepend('VueLoaderPlugin', new VueLoaderPlugin())\n", before: "module.exports" say "Adding vue loader to config/webpack/environment.js" @@ -18,11 +18,11 @@ after: "require('vue-loader')\n" insert_into_file Rails.root.join("config/webpack/environment.js").to_s, - "environment.loaders.append('vue', vue)\n", + "environment.loaders.prepend('vue', vue)\n", before: "module.exports" say "Updating webpack paths to include .vue file extension" -insert_into_file Webpacker.config.config_path, "- .vue\n".indent(4), after: /extensions:\n/ +insert_into_file Webpacker.config.config_path, "- .vue\n".indent(4), after: /\s+extensions:\n/ say "Copying the example entry file to #{Webpacker.config.source_entry_path}" copy_file "#{__dir__}/examples/vue/hello_vue.js", diff --git a/package.json b/package.json index 40fdf7bdf..0d1d112b3 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "node-sass": "^4.10.0", "optimize-css-assets-webpack-plugin": "^5.0.1", "path-complete-extname": "^1.0.0", + "pnp-webpack-plugin": "^1.2.1", "postcss-flexbugs-fixes": "^4.1.0", "postcss-import": "^12.0.1", "postcss-loader": "^3.0.0", diff --git a/package/__tests__/config.js b/package/__tests__/config.js index ee722cf43..fadd35962 100644 --- a/package/__tests__/config.js +++ b/package/__tests__/config.js @@ -1,18 +1,17 @@ /* global test expect, describe */ -const { chdirCwd, chdirTestApp } = require('../utils/helpers') +const { chdirCwd, chdirTestApp, resetEnv } = require('../utils/helpers') chdirTestApp() const config = require('../config') describe('Config', () => { - beforeEach(() => jest.resetModules()) + beforeEach(() => jest.resetModules() && resetEnv()) afterAll(chdirCwd) test('public path', () => { process.env.RAILS_ENV = 'development' - delete process.env.RAILS_RELATIVE_URL_ROOT const config = require('../config') expect(config.publicPath).toEqual('/packs/') }) @@ -25,8 +24,31 @@ describe('Config', () => { expect(config.publicPath).toEqual('/foo/packs/') }) + test('public path with relative root without slash', () => { + process.env.RAILS_ENV = 'development' + process.env.RAILS_RELATIVE_URL_ROOT = 'foo' + const config = require('../config') + expect(config.publicPath).toEqual('/foo/packs/') + }) + + test('public path with asset host and relative root', () => { + process.env.RAILS_ENV = 'development' + process.env.RAILS_RELATIVE_URL_ROOT = '/foo/' + process.env.WEBPACKER_ASSET_HOST = 'http://foo.com/' + const config = require('../config') + expect(config.publicPath).toEqual('http://foo.com/foo/packs/') + }) + + test('public path with asset host', () => { + process.env.RAILS_ENV = 'development' + process.env.WEBPACKER_ASSET_HOST = 'http://foo.com/' + const config = require('../config') + expect(config.publicPath).toEqual('http://foo.com/packs/') + }) + test('should return extensions as listed in app config', () => { expect(config.extensions).toEqual([ + '.mjs', '.js', '.sass', '.scss', diff --git a/package/config.js b/package/config.js index e0dba2cec..3bc38a139 100644 --- a/package/config.js +++ b/package/config.js @@ -2,7 +2,7 @@ const { resolve } = require('path') const { safeLoad } = require('js-yaml') const { readFileSync } = require('fs') const deepMerge = require('./utils/deep_merge') -const { isArray } = require('./utils/helpers') +const { isArray, ensureTrailingSlash } = require('./utils/helpers') const { railsEnv } = require('./env') const defaultConfigPath = require.resolve('../lib/install/config/webpacker.yml') @@ -21,13 +21,21 @@ if (isArray(app.extensions) && app.extensions.length) delete defaults.extensions const config = deepMerge(defaults, app) config.outputPath = resolve('public', config.public_output_path) -let publicPath = `/${config.public_output_path}/` -// Add prefix to publicPath. -if (process.env.RAILS_RELATIVE_URL_ROOT) { - publicPath = `/${process.env.RAILS_RELATIVE_URL_ROOT}${publicPath}` +// Ensure that the publicPath includes our asset host so dynamic imports +// (code-splitting chunks and static assets) load from the CDN instead of a relative path. +const getPublicPath = () => { + const rootUrl = process.env.WEBPACKER_ASSET_HOST || '/' + let packPath = `${config.public_output_path}/` + // Add relative root prefix to pack path. + if (process.env.RAILS_RELATIVE_URL_ROOT) { + let relativeRoot = process.env.RAILS_RELATIVE_URL_ROOT + relativeRoot = relativeRoot.startsWith('/') ? relativeRoot.substr(1) : relativeRoot + packPath = `${ensureTrailingSlash(relativeRoot)}${packPath}` + } + + return ensureTrailingSlash(rootUrl) + packPath } -// Remove extra slashes. -config.publicPath = publicPath.replace(/(^\/|[^:]\/)\/+/g, '$1') +config.publicPath = getPublicPath() module.exports = config diff --git a/package/environments/__tests__/base.js b/package/environments/__tests__/base.js index 720d974c1..c336b03e5 100644 --- a/package/environments/__tests__/base.js +++ b/package/environments/__tests__/base.js @@ -24,7 +24,9 @@ describe('Environment', () => { test('should return entry', () => { const config = environment.toWebpackConfig() - expect(config.entry.application).toEqual(resolve('app', 'javascript', 'packs', 'application.js')) + expect(config.entry.application).toEqual( + resolve('app', 'javascript', 'packs', 'application.js') + ) }) test('should return output', () => { @@ -38,8 +40,8 @@ describe('Environment', () => { const defaultRules = Object.keys(rules) const configRules = config.module.rules - expect(defaultRules.length).toBeGreaterThan(1) - expect(configRules.length).toEqual(defaultRules.length) + expect(defaultRules.length).toEqual(7) + expect(configRules.length).toEqual(8) }) test('should return default plugins', () => { diff --git a/package/environments/base.js b/package/environments/base.js index 2e9f6264d..1d29d2644 100644 --- a/package/environments/base.js +++ b/package/environments/base.js @@ -11,6 +11,10 @@ const webpack = require('webpack') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const WebpackAssetsManifest = require('webpack-assets-manifest') const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin') +const PnpWebpackPlugin = require('pnp-webpack-plugin') + +const { isNotObject, prettyPrint } = require('../utils/helpers') +const deepMerge = require('../utils/deep_merge') const { ConfigList, ConfigObject } = require('../config_types') const rules = require('../rules') @@ -30,7 +34,7 @@ const getPluginList = () => { ) result.append('CaseSensitivePaths', new CaseSensitivePathsPlugin()) result.append( - 'ExtractText', + 'MiniCssExtract', new MiniCssExtractPlugin({ filename: '[name]-[contenthash:8].css', chunkFilename: '[name]-[contenthash:8].chunk.css' @@ -39,6 +43,8 @@ const getPluginList = () => { result.append( 'Manifest', new WebpackAssetsManifest({ + integrity: true, + entrypoints: true, writeToDisk: true, publicPath: true }) @@ -85,11 +91,13 @@ const getBaseConfig = () => new ConfigObject({ }, resolve: { - extensions: config.extensions + extensions: config.extensions, + plugins: [PnpWebpackPlugin] }, resolveLoader: { - modules: ['node_modules'] + modules: ['node_modules'], + plugins: [PnpWebpackPlugin.moduleLoader(module)] }, node: { @@ -110,13 +118,42 @@ module.exports = class Base { this.resolvedModules = getModulePaths() } + splitChunks(callback = null) { + let appConfig = {} + const defaultConfig = { + optimization: { + // Split vendor and common chunks + // https://twitter.com/wSokra/status/969633336732905474 + splitChunks: { + chunks: 'all', + name: false + }, + // Separate runtime chunk to enable long term caching + // https://twitter.com/wSokra/status/969679223278505985 + runtimeChunk: true + } + } + + if (callback) { + appConfig = callback(defaultConfig) + if (isNotObject(appConfig)) { + throw new Error(` + ${prettyPrint(appConfig)} is not a valid splitChunks configuration. + See https://webpack.js.org/plugins/split-chunks-plugin/#configuration + `) + } + } + + return this.config.merge(deepMerge(defaultConfig, appConfig)) + } + toWebpackConfig() { return this.config.merge({ entry: this.entry.toObject(), module: { strictExportPresence: true, - rules: this.loaders.values() + rules: [{ parser: { requireEnsure: false } }, ...this.loaders.values()] }, plugins: this.plugins.values(), diff --git a/package/rules/babel.js b/package/rules/babel.js index 31714ec70..c9dda22fa 100644 --- a/package/rules/babel.js +++ b/package/rules/babel.js @@ -1,14 +1,20 @@ -const { join } = require('path') -const { cache_path: cachePath } = require('../config') +const { join, resolve } = require('path') +const { cache_path: cachePath, source_path: sourcePath } = require('../config') +const { nodeEnv } = require('../env') +// Process application Javascript code with Babel. +// Uses application .babelrc to apply any transformations module.exports = { - test: /\.(js|jsx)?(\.erb)?$/, + test: /\.(js|jsx|mjs)?(\.erb)?$/, + include: resolve(sourcePath), exclude: /node_modules/, use: [ { loader: 'babel-loader', options: { - cacheDirectory: join(cachePath, 'babel-loader') + cacheDirectory: join(cachePath, 'babel-loader-node-modules'), + cacheCompression: nodeEnv === 'production', + compact: nodeEnv === 'production' } } ] diff --git a/package/rules/file.js b/package/rules/file.js index 423836986..917349260 100644 --- a/package/rules/file.js +++ b/package/rules/file.js @@ -1,8 +1,8 @@ const { join } = require('path') -const { source_path: sourcePath } = require('../config') +const { source_path: sourcePath, static_assets_extensions: fileExtensions } = require('../config') module.exports = { - test: /\.(jpg|jpeg|png|gif|tiff|ico|svg|eot|otf|ttf|woff|woff2)$/i, + test: new RegExp(`(${fileExtensions.join('|')})`, 'i'), use: [ { loader: 'file-loader', diff --git a/package/rules/index.js b/package/rules/index.js index 1a4ea3c2a..923a38f1b 100644 --- a/package/rules/index.js +++ b/package/rules/index.js @@ -4,12 +4,17 @@ const css = require('./css') const sass = require('./sass') const moduleCss = require('./module.css') const moduleSass = require('./module.sass') +const nodeModules = require('./node_modules') +// Webpack loaders are processed in reverse order +// https://webpack.js.org/concepts/loaders/#loader-features +// Lastly, process static files using file loader module.exports = { - babel, + file, css, sass, moduleCss, moduleSass, - file + nodeModules, + babel } diff --git a/package/rules/node_modules.js b/package/rules/node_modules.js new file mode 100644 index 000000000..829643c11 --- /dev/null +++ b/package/rules/node_modules.js @@ -0,0 +1,22 @@ +const { join } = require('path') +const { cache_path: cachePath } = require('../config') +const { nodeEnv } = require('../env') + +// Compile standard ES features for JS in node_modules with Babel. +module.exports = { + test: /\.(js|mjs)$/, + exclude: /@babel(?:\/|\\{1,2})runtime/, + use: [ + { + loader: 'babel-loader', + options: { + babelrc: false, + presets: [['@babel/preset-env', { modules: false }]], + cacheDirectory: join(cachePath, 'babel-loader-node-modules'), + cacheCompression: nodeEnv === 'production', + compact: false, + sourceMaps: false + } + } + ] +} diff --git a/package/utils/get_style_rule.js b/package/utils/get_style_rule.js index a38316aba..218c4c02a 100644 --- a/package/utils/get_style_rule.js +++ b/package/utils/get_style_rule.js @@ -3,10 +3,9 @@ const { resolve } = require('path') const devServer = require('../dev_server') const { nodeEnv } = require('../env') -const isProduction = nodeEnv === 'production' const inDevServer = process.argv.find(v => v.includes('webpack-dev-server')) const isHMR = inDevServer && (devServer && devServer.hmr) -const extractCSS = !isHMR || isProduction +const extractCSS = !isHMR || nodeEnv === 'production' const styleLoader = { loader: 'style-loader', @@ -45,7 +44,8 @@ const getStyleRule = (test, modules = false, preprocessors = []) => { use.unshift(styleLoader) } - return Object.assign({}, { test, use }, options) + // sideEffects - See https://github.com/webpack/webpack/issues/6571 + return Object.assign({}, { test, use, sideEffects: !modules }, options) } module.exports = getStyleRule diff --git a/package/utils/helpers.js b/package/utils/helpers.js index 54c4de4f2..6456fd8ec 100644 --- a/package/utils/helpers.js +++ b/package/utils/helpers.js @@ -2,6 +2,8 @@ const isObject = value => typeof value === 'object' && value !== null && (value.length === undefined || value.length === null) +const isNotObject = value => !isObject(value) + const isBoolean = str => /^true/.test(str) || /^false/.test(str) const isEmpty = value => value === null || value === undefined @@ -31,15 +33,24 @@ const chdirTestApp = () => { const chdirCwd = () => process.chdir(process.cwd()) +const resetEnv = () => { + process.env = {} +} + +const ensureTrailingSlash = path => (path.endsWith('/') ? path : `${path}/`) + module.exports = { chdirTestApp, chdirCwd, + ensureTrailingSlash, isObject, + isNotObject, isBoolean, isArray, isEqual, isEmpty, isStrPath, canMerge, - prettyPrint + prettyPrint, + resetEnv } diff --git a/test/test_app/config/webpacker.yml b/test/test_app/config/webpacker.yml index d5b4560a8..79d7b921f 100644 --- a/test/test_app/config/webpacker.yml +++ b/test/test_app/config/webpacker.yml @@ -16,7 +16,22 @@ default: &default # Reload manifest.json on all requests so we reload latest compiled packs cache_manifest: false + static_assets_extensions: + - .jpg + - .jpeg + - .png + - .gif + - .tiff + - .ico + - .svg + - .eot + - .otf + - .ttf + - .woff + - .woff2 + extensions: + - .mjs - .js - .sass - .scss diff --git a/yarn.lock b/yarn.lock index 32fc65cc8..b524570e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5619,6 +5619,13 @@ pn@^1.1.0: resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== +pnp-webpack-plugin@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.2.1.tgz#cd9d698df2a6fcf7255093c1c9511adf65b9421b" + integrity sha512-W6GctK7K2qQiVR+gYSv/Gyt6jwwIH4vwdviFqx+Y2jAtVf5eZyYIDf5Ac2NCDMBiX5yWscBLZElPTsyA1UtVVA== + dependencies: + ts-pnp "^1.0.0" + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -7545,6 +7552,11 @@ trim-right@^1.0.1: dependencies: glob "^7.1.2" +ts-pnp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.0.0.tgz#44a3a9e8c13fcb711bcda75d7b576c21af120c9d" + integrity sha512-qgwM7eBrxFvZSXLtSvjf3c2mXwJOOGD49VlE+KocUGX95DuMdLc/psZHBnPpZL5b2NU7VtQGHRCWF3cNfe5kxQ== + tslib@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"