diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..98295bdd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +; top-most EditorConfig file +root = true + +; Unix-style newlines +[*] +end_of_line = LF + +[*.js] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 1a6fc5b0..b4408365 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -54,7 +54,7 @@ module.exports = { "node/no-unpublished-bin": "error", "node/no-unpublished-require": "error", "node/process-exit-as-throw": "error", - "header/header": [2, "block", {"pattern": "This file is part of the Symfony package"}] + "header/header": [2, "block", {"pattern": "This file is part of the Symfony Webpack Encore package"}] } }; diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d58b258..14941365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,72 @@ # CHANGELOG +## 0.15.1 + + * Fixed bug with using `?` in your versioning strategy with + `addStyleEntry` - #161 via @Lyrkan + + * Fixed bug when using `webpack.config.babel.js` with ES6 + imports - #167 via @Lyrkan + +## 0.15.0 + + * Add support for [Preact](https://preactjs.com/) - #144 via @Lyrkan + + * Added `Encore.configureManifestPlugin()` method - #142 via @Seikyo + + * Added 5 new methods to configure plugins! #152 via @Lyrkan + * `Encore.configureDefinePlugin()` + * `Encore.configureExtractTextPlugin()` + * `Encore.configureFriendlyErrorsPlugin()` + * `Encore.configureLoaderOptionsPlugin()` + * `Encore.configureUglifyJsPlugin()` + +## 0.14.0 + + * Added `Encore.configureFilenames()` so that you can fully control + the filename patterns for all types of files - #137 via @Lyrkan + + * Added `Encore.configureRuntimeEnvironment()`, which is useful + if you need to require `webpack.config.js` from some non-Encore + process (e.g. Karma) - #115 via @Lyrkan + +## 0.13.0 + + * [BEHAVIOR CHANGE] Image and font files now *always* include + a hash in their filename, and the hash is shorter - #110 via @Lyrkan + + * Fixed a bug that caused extra comments to be in the final production + compiled JavaScript - #132 via @weaverryan + + * `Encore.enablePostCssLoader()` now accepts an options callback - + #130 via @Lyrkan + + * `Encore.enableLessLoader()` now accepts an options callback - + #134 via @Lyrkan + + * Added `Encore.enableForkedTypeScriptTypesChecking()` to enable + [fork-ts-checker-webpack-plugin](https://github.com/Realytics/fork-ts-checker-webpack-plugin) + for faster typescript type checking - #101 via @davidmpaz + + * Added `Encore.disableImagesLoader()` and `Encore.disableFontsLoader()` + to totally disable the `file-loader` rules for images and fonts - + #103 via @Lyrkan + +## 0.12.0 + + * Fixed a bug with webpack 3.4.0 ("Can't resolve dev") - #114. + + * Added `--keep-public-path` option to `dev-server` that allows + you to specify that you do *not* want your `publicPath` to + automatically point at the dev-server URL. Also relaxed the + requirements when using `dev-server` so that you *can* now + specify a custom, fully-qualified `publicPath` URL - #96 + + * Fixed bug where `@import` CSS wouldn't use postcss - #108 + ## 0.11.0 - * The `webpack` page was upgraded from version 2.2 to 3.1 #53. The + * The `webpack` package was upgraded from version 2.2 to 3.1 #53. The `extract-text-webpack-plugin` package was also upgraded from 2.1 to 3.0. diff --git a/bin/encore.js b/bin/encore.js index 73087d4f..614ac10f 100755 --- a/bin/encore.js +++ b/bin/encore.js @@ -1,6 +1,6 @@ #!/usr/bin/env node /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * @@ -10,13 +10,12 @@ 'use strict'; -const path = require('path'); const parseRuntime = require('../lib/config/parse-runtime'); const context = require('../lib/context'); const chalk = require('chalk'); const runtimeConfig = parseRuntime( - require('yargs').argv, + require('yargs/yargs')(process.argv.slice(2)).argv, process.cwd() ); context.runtimeConfig = runtimeConfig; @@ -62,11 +61,14 @@ function showUsageInstructions() { console.log('Commands:'); console.log(` ${chalk.green('dev')} : runs webpack for development`); console.log(' - Supports any webpack options (e.g. --watch)'); + console.log(); console.log(` ${chalk.green('dev-server')} : runs webpack-dev-server`); console.log(` - ${chalk.yellow('--host')} The hostname/ip address the webpack-dev-server will bind to`); console.log(` - ${chalk.yellow('--port')} The port the webpack-dev-server will bind to`); console.log(` - ${chalk.yellow('--hot')} Enable HMR on webpack-dev-server`); + console.log(` - ${chalk.yellow('--keep-public-path')} Do not change the public path (it is usually prefixed by the dev server URL)`); console.log(' - Supports any webpack-dev-server options'); + console.log(); console.log(` ${chalk.green('production')} : runs webpack for production`); console.log(' - Supports any webpack options (e.g. --watch)'); console.log(); diff --git a/fixtures/css/imports_autoprefixer.css b/fixtures/css/imports_autoprefixer.css new file mode 100644 index 00000000..4dce6d71 --- /dev/null +++ b/fixtures/css/imports_autoprefixer.css @@ -0,0 +1 @@ +@import "autoprefixer_test.css"; \ No newline at end of file diff --git a/fixtures/css/same_filename.css b/fixtures/css/same_filename.css new file mode 100644 index 00000000..78b2927b --- /dev/null +++ b/fixtures/css/same_filename.css @@ -0,0 +1,17 @@ +h4 { + background: top left url('./../images/symfony_logo.png') no-repeat; +} + +h5 { + background: top left url('./../images/same_filename/symfony_logo.png') no-repeat; +} + +@font-face { + font-family: 'Roboto'; + src: url('./../fonts/Roboto.woff2') format('woff2'); +} + +@font-face { + font-family: 'Roboto2'; + src: url('./../fonts/same_filename/Roboto.woff2') format('woff2'); +} \ No newline at end of file diff --git a/fixtures/css/style_with_fonts.scss b/fixtures/css/style_with_fonts.scss new file mode 100644 index 00000000..590236ea --- /dev/null +++ b/fixtures/css/style_with_fonts.scss @@ -0,0 +1,9 @@ +$font-path: '../fonts'; + +@font-face { + font-family: 'Roboto'; + src:url('#{$font-path}/Roboto.woff2') format('woff2'), + url('#{$font-path}/Roboto.svg') format('svg'); + font-weight: normal; + font-style: normal; +} diff --git a/fixtures/fonts/Roboto.svg b/fixtures/fonts/Roboto.svg new file mode 100644 index 00000000..417a2a9e --- /dev/null +++ b/fixtures/fonts/Roboto.svg @@ -0,0 +1,643 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fixtures/fonts/same_filename/Roboto.woff2 b/fixtures/fonts/same_filename/Roboto.woff2 new file mode 100644 index 00000000..0707d9ab Binary files /dev/null and b/fixtures/fonts/same_filename/Roboto.woff2 differ diff --git a/fixtures/images/same_filename/symfony_logo.png b/fixtures/images/same_filename/symfony_logo.png new file mode 100644 index 00000000..c065647d Binary files /dev/null and b/fixtures/images/same_filename/symfony_logo.png differ diff --git a/index.js b/index.js index 9eea6a31..8f1ceda1 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * @@ -13,16 +13,29 @@ const WebpackConfig = require('./lib/WebpackConfig'); const configGenerator = require('./lib/config-generator'); const validator = require('./lib/config/validator'); const PrettyError = require('pretty-error'); -const runtimeConfig = require('./lib/context').runtimeConfig; +const logger = require('./lib/logger'); +const parseRuntime = require('./lib/config/parse-runtime'); +const chalk = require('chalk'); +const levenshtein = require('fast-levenshtein'); -// at this time, the encore executable should have set the runtimeConfig -if (!runtimeConfig) { - throw new Error('Are you trying to require index.js directly?'); +let webpackConfig = null; +let runtimeConfig = require('./lib/context').runtimeConfig; + +function initializeWebpackConfig() { + if (runtimeConfig.verbose) { + logger.verbose(); + } + + webpackConfig = new WebpackConfig(runtimeConfig); } -let webpackConfig = new WebpackConfig(runtimeConfig); +// If runtimeConfig is already set webpackConfig can directly +// be initialized here. +if (runtimeConfig) { + initializeWebpackConfig(); +} -module.exports = { +const publicApi = { /** * The directory where your files should be output. * @@ -62,6 +75,18 @@ module.exports = { return this; }, + setFontsPublicPath(publicPath) { + webpackConfig.setFontsPublicPath(publicPath); + + return this; + }, + + setImagesPublicPath(publicPath) { + webpackConfig.setImagesPublicPath(publicPath); + + return this; + }, + /** * Used as a prefix to the *keys* in manifest.json. Not usually needed. * @@ -91,6 +116,121 @@ module.exports = { return this; }, + /** + * Allows you to configure the options passed to the DefinePlugin. + * A list of available options can be found at https://webpack.js.org/plugins/define-plugin/ + * + * For example: + * + * Encore.configureDefinePlugin((options) => { + * options.VERSION = JSON.stringify('1.0.0'); + * }) + * + * @param {function} definePluginOptionsCallback + * @returns {exports} + */ + configureDefinePlugin(definePluginOptionsCallback = () => {}) { + webpackConfig.configureDefinePlugin(definePluginOptionsCallback); + + return this; + }, + + /** + * Allows you to configure the options passed to the extract-text-webpack-plugin. + * A list of available options can be found at https://github.com/webpack-contrib/extract-text-webpack-plugin + * + * For example: + * + * Encore.configureExtractTextPlugin((options) => { + * options.ignoreOrder = true; + * }) + * + * @param {function} extractTextPluginOptionsCallback + * @returns {exports} + */ + configureExtractTextPlugin(extractTextPluginOptionsCallback = () => {}) { + webpackConfig.configureExtractTextPlugin(extractTextPluginOptionsCallback); + + return this; + }, + + /** + * Allows you to configure the options passed to the friendly-errors-webpack-plugin. + * A list of available options can be found at https://github.com/geowarin/friendly-errors-webpack-plugin + * + * For example: + * + * Encore.configureFriendlyErrorsPlugin((options) => { + * options.clearConsole = true; + * }) + * + * @param {function} friendlyErrorsPluginOptionsCallback + * @returns {exports} + */ + configureFriendlyErrorsPlugin(friendlyErrorsPluginOptionsCallback = () => {}) { + webpackConfig.configureFriendlyErrorsPlugin(friendlyErrorsPluginOptionsCallback); + + return this; + }, + + /** + * Allows you to configure the options passed to the LoaderOptionsPlugins. + * A list of available options can be found at https://webpack.js.org/plugins/loader-options-plugin/ + * + * For example: + * + * Encore.configureLoaderOptionsPlugin((options) => { + * options.minimize = true; + * }) + * + * @param {function} loaderOptionsPluginOptionsCallback + * @returns {exports} + */ + configureLoaderOptionsPlugin(loaderOptionsPluginOptionsCallback = () => {}) { + webpackConfig.configureLoaderOptionsPlugin(loaderOptionsPluginOptionsCallback); + + return this; + }, + + /** + * Allows you to configure the options passed to webpack-manifest-plugin. + * A list of available options can be found at https://github.com/danethurber/webpack-manifest-plugin + * + * For example: + * + * Encore.configureManifestPlugin((options) => { + * options.fileName = '../../var/assets/manifest.json'; + * }) + * + * @param {function} manifestPluginOptionsCallback + * @returns {exports} + */ + configureManifestPlugin(manifestPluginOptionsCallback = () => {}) { + webpackConfig.configureManifestPlugin(manifestPluginOptionsCallback); + + return this; + }, + + /** + * Allows you to configure the options passed to the uglifyjs-webpack-plugin. + * A list of available options can be found at https://github.com/webpack-contrib/uglifyjs-webpack-plugin/tree/v0.4.6 + * + * For example: + * + * Encore.configureUglifyJsPlugin((options) => { + * options.compress = false; + * options.beautify = true; + * }) + * + * @param {function} uglifyJsPluginOptionsCallback + * @returns {exports} + */ + configureUglifyJsPlugin(uglifyJsPluginOptionsCallback = () => {}) { + webpackConfig.configureUglifyJsPlugin(uglifyJsPluginOptionsCallback); + + return this; + }, + /** * Adds a JavaScript file that should be webpacked: * @@ -271,10 +411,20 @@ module.exports = { * * https://github.com/postcss/postcss-loader * + * Encore.enablePostCssLoader(); + * + * Or pass options to the loader + * + * Encore.enablePostCssLoader(function(options) { + * // https://github.com/postcss/postcss-loader#options + * // options.config = {...} + * }) + * + * @param {function} postCssLoaderOptionsCallback * @return {exports} */ - enablePostCssLoader() { - webpackConfig.enablePostCssLoader(); + enablePostCssLoader(postCssLoaderOptionsCallback = () => {}) { + webpackConfig.enablePostCssLoader(postCssLoaderOptionsCallback); return this; }, @@ -291,11 +441,11 @@ module.exports = { * // options.includePaths = [...] * }, { * // set optional Encore-specific options - * // resolve_url_loader: true + * // resolveUrlLoader: true * }); * * Supported options: - * * {bool} resolve_url_loader (default=true) + * * {bool} resolveUrlLoader (default=true) * Whether or not to use the resolve-url-loader. * Setting to false can increase performance in some * cases, especially when using bootstrap_sass. But, @@ -316,10 +466,21 @@ module.exports = { /** * Call this if you plan on loading less files. * + * Encore.enableLessLoader(); + * + * Or pass options to the loader + * + * Encore.enableLessLoader(function(options) { + * // https://github.com/webpack-contrib/less-loader#examples + * // http://lesscss.org/usage/#command-line-usage-options + * // options.relativeUrls = false; + * }); + * + * @param {function} lessLoaderOptionsCallback * @return {exports} */ - enableLessLoader() { - webpackConfig.enableLessLoader(); + enableLessLoader(lessLoaderOptionsCallback = () => {}) { + webpackConfig.enableLessLoader(lessLoaderOptionsCallback); return this; }, @@ -355,6 +516,26 @@ module.exports = { return this; }, + /** + * If enabled, a Preact preset will be applied to + * the generated Webpack configuration. + * + * Encore.enablePreactPreset() + * + * If you wish to also use preact-compat (https://github.com/developit/preact-compat) + * you can enable it by setting the "preactCompat" option to true: + * + * Encore.enablePreactPreset({ preactCompat: true }) + * + * @param {object} options + * @returns {exports} + */ + enablePreactPreset(options = {}) { + webpackConfig.enablePreactPreset(options); + + return this; + }, + /** * Call this if you plan on loading TypeScript files. * @@ -372,6 +553,25 @@ module.exports = { */ enableTypeScriptLoader(callback = () => {}) { webpackConfig.enableTypeScriptLoader(callback); + + return this; + }, + + /** + * Call this to enable forked type checking for TypeScript loader + * https://github.com/TypeStrong/ts-loader/blob/v2.3.0/README.md#faster-builds + * + * This is a build optimization API to reduce build times. + * + * @param {function} forkedTypeScriptTypesCheckOptionsCallback + * @return {exports} + */ + enableForkedTypeScriptTypesChecking(forkedTypeScriptTypesCheckOptionsCallback = () => {}) { + webpackConfig.enableForkedTypeScriptTypesChecking( + forkedTypeScriptTypesCheckOptionsCallback + ); + + return this; }, /** @@ -397,13 +597,74 @@ module.exports = { }, /** - * If enabled, the output directory is emptied between - * each build (to remove old files). + * Call this if you wish to disable the default + * images loader. + * + * @returns {exports} + */ + disableImagesLoader() { + webpackConfig.disableImagesLoader(); + + return this; + }, + + /** + * Call this if you wish to disable the default + * fonts loader. + * + * @returns {exports} + */ + disableFontsLoader() { + webpackConfig.disableFontsLoader(); + + return this; + }, + + /** + * Call this to change how the name of each output + * file is generated. + * + * Encore.configureFilenames({ + * js: '[name].[chunkhash].js', + * css: '[name].[contenthash].css', + * images: 'images/[name].[hash:8].[ext]', + * fonts: 'fonts/[name].[hash:8].[ext]' + * }); + * + * It's safe to omit a key (e.g. css): the default naming strategy + * will be used for any file types not passed. + * + * If you are using Encore.enableVersioning() + * make sure that your "js" filenames contain + * "[chunkhash]" and your "css" filenames contain + * "[contenthash]". * + * @param {object} filenames * @returns {exports} */ - cleanupOutputBeforeBuild() { - webpackConfig.cleanupOutputBeforeBuild(); + configureFilenames(filenames) { + webpackConfig.configureFilenames(filenames); + + return this; + }, + + /** + * If enabled, the output directory is emptied between each build (to remove old files). + * + * A list of available options can be found at https://github.com/johnagan/clean-webpack-plugin + * + * For example: + * + * Encore.cleanupOutputBeforeBuild(['*.js'], (options) => { + * options.dry = true; + * }) + * + * @param {Array} paths Paths that should be cleaned, relative to the "root" option + * @param {function} cleanWebpackPluginOptionsCallback + * @returns {exports} + */ + cleanupOutputBeforeBuild(paths = ['**/*'], cleanWebpackPluginOptionsCallback = () => {}) { + webpackConfig.cleanupOutputBeforeBuild(paths, cleanWebpackPluginOptionsCallback); return this; }, @@ -425,17 +686,9 @@ module.exports = { * @returns {*} */ getWebpackConfig() { - try { - validator(webpackConfig); - - return configGenerator(webpackConfig); - } catch (error) { - // prettifies errors thrown by our library - const pe = new PrettyError(); + validator(webpackConfig); - console.log(pe.render(error)); - process.exit(1); // eslint-disable-line - } + return configGenerator(webpackConfig); }, /** @@ -448,5 +701,128 @@ module.exports = { */ reset() { webpackConfig = new WebpackConfig(runtimeConfig); - } + }, + + /** + * Initialize the runtime environment. + * + * This can be used to configure the Encore runtime if you're + * using Encore without executing the "./node_module/.bin/encore" + * utility (e.g. with karma-webpack). + * + * Encore.configureRuntimeEnvironment( + * // Environment to use (dev, dev-server, production) + * 'dev-server', + * + * // Same options you would use with the + * // CLI utility with their name in + * // camelCase. + * { + * https: true, + * keepPublicPath: true + * } + * ) + * + * Be aware than using this method will also reset the current + * webpack configuration. + * + * @param {string} environment + * @param {object} options + * @returns {exports} + */ + configureRuntimeEnvironment(environment, options = {}) { + runtimeConfig = parseRuntime( + Object.assign( + {}, + require('yargs/yargs')([environment]).argv, + options + ), + process.cwd() + ); + + initializeWebpackConfig(); + + return this; + }, + + /** + * Clear the runtime environment. + * + * Be aware than using this method will also reset the + * current webpack configuration. + * + * @returns {void} + */ + clearRuntimeEnvironment() { + runtimeConfig = null; + webpackConfig = null; + }, }; + +// Proxy the API in order to prevent calls to most of its methods +// if the webpackConfig object hasn't been initialized yet. +const publicApiProxy = new Proxy(publicApi, { + get: (target, prop) => { + if (prop === '__esModule') { + // When using Babel to preprocess a webpack.config.babel.js file + // (for instance if we want to use ES6 syntax) the __esModule + // property needs to be whitelisted to avoid an "Unknown property" + // error. + return target[prop]; + } + + if (typeof target[prop] === 'function') { + // These methods of the public API can be called even if the + // webpackConfig object hasn't been initialized yet. + const safeMethods = [ + 'configureRuntimeEnvironment', + 'clearRuntimeEnvironment', + ]; + + if (!webpackConfig && (safeMethods.indexOf(prop) === -1)) { + throw new Error(`Encore.${prop}() cannot be called yet because the runtime environment doesn't appear to be configured. Make sure you're using the encore executable or call Encore.configureRuntimeEnvironment() first if you're purposely not calling Encore directly.`); + } + + // Either a safe method has been called or the webpackConfig + // object is already available. In this case act as a passthrough. + return (...parameters) => { + try { + const res = target[prop](...parameters); + return (res === target) ? publicApiProxy : res; + } catch (error) { + // prettifies errors thrown by our library + const pe = new PrettyError(); + + console.log(pe.render(error)); + process.exit(1); // eslint-disable-line + } + }; + } + + if (typeof target[prop] === 'undefined') { + // Find the property with the closest Levenshtein distance + let similarProperty; + let minDistance = Number.MAX_VALUE; + for (const apiProperty in target) { + const distance = levenshtein.get(apiProperty, prop); + if (distance <= minDistance) { + similarProperty = apiProperty; + minDistance = distance; + } + } + + let errorMessage = `${chalk.red(`Encore.${prop}`)} is not a recognized property or method.`; + if (minDistance < (prop.length / 3)) { + errorMessage += ` Did you mean ${chalk.green(`Encore.${similarProperty}`)}?`; + } + + const error = new Error(errorMessage); + console.log(new PrettyError().render(error)); + process.exit(1); // eslint-disable-line + } + + return target[prop]; + } +}); + +module.exports = publicApiProxy; diff --git a/lib/WebpackConfig.js b/lib/WebpackConfig.js index 79458ca6..dab6cd0a 100644 --- a/lib/WebpackConfig.js +++ b/lib/WebpackConfig.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * @@ -11,6 +11,7 @@ const path = require('path'); const fs = require('fs'); +const logger = require('./logger'); /** * @param {RuntimeConfig} runtimeConfig @@ -30,32 +31,69 @@ function validateRuntimeConfig(runtimeConfig) { class WebpackConfig { constructor(runtimeConfig) { validateRuntimeConfig(runtimeConfig); + this.runtimeConfig = runtimeConfig; this.outputPath = null; this.publicPath = null; + this.imagesPublicPath = null; + this.fontsPublicPath = null; this.manifestKeyPrefix = null; this.entries = new Map(); this.styleEntries = new Map(); this.plugins = []; + this.loaders = []; + + // Global settings + this.outputPath = null; + this.publicPath = null; + this.manifestKeyPrefix = null; + this.sharedCommonsEntryName = null; + this.providedVariables = {}; + this.configuredFilenames = {}; + + // Features/Loaders flags this.useVersioning = false; this.useSourceMaps = false; + this.cleanupOutput = false; + this.useImagesLoader = true; + this.useFontsLoader = true; this.usePostCssLoader = false; - this.useSassLoader = false; - this.sassLoaderOptionsCallback = function() {}; - this.sassOptions = { - resolve_url_loader: true - }; this.useLessLoader = false; - this.cleanupOutput = false; - this.sharedCommonsEntryName = null; - this.providedVariables = {}; - this.babelConfigurationCallback = function() {}; + this.useSassLoader = false; this.useReact = false; + this.usePreact = false; this.useVueLoader = false; - this.vueLoaderOptionsCallback = () => {}; - this.loaders = []; this.useTypeScriptLoader = false; - this.tsConfigurationCallback = function() {}; + this.useForkedTypeScriptTypeChecking = false; + + // Features/Loaders options + this.sassOptions = { + resolveUrlLoader: true + }; + this.preactOptions = { + preactCompat: false + }; + + // Features/Loaders options callbacks + this.postCssLoaderOptionsCallback = () => {}; + this.sassLoaderOptionsCallback = () => {}; + this.lessLoaderOptionsCallback = () => {}; + this.babelConfigurationCallback = () => {}; + this.vueLoaderOptionsCallback = () => {}; + this.tsConfigurationCallback = () => {}; + + // Plugins options + this.cleanWebpackPluginPaths = ['**/*']; + + // Plugins callbacks + this.cleanWebpackPluginOptionsCallback = () => {}; + this.definePluginOptionsCallback = () => {}; + this.extractTextPluginOptionsCallback = () => {}; + this.forkedTypeScriptTypesCheckOptionsCallback = () => {}; + this.friendlyErrorsPluginOptionsCallback = () => {}; + this.loaderOptionsPluginOptionsCallback = () => {}; + this.manifestPluginOptionsCallback = () => {}; + this.uglifyJsPluginOptionsCallback = () => {}; } getContext() { @@ -86,23 +124,11 @@ class WebpackConfig { } setPublicPath(publicPath) { - /* - * Do not allow absolute URLs *and* the webpackDevServer - * to be used at the same time. The webpackDevServer basically - * provides the publicPath (and so in those cases, publicPath) - * is simply used as the default manifestKeyPrefix. - */ - if (publicPath.includes('://')) { - if (this.useDevServer()) { - throw new Error('You cannot pass an absolute URL to setPublicPath() and use the dev-server at the same time. Try using Encore.isProduction() to only configure your absolute publicPath for production.'); - } - } else { - if (publicPath.indexOf('/') !== 0) { - // technically, not starting with "/" is legal, but not - // what you want in most cases. Let's not let the user make - // a mistake (and we can always change this later). - throw new Error('The value passed to setPublicPath() must start with "/" or be a full URL (http://...)'); - } + if (publicPath.includes('://') === false && publicPath.indexOf('/') !== 0) { + // technically, not starting with "/" is legal, but not + // what you want in most cases. Let's not let the user make + // a mistake (and we can always change this later). + throw new Error('The value passed to setPublicPath() must start with "/" or be a full URL (http://...)'); } // guarantee a single trailing slash @@ -112,7 +138,35 @@ class WebpackConfig { this.publicPath = publicPath; } + setFontsPublicPath(publicPath) { + // guarantee a single trailing slash + publicPath = publicPath.replace(/\/$/,''); + publicPath = publicPath + '/'; + + this.fontsPublicPath = publicPath; + } + + setImagesPublicPath(publicPath) { + // guarantee a single trailing slash + publicPath = publicPath.replace(/\/$/,''); + publicPath = publicPath + '/'; + + this.imagesPublicPath = publicPath; + } + setManifestKeyPrefix(manifestKeyPrefix) { + /* + * Normally, we make sure that the manifest keys don't start + * with an opening "/" ever... for consistency. If you need + * to manually specify the manifest key (e.g. because you're + * publicPath is absolute), it's easy to accidentally add + * an opening slash (thereby changing your key prefix) without + * intending to. Hence, the warning. + */ + if (manifestKeyPrefix.indexOf('/') === 0) { + logger.warning(`The value passed to setManifestKeyPrefix "${manifestKeyPrefix}" starts with "/". This is allowed, but since the key prefix does not normally start with a "/", you may have just changed the prefix accidentally.`); + } + // guarantee a single trailing slash, except for blank strings if (manifestKeyPrefix !== '') { manifestKeyPrefix = manifestKeyPrefix.replace(/\/$/, ''); @@ -122,6 +176,54 @@ class WebpackConfig { this.manifestKeyPrefix = manifestKeyPrefix; } + configureDefinePlugin(definePluginOptionsCallback = () => {}) { + if (typeof definePluginOptionsCallback !== 'function') { + throw new Error('Argument 1 to configureDefinePlugin() must be a callback function'); + } + + this.definePluginOptionsCallback = definePluginOptionsCallback; + } + + configureExtractTextPlugin(extractTextPluginOptionsCallback = () => {}) { + if (typeof extractTextPluginOptionsCallback !== 'function') { + throw new Error('Argument 1 to configureExtractTextPlugin() must be a callback function'); + } + + this.extractTextPluginOptionsCallback = extractTextPluginOptionsCallback; + } + + configureFriendlyErrorsPlugin(friendlyErrorsPluginOptionsCallback = () => {}) { + if (typeof friendlyErrorsPluginOptionsCallback !== 'function') { + throw new Error('Argument 1 to configureFriendlyErrorsPlugin() must be a callback function'); + } + + this.friendlyErrorsPluginOptionsCallback = friendlyErrorsPluginOptionsCallback; + } + + configureLoaderOptionsPlugin(loaderOptionsPluginOptionsCallback = () => {}) { + if (typeof loaderOptionsPluginOptionsCallback !== 'function') { + throw new Error('Argument 1 to configureLoaderOptionsPlugin() must be a callback function'); + } + + this.loaderOptionsPluginOptionsCallback = loaderOptionsPluginOptionsCallback; + } + + configureManifestPlugin(manifestPluginOptionsCallback = () => {}) { + if (typeof manifestPluginOptionsCallback !== 'function') { + throw new Error('Argument 1 to configureManifestPlugin() must be a callback function'); + } + + this.manifestPluginOptionsCallback = manifestPluginOptionsCallback; + } + + configureUglifyJsPlugin(uglifyJsPluginOptionsCallback = () => {}) { + if (typeof uglifyJsPluginOptionsCallback !== 'function') { + throw new Error('Argument 1 to configureUglifyJsPlugin() must be a callback function'); + } + + this.uglifyJsPluginOptionsCallback = uglifyJsPluginOptionsCallback; + } + /** * Returns the value that should be used as the publicPath, * which can be overridden by enabling the webpackDevServer @@ -129,13 +231,40 @@ class WebpackConfig { * @returns {string} */ getRealPublicPath() { + if (!this.useDevServer()) { + return this.publicPath; + } + + if (this.runtimeConfig.devServerKeepPublicPath) { + return this.publicPath; + } + + if (this.publicPath.includes('://')) { + return this.publicPath; + } + + // if using dev-server, prefix the publicPath with the dev server URL + return this.runtimeConfig.devServerUrl.replace(/\/$/,'') + this.publicPath; + } + + getFontsPublicPath() { // if we're using webpack-dev-server, use it & add the publicPath if (this.useDevServer()) { // avoid 2 middle slashes return this.runtimeConfig.devServerUrl.replace(/\/$/,'') + this.publicPath; + } else { + return this.fontsPublicPath; } + } - return this.publicPath; + getImagesPublicPath() { + // if we're using webpack-dev-server, use it & add the publicPath + if (this.useDevServer()) { + // avoid 2 middle slashes + return this.runtimeConfig.devServerUrl.replace(/\/$/,'') + this.publicPath; + } else { + return this.imagesPublicPath; + } } addEntry(name, src) { @@ -203,8 +332,14 @@ class WebpackConfig { this.addEntry(name, files); } - enablePostCssLoader() { + enablePostCssLoader(postCssLoaderOptionsCallback = () => {}) { this.usePostCssLoader = true; + + if (typeof postCssLoaderOptionsCallback !== 'function') { + throw new Error('Argument 1 to enablePostCssLoader() must be a callback function.'); + } + + this.postCssLoaderOptionsCallback = postCssLoaderOptionsCallback; } enableSassLoader(sassLoaderOptionsCallback = () => {}, options = {}) { @@ -217,22 +352,46 @@ class WebpackConfig { this.sassLoaderOptionsCallback = sassLoaderOptionsCallback; for (const optionKey of Object.keys(options)) { - if (!(optionKey in this.sassOptions)) { - throw new Error(`Invalid option "${optionKey}" passed to enableSassLoader(). Valid keys are ${Object.keys(this.sassOptions).join(', ')}`); + let normalizedOptionKey = optionKey; + if (optionKey === 'resolve_url_loader') { + logger.deprecation('enableSassLoader: "resolve_url_loader" is deprecated. Please use "resolveUrlLoader" instead.'); + normalizedOptionKey = 'resolveUrlLoader'; + } + + if (!(normalizedOptionKey in this.sassOptions)) { + throw new Error(`Invalid option "${normalizedOptionKey}" passed to enableSassLoader(). Valid keys are ${Object.keys(this.sassOptions).join(', ')}`); } - this.sassOptions[optionKey] = options[optionKey]; + this.sassOptions[normalizedOptionKey] = options[optionKey]; } } - enableLessLoader() { + enableLessLoader(lessLoaderOptionsCallback = () => {}) { this.useLessLoader = true; + + if (typeof lessLoaderOptionsCallback !== 'function') { + throw new Error('Argument 1 to enableLessLoader() must be a callback function.'); + } + + this.lessLoaderOptionsCallback = lessLoaderOptionsCallback; } enableReactPreset() { this.useReact = true; } + enablePreactPreset(options = {}) { + this.usePreact = true; + + for (const optionKey of Object.keys(options)) { + if (!(optionKey in this.preactOptions)) { + throw new Error(`Invalid option "${optionKey}" passed to enablePreactPreset(). Valid keys are ${Object.keys(this.preactOptions).join(', ')}`); + } + + this.preactOptions[optionKey] = options[optionKey]; + } + } + enableTypeScriptLoader(callback = () => {}) { this.useTypeScriptLoader = true; @@ -243,6 +402,17 @@ class WebpackConfig { this.tsConfigurationCallback = callback; } + enableForkedTypeScriptTypesChecking(forkedTypeScriptTypesCheckOptionsCallback = () => {}) { + + if (typeof forkedTypeScriptTypesCheckOptionsCallback !== 'function') { + throw new Error('Argument 1 to enableForkedTypeScriptTypesChecking() must be a callback function.'); + } + + this.useForkedTypeScriptTypeChecking = true; + this.forkedTypeScriptTypesCheckOptionsCallback = + forkedTypeScriptTypesCheckOptionsCallback; + } + enableVueLoader(vueLoaderOptionsCallback = () => {}) { this.useVueLoader = true; @@ -253,8 +423,42 @@ class WebpackConfig { this.vueLoaderOptionsCallback = vueLoaderOptionsCallback; } - cleanupOutputBeforeBuild() { + disableImagesLoader() { + this.useImagesLoader = false; + } + + disableFontsLoader() { + this.useFontsLoader = false; + } + + configureFilenames(configuredFilenames = {}) { + if (typeof configuredFilenames !== 'object') { + throw new Error('Argument 1 to configureFilenames() must be an object.'); + } + + // Check allowed keys + const validKeys = ['js', 'css', 'images', 'fonts']; + for (const key of Object.keys(configuredFilenames)) { + if (validKeys.indexOf(key) === -1) { + throw new Error(`"${key}" is not a valid key for configureFilenames(). Valid keys: ${validKeys.join(', ')}.`); + } + } + + this.configuredFilenames = configuredFilenames; + } + + cleanupOutputBeforeBuild(paths = ['**/*'], cleanWebpackPluginOptionsCallback = () => {}) { + if (!Array.isArray(paths)) { + throw new Error('Argument 1 to cleanupOutputBeforeBuild() must be an Array of paths - e.g. [\'**/*\']'); + } + + if (typeof cleanWebpackPluginOptionsCallback !== 'function') { + throw new Error('Argument 2 to cleanupOutputBeforeBuild() must be a callback function'); + } + this.cleanupOutput = true; + this.cleanWebpackPluginPaths = paths; + this.cleanWebpackPluginOptionsCallback = cleanWebpackPluginOptionsCallback; } autoProvideVariables(variables) { diff --git a/lib/config-generator.js b/lib/config-generator.js index 26fca25c..b4927e5b 100644 --- a/lib/config-generator.js +++ b/lib/config-generator.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * @@ -9,28 +9,28 @@ 'use strict'; -const webpack = require('webpack'); -const ExtractTextPlugin = require('extract-text-webpack-plugin'); const extractText = require('./loaders/extract-text'); -const ManifestPlugin = require('./webpack/webpack-manifest-plugin'); -const DeleteUnusedEntriesJSPlugin = require('./webpack/delete-unused-entries-js-plugin'); -const AssetOutputDisplayPlugin = require('./friendly-errors/asset-output-display-plugin'); -const CleanWebpackPlugin = require('clean-webpack-plugin'); -const WebpackChunkHash = require('webpack-chunk-hash'); -const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin'); -const missingLoaderTransformer = require('./friendly-errors/transformers/missing-loader'); -const missingLoaderFormatter = require('./friendly-errors/formatters/missing-loader'); -const missingPostCssConfigTransformer = require('./friendly-errors/transformers/missing-postcss-config'); -const missingPostCssConfigFormatter = require('./friendly-errors/formatters/missing-postcss-config'); -const vueUnactivatedLoaderTransformer = require('./friendly-errors/transformers/vue-unactivated-loader-error'); -const vueUnactivatedLoaderFormatter = require('./friendly-errors/formatters/vue-unactivated-loader-error'); const pathUtil = require('./config/path-util'); +// loaders utils const cssLoaderUtil = require('./loaders/css'); const sassLoaderUtil = require('./loaders/sass'); const lessLoaderUtil = require('./loaders/less'); const babelLoaderUtil = require('./loaders/babel'); const tsLoaderUtil = require('./loaders/typescript'); const vueLoaderUtil = require('./loaders/vue'); +// plugins utils +const extractTextPluginUtil = require('./plugins/extract-text'); +const deleteUnusedEntriesPluginUtil = require('./plugins/delete-unused-entries'); +const manifestPluginUtil = require('./plugins/manifest'); +const loaderOptionsPluginUtil = require('./plugins/loader-options'); +const versioningPluginUtil = require('./plugins/versioning'); +const variableProviderPluginUtil = require('./plugins/variable-provider'); +const cleanPluginUtil = require('./plugins/clean'); +const commonsChunksPluginUtil = require('./plugins/commons-chunks'); +const definePluginUtil = require('./plugins/define'); +const uglifyPluginUtil = require('./plugins/uglify'); +const friendlyErrorPluginUtil = require('./plugins/friendly-errors'); +const assetOutputDisplay = require('./plugins/asset-output-display'); class ConfigGenerator { /** @@ -81,6 +81,11 @@ class ConfigGenerator { config.resolve.alias['vue$'] = 'vue/dist/vue.esm.js'; } + if (this.webpackConfig.usePreact && this.webpackConfig.preactOptions.preactCompat) { + config.resolve.alias['react'] = 'preact-compat'; + config.resolve.alias['react-dom'] = 'preact-compat'; + } + return config; } @@ -101,13 +106,19 @@ class ConfigGenerator { } buildOutputConfig() { + // Default filename can be overriden using Encore.configureFilenames({ js: '...' }) + let filename = this.webpackConfig.useVersioning ? '[name].[chunkhash].js' : '[name].js'; + if (this.webpackConfig.configuredFilenames.js) { + filename = this.webpackConfig.configuredFilenames.js; + } + return { path: this.webpackConfig.outputPath, - filename: this.webpackConfig.useVersioning ? '[name].[chunkhash].js' : '[name].js', + filename: filename, // will use the CDN path (if one is available) so that split // chunks load internally through the CDN. publicPath: this.webpackConfig.getRealPublicPath(), - pathinfo: this.webpackConfig.isProduction() + pathinfo: !this.webpackConfig.isProduction() }; } @@ -122,24 +133,42 @@ class ConfigGenerator { { test: /\.css$/, use: extractText.extract(this.webpackConfig, cssLoaderUtil.getLoaders(this.webpackConfig, false)) - }, - { - test: /\.(png|jpg|jpeg|gif|ico|svg)$/, + } + ]; + + if (this.webpackConfig.useImagesLoader) { + // Default filename can be overriden using Encore.configureFilenames({ images: '...' }) + let filename = 'images/[name].[hash:8].[ext]'; + if (this.webpackConfig.configuredFilenames.images) { + filename = this.webpackConfig.configuredFilenames.images; + } + + rules.push({ + test: /\.(png|jpg|jpeg|gif|ico|svg|webp)$/, loader: 'file-loader', options: { - name: `images/[name]${this.webpackConfig.useVersioning ? '.[hash]' : ''}.[ext]`, - publicPath: this.webpackConfig.getRealPublicPath() + name: `images/[name]${this.webpackConfig.useVersioning ? '.[hash:8]' : ''}.[ext]`, + publicPath: this.webpackConfig.getImagesPublicPath() } - }, - { + }); + } + + if (this.webpackConfig.useFontsLoader) { + // Default filename can be overriden using Encore.configureFilenames({ fonts: '...' }) + let filename = 'fonts/[name].[hash:8].[ext]'; + if (this.webpackConfig.configuredFilenames.fonts) { + filename = this.webpackConfig.configuredFilenames.fonts; + } + + rules.push({ test: /\.(woff|woff2|ttf|eot|otf)$/, loader: 'file-loader', options: { - name: `fonts/[name]${this.webpackConfig.useVersioning ? '.[hash]' : ''}.[ext]`, - publicPath: this.webpackConfig.getRealPublicPath() + name: `fonts/[name]${this.webpackConfig.useVersioning ? '.[hash:8]' : ''}.[ext]`, + publicPath: this.webpackConfig.getFontsPublicPath() } - }, - ]; + }); + } if (this.webpackConfig.useSassLoader) { rules.push({ @@ -180,173 +209,32 @@ class ConfigGenerator { buildPluginsConfig() { let plugins = []; - /* - * All CSS/SCSS content (due to the loaders above) will be - * extracted into an [entrypointname].css files. The result - * is that NO css will be inlined, *except* CSS that is required - * in an async way (e.g. via require.ensure()). - * - * This may not be ideal in some cases, but it's at least - * predictable. It means that you must manually add a - * link tag for an entry point's CSS (unless no CSS file - * was imported - in which case no CSS file will be dumped). - */ - plugins.push(new ExtractTextPlugin({ - filename: this.webpackConfig.useVersioning ? '[name].[contenthash].css' : '[name].css', - // if true, async CSS (e.g. loaded via require.ensure()) - // is extracted to the entry point CSS. If false, it's - // inlined in the AJAX-loaded .js file. - allChunks: false - })); + extractTextPluginUtil(plugins, this.webpackConfig); // register the pure-style entries that should be deleted - plugins.push(new DeleteUnusedEntriesJSPlugin( - // transform into an Array - [... this.webpackConfig.styleEntries.keys()] - )); - - /* - * Dump the manifest.json file - */ - let manifestPrefix = this.webpackConfig.manifestKeyPrefix; - if (null === manifestPrefix) { - // by convention, we remove the opening slash on the manifest keys - manifestPrefix = this.webpackConfig.publicPath.replace(/^\//,''); - } - plugins.push(new ManifestPlugin({ - basePath: manifestPrefix, - // guarantee the value uses the public path (or CDN public path) - publicPath: this.webpackConfig.getRealPublicPath(), - // always write a manifest.json file, even with webpack-dev-server - writeToFileEmit: true, - })); - - /* - * This section is a bit mysterious. The "minimize" - * true is read and used to minify the CSS. - * But as soon as this plugin is included - * at all, SASS begins to have errors, until the context - * and output options are specified. At this time, I'm - * not totally sure what's going on here - * https://github.com/jtangelder/sass-loader/issues/285 - */ - plugins.push(new webpack.LoaderOptionsPlugin({ - debug: !this.webpackConfig.isProduction(), - options: { - context: this.webpackConfig.getContext(), - output: { path: this.webpackConfig.outputPath } - } - })); - - /* - * With versioning, the "chunkhash" used in the filenames and - * the module ids (i.e. the internal names of modules that - * are required) become important. Specifically: - * - * 1) If the contents of a module don't change, then you don't want its - * internal module id to change. Otherwise, whatever file holds the - * webpack "manifest" will change because the module id will change. - * Solved by HashedModuleIdsPlugin or NamedModulesPlugin - * - * 2) Similarly, if the final contents of a file don't change, - * then we also don't want that file to have a new filename. - * The WebpackChunkHash() handles this, by making sure that - * the chunkhash is based off of the file contents. - * - * Even in the webpack community, the ideal setup seems to be - * a bit of a mystery: - * * https://github.com/webpack/webpack/issues/1315 - * * https://github.com/webpack/webpack.js.org/issues/652#issuecomment-273324529 - * * https://webpack.js.org/guides/caching/#deterministic-hashes - */ - if (this.webpackConfig.isProduction()) { - // shorter, and obfuscated module ids (versus NamedModulesPlugin) - // makes the final assets *slightly* larger, but prevents contents - // from sometimes changing when nothing really changed - plugins.push(new webpack.HashedModuleIdsPlugin()); - } else { - // human-readable module names, helps debug in HMR - // enable always when not in production for consistency - plugins.push(new webpack.NamedModulesPlugin()); - } + deleteUnusedEntriesPluginUtil(plugins, this.webpackConfig); - if (this.webpackConfig.useVersioning) { - // enables the [chunkhash] ability - plugins.push(new WebpackChunkHash()); - } + // Dump the manifest.json file + manifestPluginUtil(plugins, this.webpackConfig); - if (Object.keys(this.webpackConfig.providedVariables).length > 0) { - plugins = plugins.concat([ - new webpack.ProvidePlugin(this.webpackConfig.providedVariables) - ]); - } + loaderOptionsPluginUtil(plugins, this.webpackConfig); - if (this.webpackConfig.cleanupOutput) { - plugins.push( - new CleanWebpackPlugin(['**/*'], { - root: this.webpackConfig.outputPath, - verbose: false, - }) - ); - } + versioningPluginUtil(plugins, this.webpackConfig); - // if we're extracting a vendor chunk, set it up! - if (this.webpackConfig.sharedCommonsEntryName) { - plugins = plugins.concat([ - new webpack.optimize.CommonsChunkPlugin({ - name: [ - this.webpackConfig.sharedCommonsEntryName, - /* - * Always dump a 2nd file - manifest.json that - * will contain the webpack manifest information. - * This changes frequently, and without this line, - * it would be packaged inside the "shared commons entry" - * file - e.g. vendor.js, which would prevent long-term caching. - */ - 'manifest' - ], - minChunks: Infinity, - }), - ]); - } + variableProviderPluginUtil(plugins, this.webpackConfig); - if (this.webpackConfig.isProduction()) { - plugins = plugins.concat([ - new webpack.DefinePlugin({ - 'process.env': { - NODE_ENV: '"production"' - } - }), - - // todo - options here should be configurable - new webpack.optimize.UglifyJsPlugin({ - sourceMap: this.webpackConfig.useSourceMaps - }) - ]); - } + cleanPluginUtil(plugins, this.webpackConfig); - const friendlyErrorsPlugin = new FriendlyErrorsWebpackPlugin({ - clearConsole: false, - additionalTransformers: [ - missingLoaderTransformer, - missingPostCssConfigTransformer, - vueUnactivatedLoaderTransformer - ], - additionalFormatters: [ - missingLoaderFormatter, - missingPostCssConfigFormatter, - vueUnactivatedLoaderFormatter - ], - compilationSuccessInfo: { - messages: [] - } - }); - plugins.push(friendlyErrorsPlugin); + commonsChunksPluginUtil(plugins, this.webpackConfig); - if (!this.webpackConfig.useDevServer()) { - const outputPath = pathUtil.getRelativeOutputPath(this.webpackConfig); - plugins.push(new AssetOutputDisplayPlugin(outputPath, friendlyErrorsPlugin)); - } + definePluginUtil(plugins, this.webpackConfig); + + uglifyPluginUtil(plugins, this.webpackConfig); + + const friendlyErrorPlugin = friendlyErrorPluginUtil(this.webpackConfig); + plugins.push(friendlyErrorPlugin); + + assetOutputDisplay(plugins, this.webpackConfig, friendlyErrorPlugin); this.webpackConfig.plugins.forEach(function(plugin) { plugins.push(plugin); @@ -381,11 +269,12 @@ class ConfigGenerator { return { contentBase: contentBase, - publicPath: this.webpackConfig.publicPath, + // this doesn't appear to be necessary, but here in case + publicPath: this.webpackConfig.getRealPublicPath(), // avoid CORS concerns trying to load things like fonts from the dev server headers: { 'Access-Control-Allow-Origin': '*' }, - // required by FriendlyErrorsWebpackPlugin hot: this.webpackConfig.useHotModuleReplacementPlugin(), + // required by FriendlyErrorsWebpackPlugin quiet: true, compress: true, historyApiFallback: true, diff --git a/lib/config/RuntimeConfig.js b/lib/config/RuntimeConfig.js index 37ed2ee9..1d9bcfa9 100644 --- a/lib/config/RuntimeConfig.js +++ b/lib/config/RuntimeConfig.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * @@ -19,11 +19,13 @@ class RuntimeConfig { this.useDevServer = null; this.devServerUrl = null; this.devServerHttps = null; + this.devServerKeepPublicPath = false; this.useHotModuleReplacement = null; this.babelRcFileExists = null; this.helpRequested = false; + this.verbose = false; } } diff --git a/lib/config/parse-runtime.js b/lib/config/parse-runtime.js index 7295a560..35717b7f 100644 --- a/lib/config/parse-runtime.js +++ b/lib/config/parse-runtime.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * @@ -29,17 +29,22 @@ module.exports = function(argv, cwd) { case 'dev': runtimeConfig.isValidCommand = true; runtimeConfig.environment = 'dev'; + runtimeConfig.verbose = true; break; case 'production': runtimeConfig.isValidCommand = true; runtimeConfig.environment = 'production'; + runtimeConfig.verbose = false; break; case 'dev-server': runtimeConfig.isValidCommand = true; runtimeConfig.environment = 'dev'; + runtimeConfig.verbose = true; + runtimeConfig.useDevServer = true; runtimeConfig.devServerHttps = argv.https; runtimeConfig.useHotModuleReplacement = argv.hot || false; + runtimeConfig.devServerKeepPublicPath = argv.keepPublicPath || false; var host = argv.host ? argv.host : 'localhost'; var port = argv.port ? argv.port : '8080'; diff --git a/lib/config/path-util.js b/lib/config/path-util.js index 0476bc77..bbafefa4 100644 --- a/lib/config/path-util.js +++ b/lib/config/path-util.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * diff --git a/lib/config/validator.js b/lib/config/validator.js index 14f6952c..ee079709 100644 --- a/lib/config/validator.js +++ b/lib/config/validator.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * @@ -10,6 +10,7 @@ 'use strict'; const pathUtil = require('./path-util'); +const logger = require('./../logger'); class Validator { /** @@ -46,9 +47,24 @@ class Validator { } _validateDevServer() { - if (this.webpackConfig.useVersioning && this.webpackConfig.useDevServer()) { + if (!this.webpackConfig.useDevServer()) { + return; + } + + if (this.webpackConfig.useVersioning) { throw new Error('Don\'t enable versioning with the dev-server. A good setting is Encore.enableVersioning(Encore.isProduction()).'); } + + /* + * An absolute publicPath is incompatible with webpackDevServer. + * This is because we want to *change* the publicPath to point + * to the webpackDevServer URL (e.g. http://localhost:8080/). + * There are some valid use-cases for not wanting this behavior + * (see #59), but we want to warn the user. + */ + if (this.webpackConfig.publicPath.includes('://')) { + logger.warning(`Passing an absolute URL to setPublicPath() *and* using the dev-server can cause issues. Your assets will load from the publicPath (${this.webpackConfig.publicPath}) instead of from the dev server URL (${this.webpackConfig.runtimeConfig.devServerUrl}).`); + } } } diff --git a/lib/context.js b/lib/context.js index 940b1818..ccfd2dd5 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * diff --git a/lib/loader-features.js b/lib/features.js similarity index 59% rename from lib/loader-features.js rename to lib/features.js index e5cf87e7..6d419dea 100644 --- a/lib/loader-features.js +++ b/lib/features.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * @@ -13,9 +13,9 @@ const packageHelper = require('./package-helper'); /** * An object that holds internal configuration about different - * "loaders" that can be enabled. + * "loaders"/"plugins" that can be enabled/used. */ -const loaderFeatures = { +const features = { sass: { method: 'enableSassLoader()', packages: ['sass-loader', 'node-sass'], @@ -36,11 +36,21 @@ const loaderFeatures = { packages: ['babel-preset-react'], description: 'process React JS files' }, + preact: { + method: 'enablePreactPreset()', + packages: ['babel-plugin-transform-react-jsx'], + description: 'process Preact JS files' + }, typescript: { method: 'enableTypeScriptLoader()', packages: ['typescript', 'ts-loader'], description: 'process TypeScript files' }, + forkedtypecheck: { + method: 'enableForkedTypeScriptTypesChecking()', + packages: ['typescript', 'ts-loader', 'fork-ts-checker-webpack-plugin'], + description: 'check TypeScript types in a separate process' + }, vue: { method: 'enableVueLoader()', // vue is needed so the end-user can do things @@ -50,19 +60,19 @@ const loaderFeatures = { } }; -function getLoaderFeatureConfig(loaderName) { - if (!loaderFeatures[loaderName]) { - throw new Error(`Unknown loader feature ${loaderName}`); +function getFeatureConfig(featureName) { + if (!features[featureName]) { + throw new Error(`Unknown feature ${featureName}`); } - return loaderFeatures[loaderName]; + return features[featureName]; } module.exports = { - getLoaderFeatureConfig, + getFeatureConfig, - ensureLoaderPackagesExist: function(loaderName) { - const config = getLoaderFeatureConfig(loaderName); + ensurePackagesExist: function(featureName) { + const config = getFeatureConfig(featureName); packageHelper.ensurePackagesExist( config.packages, @@ -70,11 +80,11 @@ module.exports = { ); }, - getLoaderFeatureMethod: function(loaderName) { - return getLoaderFeatureConfig(loaderName).method; + getFeatureMethod: function(featureName) { + return getFeatureConfig(featureName).method; }, - getLoaderFeatureDescription: function(loaderName) { - return getLoaderFeatureConfig(loaderName).description; + getFeatureDescription: function(featureName) { + return getFeatureConfig(featureName).description; } }; diff --git a/lib/friendly-errors/asset-output-display-plugin.js b/lib/friendly-errors/asset-output-display-plugin.js index 40b15ded..846f3419 100644 --- a/lib/friendly-errors/asset-output-display-plugin.js +++ b/lib/friendly-errors/asset-output-display-plugin.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * diff --git a/lib/friendly-errors/formatters/missing-loader.js b/lib/friendly-errors/formatters/missing-loader.js index bebf16a9..d8575060 100644 --- a/lib/friendly-errors/formatters/missing-loader.js +++ b/lib/friendly-errors/formatters/missing-loader.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * @@ -10,7 +10,7 @@ 'use strict'; const chalk = require('chalk'); -const loaderFeatures = require('../../loader-features'); +const loaderFeatures = require('../../features'); const packageHelper = require('../../package-helper'); function formatErrors(errors) { @@ -23,10 +23,10 @@ function formatErrors(errors) { const fixes = []; if (error.loaderName) { - let neededCode = `Encore.${loaderFeatures.getLoaderFeatureMethod(error.loaderName)}`; + let neededCode = `Encore.${loaderFeatures.getFeatureMethod(error.loaderName)}`; fixes.push(`Add ${chalk.green(neededCode)} to your webpack.config.js file.`); - const loaderFeatureConfig = loaderFeatures.getLoaderFeatureConfig(error.loaderName); + const loaderFeatureConfig = loaderFeatures.getFeatureConfig(error.loaderName); const packageRecommendations = packageHelper.getPackageRecommendations( loaderFeatureConfig.packages ); @@ -44,7 +44,7 @@ function formatErrors(errors) { ]); if (error.loaderName) { - messages.push(`${chalk.bgGreen.black('', 'FIX', '')} To ${loaderFeatures.getLoaderFeatureDescription(error.loaderName)}:`); + messages.push(`${chalk.bgGreen.black('', 'FIX', '')} To ${loaderFeatures.getFeatureDescription(error.loaderName)}:`); } else { messages.push(`${chalk.bgGreen.black('', 'FIX', '')} To load ${error.file}:`); } diff --git a/lib/friendly-errors/formatters/missing-postcss-config.js b/lib/friendly-errors/formatters/missing-postcss-config.js index 3cfaf9fc..ffe5de00 100644 --- a/lib/friendly-errors/formatters/missing-postcss-config.js +++ b/lib/friendly-errors/formatters/missing-postcss-config.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * @@ -25,7 +25,18 @@ function formatErrors(errors) { ); messages.push(''); messages.push(`${chalk.bgGreen.black('', 'FIX', '')} Create a ${chalk.yellow('postcss.config.js')} file at the root of your project.`); + messages.push(''); + messages.push('Here is an example to get you started!'); + messages.push(chalk.yellow(` +// postcss.config.js +module.exports = { + plugins: { + 'autoprefixer': {}, + } +} + `)); + messages.push(''); messages.push(''); return messages; diff --git a/lib/friendly-errors/formatters/vue-unactivated-loader-error.js b/lib/friendly-errors/formatters/vue-unactivated-loader-error.js index 267a0c68..b540b344 100644 --- a/lib/friendly-errors/formatters/vue-unactivated-loader-error.js +++ b/lib/friendly-errors/formatters/vue-unactivated-loader-error.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * diff --git a/lib/friendly-errors/transformers/missing-loader.js b/lib/friendly-errors/transformers/missing-loader.js index a955f83c..4527d1b7 100644 --- a/lib/friendly-errors/transformers/missing-loader.js +++ b/lib/friendly-errors/transformers/missing-loader.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * diff --git a/lib/friendly-errors/transformers/missing-postcss-config.js b/lib/friendly-errors/transformers/missing-postcss-config.js index 8cd5f2db..39565784 100644 --- a/lib/friendly-errors/transformers/missing-postcss-config.js +++ b/lib/friendly-errors/transformers/missing-postcss-config.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * diff --git a/lib/friendly-errors/transformers/vue-unactivated-loader-error.js b/lib/friendly-errors/transformers/vue-unactivated-loader-error.js index 84b1b1c1..03b39999 100644 --- a/lib/friendly-errors/transformers/vue-unactivated-loader-error.js +++ b/lib/friendly-errors/transformers/vue-unactivated-loader-error.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * diff --git a/lib/loaders/babel.js b/lib/loaders/babel.js index 5aad229e..83cf9b20 100644 --- a/lib/loaders/babel.js +++ b/lib/loaders/babel.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * @@ -9,7 +9,7 @@ 'use strict'; -const loaderFeatures = require('../loader-features'); +const loaderFeatures = require('../features'); /** * @param {WebpackConfig} webpackConfig @@ -41,14 +41,32 @@ module.exports = { useBuiltIns: true }] ], + plugins: [] }); if (webpackConfig.useReact) { - loaderFeatures.ensureLoaderPackagesExist('react'); + loaderFeatures.ensurePackagesExist('react'); babelConfig.presets.push('react'); } + if (webpackConfig.usePreact) { + loaderFeatures.ensurePackagesExist('preact'); + + if (webpackConfig.preactOptions.preactCompat) { + // If preact-compat is enabled tell babel to + // transform JSX into React.createElement calls. + babelConfig.plugins.push(['transform-react-jsx']); + } else { + // If preact-compat is disabled tell babel to + // transform JSX into Preact h() calls. + babelConfig.plugins.push([ + 'transform-react-jsx', + { 'pragma': 'h' } + ]); + } + } + // allow for babel config to be controlled webpackConfig.babelConfigurationCallback.apply( // use babelConfig as the this variable diff --git a/lib/loaders/css.js b/lib/loaders/css.js index 002e2e46..ab8baed6 100644 --- a/lib/loaders/css.js +++ b/lib/loaders/css.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * @@ -9,7 +9,7 @@ 'use strict'; -const loaderFeatures = require('../loader-features'); +const loaderFeatures = require('../features'); /** * @param {WebpackConfig} webpackConfig @@ -18,24 +18,40 @@ const loaderFeatures = require('../loader-features'); */ module.exports = { getLoaders(webpackConfig, skipPostCssLoader) { + const usePostCssLoader = webpackConfig.usePostCssLoader && !skipPostCssLoader; + const cssLoaders = [ { loader: 'css-loader', options: { minimize: webpackConfig.isProduction(), - sourceMap: webpackConfig.useSourceMaps + sourceMap: webpackConfig.useSourceMaps, + // when using @import, how many loaders *before* css-loader should + // be applied to those imports? This defaults to 0. When postcss-loader + // is used, we set it to 1, so that postcss-loader is applied + // to @import resources. + importLoaders: usePostCssLoader ? 1 : 0 } }, ]; - if (webpackConfig.usePostCssLoader && !skipPostCssLoader) { - loaderFeatures.ensureLoaderPackagesExist('postcss'); + if (usePostCssLoader) { + loaderFeatures.ensurePackagesExist('postcss'); + + const postCssLoaderOptions = { + sourceMap: webpackConfig.useSourceMaps + }; + + // allow options to be configured + webpackConfig.postCssLoaderOptionsCallback.apply( + // use config as the this variable + postCssLoaderOptions, + [postCssLoaderOptions] + ); cssLoaders.push({ loader: 'postcss-loader', - options: { - sourceMap: webpackConfig.useSourceMaps - } + options: postCssLoaderOptions }); } diff --git a/lib/loaders/extract-text.js b/lib/loaders/extract-text.js index 99c9b595..30b6a8b2 100644 --- a/lib/loaders/extract-text.js +++ b/lib/loaders/extract-text.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * diff --git a/lib/loaders/less.js b/lib/loaders/less.js index 1324f1a7..15d1c8c6 100644 --- a/lib/loaders/less.js +++ b/lib/loaders/less.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * @@ -9,7 +9,7 @@ 'use strict'; -const loaderFeatures = require('../loader-features'); +const loaderFeatures = require('../features'); const cssLoader = require('./css'); /** @@ -19,15 +19,24 @@ const cssLoader = require('./css'); */ module.exports = { getLoaders(webpackConfig, ignorePostCssLoader = false) { - loaderFeatures.ensureLoaderPackagesExist('less'); + loaderFeatures.ensurePackagesExist('less'); + + const config = { + sourceMap: webpackConfig.useSourceMaps + }; + + // allow options to be configured + webpackConfig.lessLoaderOptionsCallback.apply( + // use config as the this variable + config, + [config] + ); return [ ...cssLoader.getLoaders(webpackConfig, ignorePostCssLoader), { loader: 'less-loader', - options: { - sourceMap: webpackConfig.useSourceMaps - } + options: config }, ]; } diff --git a/lib/loaders/sass.js b/lib/loaders/sass.js index 352f578d..ad2ffec1 100644 --- a/lib/loaders/sass.js +++ b/lib/loaders/sass.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * @@ -9,7 +9,7 @@ 'use strict'; -const loaderFeatures = require('../loader-features'); +const loaderFeatures = require('../features'); const cssLoader = require('./css'); /** @@ -20,10 +20,10 @@ const cssLoader = require('./css'); */ module.exports = { getLoaders(webpackConfig, sassOptions = {}, ignorePostCssLoader = false) { - loaderFeatures.ensureLoaderPackagesExist('sass'); + loaderFeatures.ensurePackagesExist('sass'); const sassLoaders = [...cssLoader.getLoaders(webpackConfig, ignorePostCssLoader)]; - if (true === webpackConfig.sassOptions.resolve_url_loader) { + if (true === webpackConfig.sassOptions.resolveUrlLoader) { // responsible for resolving SASS url() paths // without this, all url() paths must be relative to the // entry file, not the file that contains the url() @@ -37,7 +37,7 @@ module.exports = { let config = Object.assign({}, sassOptions, { // needed by the resolve-url-loader - sourceMap: (true === webpackConfig.sassOptions.resolve_url_loader) || webpackConfig.useSourceMaps + sourceMap: (true === webpackConfig.sassOptions.resolveUrlLoader) || webpackConfig.useSourceMaps }); // allow options to be configured diff --git a/lib/loaders/typescript.js b/lib/loaders/typescript.js index 53d9f17a..23aa8663 100644 --- a/lib/loaders/typescript.js +++ b/lib/loaders/typescript.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * @@ -9,7 +9,7 @@ 'use strict'; -const loaderFeatures = require('../loader-features'); +const loaderFeatures = require('../features'); const babelLoader = require('./babel'); /** @@ -18,7 +18,7 @@ const babelLoader = require('./babel'); */ module.exports = { getLoaders(webpackConfig) { - loaderFeatures.ensureLoaderPackagesExist('typescript'); + loaderFeatures.ensurePackagesExist('typescript'); // some defaults let config = { @@ -32,6 +32,17 @@ module.exports = { [config] ); + // fork-ts-checker-webpack-plugin integration + if (webpackConfig.useForkedTypeScriptTypeChecking) { + loaderFeatures.ensurePackagesExist('forkedtypecheck'); + // force transpileOnly to speed up + config.transpileOnly = true; + + // add forked ts types plugin to the stack + const forkedTypesPluginUtil = require('../plugins/forked-ts-types'); // eslint-disable-line + forkedTypesPluginUtil(webpackConfig); + } + // use ts alongside with babel // @see https://github.com/TypeStrong/ts-loader/blob/master/README.md#babel let loaders = babelLoader.getLoaders(webpackConfig); diff --git a/lib/loaders/vue-unactivated-loader.js b/lib/loaders/vue-unactivated-loader.js index b61b5e24..e3f57f8f 100644 --- a/lib/loaders/vue-unactivated-loader.js +++ b/lib/loaders/vue-unactivated-loader.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * diff --git a/lib/loaders/vue.js b/lib/loaders/vue.js index 48d46607..3358f976 100644 --- a/lib/loaders/vue.js +++ b/lib/loaders/vue.js @@ -1,5 +1,5 @@ /* - * This file is part of the Symfony package. + * This file is part of the Symfony Webpack Encore package. * * (c) Fabien Potencier * @@ -9,7 +9,7 @@ 'use strict'; -const loaderFeatures = require('../loader-features'); +const loaderFeatures = require('../features'); const cssLoaderUtil = require('./css'); const sassLoaderUtil = require('./sass'); const lessLoaderUtil = require('./less'); @@ -23,7 +23,7 @@ const extractText = require('./extract-text'); */ module.exports = { getLoaders(webpackConfig, vueLoaderOptionsCallback) { - loaderFeatures.ensureLoaderPackagesExist('vue'); + loaderFeatures.ensurePackagesExist('vue'); /* * The vue-loader passes the contents of