From fe6f4d8352151f6459899cffef9cd490d7fb9827 Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Tue, 29 Nov 2022 14:11:11 +1300 Subject: [PATCH] ENH Update css webpack config --- README.md | 257 +++++++++++++++++++++----- configMeta/baseWebpackConfig.js | 56 ++++-- configMeta/cssWebpackConfig.js | 27 +++ configMeta/javascriptWebpackConfig.js | 15 +- css/modules.js | 46 +++-- css/plugins.js | 11 +- index.js | 1 + js/externals.js | 223 +++++++++++++--------- js/plugins.js | 5 +- js/resolve.js | 2 +- package.json | 8 +- 11 files changed, 461 insertions(+), 190 deletions(-) create mode 100644 configMeta/cssWebpackConfig.js diff --git a/README.md b/README.md index 759db55..69ed632 100644 --- a/README.md +++ b/README.md @@ -5,32 +5,181 @@ This NPM package provides a shared common webpack configuration used across all Silverstripe modules, this aims to reduce thirdparty module developer fatigue by having a source of truth for configurations and settings used in SilverStripe's webpack. -## What this package gives you: -For JS: -* **externals.js**: Provides references to packages that are provided by silverstripe-admin or another core silverstripe module. This will tell your webpack to not include the package in your output file, that it is provided and accessible through a global variable. +## What this package gives you + +### For JS + +* **JavascriptWebpackConfig** This class provides the default webpack config most modules will use for transpiling javascript, along with methods for customisation. It automatically includes all of the configuration listed in Advanced below. + +#### Advanced + +* **externals.js**: Provides references to packages that are provided by silverstripe-admin or another core silverstripe module. This will tell your webpack to not include the package in your output file, that it is provided and accessible through a global variable to keep your transpiled bundle smaller. * **modules.js**: The common list of loaders for javascript which webpack should use to get a standard output build, such as babel and modernizr. * **plugins.js**: Plugins used by webpack, such as: * A global `Provide` call for `jQuery` * The environment variable `process.env.NODE_ENV` to exclude debug functions in production builds - * UglifyJS to remove comments in production builds + * [Webpack Bundle Analyzer plugin](https://www.npmjs.com/package/webpack-bundle-analyzer) to aid profiling * **resolve.js**: Provides common ways to resolve a package in your src files, so that you reduce the number of relative path imports. -For CSS: -* **modules.js**: The common list of loaders for stylesheets to convert `*.scss` files to a css output file, handles some autoprefixing for browser specific rules. +### For CSS + +* **CssWebpackConfig** This class provides the default webpack config most modules will use for transpiling sass to css, along with methods for customisation. It automatically includes all of the configuration listed in Advanced below. + +#### Advanced + +* **modules.js**: The common list of loaders for stylesheets to convert `*.scss` files to a css output file, including exporting images and fonts. * **plugins.js**: Plugin for webpack to extract the stylesheets into a proper css file. -## An example webpack.config.js -This package provides only partial config declarations. You still need to import these into your main `webpack.config.js` file -and add them accordingly. +## Usage + +The following keys can be used in the PATHS object whenever one is required as a parameter. Note that the default values only apply when using the abstraction classes. + +|Key|Description|Required|Default| +|---|---|---|---| +|ROOT|The root path, where your `webpack.config.js` file is located|yes|No default - error if missing| +|SRC|The absolute path to your source files|only for [advanced usage](#advanced-usage)|`` `${PATHS.ROOT}/client/src` ``| +|DIST|The absolute path to the directory you want to output files to|no|`` `${PATHS.ROOT}/client/dist` ``| +|MODULES|The path (relative to `ROOT`, or an absolute path) to your `node_modules` folder|only for [advanced usage](#advanced-usage)|`'node_modules'`| +|THIRDPARTY|The path (relative to `ROOT`, or an absolute path) to your thirdparty folder containing copies of packages which wouldn't be available on NPM|no|No default| + +### Using the abstractions + +This library includes `JavascriptWebpackConfig` and `CssWebpackConfig` classes to abstract some of the webpack configuration, so it's easier to standardise config across all of your Silverstripe modules. + +#### Javascript + +To use all of the default configuration for javascript transpilation, instantiate a new `JavascriptWebpackConfig` object. + +This class's constructor takes a `name` string argument (used in the weback console output and for debugging) and a `PATHS` object. It also has a third argument (`moduleName`) which is only needed for core and supported Silverstripe modules and should be set to the name of the module (e.g. `silverstripe/admin`). + +You must set your entry points by passing a valid entry object to the `setEntry()` method. This uses [the normal syntax for webpack `entry`](https://webpack.js.org/concepts/entry-points/). + +Finally, you get the actual webpack config by calling `getConfig()`. + +#### Sass + +The API for getting a webpack config to transpile sass to css is very similar to geting javascript webpack config. You start by instantiating a new `CssWebpackConfig` object. + +`CssWebpackConfig` takes the same arguments as `JavascriptWebpackConfig` (except for `moduleName`) - but it also takes an optional `filename` argument. The `filename` ultimately gets passed to a [`MiniCssExtractPlugin`](https://webpack.js.org/plugins/mini-css-extract-plugin/#filename). Its default value is `"styles/[name].css"` + +#### Example + +This is a minimal example of using this library to build your webpack configuration. It transpiles `client/src/js/main.js` to `client/dist/js/main.js` and `client/src/styles/main.scss` to `client/dist/styles/main.css`. + +The css transpilation also includes outputting any referenced fonts to `client/dist/fonts/` and any referenced images larger than 10kb to `client/dist/images/`. + +```js +const Path = require('path'); +const { JavascriptWebpackConfig, CssWebpackConfig } = require('@silverstripe/webpack-config'); + +const PATHS = { + ROOT: Path.resolve(), +}; + +module.exports = [ + new JavascriptWebpackConfig('js', PATHS) + .setEntry({ + main: 'js/main.js' + }) + .getConfig(), + new CssWebpackConfig('css', PATHS) + .setEntry({ + main: 'styles/main.scss' + }) + .getConfig(), +]; +``` + +### Customising abstracted configuration + +`JavascriptWebpackConfig` and `CssWebpackConfig` are subclasses of `BaseWebpackConfig`, which provides a couple of methods for customising the resulting config. + +#### splitVendor + +`splitVendor()` uses the [SplitChunksPlugin](https://webpack.js.org/plugins/split-chunks-plugin/#optimizationsplitchunks) to separate out vendor code into its own file. This method takes two arguments. + +The first argument (`vendorChunk`) is the name of the chunk to be split out. This is used in the `name` portion for output filenames. For example, if the output filename is `[name].bundle.js` and `vendorChunk` is `vendor`, the name of the file will be `vendor.bundle.js`. The default value for `vendorChunk` is `vendor`. Note that this _can_ be the name of one of your entry points, in which case the vendor modules will be included in the same file as the transpiled javascript for that entrypoint. + +The second argument (`test`) is the regular expression or function that determines which modules are included in this chunk. See [the webpack docs](https://webpack.js.org/plugins/split-chunks-plugin/#splitchunkscachegroupscachegrouptest) for more information. The default value for `test` is `/[\\/]node_modules[\\/]/`. + +#### mergeConfig + +`mergeConfig()` allows you to merge your own raw webpack configuration into the configuration created by the abstractions. This will also override any default configuration which uses the same keys. + +It takes a single webpack configuration object as an argument. + +#### Additional customisation + +Most customisation will be achievable with one of the above two methods - but you might have a really specific use case where you want to dome something that can't be achieved with merging config (e.g. remove or replace one of the default plugins). In that case, you can manipulate the final configuration after calling `getConfig()` - since that method gives you the actual webpack configuration object the abstractions produced. + +In that case however you may find you are better served by avoiding the abstractions, and building your configuration [the advanced way](#advanced-usage). -This approach opens up the option to easily update or modify any of the configs without nesting. +#### Example + +This example includes several customisations (explained with comments in the example) of the abstracted configuration. It transpiles `js/src/main.js` to `js/dist/main.bundle.js` (with a separate vendor bundle in `js/dist/vendor.bundle.js`) and `css/src/main.scss` to `css/dist/main.css`. Fonts referenced in css are output to `fonts/` and images to `images/`. + +Note that in this example we use `mergeConfig()` to merge an [output](https://webpack.js.org/concepts/output/#root) object to change the name of the transpiled javascript files, but for css we pass the name into the `CssWebpackConfig` constructor. This is because sass to css transpilation is using `MiniCssExtractPlugin`, which is in control of the output name of the css. If you try to change the name of css files using `output.filename`, you'll get errors (you can still any other `output` configuration via `mergeConfig()` though). -**my-module/webpack.config.js** ```js const Path = require('path'); const webpack = require('webpack'); -// Import the core config -const webpackConfig = require('@silverstripe/webpack-config'); +const { JavascriptWebpackConfig, CssWebpackConfig } = require('@silverstripe/webpack-config'); + +const PATHS = { + ROOT: Path.resolve(), + SRC: Path.resolve(), +}; + +const config = [ + // Use a different DIST directory for js than is used for css + new JavascriptWebpackConfig('js', { ...PATHS, DIST: `${PATHS.ROOT}/js/dist` }) + .setEntry({ + main: 'js/src/main.js' + }) + // Output the javascript with a different filename schema than the default + .mergeConfig({ + output: { + filename: '[name].bundle.js', + }, + }) + // Split any vendor modules out into a separate `vendor.bundle.js` file + .splitVendor() + .getConfig(), + // Use a different DIST directory for css than is used for js, and output the css with a + // different filename schema than the default + new CssWebpackConfig('css', { ...PATHS, DIST: `${PATHS.ROOT}/css/dist` }, '[name].bundle.css') + .setEntry({ + main: 'css/src/main.scss' + }) + // Copy some files at the same time as transpiling the css + .mergeConfig({ + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + { + from: `${PATHS.ROOT}/some-extra-files`, + to: `${PATHS.ROOT}/extra-files-output` + }, + ], + }), + ], + }) + .getConfig(), +]; + +module.exports = config; +``` + +### Advanced usage + +There may be situations where you want to make complex modifications to the default webpack configuration generated by the abstractions provided in this library - or where you want to have your configuration more explicitly declared in your `webpack.config.js` file. In those cases, you can bypass the abstractions completely. + +#### Example + +This is a minimal example of how to build a webpack configuration array for Silverstripe modules without using the abstraction classes. It produces the exact same configuarion (and therefore the same output files) as the example [using the abstractions](#using-the-abstractions) above. + +```js +const Path = require('path'); const { resolveJS, externalJS, @@ -38,31 +187,30 @@ const { pluginJS, moduleCSS, pluginCSS, -} = webpackConfig; +} = require('@silverstripe/webpack-config'); const ENV = process.env.NODE_ENV; + +// All of the keys are required in your PATHS object except DIST and THIRDPARTY +// Be aware that there is no validation for this - you may not get errors if you are missing +// some of this config, but you will likely get unexpected output const PATHS = { - // the root path, where your webpack.config.js is located. ROOT: Path.resolve(), - // your node_modules folder name, or full path + SRC: Path.resolve('client/src'), + DIST: Path.resolve('client/dist'), MODULES: 'node_modules', - // relative path from your css files to your other files, such as images and fonts - FILES_PATH: '../', - // thirdparty folder containing copies of packages which wouldn't be available on NPM THIRDPARTY: 'thirdparty', - // the root path to your javascript source files - SRC: Path.resolve('client/src'), }; -const config = [ +module.exports = [ { name: 'js', entry: { - main: 'js/src/main.js' + main: 'js/main.js' }, output: { - path: 'js/dist', - filename: '[name].bundle.js', + path: PATHS.DIST, + filename: 'js/[name].js', }, devtool: (ENV !== 'production') ? 'source-map' : '', resolve: resolveJS(ENV, PATHS), @@ -73,36 +221,57 @@ const config = [ { name: 'css', entry: { - main: 'css/src/main.scss' + main: 'styles/main.scss' }, + // Just like with the abstractions, we don't include output.filename, because the filename + // is handled by MiniCssExtractPlugin output: { - path: 'css/dist', - filename: '[name].css' + path: PATHS.DIST, }, devtool: (ENV !== 'production') ? 'source-map' : '', module: moduleCSS(ENV, PATHS), - plugins: pluginCSS(ENV, PATHS), + // Pass the filename here, which will get passed down to MiniCssExtractPlugin + plugins: pluginCSS(ENV, PATHS, 'css/[name].css'), }, ]; - -module.exports = config; ``` -## To customise -You can easily extend the configuration provided, for example to add another external to the list provided: +#### Customising raw configuration + +Because you're dealing with a raw webpack configuration object already, it can be easier to customise than the abstracted config. + +For example to add another external module to the externals configuration, merge your externals configuration with `externalJS()` (you could also achieve this using `mergeConfig()` with the abstractions): + ```js -const config = { - external: externalJS(ENV, PATHS), -} +module.exports = [ + { + name: 'js', + //... + external: Object.assign({}, + externalJS(ENV, PATHS), + { + 'components/MyCustomComponent': 'MyCustomComponent', + } + ), + }, +]; ``` -will become: + +Or to modify the directory for images from `images/` to `assets/`, you can modify `rule.generator.filename` for the appropriate rule in `moduleCSS()` (which you cannot achieve using the abstractions without modifying the configuration object after calling `getConfig()`): + ```js -const config = { - external: Object.assign({}, - externalJS(ENV, PATHS), - { - 'components/MyCustomComponent': 'MyCustomComponent', - } - ), +const cssModules = moduleCSS(ENV, PATHS); +for (let rule of cssModules.rules) { + if (rule.test === '/\.(png|gif|jpe?g|svg)$/') { + rule.generator.filename = 'assets/[name][ext]'; + } } + +module.exports = [ + { + name: 'css', + //... + module: cssModules, + }, +]; ``` diff --git a/configMeta/baseWebpackConfig.js b/configMeta/baseWebpackConfig.js index 80f2f54..f4da680 100644 --- a/configMeta/baseWebpackConfig.js +++ b/configMeta/baseWebpackConfig.js @@ -4,37 +4,61 @@ const lodash = require('lodash'); * A base class for dynamically generating webpack config using Silverstripe default settings */ module.exports = class BaseWebpackConfig { - vendorChunk = null; - config = {}; + #vendorChunk = null; + #config = {}; + + /** + * Validate the paths and provide defaults for missing entries + * @param {object} PATHS + */ + validatePaths(PATHS) { + if (!PATHS.ROOT) { + throw new Error('PATHS must contain a ROOT key'); + } + const defaults = { + MODULES: 'node_modules', + SRC: `${PATHS.ROOT}/client/src`, + DIST: `${PATHS.ROOT}/client/dist`, + }; + lodash.defaults(PATHS, defaults); + } /** * Get the webpack config */ getConfig() { - if (!this.config.entry || lodash.isEmpty(this.config.entry)) { + if (!this.#config.entry || lodash.isEmpty(this.#config.entry)) { throw new Error('At least one valid entry is required'); } - return this.config; + return this.#config; + } + + /** + * Completely override the webpack config + * @param {object} config + */ + setConfig(config) { + this.#config = config; } /** * Split vendor modules out as their own file * @param {string} vendorChunk - name of the vendor file (without extension) - * @param {string} regex - regex to match modules against to include them in the vendor file + * @param {string|function} test - how to match modules against to include them in the vendor file (see https://webpack.js.org/plugins/split-chunks-plugin/#splitchunkscachegroupscachegrouptest) */ - splitVendor(vendorChunk = 'vendor', regex = /[\\/]node_modules[\\/]/) { - if (this.vendorChunk) { + splitVendor(vendorChunk = 'vendor', test = /[\\/]node_modules[\\/]/) { + if (this.#vendorChunk) { throw new Error('Vendor can only be split once. For additional cacheGroups use merge().'); } - this.vendorChunk = vendorChunk; + this.#vendorChunk = vendorChunk; - this.config.optimization = { + this.#config.optimization = { splitChunks: { cacheGroups: { vendor: { name: vendorChunk, - test: regex, + test: test, reuseExistingChunk: true, enforce: true, chunks: 'all', @@ -57,7 +81,7 @@ module.exports = class BaseWebpackConfig { if (!entry || lodash.isEmpty(entry)) { throw new Error('entry object must contain at least one valid entry'); } - this.config.entry = entry; + this.#config.entry = entry; this.#splitVendorFromEntries(); return this; } @@ -66,15 +90,15 @@ module.exports = class BaseWebpackConfig { * If we are splitting a vendor bundle, make sure all entries "depend on" it as appopriate. */ #splitVendorFromEntries() { - const entryConfig = this.config.entry; + const entryConfig = this.#config.entry; // No action if there's no vendor chunk or no entry for the vendor chunk. - if (!this.vendorChunk || !entryConfig || !entryConfig.hasOwnProperty(this.vendorChunk)) { + if (!this.#vendorChunk || !entryConfig || !entryConfig.hasOwnProperty(this.#vendorChunk)) { return; } for (let name in entryConfig) { - if (name !== this.vendorChunk) { + if (name !== this.#vendorChunk) { const entry = entryConfig[name]; // Convert string entry to object so we can add dependOn if (typeof entry === 'string') { @@ -83,7 +107,7 @@ module.exports = class BaseWebpackConfig { } } // The entry must "dependOn" vendor so we can reuse the bundle - entryConfig[name].dependOn = this.vendorChunk; + entryConfig[name].dependOn = this.#vendorChunk; } } } @@ -93,7 +117,7 @@ module.exports = class BaseWebpackConfig { * @param {object} config */ mergeConfig(config) { - this.config = lodash.merge(this.config, config); + this.#config = lodash.merge(this.#config, config); return this; } } diff --git a/configMeta/cssWebpackConfig.js b/configMeta/cssWebpackConfig.js new file mode 100644 index 0000000..c49c7a2 --- /dev/null +++ b/configMeta/cssWebpackConfig.js @@ -0,0 +1,27 @@ +const BaseWebpackConfig = require('./baseWebpackConfig'); +const moduleCSS = require('../css/modules'); +const pluginCSS = require('../css/plugins'); + +/** + * Dynamically generates webpack config for transpiling sass to css using Silverstripe default settings. + * + * IMPORTANT: Instead of setting the output name in the output key, add it as the filename in this constructor. + * Otherwise you will get errors because webpack wants to create a js file for each css file. The js files + * aren't emitted thanks to the IgnoreEmitPlugin, but this happens after chunk validation. + */ +module.exports = class CssWebpackConfig extends BaseWebpackConfig { + constructor(name, PATHS, filename = 'styles/[name].css') { + super(); + const ENV = process.env.NODE_ENV; + this.validatePaths(PATHS); + this.setConfig({ + name, + output: { + path: PATHS.DIST, + }, + devtool: (ENV !== 'production') ? 'source-map' : '', + module: moduleCSS(ENV, PATHS), + plugins: pluginCSS(ENV, PATHS, filename), + }); + } +} diff --git a/configMeta/javascriptWebpackConfig.js b/configMeta/javascriptWebpackConfig.js index abe86a2..4f837e6 100644 --- a/configMeta/javascriptWebpackConfig.js +++ b/configMeta/javascriptWebpackConfig.js @@ -8,9 +8,12 @@ const resolveJS = require('../js/resolve'); * Dynamically generates webpack config for transpiling javascript using Silverstripe default settings */ module.exports = class JavascriptWebpackConfig extends BaseWebpackConfig { - constructor(name, ENV, PATHS, withExternals = true) { + constructor(name, PATHS, moduleName) { super(); - this.config = { + moduleName = moduleName ? `${moduleName}/${name}` : null; + const ENV = process.env.NODE_ENV; + this.validatePaths(PATHS); + this.setConfig({ name, output: { path: PATHS.DIST, @@ -19,10 +22,8 @@ module.exports = class JavascriptWebpackConfig extends BaseWebpackConfig { devtool: (ENV !== 'production') ? 'source-map' : '', resolve: resolveJS(ENV, PATHS), module: moduleJS(ENV, PATHS), - externals: withExternals ? externalJS : {}, - plugins: [ - ...pluginJS(ENV), - ], - }; + externals: externalJS(ENV, PATHS, moduleName), + plugins: pluginJS(ENV), + }); } } diff --git a/css/modules.js b/css/modules.js index 6214f0a..d4fdcfb 100644 --- a/css/modules.js +++ b/css/modules.js @@ -1,27 +1,20 @@ -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const Path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); /** * Exports the settings for css modules in webpack.config * * @param {string} ENV Environment to build for, expects 'production' for production and * anything else for non-production - * @param {string} FILES_PATH The relative path from dist-css file to dist-images/dist-fonts * @param {string} SRC The path to the source scss files * @param {string} ROOT The path to the root of the project, this is so we can scope for - * silverstripe-admin variables.scss + * importing silverstripe-admin sass from other modules * @returns {{rules: [*,*,*,*]}} */ -module.exports = (ENV, { FILES_PATH, SRC, ROOT }) => { +module.exports = (ENV, { SRC, ROOT }) => { const useSourceMap = ENV !== 'production'; const cssLoaders = [ - { - loader: 'style-loader', - options: { - sourceMap: useSourceMap, - }, - }, { loader: MiniCssExtractPlugin.loader, }, @@ -29,8 +22,6 @@ module.exports = (ENV, { FILES_PATH, SRC, ROOT }) => { loader: 'css-loader', options: { sourceMap: useSourceMap, - minimize: true, - discardComments: true, }, }, { @@ -39,13 +30,14 @@ module.exports = (ENV, { FILES_PATH, SRC, ROOT }) => { sourceMap: useSourceMap, postcssOptions: { plugins: [ - require('autoprefixer'), - require('postcss-custom-properties'), + 'autoprefixer', + 'postcss-custom-properties', ], }, }, }, - ].filter(loader => loader); + ]; + const scssLoaders = [ ...cssLoaders, { @@ -65,8 +57,7 @@ module.exports = (ENV, { FILES_PATH, SRC, ROOT }) => { Path.resolve(ROOT, '../../silverstripe/admin/client/src/styles'), ], }, - implementation: require('sass'), - sourceMap: useSourceMap, + sourceMap: true, // required for resolve-url-loader to be happy }, }, ]; @@ -84,17 +75,22 @@ module.exports = (ENV, { FILES_PATH, SRC, ROOT }) => { { test: /\.(png|gif|jpe?g|svg)$/, exclude: /fonts[\/\\]([\w_-]+)\.svg$/, - loader: 'url-loader', - options: { - limit: 10000, - name: 'images/[name].[ext]', + type: 'asset', + parser: { + dataUrlCondition: { + maxSize: 10 * 1024 // 10kb + } + }, + generator: { + filename: 'images/[name][ext]', }, }, { - test: /fonts[\/\\]([\w_-]+)\.(woff2?|eot|ttf|svg)$/, - loader: 'file-loader', - options: { - name: 'fonts/[name].[ext]?h=[contenthash]', + // Any .woff, woff2, eot, ttf, or otf file - or svgs specifically in a fonts folder. + test: /(\.(woff2?|eot|ttf|otf)|fonts[\/\\]([\w_-]+)\.svg)$/, + type: 'asset/resource', + generator: { + filename: 'fonts/[name][ext]?h=[contenthash]', }, }, ], diff --git a/css/plugins.js b/css/plugins.js index 1179ee8..51ef739 100644 --- a/css/plugins.js +++ b/css/plugins.js @@ -1,10 +1,17 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const IgnoreEmitPlugin = require('ignore-emit-webpack-plugin'); /** * Exports the settings for plugins in webpack.config + * @param {string} ENV Environment to build for, expects 'production' for production and + * anything else for non-production - reserved in case it's needed in the future + * @param {object} PATHS Various important paths - reserved in case they're needed in the future + * @param {string} filename determines the name of each output CSS file. + * See https://webpack.js.org/plugins/mini-css-extract-plugin/#filename */ -module.exports = () => ([ +module.exports = (ENV, PATHS, filename) => ([ new MiniCssExtractPlugin({ - filename: 'styles/[name].css', + filename, }), + new IgnoreEmitPlugin(/\.js$/), ]); diff --git a/index.js b/index.js index 4fe0356..e55d6cc 100644 --- a/index.js +++ b/index.js @@ -8,4 +8,5 @@ module.exports = { resolveJS: require('./js/resolve'), JavascriptWebpackConfig: require('./configMeta/javascriptWebpackConfig'), + CssWebpackConfig: require('./configMeta/cssWebpackConfig'), }; diff --git a/js/externals.js b/js/externals.js index 4e1daf5..241927a 100644 --- a/js/externals.js +++ b/js/externals.js @@ -1,93 +1,140 @@ /** * Exports a list of modules provided by SilverStripe + * @param {string} ENV Environment to build for, expects 'production' for production and + * anything else for non-production - reserved in case it's needed in the future + * @param {object} PATHS Various important paths - reserved in case they're needed in the future + * @param {string} module Name of the module and config which is being transpiled. + * This is only necessary for core module bundles, to avoid including "exports" config for the same + * bundle that's exposing the given groups of modules. */ -module.exports = { - '@apollo/client': 'ApolloClient', - classnames: 'classnames', - 'deep-freeze-strict': 'DeepFreezeStrict', - 'graphql-fragments': 'GraphQLFragments', - 'graphql-tag': 'GraphQLTag', - 'isomorphic-fetch': 'IsomorphicFetch', - i18n: 'i18n', - jquery: 'jQuery', - merge: 'merge', - 'page.js': 'Page', - 'react-dom/test-utils': 'ReactAddonsTestUtils', - 'react-dom': 'ReactDom', - poppers: 'Poppers', - reactstrap: 'Reactstrap', - 'react-redux': 'ReactRedux', - 'react-router-dom': 'ReactRouterDom', - 'react-select': 'ReactSelect', - react: 'React', - 'redux-form': 'ReduxForm', - 'redux-thunk': 'ReduxThunk', - redux: 'Redux', - config: 'Config', - url: 'NodeUrl', - qs: 'qs', - moment: 'moment', - modernizr: 'modernizr', - 'react-dnd': 'ReactDND', - 'react-dnd-html5-backend': 'ReactDNDHtml5Backend', - validator: 'validator', - 'prop-types': 'PropTypes', +module.exports = (ENV, PATHS, module) => { + const exports = { + 'silverstripe/admin/js': { + // bundles/bundle.js + 'components/Accordion/Accordion': 'Accordion', + 'components/Accordion/AccordionBlock': 'AccordionBlock', + 'components/Badge/Badge': 'Badge', + 'components/Breadcrumb/Breadcrumb': 'Breadcrumb', + 'components/Button/Button': 'Button', + 'components/Button/BackButton': 'BackButton', + 'components/CheckboxSetField/CheckboxSetField': 'CheckboxSetField', + 'components/FieldHolder/FieldHolder': 'FieldHolder', + 'components/Focusedzone/Focusedzone': 'Focusedzone', + 'components/Form/Form': 'Form', + 'components/Form/FormConstants': 'FormConstants', + 'components/FormAction/FormAction': 'FormAction', + 'components/FormAlert/FormAlert': 'FormAlert', + 'components/FormBuilder/FormBuilder': 'FormBuilder', + 'components/FormBuilderModal/FormBuilderModal': 'FormBuilderModal', + 'components/FileStatusIcon/FileStatusIcon': 'FileStatusIcon', + 'components/GridField/GridField': 'GridField', + 'components/GridField/GridFieldCell': 'GridFieldCell', + 'components/GridField/GridFieldHeader': 'GridFieldHeader', + 'components/GridField/GridFieldHeaderCell': 'GridFieldHeaderCell', + 'components/GridField/GridFieldRow': 'GridFieldRow', + 'components/GridField/GridFieldTable': 'GridFieldTable', + 'components/HiddenField/HiddenField': 'HiddenField', + 'components/ListGroup/ListGroup': 'ListGroup', + 'components/ListGroup/ListGroupItem': 'ListGroupItem', + 'components/LiteralField/LiteralField': 'LiteralField', + 'components/Loading/Loading': 'Loading', + 'components/PopoverField/PopoverField': 'PopoverField', + 'components/Preview/Preview': 'Preview', + 'components/ResizeAware/ResizeAware': 'ResizeAware', + 'components/Search/Search': 'Search', + 'components/Search/SearchToggle': 'SearchToggle', + 'components/Tag/CompactTagList': 'CompactTagList', + 'components/Tag/Tag': 'Tag', + 'components/Tag/TagList': 'TagList', + 'components/TextField/TextField': 'TextField', + 'components/Tip/Tip': 'Tip', + 'components/Toolbar/Toolbar': 'Toolbar', + 'components/TreeDropdownField/TreeDropdownField': 'TreeDropdownField', + 'components/TreeDropdownField/TreeDropdownFieldMenu': 'TreeDropdownFieldMenu', + 'components/TreeDropdownField/TreeDropdownFieldNode': 'TreeDropdownFieldNode', + 'components/VersionedBadge/VersionedBadge': 'VersionedBadge', + 'components/ViewModeToggle/ViewModeToggle': 'ViewModeToggle', + 'containers/FormBuilderLoader/FormBuilderLoader': 'FormBuilderLoader', + 'containers/InsertLinkModal/InsertLinkModal': 'InsertLinkModal', + 'containers/InsertLinkModal/fileSchemaModalHandler': 'FileSchemaModalHandler', + 'lib/Backend': 'Backend', + 'lib/Config': 'Config', + 'lib/DataFormat': 'DataFormat', + 'lib/formatWrittenNumber': 'formatWrittenNumber', + 'lib/getFormState': 'getFormState', + 'lib/Injector': 'Injector', + 'lib/ReactRouteRegister': 'ReactRouteRegister', + 'lib/reduxFieldReducer': 'reduxFieldReducer', + 'lib/Router': 'Router', + 'lib/schemaFieldValues': 'schemaFieldValues', + 'lib/ShortcodeSerialiser': 'ShortcodeSerialiser', + 'lib/SilverStripeComponent': 'SilverStripeComponent', + 'lib/TinyMCEActionRegistrar': 'TinyMCEActionRegistrar', + 'lib/withDragDropContext': 'withDragDropContext', + 'lib/withRouter': 'withRouter', + 'state/breadcrumbs/BreadcrumbsActions': 'BreadcrumbsActions', + 'state/records/RecordsActions': 'RecordsActions', + 'state/records/RecordsActionTypes': 'RecordsActionTypes', + 'state/schema/SchemaActions': 'SchemaActions', + 'state/tabs/TabsActions': 'TabsActions', + 'state/toasts/ToastsActions': 'ToastsActions', + 'state/unsavedForms/UnsavedFormsActions': 'UnsavedFormsActions', + 'state/viewMode/ViewModeActions': 'ViewModeActions', + 'state/viewMode/ViewModeStates': 'ViewModeStates', + // bundles/bundle.js aliases + config: 'Config', // alias for lib/Config + // bundles/vendor.js + // @apollo/client can't be exposed - see https://github.com/webpack-contrib/expose-loader/issues/188 + // '@apollo/client': 'ApolloClient', + classnames: 'classnames', + 'deep-freeze-strict': 'DeepFreezeStrict', + 'graphql-fragments': 'GraphQLFragments', + 'graphql-tag': 'GraphQLTag', + 'isomorphic-fetch': 'IsomorphicFetch', + jquery: 'jQuery', + merge: 'merge', + modernizr: 'modernizr', + moment: 'moment', + 'page.js': 'Page', + 'prop-types': 'PropTypes', + qs: 'qs', + react: 'React', + 'react-dnd': 'ReactDND', + 'react-dnd-html5-backend': 'ReactDNDHtml5Backend', + 'react-dom': 'ReactDom', + 'react-redux': 'ReactRedux', + 'react-router-dom': 'ReactRouterDom', + 'react-select': 'ReactSelect', + reactstrap: 'Reactstrap', + redux: 'Redux', + 'redux-form': 'ReduxForm', + 'redux-thunk': 'ReduxThunk', + url: 'NodeUrl', + validator: 'validator', + }, + 'silverstripe/asset-admin/js': { + // bundles/bundle.js + 'containers/InsertEmbedModal/InsertEmbedModal': 'InsertEmbedModal', + 'containers/InsertMediaModal/InsertMediaModal': 'InsertMediaModal', + }, + // Provided by silverstripe/admin's i18n.js, but doesn't use expose-loader + i18n: 'i18n', + }; - // provided by silverstripe or modules - 'components/Accordion/Accordion': 'Accordion', - 'components/Accordion/AccordionBlock': 'AccordionBlock', - 'components/Button/Button': 'Button', - 'components/Button/BackButton': 'BackButton', - 'components/Breadcrumb/Breadcrumb': 'Breadcrumb', - 'components/FormAction/FormAction': 'FormAction', - 'components/FormBuilder/FormBuilder': 'FormBuilder', - 'components/FormBuilderModal/FormBuilderModal': 'FormBuilderModal', - 'components/FileStatusIcon/FileStatusIcon': 'FileStatusIcon', - 'components/FieldHolder/FieldHolder': 'FieldHolder', - 'components/GridField/GridField': 'GridField', - 'components/Toolbar/Toolbar': 'Toolbar', - 'components/LiteralField/LiteralField': 'LiteralField', - 'components/Preview/Preview': 'Preview', - 'components/ListGroup/ListGroup': 'ListGroup', - 'components/ListGroup/ListGroupItem': 'ListGroupItem', - 'components/Loading/Loading': 'Loading', - 'components/FormAlert/FormAlert': 'FormAlert', - 'components/Badge/Badge': 'Badge', - 'components/VersionedBadge/VersionedBadge': 'VersionedBadge', - 'components/TreeDropdownField/TreeDropdownField': 'TreeDropdownField', - 'components/Focusedzone/Focusedzone': 'Focusedzone', - 'components/ViewModeToggle/ViewModeToggle': 'ViewModeToggle', - 'components/ResizeAware/ResizeAware': 'ResizeAware', - 'components/Tag/Tag': 'Tag', - 'components/Tag/TagList': 'TagList', - 'components/Tag/CompactTagList': 'CompactTagList', - 'components/Search/Search': 'Search', - 'components/Search/SearchToggle': 'SearchToggle', - 'containers/FormBuilderLoader/FormBuilderLoader': 'FormBuilderLoader', - 'containers/InsertMediaModal/InsertMediaModal': 'InsertMediaModal', - 'containers/InsertLinkModal/InsertLinkModal': 'InsertLinkModal', - 'containers/InsertLinkModal/fileSchemaModalHandler': 'FileSchemaModalHandler', - 'state/breadcrumbs/BreadcrumbsActions': 'BreadcrumbsActions', - 'state/schema/SchemaActions': 'SchemaActions', - 'state/toasts/ToastsActions': 'ToastsActions', - 'state/records/RecordsActions': 'RecordsActions', - 'state/records/RecordsActionTypes': 'RecordsActionTypes', - 'state/tabs/TabsActions': 'TabsActions', - 'state/viewMode/ViewModeActions': 'ViewModeActions', - 'lib/DataFormat': 'DataFormat', - 'lib/Backend': 'Backend', - 'lib/getFormState': 'getFormState', - 'lib/ReactRouteRegister': 'ReactRouteRegister', - 'lib/ReducerRegister': 'ReducerRegister', - 'lib/SilverStripeComponent': 'SilverStripeComponent', - 'lib/formatWrittenNumber': 'formatWrittenNumber', - 'lib/Router': 'Router', - 'lib/schemaFieldValues': 'schemaFieldValues', - 'lib/Config': 'Config', - 'lib/Injector': 'Injector', - 'lib/reduxFieldReducer': 'reduxFieldReducer', - 'lib/TinyMCEActionRegistrar': 'TinyMCEActionRegistrar', - 'lib/ShortcodeSerialiser': 'ShortcodeSerialiser', - 'lib/withDragDropContext': 'withDragDropContext', - 'lib/withRouter': 'withRouter', + // Don't include the exports provided by the current module + if (module && exports[module] !== undefined) { + delete exports[module]; + } + + // Flatten exports object down to a single level object + let retVal = {}; + for (const [key, val] of Object.entries(exports)) { + if (typeof val === 'object') { + retVal = Object.assign(retVal, val); + } else { + retVal[key] = val; + } + } + + return retVal; }; diff --git a/js/plugins.js b/js/plugins.js index afbb294..cac5894 100644 --- a/js/plugins.js +++ b/js/plugins.js @@ -3,8 +3,11 @@ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); /** * Exports the settings for plugins in webpack.config + * @param {string} ENV Environment to build for, expects 'production' for production and + * anything else for non-production + * @param {object} PATHS Various important paths - reserved in case they're needed in the future */ -module.exports = (ENV) => { +module.exports = (ENV, PATHS) => { return [ new webpack.ProvidePlugin({ jQuery: 'jquery', diff --git a/js/resolve.js b/js/resolve.js index 6eb07e7..8447679 100644 --- a/js/resolve.js +++ b/js/resolve.js @@ -2,7 +2,7 @@ * Exports the settings for resolve in webpack.config * * @param {string} ENV Environment to build for, expects 'production' for production and - * anything else for non-production + * anything else for non-production - reserved in case it's needed in the future * @param {string} ROOT The root folder containing package.json * @param {string} SRC Path to source files * @param {string} MODULES The modules folder diff --git a/package.json b/package.json index c530224..24f2346 100644 --- a/package.json +++ b/package.json @@ -30,11 +30,10 @@ "@sect/modernizr-loader": "^1.0.3", "autoprefixer": "^10.4.13", "babel-loader": "^9.0.1", + "core-js": "^3.26.0", "css-loader": "^6.7.1", - "core-js": "^3.26.0", "expose-loader": "^4.0.0", - "extract-loader": "^5.1.0", - "file-loader": "^6.2.0", + "ignore-emit-webpack-plugin": "^2.0.6", "imports-loader": "^4.0.1", "json-loader": "^0.5.7", "lodash": "^4.17.21", @@ -45,13 +44,10 @@ "postcss-custom-properties": "^12.1.10", "postcss-load-config": "^4.0.1", "postcss-loader": "^7.0.1", - "raw-loader": "^4.0.2", "resolve-url-loader": "^5.0.0", "sass": "^1.55.0", "sass-loader": "^13.1.0", - "style-loader": "3.3.1", "stylelint": "^14.14.0", - "url-loader": "^4.1.1", "webpack": "^5.74.0", "webpack-bundle-analyzer": "^4.7.0" },