@shopgate/webpack 7.29.9 → 7.30.0-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,13 @@
1
1
  const importFresh = require('import-fresh');
2
2
 
3
+ /**
4
+ * @typedef {import('../../../themes/theme-ios11/config/app.json')} AppSettings
5
+ */
6
+
3
7
  /**
4
8
  * Returns the app settings from the remote project.
5
9
  * @param {string} themePath The path to the theme.
6
- * @return {Object} The app settings.
10
+ * @return {AppSettings} The app settings.
7
11
  */
8
12
  module.exports = function getAppSettings(themePath) {
9
13
  try {
@@ -1,6 +1,16 @@
1
+ /**
2
+ * @typedef {Object} DevConfig
3
+ * @property {string} ip The IP address.
4
+ * @property {number} port The port number.
5
+ * @property {number} apiPort The API port number.
6
+ * @property {number} hmrPort The HMR port number.
7
+ * @property {number} remotePort The remote port number.
8
+ * @property {string} sourceMap The source map type.
9
+ */
10
+
1
11
  /**
2
12
  * Returns the development configuration.
3
- * @return {Object} The development configuration.
13
+ * @return {DevConfig} The development configuration.
4
14
  */
5
15
  module.exports = function getDevConfig() {
6
16
  const defaultConfig = {
@@ -9,7 +19,7 @@ module.exports = function getDevConfig() {
9
19
  apiPort: 9666,
10
20
  hmrPort: 3000,
11
21
  remotePort: 8000,
12
- sourceMap: 'cheap-module-eval-source-map',
22
+ sourceMap: 'eval-cheap-module-source-map',
13
23
  };
14
24
 
15
25
  try {
@@ -29,7 +39,12 @@ module.exports = function getDevConfig() {
29
39
  apiPort,
30
40
  hmrPort,
31
41
  remotePort,
32
- sourceMap: sourceMapsType,
42
+ // The source map type 'cheap-module-eval-source-map' is renamed to
43
+ // 'eval-cheap-module-source-map' in webpack 5. Since it is the default type created by
44
+ // the platform-sdk, we need to map it here.
45
+ sourceMap: sourceMapsType === 'cheap-module-eval-source-map'
46
+ ? 'eval-cheap-module-source-map'
47
+ : defaultConfig.sourceMap,
33
48
  };
34
49
  } catch (e) {
35
50
  return defaultConfig;
package/lib/helpers.js ADDED
@@ -0,0 +1,41 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Resolves the absolute path to a given package, taking into account different development
6
+ * and deployment contexts (theme, monorepo, or globally hoisted dependencies).
7
+ *
8
+ * The resolution strategy tries multiple locations in order:
9
+ * 1. The theme’s local `node_modules` (used in deployment or external developer mode).
10
+ * 2. The global monorepo root’s `node_modules` (used in monorepo development mode).
11
+ * 3. The current working directory’s `node_modules`.
12
+ * 4. Falls back to Node's `require.resolve()` if no path exists, allowing Yarn workspaces
13
+ * or hoisting to handle resolution.
14
+ *
15
+ * @param {string} pkgName - The name of the package to resolve.
16
+ * @param {string} [extraSubPath=''] - Optional subpath inside the package.
17
+ * @param {string} [themePath=process.cwd()] - Base path of the theme. Defaults to the current
18
+ * working directory.
19
+ * @returns {string} The resolved absolute path to the requested package or subpath.
20
+ *
21
+ */
22
+ const resolveForAliasPackage = (pkgName, extraSubPath = '', themePath = process.cwd()) => {
23
+ const tryPaths = [
24
+ path.resolve(themePath, 'node_modules', pkgName + extraSubPath),
25
+ path.resolve(themePath, '..', '..', 'node_modules', pkgName + extraSubPath),
26
+ path.resolve(process.cwd(), 'node_modules', pkgName + extraSubPath),
27
+ ];
28
+
29
+ const hit = tryPaths.find(candidate => fs.existsSync(candidate)) || null;
30
+
31
+ if (!hit) {
32
+ // fallback to Node's own algorithm (which will follow Yarn workspaces / hoisting)
33
+ return require.resolve(pkgName + extraSubPath);
34
+ }
35
+
36
+ return hit;
37
+ };
38
+
39
+ module.exports = {
40
+ resolveForAliasPackage,
41
+ };
package/lib/i18n.js CHANGED
@@ -1,62 +1,162 @@
1
1
  const path = require('path');
2
- const fsEx = require('fs-extra');
3
- const MessageFormat = require('messageformat');
4
- const Messages = require('messageformat/messages');
2
+ const fs = require('fs');
5
3
 
6
4
  const rootDirectory = path.resolve(__dirname, '..');
7
5
  const localesDirectory = path.resolve(rootDirectory, 'locales');
8
6
 
9
7
  /**
10
- * The i18n class.
8
+ * Reads and parses a JSON file with friendly error handling.
9
+ *
10
+ * @private
11
+ * @param {string} file - Absolute path to the JSON file.
12
+ * @returns {Object} Parsed JSON object.
13
+ * @throws {Error} When the file cannot be read or parsed.
14
+ */
15
+ function readJSON(file) {
16
+ try {
17
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
18
+ } catch (err) {
19
+ throw new Error(`Failed to read locale file "${file}": ${err.message}`);
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Flattens nested objects to dot-separated key paths.
25
+ *
26
+ * Example:
27
+ * ```js
28
+ * flatten({ a: { b: 'c' } }) // -> { 'a.b': 'c' }
29
+ * ```
30
+ *
31
+ * @private
32
+ * @param {Object} obj - The source object.
33
+ * @param {string} [prefix=''] - The prefix for the keys (used for recursion).
34
+ * @param {Object} [out={}] - The accumulator for flattened entries.
35
+ * @returns {Object} A flattened key-value map.
36
+ */
37
+ function flatten(obj, prefix = '', out = {}) {
38
+ const result = out;
39
+
40
+ for (const [key, value] of Object.entries(obj)) {
41
+ const fullKey = prefix ? `${prefix}.${key}` : key;
42
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
43
+ flatten(value, fullKey, result);
44
+ } else {
45
+ result[fullKey] = String(value);
46
+ }
47
+ }
48
+ return result;
49
+ }
50
+
51
+ /**
52
+ * Replaces placeholders of the form `{var}` in a message string.
53
+ *
54
+ * Example:
55
+ * ```js
56
+ * format("Hello {name}", { name: "World" }); // -> "Hello World"
57
+ * ```
58
+ *
59
+ * @private
60
+ * @param {string} template - Message string with `{var}` placeholders.
61
+ * @param {Record<string, string|number>} [values={}] - Replacement values.
62
+ * @returns {string} Formatted string.
63
+ */
64
+ function format(template, values = {}) {
65
+ return template.replace(/\{(\w+)\}/g, (_, k) =>
66
+ (values[k] != null ? String(values[k]) : `{${k}}`));
67
+ }
68
+
69
+ /**
70
+ * Provides localized message lookup and interpolation for a given locale.
71
+ *
72
+ * @class
11
73
  */
12
74
  class I18n {
13
75
  /**
14
- * @param {string} [locale='en'] The desired locale for the module.
76
+ * Creates a new I18n instance.
77
+ *
78
+ * @param {string} [locale='en'] - The locale to load (e.g. "en", "de").
15
79
  */
16
80
  constructor(locale = 'en') {
17
81
  const locales = [locale];
82
+ if (!locales.includes('en')) locales.unshift('en');
18
83
 
19
- if (!locales.includes('en')) {
20
- locales.unshift('en');
21
- }
22
-
23
- const messageSet = {};
84
+ let merged = {};
24
85
  locales.forEach((entry) => {
25
- const localeFilePath = path.resolve(rootDirectory, localesDirectory, `${entry}.json`);
26
- messageSet[entry] = fsEx.readJSONSync(localeFilePath);
86
+ const localeFilePath = path.join(localesDirectory, `${entry}.json`);
87
+ if (fs.existsSync(localeFilePath)) {
88
+ const data = flatten(readJSON(localeFilePath));
89
+ merged = {
90
+ ...merged,
91
+ ...data,
92
+ };
93
+ }
27
94
  });
28
95
 
29
- const messageFormat = new MessageFormat(locales);
30
- this.messages = new Messages(messageFormat.compile(messageSet));
31
- this.messages.locale = locale;
96
+ /**
97
+ * The flattened dictionary of messages for this locale.
98
+ * @type {Record<string, string>}
99
+ */
100
+ this.messages = merged;
101
+
102
+ /**
103
+ * The current locale string.
104
+ * @type {string}
105
+ */
106
+ this.locale = locale;
32
107
  }
33
108
 
34
109
  /**
35
- * @param {Array} keyPath The key path of the translation.
36
- * @param {Object} data Additional data for the translation.
37
- * @returns {string|Array}
110
+ * Returns the translated message for the given key path.
111
+ *
112
+ * @param {string[]|string} keyPath - Key path or array of keys representing the translation key.
113
+ * @param {Record<string, string|number>} [data={}] - Optional interpolation data.
114
+ * @returns {string} The translated and formatted message, or the key if not found.
38
115
  */
39
116
  get(keyPath, data = {}) {
40
- if (this.messages.hasObject(keyPath)) {
41
- return keyPath;
117
+ const id = Array.isArray(keyPath) ? keyPath.join('.') : keyPath;
118
+ const message = this.messages[id];
119
+ if (message == null) {
120
+ return Array.isArray(keyPath) ? keyPath[keyPath.length - 1] : keyPath;
42
121
  }
43
-
44
- return this.messages.get(keyPath, data);
122
+ return format(message, data);
45
123
  }
46
124
  }
47
125
 
48
126
  let i18n;
49
127
 
128
+ /**
129
+ * @typedef {function(key: string|string[], data?: Object): string} TranslatorFn
130
+ */
131
+
132
+ /**
133
+ * Initializes or retrieves the singleton I18n instance and
134
+ * returns a translation function scoped to the given module.
135
+ *
136
+ * This function automatically derives the message namespace from the
137
+ * relative module path, so keys can be referenced without repeating the prefix.
138
+ *
139
+ * Example:
140
+ * ```js
141
+ * const i18n = require('./lib/i18n');
142
+ * const t = i18n(__filename);
143
+ * console.log(t('HELLO', { name: 'World' }));
144
+ * ```
145
+ *
146
+ * @param {string} modulePath - Absolute path to the module requesting translations
147
+ * (usually `__filename`).
148
+ * @returns {TranslatorFn} Translator function.
149
+ */
50
150
  module.exports = (modulePath) => {
51
151
  if (!i18n) {
52
152
  i18n = new I18n('en');
53
153
  }
54
154
 
55
- const moduleNamespace = path.relative(rootDirectory, modulePath)
155
+ const moduleNamespace = path
156
+ .relative(rootDirectory, modulePath)
56
157
  .replace(/(^(lib|src)+[/\\])|(\.js$)/ig, '')
57
- // Normalize OS specific path separators to forward slashes to be able to access translation
58
- // keys inside the translation JSON file.
59
- .split(path.sep).join('/');
158
+ .split(path.sep)
159
+ .join('/');
60
160
 
61
161
  return (key, data) => {
62
162
  const keyPath = [moduleNamespace];
@@ -69,14 +169,12 @@ module.exports = (modulePath) => {
69
169
  throw new Error(`'${key}' is not a valid message key`);
70
170
  }
71
171
 
72
- const message = i18n.get(keyPath, data);
73
-
74
- if (message === keyPath) {
75
- return key;
76
- }
77
-
78
- return message;
172
+ return i18n.get(keyPath, data);
79
173
  };
80
174
  };
81
175
 
176
+ /**
177
+ * Exposes the I18n class for testing or advanced usage.
178
+ * @type {typeof I18n}
179
+ */
82
180
  module.exports.I18n = I18n;
package/lib/polyfill.js CHANGED
@@ -1,49 +1 @@
1
1
  window.SGEvent = {}
2
-
3
- // https://github.com/uxitten/polyfill/blob/master/string.polyfill.js
4
- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd
5
- if (!String.prototype.padEnd) {
6
- String.prototype.padEnd = function padEnd(targetLength, padString) {
7
- // Floor if number or convert non-number to 0
8
- targetLength = targetLength >> 0
9
- padString = String(padString || ' ')
10
- if (this.length > targetLength) {
11
- return String(this)
12
- } else {
13
- targetLength = targetLength - this.length
14
-
15
- if (targetLength > padString.length) {
16
- // Append to original to ensure we are longer than needed.
17
- padString += padString.repeat(targetLength / padString.length)
18
- }
19
-
20
- return String(this) + padString.slice(0, targetLength)
21
- }
22
- }
23
- }
24
-
25
- if (!Array.prototype.flat) {
26
- Array.prototype.flat = function(depth) {
27
- var flattened = [];
28
-
29
- function flatten(arr, currentDepth) {
30
- for (var i = 0; i < arr.length; i++) {
31
- if (Array.isArray(arr[i]) && currentDepth < depth) {
32
- flatten(arr[i], currentDepth + 1);
33
- } else {
34
- flattened.push(arr[i]);
35
- }
36
- }
37
- }
38
-
39
- flatten(this, 0);
40
-
41
- return flattened;
42
- };
43
- }
44
-
45
- if (!Array.prototype.flatMap) {
46
- Array.prototype.flatMap = function(callback, thisArg) {
47
- return this.map(callback, thisArg).flat();
48
- };
49
- }
package/lib/variables.js CHANGED
@@ -2,6 +2,7 @@ const path = require('path');
2
2
 
3
3
  const ENV_KEY_DEVELOPMENT = 'development';
4
4
 
5
+ /** @type {"development" | "production"} */
5
6
  exports.ENV = process.env.NODE_ENV || ENV_KEY_DEVELOPMENT;
6
7
  exports.isDev = exports.ENV === ENV_KEY_DEVELOPMENT;
7
8
  exports.PUBLIC_FOLDER = 'public';
package/locales/en.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "STRINGS_COULD_NOT_BE_CREATED": "Strings could not be created",
12
12
  "INDEXING_TYPE": "Indexing {type}...",
13
13
  "INDEXED_TYPE": "... {type} indexed.",
14
- "NO_EXTENSIONS_FOUND_FOR_TYPE": "No extensions found for ''{type}''",
14
+ "NO_EXTENSIONS_FOUND_FOR_TYPE": "No extensions found for \"{type}\"",
15
15
  "TYPE_PORTALS": "portals",
16
16
  "TYPE_REDUCERS": "reducers",
17
17
  "TYPE_SUBSCRIBERS": "subscribers",
package/package.json CHANGED
@@ -1,29 +1,31 @@
1
1
  {
2
2
  "name": "@shopgate/webpack",
3
- "version": "7.29.9",
3
+ "version": "7.30.0-alpha.11",
4
4
  "description": "The webpack configuration for Shopgate's Engage.",
5
5
  "main": "webpack.config.js",
6
6
  "license": "Apache-2.0",
7
7
  "dependencies": {
8
- "ajv": "^6.10.2",
9
- "babel-loader": "8.4.1",
10
- "chalk": "^2.4.2",
11
- "color": "^3.1.2",
12
- "compression-webpack-plugin": "^3.0.0",
13
- "css-loader": "1.0.1",
14
- "file-loader": "4.3.0",
15
- "html-webpack-plugin": "^3.2.0",
16
- "import-fresh": "3.0.0",
8
+ "@pmmmwh/react-refresh-webpack-plugin": "0.5.17",
9
+ "ajv": "^8.17.1",
10
+ "babel-loader": "^9.2.1",
11
+ "chalk": "^4.1.2",
12
+ "color": "^4.2.3",
13
+ "compression-webpack-plugin": "^10.0.0",
14
+ "css-loader": "^6.11.0",
15
+ "css-minimizer-webpack-plugin": "^5.0.1",
16
+ "html-webpack-plugin": "^5.6.4",
17
+ "import-fresh": "^3.3.1",
17
18
  "intl": "1.2.5",
18
- "lodash": "^4.17.4",
19
- "messageformat": "^2.3.0",
19
+ "lodash": "^4.17.21",
20
+ "mini-css-extract-plugin": "^2.9.4",
20
21
  "progress-bar-webpack-plugin": "^2.1.0",
21
- "script-ext-html-webpack-plugin": "^2.1.5",
22
- "style-loader": "0.23.1",
23
- "terser-webpack-plugin": "^4.2.3",
24
- "webpack": "^4.47.0",
25
- "webpack-cli": "4.10.0",
26
- "workbox-webpack-plugin": "4.3.1"
22
+ "react-refresh": "^0.18.0",
23
+ "style-loader": "3.3.4",
24
+ "terser-webpack-plugin": "^5.3.14",
25
+ "webpack": "^5.102.1",
26
+ "webpack-bundle-analyzer": "^4.10.2",
27
+ "webpack-cli": "^5.1.4",
28
+ "workbox-webpack-plugin": "6.5.4"
27
29
  },
28
30
  "devDependencies": {
29
31
  "rxjs": "~5.5.12",
@@ -76,7 +76,6 @@ function readConfig(options) {
76
76
  const exports = [exportsStart]; // Holds the export strings.
77
77
 
78
78
  if (type === TYPE_PORTALS || type === TYPE_WIDGETS || type === TYPE_WIDGETS_V2) {
79
- imports.push('import { hot } from \'react-hot-loader/root\';');
80
79
  imports.push('import { lazy } from \'react\';');
81
80
  imports.push('');
82
81
  }
@@ -107,11 +106,7 @@ function readConfig(options) {
107
106
  return;
108
107
  }
109
108
 
110
- if (isPortalsOrWidgets) {
111
- exports.push(` '${id}': hot(${variableName}),`);
112
- } else {
113
- exports.push(` '${id}': ${variableName},`);
114
- }
109
+ exports.push(` '${id}': ${variableName},`);
115
110
  });
116
111
 
117
112
  if (importsEnd) {
@@ -10,8 +10,14 @@
10
10
  <title>
11
11
  <%= htmlWebpackPlugin.options.title %>
12
12
  </title>
13
- <script src="<%= htmlWebpackPlugin.files.chunks.common.entry %>"></script>
14
- <% debugger; %>
13
+ <% if (htmlWebpackPlugin.files && htmlWebpackPlugin.files.css) { %>
14
+ <% htmlWebpackPlugin.files.css.forEach(function(css) { %>
15
+ <link rel="stylesheet" href="<%= css %>">
16
+ <% }) %>
17
+ <% } %>
18
+ <% const scripts = htmlWebpackPlugin.files.js; %>
19
+ <% const vendor = scripts.find(s => s.includes('vendor')); %>
20
+ <% if (vendor) { %><script src="<%= vendor %>"></script><% } %>
15
21
  <script>
16
22
  var SGConnect = {
17
23
  appId: "<%- htmlWebpackPlugin.options.appId %>"
@@ -25,7 +31,9 @@
25
31
  <body>
26
32
  <div id="root"></div>
27
33
  <div id="portals"></div>
28
- <script src="<%= htmlWebpackPlugin.files.chunks.app.entry %>"></script>
34
+ <% for (const js of scripts) { if (js !== vendor) { %>
35
+ <script src="<%= js %>"></script>
36
+ <% }} %>
29
37
  </body>
30
38
 
31
39
  </html>
package/webpack.config.js CHANGED
@@ -1,13 +1,15 @@
1
- /* eslint-disable camelcase */
2
1
  const path = require('path');
3
2
  const webpack = require('webpack');
4
3
  const chalk = require('chalk');
5
4
  const TerserPlugin = require('terser-webpack-plugin');
6
5
  const HTMLWebpackPlugin = require('html-webpack-plugin');
7
6
  const ProgressBarWebpackPlugin = require('progress-bar-webpack-plugin');
8
- const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
9
7
  const CompressionWebpackPlugin = require('compression-webpack-plugin');
10
8
  const { GenerateSW } = require('workbox-webpack-plugin');
9
+ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
10
+ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
11
+ const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
12
+ const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
11
13
  const rxPaths = require('rxjs/_esm5/path-mapping');
12
14
  const ShopgateIndexerPlugin = require('./plugins/ShopgateIndexerPlugin');
13
15
  const ShopgateThemeConfigValidatorPlugin = require('./plugins/ShopgateThemeConfigValidatorPlugin');
@@ -19,6 +21,7 @@ const getThemeConfig = require('./lib/getThemeConfig');
19
21
  const getThemeLanguage = require('./lib/getThemeLanguage');
20
22
  const getDevConfig = require('./lib/getDevConfig');
21
23
  const i18n = require('./lib/i18n');
24
+ const { resolveForAliasPackage } = require('./lib/helpers');
22
25
  const getExtensionsNodeModulesPaths = require('./lib/getExtensionsNodeModulesPaths');
23
26
 
24
27
  const themePath = process.cwd();
@@ -26,44 +29,86 @@ const appConfig = getAppSettings(themePath);
26
29
  const themeConfig = getThemeConfig(themePath, appConfig);
27
30
  const isoLang = convertLanguageToISO(appConfig.language);
28
31
  const { sourceMap, ip, apiPort } = getDevConfig();
32
+ const themeLanguage = getThemeLanguage(themePath, appConfig.language);
29
33
  const t = i18n(__filename);
30
34
 
31
35
  const devtool = isDev ? sourceMap : (process.env.SOURCE_MAPS || false);
32
36
  const fileSuffix = devtool ? '.sm' : '';
37
+ const addBundleAnalyzer = !!process.env.BUNDLE_ANALYZER;
33
38
 
39
+ /**
40
+ * @type {import('webpack').Configuration}
41
+ */
34
42
  const config = {
35
43
  mode: ENV,
44
+ target: ['web', 'browserslist'],
36
45
  entry: {
37
- app: [
38
- ...(!isDev ? [
39
- path.resolve(__dirname, 'lib', 'offline.js'),
40
- ] : []),
41
- path.resolve(__dirname, 'lib', 'polyfill.js'),
42
- path.resolve(themePath, 'index.jsx'),
43
- ],
44
- common: [
46
+ app: {
47
+ import: [
48
+ ...(!isDev ? [
49
+ path.resolve(__dirname, 'lib', 'offline.js'),
50
+ ] : []),
51
+ path.resolve(__dirname, 'lib', 'polyfill.js'),
52
+ path.resolve(themePath, 'index.jsx'),
53
+ ],
54
+ dependOn: 'vendor',
55
+ },
56
+ vendor: [
57
+ 'glamor',
45
58
  'intl',
46
59
  `intl/locale-data/jsonp/${isoLang}`,
47
60
  'react',
48
61
  'react-dom',
49
- 'glamor',
50
62
  'react-redux',
51
63
  'reselect',
52
64
  ],
53
65
  },
54
66
  output: {
55
- filename: !isDev ? `[name].[hash]${fileSuffix}.js` : `[name]${fileSuffix}.js`,
67
+ filename: !isDev ? `[name].[contenthash]${fileSuffix}.js` : `[name]${fileSuffix}.js`,
56
68
  chunkFilename: `[name].[chunkhash]${fileSuffix}.js`,
57
69
  path: path.resolve(themePath, PUBLIC_FOLDER),
58
70
  publicPath: isDev ? '/' : (process.env.publicPath || './'),
59
71
  },
60
72
  resolve: {
61
73
  extensions: ['.json', '.js', '.jsx', '.mjs'],
74
+ /**
75
+ * Aliases for module resolution. They guarantee that whenever one of the bundled modules
76
+ * uses in import to one of the packages, it will always resolve to the version of the core.
77
+ */
62
78
  alias: {
63
79
  ...rxPaths(),
64
- 'react-dom': '@hot-loader/react-dom',
80
+
81
+ // Packages from common module
82
+ react: resolveForAliasPackage('react'),
83
+ 'react-dom': resolveForAliasPackage('react-dom'),
84
+ 'react-redux': resolveForAliasPackage('react-redux'),
85
+ reselect: resolveForAliasPackage('reselect'),
86
+ glamor: resolveForAliasPackage('glamor'),
87
+ intl: resolveForAliasPackage('intl'),
88
+ 'intl/locale-data/jsonp': resolveForAliasPackage('intl', '/locale-data/jsonp'),
89
+
90
+ // Additional packages that are sometimes used in devDependencies of extensions
91
+ 'react-helmet': resolveForAliasPackage('react-helmet'),
92
+ 'css-spring': resolveForAliasPackage('css-spring'),
93
+ 'react-transition-group': resolveForAliasPackage('react-transition-group'),
94
+ '@virtuous': resolveForAliasPackage('@virtuous'),
95
+ lodash: resolveForAliasPackage('lodash'),
96
+ 'prop-types': resolveForAliasPackage('prop-types'),
97
+
98
+ // Internal Shopgate packages
99
+ '@shopgate/engage': resolveForAliasPackage('@shopgate/engage'),
100
+ '@shopgate/pwa-common': resolveForAliasPackage('@shopgate/pwa-common'),
101
+ '@shopgate/pwa-common-commerce': resolveForAliasPackage('@shopgate/pwa-common-commerce'),
102
+ '@shopgate/pwa-core': resolveForAliasPackage('@shopgate/pwa-core'),
103
+ '@shopgate/pwa-tracking': resolveForAliasPackage('@shopgate/pwa-tracking'),
104
+ '@shopgate/pwa-ui-ios': resolveForAliasPackage('@shopgate/pwa-ui-ios'),
105
+ '@shopgate/pwa-ui-material': resolveForAliasPackage('@shopgate/pwa-ui-material'),
106
+ '@shopgate/pwa-ui-shared': resolveForAliasPackage('@shopgate/pwa-ui-shared'),
107
+ '@shopgate/pwa-webcheckout-shopify': resolveForAliasPackage('@shopgate/pwa-webcheckout-shopify'),
108
+ '@shopgate/tracking-core': resolveForAliasPackage('@shopgate/tracking-core'),
65
109
  },
66
110
  modules: [
111
+ 'node_modules',
67
112
  path.resolve(themePath, 'widgets'),
68
113
  path.resolve(themePath, 'node_modules'),
69
114
  path.resolve(themePath, '..', '..', 'node_modules'),
@@ -73,23 +118,12 @@ const config = {
73
118
  },
74
119
  plugins: [
75
120
  new ShopgateThemeConfigValidatorPlugin(),
121
+
122
+ // Create mapping files inside the theme extensions folder the enable access to code that's
123
+ // provided by extensions via extension-config.json
76
124
  new ShopgateIndexerPlugin(),
77
- /**
78
- * Workaround to enable latest swiper version (11.2.1) with webpack.
79
- * The utils.mjs file in swiper/shared/utils.mjs is not compatible with webpack due to use of
80
- * optional chaining.
81
- *
82
- * Processing the module with babel-loader doesn't work, since transpilation of some array
83
- * operations break the module logic inside the browser.
84
- *
85
- * As a workaround we replace the file with a local patched version.
86
- * Alternative approaches e.g. via patch-package didn't work as expected due to issues in
87
- * release process.
88
- */
89
- new webpack.NormalModuleReplacementPlugin(
90
- /swiper[/\\]shared[/\\]utils\.mjs$/,
91
- path.resolve(__dirname, 'patches', 'swiper', 'shared', 'utils.mjs')
92
- ),
125
+
126
+ // Inject environment variables so that they are available within the bundled code
93
127
  new webpack.DefinePlugin({
94
128
  'process.env': {
95
129
  NODE_ENV: JSON.stringify(ENV),
@@ -98,10 +132,9 @@ const config = {
98
132
  THEME_CONFIG: JSON.stringify(themeConfig),
99
133
  THEME: JSON.stringify(process.env.theme),
100
134
  THEME_PATH: JSON.stringify(themePath),
101
- // @deprecated Replaced by LOCALE and LOCALE_FILE - kept for now for theme compatibility.
102
- LANG: JSON.stringify(isoLang),
103
135
  LOCALE: JSON.stringify(isoLang),
104
- LOCALE_FILE: JSON.stringify(getThemeLanguage(themePath, appConfig.language)),
136
+ LOCALE_FILE: JSON.stringify(themeLanguage),
137
+ LOCALE_FILE_LOWER_CASE: JSON.stringify(themeLanguage.toLowerCase()),
105
138
  IP: JSON.stringify(ip),
106
139
  PORT: JSON.stringify(apiPort),
107
140
  },
@@ -115,9 +148,8 @@ const config = {
115
148
  },
116
149
  },
117
150
  }),
118
- new webpack.optimize.ModuleConcatenationPlugin(),
119
- new webpack.HashedModuleIdsPlugin(),
120
- new webpack.NoEmitOnErrorsPlugin(),
151
+
152
+ // Plugin to minify the HTML output fo the default.ejs template
121
153
  new HTMLWebpackPlugin({
122
154
  title: appConfig.shopName || process.env.theme,
123
155
  filename: path.resolve(themePath, PUBLIC_FOLDER, 'index.html'),
@@ -136,11 +168,8 @@ const config = {
136
168
  minifyCSS: true,
137
169
  } : false,
138
170
  }),
139
- new ScriptExtHtmlWebpackPlugin({
140
- sync: ['app', 'common'],
141
- prefetch: /\.js$/,
142
- defaultAttribute: 'async',
143
- }),
171
+
172
+ // Progress bar that shows build progress in the console
144
173
  new ProgressBarWebpackPlugin({
145
174
  format: ` ${t('WEBPACK_PROGRESS', {
146
175
  bar: chalk.blue(':bar'),
@@ -150,9 +179,17 @@ const config = {
150
179
  })}`,
151
180
  clear: false,
152
181
  }),
182
+
183
+ // Bundle analyzer plugin to visualize size of webpack output files
184
+ ...(isDev && addBundleAnalyzer ? [
185
+ new BundleAnalyzerPlugin(),
186
+ ] : []),
187
+ ...(isDev ? [new ReactRefreshWebpackPlugin({
188
+ overlay: false,
189
+ })] : []),
153
190
  ...(!isDev ? [
154
191
  new CompressionWebpackPlugin({
155
- filename: '[path].gz[query]',
192
+ filename: '[path][base].gz[query]',
156
193
  algorithm: 'gzip',
157
194
  test: /\.js$|\.css$/,
158
195
  minRatio: 1,
@@ -162,46 +199,82 @@ const config = {
162
199
  clientsClaim: true,
163
200
  skipWaiting: true,
164
201
  }),
202
+ // Extract CSS into separate minified files on production builds
203
+ new MiniCssExtractPlugin({
204
+ filename: '[name].[contenthash].css',
205
+ chunkFilename: '[name].[contenthash].css',
206
+ }),
207
+ new CssMinimizerPlugin(),
165
208
  ] : []),
166
209
  ],
167
210
  module: {
168
211
  rules: [
169
212
  {
170
213
  test: /\.(png|jpe?g|gif|svg)$/i,
171
- use: [
172
- {
173
- loader: 'file-loader',
174
- },
175
- ],
214
+ type: 'asset/resource',
215
+ generator: {
216
+ filename: '[name].[contenthash][ext][query]',
217
+ },
176
218
  },
177
219
  {
178
220
  test: /\.css$/,
179
- use: [
221
+ // Bundle CSS on development, extract it into separate files on production
222
+ use: isDev ? [
180
223
  'style-loader',
181
224
  'css-loader',
225
+ ] : [
226
+ MiniCssExtractPlugin.loader,
227
+ 'css-loader',
182
228
  ],
183
229
  },
184
230
  {
185
231
  test: /\.mjs$/,
186
232
  type: 'javascript/auto',
187
233
  },
234
+ // Special treatment for PWA Extension Kit to ensure compatibility with current Babel setup
235
+ // https://github.com/shopgate-professional-services/pwa-extension-kit
236
+ {
237
+ test: /\.js$/,
238
+ include: /node_modules\/@shopgate-ps\/pwa-extension-kit/,
239
+ use: {
240
+ loader: 'babel-loader',
241
+ options: {
242
+ presets: [
243
+ ['@babel/preset-env', { modules: 'commonjs' }],
244
+ '@babel/preset-react',
245
+ ],
246
+ },
247
+ },
248
+ },
188
249
  {
189
250
  test: /\.(js|jsx)$/,
190
- exclude: new RegExp(`node_modules\\b(?!${path.sep}@shopgate|${path.sep}react-leaflet|${path.sep}@react-leaflet)\\b.*`),
251
+ exclude: new RegExp(`node_modules\\b(?!\\${path.sep}@shopgate)\\b.*`),
191
252
  use: [
192
253
  {
193
254
  loader: 'babel-loader',
194
255
  options: {
195
256
  configFile: path.resolve(themePath, 'babel.config.js'),
196
257
  cacheDirectory: path.resolve(themePath, '..', '..', '.cache-loader'),
258
+ plugins: [isDev && require.resolve('react-refresh/babel')].filter(Boolean),
197
259
  },
198
260
  },
199
261
  ],
200
262
  },
263
+ {
264
+ test: /\.js$/,
265
+ include: /@babel\/runtime[\\/]+helpers[\\/]esm/,
266
+ resolve: {
267
+ fullySpecified: false,
268
+ },
269
+ },
201
270
  ],
202
271
  },
203
272
  devtool,
204
273
  stats: isDev ? 'normal' : 'errors-only',
274
+ ignoreWarnings: [
275
+ // Disable warning about named imports from JSON files. It's covered by our linter rules.
276
+ /from default-exporting module \(only default export is available soon\)/,
277
+ ],
205
278
  performance: {
206
279
  hints: isDev ? false : 'warning',
207
280
  },
@@ -242,33 +315,18 @@ const config = {
242
315
  } : undefined,
243
316
  },
244
317
  optimization: {
318
+ emitOnErrors: false,
245
319
  usedExports: true,
246
320
  sideEffects: true,
247
- namedModules: true,
248
- namedChunks: true,
321
+ moduleIds: 'deterministic',
322
+ chunkIds: 'deterministic',
249
323
  nodeEnv: ENV,
250
324
  removeAvailableModules: true,
251
- splitChunks: {
252
- cacheGroups: {
253
- commons: {
254
- test: /node_modules/,
255
- name: 'common',
256
- chunks: 'all',
257
- minChunks: 2,
258
- },
259
- },
260
- },
261
325
  minimizer: [
262
326
  new TerserPlugin({
263
- parallel: true,
264
327
  extractComments: false,
265
328
  terserOptions: {
266
329
  ecma: 5,
267
- keep_fnames: false,
268
- mangle: true,
269
- safari10: false,
270
- toplevel: false,
271
- warnings: false,
272
330
  output: {
273
331
  comments: false,
274
332
  },
@@ -286,4 +344,4 @@ const config = {
286
344
  };
287
345
 
288
346
  module.exports = config;
289
- /* eslint-enable camelcase */
347
+
@@ -1,347 +0,0 @@
1
- /* eslint-disable import/extensions, require-jsdoc, no-void, no-param-reassign,
2
- prefer-destructuring, no-underscore-dangle, consistent-return, no-mixed-operators,
3
- eslint-comments/no-unlimited-disable, prefer-rest-params, max-len */
4
- /**
5
- * Replacement of a sub-module of the Swiper library.
6
- * Contains some refactored code inside the elementIsChildOfSlot function to remove optional
7
- * chaining which causes issues with webpack.
8
- */
9
- import { a as getWindow, g as getDocument } from 'swiper/shared/ssr-window.esm.mjs';
10
-
11
- function classesToTokens(classes) {
12
- if (classes === void 0) {
13
- classes = '';
14
- }
15
- return classes.trim().split(' ').filter(c => !!c.trim());
16
- }
17
-
18
- function deleteProps(obj) {
19
- const object = obj;
20
- Object.keys(object).forEach((key) => {
21
- try {
22
- object[key] = null;
23
- } catch (e) {
24
- // no getter for object
25
- }
26
- try {
27
- delete object[key];
28
- } catch (e) {
29
- // something got wrong
30
- }
31
- });
32
- }
33
- function nextTick(callback, delay) {
34
- if (delay === void 0) {
35
- delay = 0;
36
- }
37
- return setTimeout(callback, delay);
38
- }
39
- function now() {
40
- return Date.now();
41
- }
42
- function getComputedStyle(el) {
43
- const window = getWindow();
44
- let style;
45
- if (window.getComputedStyle) {
46
- style = window.getComputedStyle(el, null);
47
- }
48
- if (!style && el.currentStyle) {
49
- style = el.currentStyle;
50
- }
51
- if (!style) {
52
- style = el.style;
53
- }
54
- return style;
55
- }
56
- function getTranslate(el, axis) {
57
- if (axis === void 0) {
58
- axis = 'x';
59
- }
60
- const window = getWindow();
61
- let matrix;
62
- let curTransform;
63
- let transformMatrix;
64
- const curStyle = getComputedStyle(el);
65
- if (window.WebKitCSSMatrix) {
66
- curTransform = curStyle.transform || curStyle.webkitTransform;
67
- if (curTransform.split(',').length > 6) {
68
- curTransform = curTransform.split(', ').map(a => a.replace(',', '.')).join(', ');
69
- }
70
- // Some old versions of Webkit choke when 'none' is passed; pass
71
- // empty string instead in this case
72
- transformMatrix = new window.WebKitCSSMatrix(curTransform === 'none' ? '' : curTransform);
73
- } else {
74
- transformMatrix = curStyle.MozTransform || curStyle.OTransform || curStyle.MsTransform || curStyle.msTransform || curStyle.transform || curStyle.getPropertyValue('transform').replace('translate(', 'matrix(1, 0, 0, 1,');
75
- matrix = transformMatrix.toString().split(',');
76
- }
77
- if (axis === 'x') {
78
- // Latest Chrome and webkits Fix
79
- if (window.WebKitCSSMatrix) curTransform = transformMatrix.m41;
80
- // Crazy IE10 Matrix
81
- else if (matrix.length === 16) curTransform = parseFloat(matrix[12]);
82
- // Normal Browsers
83
- else curTransform = parseFloat(matrix[4]);
84
- }
85
- if (axis === 'y') {
86
- // Latest Chrome and webkits Fix
87
- if (window.WebKitCSSMatrix) curTransform = transformMatrix.m42;
88
- // Crazy IE10 Matrix
89
- else if (matrix.length === 16) curTransform = parseFloat(matrix[13]);
90
- // Normal Browsers
91
- else curTransform = parseFloat(matrix[5]);
92
- }
93
- return curTransform || 0;
94
- }
95
- function isObject(o) {
96
- return typeof o === 'object' && o !== null && o.constructor && Object.prototype.toString.call(o).slice(8, -1) === 'Object';
97
- }
98
- function isNode(node) {
99
- if (typeof window !== 'undefined' && typeof window.HTMLElement !== 'undefined') {
100
- return node instanceof HTMLElement;
101
- }
102
- return node && (node.nodeType === 1 || node.nodeType === 11);
103
- }
104
- function extend() {
105
- const to = Object(arguments.length <= 0 ? undefined : arguments[0]);
106
- const noExtend = ['__proto__', 'constructor', 'prototype'];
107
- for (let i = 1; i < arguments.length; i += 1) {
108
- const nextSource = i < 0 || arguments.length <= i ? undefined : arguments[i];
109
- if (nextSource !== undefined && nextSource !== null && !isNode(nextSource)) {
110
- const keysArray = Object.keys(Object(nextSource)).filter(key => noExtend.indexOf(key) < 0);
111
- for (let nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex += 1) {
112
- const nextKey = keysArray[nextIndex];
113
- const desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
114
- if (desc !== undefined && desc.enumerable) {
115
- if (isObject(to[nextKey]) && isObject(nextSource[nextKey])) {
116
- if (nextSource[nextKey].__swiper__) {
117
- to[nextKey] = nextSource[nextKey];
118
- } else {
119
- extend(to[nextKey], nextSource[nextKey]);
120
- }
121
- } else if (!isObject(to[nextKey]) && isObject(nextSource[nextKey])) {
122
- to[nextKey] = {};
123
- if (nextSource[nextKey].__swiper__) {
124
- to[nextKey] = nextSource[nextKey];
125
- } else {
126
- extend(to[nextKey], nextSource[nextKey]);
127
- }
128
- } else {
129
- to[nextKey] = nextSource[nextKey];
130
- }
131
- }
132
- }
133
- }
134
- }
135
- return to;
136
- }
137
- function setCSSProperty(el, varName, varValue) {
138
- el.style.setProperty(varName, varValue);
139
- }
140
- function animateCSSModeScroll(_ref) {
141
- const {
142
- swiper,
143
- targetPosition,
144
- side,
145
- } = _ref;
146
- const window = getWindow();
147
- const startPosition = -swiper.translate;
148
- let startTime = null;
149
- let time;
150
- const duration = swiper.params.speed;
151
- swiper.wrapperEl.style.scrollSnapType = 'none';
152
- window.cancelAnimationFrame(swiper.cssModeFrameID);
153
- const dir = targetPosition > startPosition ? 'next' : 'prev';
154
- const isOutOfBound = (current, target) => dir === 'next' && current >= target || dir === 'prev' && current <= target;
155
- const animate = () => {
156
- time = new Date().getTime();
157
- if (startTime === null) {
158
- startTime = time;
159
- }
160
- const progress = Math.max(Math.min((time - startTime) / duration, 1), 0);
161
- const easeProgress = 0.5 - Math.cos(progress * Math.PI) / 2;
162
- let currentPosition = startPosition + easeProgress * (targetPosition - startPosition);
163
- if (isOutOfBound(currentPosition, targetPosition)) {
164
- currentPosition = targetPosition;
165
- }
166
- swiper.wrapperEl.scrollTo({
167
- [side]: currentPosition,
168
- });
169
- if (isOutOfBound(currentPosition, targetPosition)) {
170
- swiper.wrapperEl.style.overflow = 'hidden';
171
- swiper.wrapperEl.style.scrollSnapType = '';
172
- setTimeout(() => {
173
- swiper.wrapperEl.style.overflow = '';
174
- swiper.wrapperEl.scrollTo({
175
- [side]: currentPosition,
176
- });
177
- });
178
- window.cancelAnimationFrame(swiper.cssModeFrameID);
179
- return;
180
- }
181
- swiper.cssModeFrameID = window.requestAnimationFrame(animate);
182
- };
183
- animate();
184
- }
185
- function getSlideTransformEl(slideEl) {
186
- return slideEl.querySelector('.swiper-slide-transform') || slideEl.shadowRoot && slideEl.shadowRoot.querySelector('.swiper-slide-transform') || slideEl;
187
- }
188
- function elementChildren(element, selector) {
189
- if (selector === void 0) {
190
- selector = '';
191
- }
192
- const window = getWindow();
193
- const children = [...element.children];
194
- if (window.HTMLSlotElement && element instanceof HTMLSlotElement) {
195
- children.push(...element.assignedElements());
196
- }
197
- if (!selector) {
198
- return children;
199
- }
200
- return children.filter(el => el.matches(selector));
201
- }
202
- function elementIsChildOfSlot(el, slot) {
203
- // Breadth-first search through all parent's children and assigned elements
204
- const elementsQueue = [slot];
205
- while (elementsQueue.length > 0) {
206
- const elementToCheck = elementsQueue.shift();
207
- if (el === elementToCheck) {
208
- return true;
209
- }
210
-
211
- // !!!! Rewrote this code to remove optional chaining syntax
212
- elementsQueue.push(
213
- ...elementToCheck.children,
214
- ...(elementToCheck.shadowRoot && elementToCheck.shadowRoot.children ? elementToCheck.shadowRoot.children : []),
215
- ...(elementToCheck.assignedElements ? elementToCheck.assignedElements() : [])
216
- );
217
- }
218
- }
219
- function elementIsChildOf(el, parent) {
220
- const window = getWindow();
221
- let isChild = parent.contains(el);
222
- if (!isChild && window.HTMLSlotElement && parent instanceof HTMLSlotElement) {
223
- const children = [...parent.assignedElements()];
224
- isChild = children.includes(el);
225
- if (!isChild) {
226
- isChild = elementIsChildOfSlot(el, parent);
227
- }
228
- }
229
- return isChild;
230
- }
231
- function showWarning(text) {
232
- try {
233
- console.warn(text);
234
- } catch (err) {
235
- // err
236
- }
237
- }
238
- function createElement(tag, classes) {
239
- if (classes === void 0) {
240
- classes = [];
241
- }
242
- const el = document.createElement(tag);
243
- el.classList.add(...(Array.isArray(classes) ? classes : classesToTokens(classes)));
244
- return el;
245
- }
246
- function elementOffset(el) {
247
- const window = getWindow();
248
- const document = getDocument();
249
- const box = el.getBoundingClientRect();
250
- const body = document.body;
251
- const clientTop = el.clientTop || body.clientTop || 0;
252
- const clientLeft = el.clientLeft || body.clientLeft || 0;
253
- const scrollTop = el === window ? window.scrollY : el.scrollTop;
254
- const scrollLeft = el === window ? window.scrollX : el.scrollLeft;
255
- return {
256
- top: box.top + scrollTop - clientTop,
257
- left: box.left + scrollLeft - clientLeft,
258
- };
259
- }
260
- function elementPrevAll(el, selector) {
261
- const prevEls = [];
262
- while (el.previousElementSibling) {
263
- const prev = el.previousElementSibling; // eslint-disable-line
264
- if (selector) {
265
- if (prev.matches(selector)) prevEls.push(prev);
266
- } else prevEls.push(prev);
267
- el = prev;
268
- }
269
- return prevEls;
270
- }
271
- function elementNextAll(el, selector) {
272
- const nextEls = [];
273
- while (el.nextElementSibling) {
274
- const next = el.nextElementSibling; // eslint-disable-line
275
- if (selector) {
276
- if (next.matches(selector)) nextEls.push(next);
277
- } else nextEls.push(next);
278
- el = next;
279
- }
280
- return nextEls;
281
- }
282
- function elementStyle(el, prop) {
283
- const window = getWindow();
284
- return window.getComputedStyle(el, null).getPropertyValue(prop);
285
- }
286
- function elementIndex(el) {
287
- let child = el;
288
- let i;
289
- if (child) {
290
- i = 0;
291
- // eslint-disable-next-line
292
- while ((child = child.previousSibling) !== null) {
293
- if (child.nodeType === 1) i += 1;
294
- }
295
- return i;
296
- }
297
- return undefined;
298
- }
299
- function elementParents(el, selector) {
300
- const parents = []; // eslint-disable-line
301
- let parent = el.parentElement; // eslint-disable-line
302
- while (parent) {
303
- if (selector) {
304
- if (parent.matches(selector)) parents.push(parent);
305
- } else {
306
- parents.push(parent);
307
- }
308
- parent = parent.parentElement;
309
- }
310
- return parents;
311
- }
312
- function elementTransitionEnd(el, callback) {
313
- function fireCallBack(e) {
314
- if (e.target !== el) return;
315
- callback.call(el, e);
316
- el.removeEventListener('transitionend', fireCallBack);
317
- }
318
- if (callback) {
319
- el.addEventListener('transitionend', fireCallBack);
320
- }
321
- }
322
- function elementOuterSize(el, size, includeMargins) {
323
- const window = getWindow();
324
- if (includeMargins) {
325
- return el[size === 'width' ? 'offsetWidth' : 'offsetHeight'] + parseFloat(window.getComputedStyle(el, null).getPropertyValue(size === 'width' ? 'margin-right' : 'margin-top')) + parseFloat(window.getComputedStyle(el, null).getPropertyValue(size === 'width' ? 'margin-left' : 'margin-bottom'));
326
- }
327
- return el.offsetWidth;
328
- }
329
- function makeElementsArray(el) {
330
- return (Array.isArray(el) ? el : [el]).filter(e => !!e);
331
- }
332
- function getRotateFix(swiper) {
333
- return (v) => {
334
- if (Math.abs(v) > 0 && swiper.browser && swiper.browser.need3dFix && Math.abs(v) % 90 === 0) {
335
- return v + 0.001;
336
- }
337
- return v;
338
- };
339
- }
340
-
341
- export {
342
- elementParents as a, elementOffset as b, createElement as c, now as d, elementChildren as e, elementOuterSize as f, getSlideTransformEl as g, elementIndex as h, classesToTokens as i, getTranslate as j, elementTransitionEnd as k, isObject as l, makeElementsArray as m, nextTick as n, getRotateFix as o, elementStyle as p, elementNextAll as q, elementPrevAll as r, setCSSProperty as s, animateCSSModeScroll as t, showWarning as u, elementIsChildOf as v, extend as w, deleteProps as x,
343
- };
344
-
345
- /* eslint-enable import/extensions, require-jsdoc, no-void, no-param-reassign,
346
- prefer-destructuring, no-underscore-dangle, consistent-return, no-mixed-operators,
347
- eslint-comments/no-unlimited-disable, prefer-rest-params, max-len */