@henderea/static-site-builder 1.9.5 → 1.9.10

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,26 +1,26 @@
1
- const path = require('path');
2
- const fs = require('fs');
3
- const webpack = require('webpack');
4
- const HtmlWebpackPlugin = require('html-webpack-plugin');
5
- const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
6
- const MiniCssExtractPlugin = require('mini-css-extract-plugin');
7
- const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
8
- const CopyPlugin = require('copy-webpack-plugin');
9
- const { GenerateSW } = require('workbox-webpack-plugin');
10
- const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
11
- const MomentLocalesPlugin = require('moment-locales-webpack-plugin');
12
- const getClientEnvironment = require('./env');
13
- const paths = require('./paths');
14
- const _ = require('lodash');
15
- const crypto = require('crypto');
16
- const globby = require('globby');
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import webpack from 'webpack';
4
+ import HtmlWebpackPlugin from 'html-webpack-plugin';
5
+ import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin';
6
+ import MiniCssExtractPlugin from 'mini-css-extract-plugin';
7
+ import { WebpackManifestPlugin } from 'webpack-manifest-plugin';
8
+ import CopyPlugin from 'copy-webpack-plugin';
9
+ import { GenerateSW } from 'workbox-webpack-plugin';
10
+ import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
11
+ import MomentLocalesPlugin from 'moment-locales-webpack-plugin';
12
+ import getClientEnvironment from './env';
13
+ import * as paths from './paths';
14
+ import _ from 'lodash';
15
+ import crypto from 'crypto';
16
+ import { globbySync } from 'globby';
17
17
 
18
18
  // Webpack uses `publicPath` to determine where the app is being served from.
19
19
  // It requires a trailing slash, or the file assets will get an incorrect path.
20
20
  let publicPath = paths.servedPath;
21
21
  // Some apps do not use client-side routing with pushState.
22
22
  // For these, "homepage" can be set to "." to enable relative asset paths.
23
- const shouldUseRelativeAssetPaths = publicPath === './';
23
+ // const shouldUseRelativeAssetPaths = publicPath === './';
24
24
  // `publicUrl` is just like `publicPath`, but we will provide it to our app
25
25
  // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
26
26
  // Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
@@ -30,339 +30,339 @@ let env = getClientEnvironment(publicUrl);
30
30
 
31
31
  const cssFilename = '[name].css';
32
32
 
33
- const getRevision = file => crypto.createHash('md5').update(fs.readFileSync(file)).digest('hex')
33
+ const getRevision = (file) => crypto.createHash('md5').update(fs.readFileSync(file)).digest('hex');
34
34
 
35
35
  let additionalManifestEntries = undefined;
36
36
 
37
37
  if(fs.existsSync(paths.publicDir)) {
38
- additionalManifestEntries = globby.sync(['**/*', '!asset-manifest.json', '!service-worker.js'], { cwd: paths.publicDir }).map(f => ({ url: `${publicUrl}/${f}`, revision: getRevision(path.join(paths.publicDir, f)) }));
38
+ additionalManifestEntries = globbySync(['**/*', '!asset-manifest.json', '!service-worker.js'], { cwd: paths.publicDir }).map((f) => ({ url: `${publicUrl}/${f}`, revision: getRevision(path.join(paths.publicDir, f)) }));
39
39
  }
40
40
 
41
41
  let ssbConfig = {};
42
42
 
43
43
  if(fs.existsSync(paths.ssbConfig)) {
44
- let ssbConfigObj = require(paths.ssbConfig);
45
- if(ssbConfigObj) {
46
- if(_.isFunction(ssbConfigObj)) {
47
- ssbConfig = ssbConfigObj(env.raw, 'production', { publicUrl, ...paths });
48
- } else if(_.isPlainObject(ssbConfigObj)) {
49
- if(_.has(ssbConfigObj, 'prod')) {
50
- ssbConfig = _.get(ssbConfigObj, 'prod');
51
- } else if(_.has(ssbConfigObj, 'production')) {
52
- ssbConfig = _.get(ssbConfigObj, 'production');
53
- } else {
54
- ssbConfig = ssbConfigObj;
55
- }
56
- }
44
+ let ssbConfigObj = require(paths.ssbConfig);
45
+ if(ssbConfigObj) {
46
+ if(_.isFunction(ssbConfigObj)) {
47
+ ssbConfig = ssbConfigObj(env.raw, 'production', { publicUrl, ...paths });
48
+ } else if(_.isPlainObject(ssbConfigObj)) {
49
+ if(_.has(ssbConfigObj, 'prod')) {
50
+ ssbConfig = _.get(ssbConfigObj, 'prod');
51
+ } else if(_.has(ssbConfigObj, 'production')) {
52
+ ssbConfig = _.get(ssbConfigObj, 'production');
53
+ } else {
54
+ ssbConfig = ssbConfigObj;
55
+ }
57
56
  }
57
+ }
58
58
  }
59
59
 
60
60
  ssbConfig = ssbConfig || {};
61
61
 
62
62
  if(ssbConfig.env && _.isPlainObject(ssbConfig.env)) {
63
- const raw = _.extend({}, env.raw, ssbConfig.env);
64
- const stringified = {
65
- 'process.env': Object.keys(raw).reduce((env, key) => {
66
- env[key] = JSON.stringify(raw[key]);
67
- return env;
68
- }, {}),
69
- };
70
- env = { raw, stringified };
63
+ const raw = _.extend({}, env.raw, ssbConfig.env);
64
+ const stringified = {
65
+ 'process.env': Object.keys(raw).reduce((env, key) => {
66
+ env[key] = JSON.stringify(raw[key]);
67
+ return env;
68
+ }, {}),
69
+ };
70
+ env = { raw, stringified };
71
71
  }
72
72
 
73
73
  if(ssbConfig.additionalManifestEntries && _.isArray(ssbConfig.additionalManifestEntries)) {
74
- additionalManifestEntries.push(...ssbConfig.additionalManifestEntries);
74
+ additionalManifestEntries.push(...ssbConfig.additionalManifestEntries);
75
75
  }
76
76
 
77
77
  let runtimeCaching = require('./cache-config');
78
78
 
79
79
  if(ssbConfig.runtimeCaching && _.isArray(ssbConfig.runtimeCaching)) {
80
- runtimeCaching = ssbConfig.runtimeCaching;
80
+ runtimeCaching = ssbConfig.runtimeCaching;
81
81
  }
82
82
 
83
83
  runtimeCaching.unshift({
84
- urlPattern: publicPath,
85
- handler: 'NetworkFirst',
86
- options: {
87
- cacheName: 'start-url',
88
- expiration: {
89
- maxEntries: 1,
90
- maxAgeSeconds: 24 * 60 * 60 // 24 hours
91
- }
84
+ urlPattern: publicPath,
85
+ handler: 'NetworkFirst',
86
+ options: {
87
+ cacheName: 'start-url',
88
+ expiration: {
89
+ maxEntries: 1,
90
+ maxAgeSeconds: 24 * 60 * 60 // 24 hours
92
91
  }
92
+ }
93
93
  });
94
94
 
95
95
  let htmlWebpackPluginOptions = {
96
- filename: 'index.html',
97
- template: paths.appTemplate,
98
- inject: 'head',
99
- minify: { collapseWhitespace: true }
96
+ filename: 'index.html',
97
+ template: paths.appTemplate,
98
+ inject: 'head',
99
+ minify: { collapseWhitespace: true }
100
100
  };
101
101
 
102
102
  if(ssbConfig.htmlWebpackPluginOptions && _.isPlainObject(ssbConfig.htmlWebpackPluginOptions)) {
103
- htmlWebpackPluginOptions = _.extend({}, htmlWebpackPluginOptions, ssbConfig.htmlWebpackPluginOptions);
103
+ htmlWebpackPluginOptions = _.extend({}, htmlWebpackPluginOptions, ssbConfig.htmlWebpackPluginOptions);
104
104
  }
105
105
 
106
106
  const plugins = [
107
- new webpack.DefinePlugin(env.stringified),
108
- new HtmlWebpackPlugin(htmlWebpackPluginOptions),
109
- new CaseSensitivePathsPlugin(),
110
- new MiniCssExtractPlugin({
111
- filename: cssFilename
112
- }),
113
- new WebpackManifestPlugin({
114
- fileName: 'asset-manifest.json',
115
- publicPath,
116
- filter(file) {
117
- if(/^[/]?[.]{2}[/]?/.test(file.path)) { return false; }
118
- if(/\.ts$/.test(file.path)) { return false; }
119
- if(/(^|[/])\./.test(file.name)) { return false; }
120
- return true;
121
- }
122
- }),
123
- new GenerateSW({
124
- // Don't precache sourcemaps (they're large) and build asset manifest:
125
- exclude: [/\.map$/, /asset-manifest\.json$/, /^[/]?[.]{2}/, /.ts$/],
126
- // `navigateFallback` and `navigateFallbackWhitelist` are disabled by default; see
127
- // https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#service-worker-considerations
128
- navigateFallback: publicUrl + '/index.html',
129
- navigateFallbackDenylist: [/^\/_/],
130
- additionalManifestEntries,
131
- cleanupOutdatedCaches: true,
132
- clientsClaim: true,
133
- skipWaiting: true,
134
- runtimeCaching,
135
- }),
107
+ new webpack.DefinePlugin(env.stringified),
108
+ new HtmlWebpackPlugin(htmlWebpackPluginOptions),
109
+ new CaseSensitivePathsPlugin(),
110
+ new MiniCssExtractPlugin({
111
+ filename: cssFilename
112
+ }),
113
+ new WebpackManifestPlugin({
114
+ fileName: 'asset-manifest.json',
115
+ publicPath,
116
+ filter(file) {
117
+ if(/^[/]?[.]{2}[/]?/.test(file.path)) { return false; }
118
+ if(/\.ts$/.test(file.path)) { return false; }
119
+ if(/(^|[/])\./.test(file.name)) { return false; }
120
+ return true;
121
+ }
122
+ }),
123
+ new GenerateSW({
124
+ // Don't precache sourcemaps (they're large) and build asset manifest:
125
+ exclude: [/\.map$/, /asset-manifest\.json$/, /^[/]?[.]{2}/, /.ts$/],
126
+ // `navigateFallback` and `navigateFallbackWhitelist` are disabled by default; see
127
+ // https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#service-worker-considerations
128
+ navigateFallback: publicUrl + '/index.html',
129
+ navigateFallbackDenylist: [/^\/_/],
130
+ additionalManifestEntries,
131
+ cleanupOutdatedCaches: true,
132
+ clientsClaim: true,
133
+ skipWaiting: true,
134
+ runtimeCaching,
135
+ }),
136
136
  ];
137
137
 
138
138
  if(ssbConfig.plugins && _.isArray(ssbConfig.plugins)) {
139
- plugins.push(...ssbConfig.plugins);
139
+ plugins.push(...ssbConfig.plugins);
140
140
  }
141
141
 
142
142
  const copyPatterns = [];
143
143
 
144
144
  if(fs.existsSync(paths.publicDir)) {
145
- copyPatterns.push({
146
- from: paths.publicDir,
147
- to: paths.appDist
148
- });
145
+ copyPatterns.push({
146
+ from: paths.publicDir,
147
+ to: paths.appDist
148
+ });
149
149
  }
150
150
 
151
151
  if(ssbConfig.copyPatterns && _.isArray(ssbConfig.copyPatterns)) {
152
- copyPatterns.push(...ssbConfig.copyPatterns);
152
+ copyPatterns.push(...ssbConfig.copyPatterns);
153
153
  }
154
154
 
155
155
  if(copyPatterns.length > 0) {
156
- plugins.push(new CopyPlugin({
157
- patterns: copyPatterns
158
- }));
156
+ plugins.push(new CopyPlugin({
157
+ patterns: copyPatterns
158
+ }));
159
159
  }
160
160
 
161
161
  let tsConfigPath = paths.tsConfig;
162
162
 
163
163
  if(ssbConfig.tsConfigPath && fs.existsSync(paths.resolveApp(ssbConfig.tsConfigPath))) {
164
- tsConfigPath = paths.resolveApp(ssbConfig.tsConfigPath);
164
+ tsConfigPath = paths.resolveApp(ssbConfig.tsConfigPath);
165
165
  }
166
166
 
167
167
  const resolvePlugins = [];
168
168
 
169
169
  if(fs.existsSync(tsConfigPath)) {
170
- resolvePlugins.push(new TsconfigPathsPlugin({ configFile: tsConfigPath }));
170
+ resolvePlugins.push(new TsconfigPathsPlugin({ configFile: tsConfigPath }));
171
171
  }
172
172
 
173
173
  let appIndex = paths.appIndex;
174
174
 
175
175
  if(ssbConfig.appIndex && fs.existsSync(paths.resolveApp(appIndex))) {
176
- appIndex = paths.resolveApp(appIndex);
176
+ appIndex = paths.resolveApp(appIndex);
177
177
  }
178
178
 
179
179
  const packageJson = require(paths.appPackageJson);
180
180
  const config = _.extend({}, packageJson.staticSiteBuilderConfig || {}, ssbConfig);
181
181
  const rawMomentLocales = config && config.momentLocales;
182
182
  if(rawMomentLocales === '') {
183
- plugins.push(new MomentLocalesPlugin());
183
+ plugins.push(new MomentLocalesPlugin());
184
184
  } else if(rawMomentLocales) {
185
- plugins.push(new MomentLocalesPlugin({ localesToKeep: rawMomentLocales.split(/\s*,\s*/g) }));
185
+ plugins.push(new MomentLocalesPlugin({ localesToKeep: rawMomentLocales.split(/\s*,\s*/g) }));
186
186
  }
187
187
 
188
188
  const performance = {};
189
189
 
190
190
  if(config && (config.sizeHints === false || config.sizeHints == 'warning' || config.sizeHints == 'error')) {
191
- performance.hints = config.sizeHints;
191
+ performance.hints = config.sizeHints;
192
192
  }
193
193
 
194
- const getSizeValue = val => {
195
- if(_.isNil(val) || val === false) { return null; }
196
- if(_.isNumber(val) && !_.isNaN(val)) { return Math.round(val); }
197
- if(_.isString(val)) {
198
- let m = val.match(/^(\d+(?:\.\d+)?|\.\d+)([bkmg])?$/i);
199
- if(m) {
200
- let num = parseFloat(m[1]);
201
- if(!_.isNaN(num)) {
202
- let unit = m[2];
203
- if(!unit || unit === '') { unit = 'b'; }
204
- unit = unit.toLowerCase();
205
- let unitIndex = 'bkmg'.indexOf(unit);
206
- if(unitIndex >= 0 || unitIndex <= 3) {
207
- return Math.round(num * Math.pow(1024, unitIndex));
208
- }
209
- }
194
+ const getSizeValue = (val) => {
195
+ if(_.isNil(val) || val === false) { return null; }
196
+ if(_.isNumber(val) && !_.isNaN(val)) { return Math.round(val); }
197
+ if(_.isString(val)) {
198
+ let m = val.match(/^(\d+(?:\.\d+)?|\.\d+)([bkmg])?$/i);
199
+ if(m) {
200
+ let num = parseFloat(m[1]);
201
+ if(!_.isNaN(num)) {
202
+ let unit = m[2];
203
+ if(!unit || unit === '') { unit = 'b'; }
204
+ unit = unit.toLowerCase();
205
+ let unitIndex = 'bkmg'.indexOf(unit);
206
+ if(unitIndex >= 0 || unitIndex <= 3) {
207
+ return Math.round(num * Math.pow(1024, unitIndex));
210
208
  }
209
+ }
211
210
  }
212
- return null;
213
- }
211
+ }
212
+ return null;
213
+ };
214
214
 
215
215
  let maxEntrypointSize = getSizeValue(config && config.maxEntrypointSize);
216
216
 
217
217
  if(config && maxEntrypointSize) {
218
- performance.maxEntrypointSize = maxEntrypointSize;
218
+ performance.maxEntrypointSize = maxEntrypointSize;
219
219
  }
220
220
 
221
221
  let maxAssetSize = getSizeValue(config && config.maxAssetSize);
222
222
 
223
223
  if(config && maxAssetSize) {
224
- performance.maxAssetSize = maxAssetSize;
224
+ performance.maxAssetSize = maxAssetSize;
225
225
  }
226
226
 
227
227
  const extraLoaders = [];
228
228
 
229
229
  if(ssbConfig.extraLoaders && _.isArray(ssbConfig.extraLoaders)) {
230
- extraLoaders.push(...ssbConfig.extraLoaders);
230
+ extraLoaders.push(...ssbConfig.extraLoaders);
231
231
  }
232
232
 
233
233
  module.exports = _.defaultsDeep({}, ssbConfig.webpack || {}, {
234
- mode: 'production',
235
- entry: {
236
- index: appIndex
237
- },
238
- devtool: 'source-map',
239
- output: {
240
- pathinfo: true,
241
- path: paths.appDist,
242
- publicPath: publicPath
243
- },
244
- resolve: {
245
- modules: ['node_modules'].concat(
246
- // It is guaranteed to exist because we tweak it in `env.js`
247
- process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
248
- ),
249
- extensions: ['.js', '.ts', '.json'],
250
- plugins: resolvePlugins,
251
- roots: [paths.appPath, paths.publicDir],
252
- },
253
- module: {
254
- strictExportPresence: true,
255
- rules: [
256
- {
257
- test: /\.[tj]s$/,
258
- parser: { requireEnsure: false }
259
- },
260
- {
261
- oneOf: [
262
- ...extraLoaders,
263
- {
264
- test: /\.ts$/,
265
- exclude: [/[/\\\\]node_modules[/\\\\]/],
266
- use: [
267
- {
268
- loader: require.resolve('ts-loader'),
269
- options: {
270
- configFile: tsConfigPath
271
- },
272
- },
273
- ]
274
- },
275
- {
276
- test: /\.js$/,
277
- exclude: [/[/\\\\]node_modules[/\\\\]/],
278
- use: [
279
- require.resolve('thread-loader'),
280
- {
281
- loader: require.resolve('babel-loader'),
282
- options: {
283
- babelrc: false,
284
- compact: true,
285
- highlightCode: true,
286
- },
287
- },
288
- ]
289
- },
290
- {
291
- test: /\.js$/,
292
- use: [
293
- require.resolve('thread-loader'),
294
- {
295
- loader: require.resolve('babel-loader'),
296
- options: {
297
- babelrc: false,
298
- compact: false,
299
- // This is a feature of `babel-loader` for webpack (not Babel itself).
300
- // It enables caching results in ./node_modules/.cache/babel-loader/
301
- // directory for faster rebuilds.
302
- cacheDirectory: true,
303
- highlightCode: true,
304
- },
305
- },
306
- ]
307
- },
308
- {
309
- test: /\.css$/,
310
- use: [
311
- MiniCssExtractPlugin.loader,
312
- {
313
- loader: 'css-loader',
314
- options: {
315
- sourceMap: true,
316
- }
317
- }
318
- ]
319
- },
320
- {
321
- test: /\.scss$/,
322
- use: [
323
- MiniCssExtractPlugin.loader,
324
- {
325
- loader: 'css-loader',
326
- options: {
327
- importLoaders: 1,
328
- sourceMap: true,
329
- }
330
- },
331
- {
332
- loader: 'postcss-loader',
333
- options: {
334
- postcssOptions: {
335
- plugins: ['postcss-preset-env']
336
- },
337
- sourceMap: true
338
- }
339
- },
340
- {
341
- loader: 'sass-loader',
342
- options: {
343
- sassOptions: {
344
- outputStyle: 'compressed'
345
- },
346
- sourceMap: true
347
- }
348
- }
349
- ]
350
- },
351
- {
352
- loader: require.resolve('file-loader'),
353
- // Exclude `js` files to keep "css" loader working as it injects
354
- // its runtime that would otherwise processed through "file" loader.
355
- // Also exclude `html` and `json` extensions so they get processed
356
- // by webpack's internal loaders.
357
- exclude: [/\.js$/, /\.ts$/, /\.html$/, /\.ejs$/, /\.hbs$/, /\.json$/],
358
- options: {
359
- name: '[name].[ext]'
360
- }
361
- }
362
- ]
234
+ mode: 'production',
235
+ entry: {
236
+ index: appIndex
237
+ },
238
+ devtool: 'source-map',
239
+ output: {
240
+ pathinfo: true,
241
+ path: paths.appDist,
242
+ publicPath: publicPath
243
+ },
244
+ resolve: {
245
+ modules: ['node_modules'].concat(
246
+ // It is guaranteed to exist because we tweak it in `env.js`
247
+ process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
248
+ ),
249
+ extensions: ['.js', '.ts', '.json'],
250
+ plugins: resolvePlugins,
251
+ roots: [paths.appPath, paths.publicDir],
252
+ },
253
+ module: {
254
+ strictExportPresence: true,
255
+ rules: [
256
+ {
257
+ test: /\.[tj]s$/,
258
+ parser: { requireEnsure: false }
259
+ },
260
+ {
261
+ oneOf: [
262
+ ...extraLoaders,
263
+ {
264
+ test: /\.ts$/,
265
+ exclude: [/[/\\\\]node_modules[/\\\\]/],
266
+ use: [
267
+ {
268
+ loader: require.resolve('ts-loader'),
269
+ options: {
270
+ configFile: tsConfigPath
271
+ },
272
+ },
273
+ ]
274
+ },
275
+ {
276
+ test: /\.js$/,
277
+ exclude: [/[/\\\\]node_modules[/\\\\]/],
278
+ use: [
279
+ require.resolve('thread-loader'),
280
+ {
281
+ loader: require.resolve('babel-loader'),
282
+ options: {
283
+ babelrc: false,
284
+ compact: true,
285
+ highlightCode: true,
286
+ },
287
+ },
288
+ ]
289
+ },
290
+ {
291
+ test: /\.js$/,
292
+ use: [
293
+ require.resolve('thread-loader'),
294
+ {
295
+ loader: require.resolve('babel-loader'),
296
+ options: {
297
+ babelrc: false,
298
+ compact: false,
299
+ // This is a feature of `babel-loader` for webpack (not Babel itself).
300
+ // It enables caching results in ./node_modules/.cache/babel-loader/
301
+ // directory for faster rebuilds.
302
+ cacheDirectory: true,
303
+ highlightCode: true,
304
+ },
305
+ },
306
+ ]
307
+ },
308
+ {
309
+ test: /\.css$/,
310
+ use: [
311
+ MiniCssExtractPlugin.loader,
312
+ {
313
+ loader: 'css-loader',
314
+ options: {
315
+ sourceMap: true,
316
+ }
317
+ }
318
+ ]
319
+ },
320
+ {
321
+ test: /\.scss$/,
322
+ use: [
323
+ MiniCssExtractPlugin.loader,
324
+ {
325
+ loader: 'css-loader',
326
+ options: {
327
+ importLoaders: 1,
328
+ sourceMap: true,
329
+ }
330
+ },
331
+ {
332
+ loader: 'postcss-loader',
333
+ options: {
334
+ postcssOptions: {
335
+ plugins: ['postcss-preset-env']
336
+ },
337
+ sourceMap: true
338
+ }
339
+ },
340
+ {
341
+ loader: 'sass-loader',
342
+ options: {
343
+ sassOptions: {
344
+ outputStyle: 'compressed'
345
+ },
346
+ sourceMap: true
347
+ }
348
+ }
349
+ ]
350
+ },
351
+ {
352
+ loader: require.resolve('file-loader'),
353
+ // Exclude `js` files to keep "css" loader working as it injects
354
+ // its runtime that would otherwise processed through "file" loader.
355
+ // Also exclude `html` and `json` extensions so they get processed
356
+ // by webpack's internal loaders.
357
+ exclude: [/\.js$/, /\.ts$/, /\.html$/, /\.ejs$/, /\.hbs$/, /\.json$/],
358
+ options: {
359
+ name: '[name].[ext]'
363
360
  }
361
+ }
364
362
  ]
365
- },
366
- plugins,
367
- performance
368
- });
363
+ }
364
+ ]
365
+ },
366
+ plugins,
367
+ performance
368
+ });