@oroinc/oro-webpack-config-builder 5.1.0-alpha33 → 5.1.0-alpha34

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.
@@ -0,0 +1,95 @@
1
+ const BaseError = require('./validation/errors/base-error');
2
+ const {isVerboseMode} = require('./utils');
3
+ const {red, yellow, green, bgRed} = require('colorette');
4
+
5
+ const emptyLine = length => new Array(length).fill(' ').join('');
6
+ const multiline = (color, msg) => {
7
+ msg = ` ${msg}`;
8
+ msg = msg + emptyLine(120 - msg.length);
9
+
10
+ return `\n${color(emptyLine(msg.length))}\n${color(msg)}\n${color(emptyLine(msg.length))}`;
11
+ };
12
+
13
+ class ErrorHandler {
14
+ constructor() {
15
+ this.failedThemes = [];
16
+
17
+ process.on('beforeExit', code => {
18
+ this.onProcess();
19
+ });
20
+ }
21
+
22
+ /**
23
+ * Show a message about failed themes
24
+ */
25
+ onProcess() {
26
+ if (this.failedThemes.length) {
27
+ let msg = '[ERROR] Assets build';
28
+
29
+ if (this.failedThemes.length === 1) {
30
+ msg = `${msg} for "${this.failedThemes[0]}" theme failed.`;
31
+ } else {
32
+ msg = `${msg} for "${this.failedThemes.join(', ')}" themes failed.`;
33
+ }
34
+ console.error(multiline(bgRed, msg));
35
+ process.exit(1);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Executes when the Webpack Compiler will complete a compilation
41
+ * In case of empty Webpack Config this method will not run
42
+ *
43
+ * @param {Object} stats
44
+ */
45
+ onBuildComplete(stats) {
46
+ const failed = stats?.compilation?.errors.length !== 0;
47
+
48
+ if (failed) {
49
+ console.error(multiline(bgRed, '[ERROR] Assets build failed.'));
50
+ process.exit(1);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * @param {Array|string} errors
56
+ * @param {string} [failedTheme]
57
+ */
58
+ displayError(errors, failedTheme) {
59
+ if (typeof failedTheme === 'string') {
60
+ this.failedThemes.push(failedTheme);
61
+ }
62
+
63
+ if (typeof errors === 'string') {
64
+ errors = [errors];
65
+ }
66
+
67
+ const errorMessage = errors.map(err => {
68
+ const highlightPrefix = str => {
69
+ const prefix = str.substring(0, str.search(':'));
70
+ return red(prefix) + str.substring(str.search(':'));
71
+ };
72
+
73
+ if (err instanceof BaseError) {
74
+ err.message = `${err.message}\n${green(err.extra)}`;
75
+ }
76
+
77
+ let msg = err.message;
78
+
79
+ if (isVerboseMode()) {
80
+ msg = highlightPrefix(err.stack);
81
+ } else {
82
+ const command = yellow('php bin/console oro:assets:build --env=dev --verbose');
83
+ const info = `${green(`Run the command ${command} to see more information`)}`;
84
+
85
+ msg = highlightPrefix(`${err.name}: ${msg}\n${info}`);
86
+ }
87
+
88
+ return `${msg}\n`;
89
+ });
90
+
91
+ console.error(errorMessage.join('\n'));
92
+ }
93
+ }
94
+
95
+ module.exports = ErrorHandler;
@@ -8,13 +8,17 @@ class LayoutModulesConfigLoader extends ModulesConfigLoader {
8
8
  */
9
9
  loadConfig(theme, filePath) {
10
10
  let themeConfig = super.loadConfig(theme, path.join('Resources/views/layouts/', theme, filePath));
11
+
11
12
  // recursive process parent theme
12
13
  const {parent: parentTheme} = this.themes[theme];
13
14
  if (typeof parentTheme === 'string') {
15
+ const processedFiles = this.processedFiles;
14
16
  const parentThemeConfig = this.loadConfig(parentTheme, filePath);
17
+
15
18
  themeConfig = merge(parentThemeConfig, themeConfig);
19
+ // processedFiles from parent theme is added to processedFiles of current theme
20
+ this._processedFiles = [...processedFiles, ...this.processedFiles];
16
21
  }
17
-
18
22
  return themeConfig;
19
23
  }
20
24
 
@@ -40,7 +44,7 @@ class LayoutModulesConfigLoader extends ModulesConfigLoader {
40
44
  * @return {string[]}
41
45
  */
42
46
  extraJSBuildNames(theme) {
43
- const {extra_js_builds: extraJSBuilds = []} = this._themes[theme] || {}
47
+ const {extra_js_builds: extraJSBuilds = []} = this._themes[theme] || {};
44
48
  return [...extraJSBuilds.map(suffix => `${theme}-${suffix}`)];
45
49
  }
46
50
 
@@ -66,7 +70,7 @@ class LayoutModulesConfigLoader extends ModulesConfigLoader {
66
70
  const result = [];
67
71
  if (marches) {
68
72
  const [, theme, suffix] = marches;
69
- const {extra_js_builds: extraJSBuilds = []} = this._themes[theme] || {}
73
+ const {extra_js_builds: extraJSBuilds = []} = this._themes[theme] || {};
70
74
  if (extraJSBuilds.includes(suffix)) {
71
75
  result.push(marches[1], marches[2]);
72
76
  } else {
@@ -2,6 +2,7 @@ const path = require('path');
2
2
  const fs = require('fs');
3
3
  const merge = require('deepmerge');
4
4
  const yaml = require('js-yaml');
5
+ const validation = require('../validation');
5
6
 
6
7
  // merge only unique items
7
8
  const arrayMerge = (target, source) => target.concat(source.filter(item => !target.includes(item)));
@@ -28,6 +29,13 @@ class ModulesConfigLoader {
28
29
  return this.themeNames;
29
30
  }
30
31
 
32
+ /**
33
+ * @returns {Array}
34
+ */
35
+ get processedFiles() {
36
+ return [...this._processedFiles];
37
+ }
38
+
31
39
  /**
32
40
  * @param {Array} bundles Array of ordered symfony bundle paths
33
41
  * @param {string} themesLocation Path inside the bundle, where to find the theme
@@ -36,6 +44,7 @@ class ModulesConfigLoader {
36
44
  constructor(bundles, themesLocation, themeInfoFileName) {
37
45
  this._bundles = bundles;
38
46
  this._themes = this._getThemes(themesLocation, themeInfoFileName);
47
+ this._processedFiles = [];
39
48
  }
40
49
 
41
50
  /**
@@ -76,6 +85,8 @@ class ModulesConfigLoader {
76
85
  loadConfig(theme, filePath) {
77
86
  let configs = {};
78
87
  const filePaths = [].concat(filePath);
88
+
89
+ this._processedFiles = [];
79
90
  this._bundles.forEach(bundle => {
80
91
  let absolutePath;
81
92
 
@@ -88,10 +99,16 @@ class ModulesConfigLoader {
88
99
  }
89
100
 
90
101
  if (absolutePath) {
91
- const doc = yaml.load(fs.readFileSync(absolutePath, 'utf8'));
102
+ const rawDoc = fs.readFileSync(absolutePath, 'utf8');
103
+ const doc = yaml.load(rawDoc);
104
+
105
+ this._processedFiles.push(absolutePath);
106
+
107
+ validation.checkSchema(absolutePath, doc, theme);
92
108
  configs = merge(configs, doc);
93
109
  }
94
110
  });
111
+
95
112
  return configs;
96
113
  }
97
114
  }
@@ -3,6 +3,7 @@ const printf = require('printf');
3
3
  const AppConfigLoader = require('./app-config-loader');
4
4
  const AppModulesFileWriter = require('./writer/app-modules-file-writer');
5
5
  const CleanupStatsPlugin = require('./plugin/stats/cleanup-stats-plugin');
6
+ const AfterWebpackLogsPlugin = require('./plugin/logs/after-webpack-logs-plugin');
6
7
  const ConfigsFileWriter = require('./writer/configs-file-writer');
7
8
  const EntryPointFileWriter = require('./writer/scss-entry-point-file-writer');
8
9
  const LayoutModulesConfigLoader = require('./modules-config/layout-modules-config-loader');
@@ -23,33 +24,44 @@ const resolve = require('enhanced-resolve');
23
24
  const {merge: webpackMerge} = require('webpack-merge');
24
25
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
25
26
  const RtlCssWebpackPlugin = require('rtlcss-webpack-plugin');
26
- const {red: colorRed} = require('colorette');
27
+ const validation = require('./validation');
28
+ const EventEmitter = require('events');
29
+ const ErrorHandler = require('./error-handler');
30
+
27
31
  require('resolve-url-loader');
28
32
 
29
33
  class ConfigBuilder {
30
34
  constructor() {
31
- this._publicPath = 'public/';
32
- this._adminTheme = 'admin.oro';
33
35
  this._enableLayoutThemes = false;
34
36
  this._defaultLayoutThemes = null;
35
- this._versionFormat = '%s?v=%s';
36
- this._babelConfig = {
37
+ this.emitter = new EventEmitter();
38
+ this.errorHandler = new ErrorHandler();
39
+ this.addListeners();
40
+ this.setPublicPath('public/');
41
+ this.setAdminTheme('admin.oro');
42
+ this.setVersionFormat('%s?v=%s');
43
+ this.setBabelConfig({
37
44
  sourceType: 'unambiguous',
38
45
  presets: [
39
46
  [
40
47
  '@babel/preset-env', {
41
- useBuiltIns: 'usage',
42
- corejs: {
43
- version: 3,
44
- proposals: true
48
+ useBuiltIns: 'usage',
49
+ corejs: {
50
+ version: 3,
51
+ proposals: true
52
+ }
45
53
  }
46
- }
47
54
  ]
48
55
  ],
49
56
  plugins: [
50
57
  '@babel/plugin-transform-runtime'
51
58
  ]
52
- };
59
+ });
60
+ }
61
+
62
+ addListeners() {
63
+ this.emitter.on('publicPath:updated', path => validation.setPublicPath(path));
64
+ this.emitter.on('build:complete', stats => this.errorHandler.onBuildComplete(stats));
53
65
  }
54
66
 
55
67
  /**
@@ -59,6 +71,7 @@ class ConfigBuilder {
59
71
  */
60
72
  setPublicPath(publicPath) {
61
73
  this._publicPath = publicPath;
74
+ this.emitter.emit('publicPath:updated', publicPath);
62
75
  return this;
63
76
  }
64
77
 
@@ -80,6 +93,7 @@ class ConfigBuilder {
80
93
  */
81
94
  setVersionFormat(versionFormat) {
82
95
  this._versionFormat = versionFormat;
96
+ this.emitter.emit('versionFormat:updated', versionFormat);
83
97
  return this;
84
98
  }
85
99
 
@@ -96,6 +110,7 @@ class ConfigBuilder {
96
110
  }
97
111
 
98
112
  this._adminTheme = adminTheme;
113
+ this.emitter.emit('adminTheme:updated', adminTheme);
99
114
  return this;
100
115
  }
101
116
 
@@ -105,8 +120,10 @@ class ConfigBuilder {
105
120
  enableLayoutThemes(themes) {
106
121
  if (themes) {
107
122
  this._defaultLayoutThemes = themes;
123
+ this.emitter.emit('defaultLayoutThemes:updated', themes);
108
124
  }
109
125
  this._enableLayoutThemes = true;
126
+ this.emitter.emit('enableLayoutThemes:updated', true);
110
127
  return this;
111
128
  }
112
129
 
@@ -117,6 +134,7 @@ class ConfigBuilder {
117
134
  */
118
135
  setBabelConfig(babelConfig) {
119
136
  this._babelConfig = babelConfig;
137
+ this.emitter.emit('babelConfig:updated', babelConfig);
120
138
  return this;
121
139
  }
122
140
 
@@ -132,19 +150,30 @@ class ConfigBuilder {
132
150
  try {
133
151
  commonConfig = this._getCommonWebpackConfig(args, env);
134
152
  } catch (e) {
135
- console.error(colorRed(`Error: ${e.message}`));
153
+ this.errorHandler.displayError(e);
136
154
  process.exit(1);
137
155
  }
138
156
 
139
157
  const webpackConfigs = [];
140
158
  const requestedBuildNames = env.theme ? env.theme.split(',') : [];
141
159
  const buildNames = this._getBuildNames(requestedBuildNames);
160
+
161
+ const buildErrors = [];
162
+ validation.jsmodulesValidator.emitter.on('error', error => buildErrors.push(error));
163
+ validation.assetsValidation.emitter.on('error', error => buildErrors.push(error));
142
164
  buildNames.forEach(buildName => {
143
165
  let buildConfig;
166
+ // flush all collected errors from previews builds
167
+ buildErrors.splice(0, buildErrors.length);
168
+
144
169
  try {
145
170
  buildConfig = this._getThemeWebpackConfig(buildName, args, env);
146
- } catch (e) {
147
- console.error(colorRed(`Error: ${e.message}`));
171
+ } catch (error) {
172
+ buildErrors.push(error);
173
+ }
174
+
175
+ if (buildErrors.length) {
176
+ this.errorHandler.displayError(buildErrors, buildName);
148
177
  return;
149
178
  }
150
179
 
@@ -331,7 +360,10 @@ class ConfigBuilder {
331
360
  }),
332
361
  new webpack.optimize.MinChunkSizePlugin({
333
362
  minChunkSize: 30000 // Minimum number of characters
334
- })
363
+ }),
364
+ new AfterWebpackLogsPlugin(
365
+ stats => this.emitter.emit('build:complete', stats)
366
+ )
335
367
  ]
336
368
  };
337
369
 
@@ -403,6 +435,11 @@ class ConfigBuilder {
403
435
  buildPublicPath = '/build/admin/';
404
436
  const jsModulesConfig = this._themeConfigFactory.loadConfig(buildName,
405
437
  ['Resources/config/oro/jsmodules.yml', 'Resources/config/jsmodules.yml']);
438
+ validation.jsmodulesValidator.checkFullSchema(
439
+ jsModulesConfig,
440
+ this._themeConfigFactory?._configLoader.processedFiles,
441
+ buildName
442
+ );
406
443
  jsBuildConfig = this._themeConfigFactory.create(buildPublicPath, jsModulesConfig);
407
444
  } else if (this._layoutModulesConfigLoader.isExtraJSBuild(buildName)) {
408
445
  const [theme, suffix] = this._layoutModulesConfigLoader.splitBuildName(buildName);
@@ -412,11 +449,21 @@ class ConfigBuilder {
412
449
  const baseConfig = this._layoutThemeConfigFactory.loadConfig(theme, 'config/jsmodules.yml');
413
450
  const extraConfig = this._layoutThemeConfigFactory.loadConfig(theme, `config/jsmodules-${suffix}.yml`);
414
451
  const jsModulesConfig = this._layoutThemeConfigFactory.extendConfig(baseConfig, extraConfig);
452
+ validation.jsmodulesValidator.checkFullSchema(
453
+ jsModulesConfig,
454
+ this._layoutThemeConfigFactory?._configLoader.processedFiles,
455
+ buildName
456
+ );
415
457
  jsBuildConfig = this._layoutThemeConfigFactory.create(buildPublicPath, jsModulesConfig);
416
458
  } else {
417
459
  themeDefinition = this._layoutModulesConfigLoader.themes[buildName];
418
460
  buildPublicPath = `/build/${buildName}/`;
419
461
  const jsModulesConfig = this._layoutThemeConfigFactory.loadConfig(buildName, 'config/jsmodules.yml');
462
+ validation.jsmodulesValidator.checkFullSchema(
463
+ jsModulesConfig,
464
+ this._layoutThemeConfigFactory?._configLoader.processedFiles,
465
+ buildName
466
+ );
420
467
  jsBuildConfig = this._layoutThemeConfigFactory.create(buildPublicPath, jsModulesConfig);
421
468
  }
422
469
  const {rtl_support: rtlSupport = false} = themeDefinition;
@@ -481,7 +528,7 @@ class ConfigBuilder {
481
528
  ...prepareModulesShim(resolver, jsBuildConfig.shim)
482
529
  ]
483
530
  }
484
- }
531
+ };
485
532
  }
486
533
 
487
534
  _initialize(args, env) {
@@ -520,7 +567,7 @@ class ConfigBuilder {
520
567
  }
521
568
 
522
569
  _getVersionedPath(name, assetVersion) {
523
- if(!assetVersion) {
570
+ if (!assetVersion) {
524
571
  return name;
525
572
  }
526
573
  return printf(this._versionFormat, name, assetVersion);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oroinc/oro-webpack-config-builder",
3
- "version": "5.1.0-alpha33",
3
+ "version": "5.1.0-alpha34",
4
4
  "author": "Oro, Inc (https://www.oroinc.com)",
5
5
  "license": "MIT",
6
6
  "description": "An integration of OroPlatform based applications with the Webpack.",
@@ -34,6 +34,7 @@
34
34
  "rtlcss-webpack-plugin": "~4.0.6",
35
35
  "sass": "~1.50.0",
36
36
  "sass-loader": "~12.6.0",
37
+ "schema-utils": "^4.0.0",
37
38
  "style-loader": "~3.3.1",
38
39
  "terser": "~5.12.1",
39
40
  "text-loader": "0.0.1",
@@ -0,0 +1,25 @@
1
+ const {once} = require('underscore');
2
+
3
+ class AfterWebpackLogsPlugin {
4
+ /**
5
+ * @param {function} afterLogsCb
6
+ */
7
+ constructor(afterLogsCb) {
8
+ if (typeof afterLogsCb !== 'function') {
9
+ throw new Error('The "afterLogsCb" arg should be a function');
10
+ }
11
+ this._afterLogsCb = once(afterLogsCb);
12
+ }
13
+
14
+ apply(compiler) {
15
+ compiler.hooks.done.tap('AfterWebpackLogsPlugin', stats => {
16
+ const compilerStats = stats;
17
+ stats.compilation.hooks.statsPrinter.tap('AfterWebpackLogsPlugin', stats => {
18
+ // Making logs to be after all webpack logs in the console
19
+ setImmediate(() => this._afterLogsCb(compilerStats));
20
+ });
21
+ });
22
+ }
23
+ }
24
+
25
+ module.exports = AfterWebpackLogsPlugin;
@@ -1,7 +1,9 @@
1
1
  const path = require('path');
2
2
  const wildcard = require('wildcard');
3
3
  const _ = require('underscore');
4
- const messages = require('../messages')
4
+ const {isProdMode} = require('../utils');
5
+ const {assetsValidation} = require('../validation');
6
+ const StylesError = require('../validation/errors/styles-error');
5
7
 
6
8
  /**
7
9
  * @typedef ThemeGroupConfig
@@ -30,17 +32,19 @@ class StyleLoader {
30
32
  getThemeEntryPoints(theme, buildPath) {
31
33
  const {themeConfig, settings = {}} = this._fetchThemeConfig(theme);
32
34
 
35
+ assetsValidation.checkFullSchema(themeConfig, this._configLoader.processedFiles, theme);
36
+
33
37
  const entryPoints = {};
34
38
  const writingOptions = {};
35
39
  for (const [group, config] of Object.entries(themeConfig)) {
36
40
  let {inputs, entries = [], output, auto_rtl_inputs: rtlMasks = []} = config;
37
41
 
38
- if (output === void 0) {
39
- throw new Error(messages.assetsMissedOutput(group, theme));
42
+ if (isProdMode() && output === void 0) {
43
+ throw new StylesError('output', group, theme);
40
44
  }
41
45
 
42
- if (inputs === void 0) {
43
- throw new Error(messages.assetsMissedInput(group, theme));
46
+ if (isProdMode() && inputs === void 0) {
47
+ throw new StylesError('inputs', group, theme);
44
48
  }
45
49
 
46
50
  inputs = this._overrideInputs(inputs);
@@ -89,7 +93,7 @@ class StyleLoader {
89
93
  const oldInputIndex = mappedInputs.findIndex(item => item === oldInput);
90
94
 
91
95
  if (oldInputIndex === -1) {
92
- // old input does not exists any more
96
+ // old input does not exist anymore
93
97
  mappedInputs.push(newInput);
94
98
  } else if (newInput) {
95
99
  // replace input
@@ -120,11 +124,11 @@ class StyleLoader {
120
124
  inputs.forEach(input => {
121
125
  if (input.indexOf('/settings/primary-settings') > 0) {
122
126
  primarySettingsInputs.push(input);
123
- } else if (input.indexOf('/settings/') > 0) {
127
+ } else if (input.indexOf('/settings/') > 0) {
124
128
  settingsInputs.push(input);
125
- } else if (input.indexOf('/variables/primary-variables') > 0) {
129
+ } else if (input.indexOf('/variables/primary-variables') > 0) {
126
130
  primaryVariablesInputs.push(input);
127
- } else if (input.indexOf('/variables/') > 0) {
131
+ } else if (input.indexOf('/variables/') > 0) {
128
132
  variablesInputs.push(input);
129
133
  } else {
130
134
  restInputs.push(input);
@@ -1,4 +1,4 @@
1
- const messages = require('./messages');
1
+ const JSModulesExtraModulesError = require('./validation/errors/jsmodules-extra-modules-error');
2
2
 
3
3
  class ThemeConfigFactory {
4
4
  /**
@@ -19,11 +19,7 @@ class ThemeConfigFactory {
19
19
  * @return {Object} Merged Configs loaded from all the bundles Yaml files matched by filePath
20
20
  */
21
21
  loadConfig(theme, configFilepath) {
22
- try {
23
- return this._configLoader.loadConfig(theme, configFilepath)
24
- } catch (e) {
25
- throw new Error(messages.jsModulesError(theme));
26
- }
22
+ return this._configLoader.loadConfig(theme, configFilepath);
27
23
  }
28
24
 
29
25
  extendConfig(baseConfig, extraConfig) {
@@ -31,7 +27,7 @@ class ThemeConfigFactory {
31
27
  aliases = {},
32
28
  configs,
33
29
  map = {},
34
- shim = {},
30
+ shim = {}
35
31
  } = baseConfig;
36
32
 
37
33
  const {
@@ -43,7 +39,7 @@ class ThemeConfigFactory {
43
39
 
44
40
  const beyondKeys = Object.keys(rest);
45
41
  if (beyondKeys.length) {
46
- throw new Error( messages.jsExtraModulesError(beyondKeys));
42
+ throw new JSModulesExtraModulesError(beyondKeys);
47
43
  }
48
44
 
49
45
  return {
@@ -69,7 +65,7 @@ class ThemeConfigFactory {
69
65
  shim = {},
70
66
  configs,
71
67
  aliases = {},
72
- ['app-modules']: appModules,
68
+ ['app-modules']: appModules = [],
73
69
  ['dynamic-imports']: dynamicImports
74
70
  } = jsModulesConfig;
75
71
 
package/utils.js ADDED
@@ -0,0 +1,30 @@
1
+ let prodMode;
2
+ let verboseMode;
3
+
4
+ module.exports = {
5
+ isVerboseMode() {
6
+ if (verboseMode === void 0) {
7
+ verboseMode = process.argv.filter(arg => {
8
+ // Depending on how the command was run (-v, -vv, -vvv, --verbose) -
9
+ // arguments can be present in different formats: stats=normal, stats=detailed, stats=verbose, verbose
10
+ return arg.search(/^verbose|(^stats=(normal|detailed|verbose))/) !== -1;
11
+ }).length > 0;
12
+ }
13
+
14
+ return verboseMode;
15
+ },
16
+ isProdMode() {
17
+ if (prodMode !== void 0) {
18
+ return prodMode;
19
+ }
20
+
21
+ process.argv.forEach(arg => {
22
+ if (prodMode) {
23
+ return;
24
+ }
25
+ prodMode = ['--mode=production', '--env=prod'].some(item => item === arg);
26
+ });
27
+
28
+ return prodMode;
29
+ }
30
+ };
@@ -0,0 +1,103 @@
1
+ const {isProdMode} = require('../utils');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const schema = require('./schemas/assets-schema');
5
+ const fullSchema = require('./schemas/assets-schema-full');
6
+ const schemaValidator = require('./schema-validator');
7
+ const EventEmitter = require('events');
8
+ const emitter = new EventEmitter();
9
+ const AssetsSchemaError = require('./errors/assets-schema-error');
10
+ const AssetsInputFileError = require('./errors/assets-input-file-error');
11
+
12
+ module.exports = Object.assign({}, schemaValidator, {
13
+ emitter,
14
+
15
+ /**
16
+ * @param {string} filePath
17
+ * @param {Object} doc
18
+ * @param {string} theme
19
+ * @returns {boolean|undefined}
20
+ */
21
+ checkSchema(filePath, doc, theme) {
22
+ if (isProdMode()) {
23
+ return;
24
+ }
25
+ const result = this.validateSchema(schema, doc);
26
+
27
+ if (!result.valid) {
28
+ const error = new AssetsSchemaError(result.formattedError, [filePath], theme);
29
+
30
+ this.emitter.emit('error', error);
31
+ }
32
+
33
+ return result.valid;
34
+ },
35
+
36
+ /**
37
+ * @param {Object} doc
38
+ * @param {Array} files
39
+ * @param {string} theme
40
+ * @returns {boolean|undefined}
41
+ */
42
+ checkFullSchema(doc, files = [], theme) {
43
+ if (isProdMode()) {
44
+ return;
45
+ }
46
+ const result = this.validateSchema(fullSchema, doc);
47
+
48
+ if (!result.valid) {
49
+ const error = new AssetsSchemaError(result.formattedError, files, theme);
50
+
51
+ this.emitter.emit('error', error);
52
+ }
53
+
54
+ return result.valid;
55
+ },
56
+
57
+ /**
58
+ * @param {string} filePath
59
+ * @param {Object} doc
60
+ * @param {string} theme
61
+ * @returns {boolean|undefined}
62
+ */
63
+ checkInputsExist(filePath, doc, theme) {
64
+ if (isProdMode()) {
65
+ return;
66
+ }
67
+
68
+ let valid = true;
69
+ for (const [, props] of Object.entries(doc)) {
70
+ if (!Array.isArray(props.inputs)) {
71
+ continue;
72
+ }
73
+
74
+ const files = [];
75
+ props.inputs.forEach(input => {
76
+ if (typeof input === 'object') {
77
+ const newPath = Object.values(input)[0];
78
+
79
+ if (newPath === '~') {
80
+ return;
81
+ }
82
+ input = newPath;
83
+ }
84
+
85
+ const fullPath = path.resolve(this._publicPath + input);
86
+ // skip the path to global node modules,
87
+ // e.g. '~bootstrap/scss/bootstrap'
88
+ if (!input.startsWith('~') && !fs.existsSync(fullPath)) {
89
+ files.push(input);
90
+ }
91
+ });
92
+ if (files.length) {
93
+ valid = false;
94
+ files.forEach(notExistFile => {
95
+ const error = new AssetsInputFileError(notExistFile, filePath);
96
+
97
+ this.emitter.emit('error', error);
98
+ });
99
+ }
100
+ }
101
+ return valid;
102
+ }
103
+ });
@@ -0,0 +1,24 @@
1
+ const BaseError = require('./base-error');
2
+
3
+ class AssetsInputFileError extends BaseError {
4
+ /**
5
+ * @example
6
+ * Invalid styles config: input file does not exist /scss/test.scss
7
+ * at /config/assets.yml
8
+ * Please, find more information in the documentation https://doc.oroinc.com/...
9
+ *
10
+ * @param {string} notExistFile
11
+ * @param {string} path
12
+ */
13
+ constructor(notExistFile, path) {
14
+ const msg = `input file does not exist: ${notExistFile}\nat ${path}:1`;
15
+
16
+ super(msg);
17
+
18
+ this.name = 'Invalid styles config';
19
+ this.docLink =
20
+ 'https://doc.oroinc.com/backend/bundles/platform/AssetBundle/#load-scss-or-css-files-from-the-bundle';
21
+ }
22
+ }
23
+
24
+ module.exports = AssetsInputFileError;
@@ -0,0 +1,40 @@
1
+ const BaseError = require('./base-error');
2
+
3
+ class AssetsSchemaError extends BaseError {
4
+ /**
5
+ * @example
6
+ * Invalid styles config: the "assets.yml" in the "default" theme file does not match the API schema.
7
+ * styles.inputs[0] should be: string | object
8
+ * at /config/assets.yml
9
+ * Please, find more information in the documentation https://doc.oroinc.com/...
10
+ *
11
+ * @example
12
+ * Invalid styles config: the "assets.yml" file the "default" theme does not match the API schema.
13
+ * styles misses the property 'output'.
14
+ * List of processed files for the "default" theme:
15
+ * /default/config/assets.yml
16
+ * /default/config/assets.yml
17
+ * Please, find more information in the documentation https://doc.oroinc.com/...
18
+ *
19
+ * @param {string} reason
20
+ * @param {Array} files
21
+ * @param {string} theme
22
+ */
23
+ constructor(reason, files, theme) {
24
+ let msg = `the "assets.yml" file in the "${theme}" theme does not match the API schema.\n${reason}`;
25
+
26
+ const filesListMsg = BaseError.generateFilesListMsg(files, theme);
27
+
28
+ if (filesListMsg.length) {
29
+ msg = `${msg}\n${filesListMsg}`;
30
+ }
31
+
32
+ super(msg);
33
+
34
+ this.name = 'Invalid styles config';
35
+ this.docLink =
36
+ 'https://doc.oroinc.com/backend/bundles/platform/AssetBundle/#load-scss-or-css-files-from-the-bundle';
37
+ }
38
+ }
39
+
40
+ module.exports = AssetsSchemaError;
@@ -0,0 +1,37 @@
1
+ const {isVerboseMode} = require('../../utils');
2
+
3
+ class BaseError extends Error {
4
+ /**
5
+ * Prepares documentation phrase
6
+ * @returns {string}
7
+ */
8
+ get extra() {
9
+ return this.docLink ? `Please, find more information in the documentation ${this.docLink}` : '';
10
+ }
11
+
12
+ /**
13
+ * Creates an instance of BaseError
14
+ * @param {string} message error message
15
+ */
16
+ constructor(message) {
17
+ super(message);
18
+ }
19
+
20
+ /**
21
+ * Creates a part of message about processed files
22
+ * @param {Array} files
23
+ * @param {string} theme
24
+ * @returns {string}
25
+ */
26
+ static generateFilesListMsg(files, theme) {
27
+ if (files.length === 1) {
28
+ return `at ${files[0]}:1`;
29
+ } else if (isVerboseMode()) {
30
+ return `List of processed files for the "${theme}" theme:\n ${files.join('\n ')}`;
31
+ }
32
+
33
+ return '';
34
+ }
35
+ }
36
+
37
+ module.exports = BaseError;
@@ -0,0 +1,22 @@
1
+ const BaseError = require('./base-error');
2
+
3
+ class JsmodulesExtraModulesError extends BaseError {
4
+ /**
5
+ * @example
6
+ * Failed assembly JS: sections ["shim"] are not allowed in extra js build definition.
7
+ * Please, find more information in the documentation https://doc.oroinc.com/...
8
+ *
9
+ * @param {Array} parts
10
+ */
11
+ constructor(parts) {
12
+ const msg = `sections ["${parts.join('", "')}"] are not allowed in extra js build definition.`;
13
+
14
+ super(msg);
15
+
16
+ this.name = 'Failed assembly JS';
17
+ this.docLink =
18
+ 'https://doc.oroinc.com/master/frontend/storefront/how-to/how-to-create-extra-js-build-for-landing-page';
19
+ }
20
+ }
21
+
22
+ module.exports = JsmodulesExtraModulesError;
@@ -0,0 +1,40 @@
1
+ const BaseError = require('./base-error');
2
+
3
+ class JSModulesSchemaError extends BaseError {
4
+ /**
5
+ * @example
6
+ * Invalid JS config: the "jsmodules.yml" files in the "default" theme do not match the API schema.
7
+ * has an unknown property 'rest'.
8
+ * at default/config/jsmodules.yml
9
+ * Please, find more information in the documentation https://doc.oroinc.com/...
10
+ *
11
+ * @example
12
+ * Invalid JS config: the "jsmodules.yml" files in the "default" theme do not match the API schema.
13
+ * misses the property 'entry'.
14
+ * List of processed files for the "default" theme:
15
+ * /default/config/jsmodules.yml
16
+ * /default/config/jsmodules.yml
17
+ * Please, find more information in the documentation https://doc.oroinc.com/...
18
+ *
19
+ * @param {string} reason
20
+ * @param {Array} files
21
+ * @param {string} theme
22
+ */
23
+ constructor(reason, files, theme) {
24
+ let msg = `the "jsmodules.yml" files in the "${theme}" theme do not match the API schema.\n${reason}`;
25
+
26
+ const filesListMsg = BaseError.generateFilesListMsg(files, theme);
27
+
28
+ if (filesListMsg.length) {
29
+ msg = `${msg}\n${filesListMsg}`;
30
+ }
31
+
32
+ super(msg);
33
+
34
+ this.name = 'Invalid JS config';
35
+ this.docLink =
36
+ 'https://doc.oroinc.com/backend/bundles/platform/AssetBundle/#create-jsmodules-yml-configuration';
37
+ }
38
+ }
39
+
40
+ module.exports = JSModulesSchemaError;
@@ -0,0 +1,24 @@
1
+ const BaseError = require('./base-error');
2
+
3
+ class StylesError extends BaseError {
4
+ /**
5
+ * @example
6
+ * Failed assembly styles: the "output" for "styles" entry point in "default" theme is not defined.
7
+ * Please, find more information in the documentation https://doc.oroinc.com/...
8
+ *
9
+ * @param {string} key
10
+ * @param {string} group
11
+ * @param {string} theme
12
+ */
13
+ constructor(key, group, theme) {
14
+ const msg = `the "${key}" for "${group}" entry point in "${theme}" theme is not defined.`;
15
+
16
+ super(msg);
17
+
18
+ this.name = 'Failed assembly styles';
19
+ this.docLink =
20
+ 'https://doc.oroinc.com/backend/bundles/platform/AssetBundle/#load-scss-or-css-files-from-the-bundle';
21
+ }
22
+ }
23
+
24
+ module.exports = StylesError;
@@ -0,0 +1,36 @@
1
+ const assetsValidation = require('./assets-validator');
2
+ const jsmodulesValidator = require('./jsmodules-validator');
3
+
4
+ const isJSModulesPath = path => /jsmodules([-a-zA-Z\d]*)\.yml$/.test(path);
5
+ const isAssetsPath = path => /assets\.yml$/.test(path);
6
+
7
+ module.exports = {
8
+ assetsValidation,
9
+ jsmodulesValidator,
10
+
11
+ /**
12
+ * set public path for all validators
13
+ * {string} path
14
+ */
15
+ setPublicPath(path) {
16
+ [assetsValidation, jsmodulesValidator].forEach(validator => validator.setPublicPath(path));
17
+ },
18
+
19
+ /**
20
+ * Run appropriate validator
21
+ * {string} filePath
22
+ * {Object} doc
23
+ * {string} theme
24
+ */
25
+ checkSchema(filePath, doc, theme) {
26
+ if (isJSModulesPath(filePath)) {
27
+ jsmodulesValidator.checkSchema(filePath, doc, theme);
28
+ } else if (isAssetsPath(filePath)) {
29
+ const validSchema = assetsValidation.checkSchema(filePath, doc, theme);
30
+
31
+ if (validSchema) {
32
+ assetsValidation.checkInputsExist(filePath, doc, theme);
33
+ }
34
+ }
35
+ }
36
+ };
@@ -0,0 +1,53 @@
1
+ const schema = require('./schemas/jsmodules-schema');
2
+ const fullSchema = require('./schemas/jsmodules-schema-full');
3
+ const schemaValidator = require('./schema-validator');
4
+ const {isProdMode} = require('../utils');
5
+ const EventEmitter = require('events');
6
+ const emitter = new EventEmitter();
7
+ const JSModulesSchemaError = require('./errors/jsmodules-schema-error');
8
+
9
+ module.exports = Object.assign({}, schemaValidator, {
10
+ emitter,
11
+
12
+ /**
13
+ * @param {string} filePath
14
+ * @param {Object} doc
15
+ * @param {string} theme
16
+ * @returns {boolean|undefined}
17
+ */
18
+ checkSchema(filePath, doc, theme) {
19
+ if (isProdMode()) {
20
+ return;
21
+ }
22
+ const result = this.validateSchema(schema, doc);
23
+
24
+ if (!result.valid) {
25
+ const error = new JSModulesSchemaError(result.formattedError, [filePath], theme);
26
+
27
+ this.emitter.emit('error', error);
28
+ }
29
+
30
+ return result.valid;
31
+ },
32
+
33
+ /**
34
+ * @param {Object} doc
35
+ * @param {Array} files
36
+ * @param {string} theme
37
+ * @returns {boolean|undefined}
38
+ */
39
+ checkFullSchema(doc, files = [], theme) {
40
+ if (isProdMode()) {
41
+ return;
42
+ }
43
+ const result = this.validateSchema(fullSchema, doc);
44
+
45
+ if (!result.valid) {
46
+ const error = new JSModulesSchemaError(result.formattedError, files, theme);
47
+
48
+ this.emitter.emit('error', error);
49
+ }
50
+
51
+ return result.valid;
52
+ }
53
+ });
@@ -0,0 +1,62 @@
1
+ const {validate} = require('schema-utils');
2
+
3
+ module.exports = {
4
+ /**
5
+ * Symfony public directory path related to application root folder
6
+ * {string}
7
+ */
8
+ _publicPath: 'public/',
9
+
10
+ setPublicPath(path) {
11
+ this._publicPath = path;
12
+ },
13
+
14
+ /**
15
+ * Validates data according to the schema
16
+ * @param {Object} scheme
17
+ * @param {Object} data
18
+ * @param {Object} [options]
19
+ * @returns {Object}
20
+ */
21
+ validateSchema(scheme, data, options) {
22
+ const result = {
23
+ valid: true
24
+ };
25
+
26
+ try {
27
+ const baseDataPath = 'configuration';
28
+ const basePathReg = /^configuration\./;
29
+ validate(scheme, data, {
30
+ baseDataPath,
31
+ ...options || {},
32
+ postFormatter(formattedError, error) {
33
+ if (result.formattedError === void 0) {
34
+ result.formattedError = [];
35
+ }
36
+ result.error = error;
37
+
38
+ if (
39
+ Array.isArray(error.params.type) ||
40
+ error.params.missingProperty
41
+ ) {
42
+ // remove an excess line break
43
+ formattedError = formattedError.replace(/\n/g, ' ');
44
+ }
45
+
46
+ formattedError = formattedError.replace(basePathReg, '');
47
+ result.formattedError.push(formattedError.trim());
48
+ return formattedError;
49
+ }
50
+ });
51
+ } catch (e) {
52
+ result.valid = false;
53
+ result.errorMessage = e.message;
54
+ }
55
+
56
+ if (result.formattedError) {
57
+ result.formattedError = result.formattedError.join('\n');
58
+ }
59
+
60
+ return result;
61
+ }
62
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Schema to validate assets.yml files
3
+ *
4
+ * @description
5
+ * Full scheme complements "assets-schema" one.
6
+ * It should have only rules which are not defined in "assets-schema" schema due to avoid duplicates in error messages
7
+ */
8
+ module.exports = {
9
+ type: 'object',
10
+ patternProperties: {
11
+ '.*': {
12
+ type: 'object',
13
+ properties: {
14
+ inputs: {
15
+ description: 'The "inputs" property is an array of files to load.',
16
+ minItems: 1
17
+ }
18
+ },
19
+ required: ['inputs', 'output']
20
+ }
21
+ }
22
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Schema to validate assets.yml files
3
+ */
4
+ module.exports = {
5
+ type: 'object',
6
+ patternProperties: {
7
+ '.*': {
8
+ type: 'object',
9
+ properties: {
10
+ inputs: {
11
+ description: 'The "inputs" property is an array of files to load.',
12
+ type: 'array',
13
+ items: {
14
+ type: ['string', 'object']
15
+ }
16
+ },
17
+ output: {
18
+ description: 'Output file path inside "public/" directory for the entry point',
19
+ type: 'string'
20
+ },
21
+ auto_rtl_inputs: {
22
+ description: 'List of wildcard file masks for inputs that has to be processed with RTL plugin.',
23
+ type: 'array',
24
+ items: {
25
+ type: 'string'
26
+ }
27
+ }
28
+ },
29
+ additionalProperties: false
30
+ }
31
+ }
32
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Schema to validate jsmodules.yml files
3
+ *
4
+ * @description
5
+ * Full scheme complements "jsmodules-schema" one.
6
+ * It should have only rules which are not defined in "jsmodules-schema" schema due to avoid duplicates in error messages
7
+ */
8
+ module.exports = {
9
+ type: 'object',
10
+ required: ['entry']
11
+ };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Schema to validate jsmodules.yml files
3
+ */
4
+ module.exports = {
5
+ type: 'object',
6
+ properties: {
7
+ 'entry': {
8
+ description: 'Webpack entry points configuration.',
9
+ type: 'object',
10
+ patternProperties: {
11
+ '.*': {
12
+ type: 'array',
13
+ items: {
14
+ type: 'string'
15
+ }
16
+ }
17
+ }
18
+ },
19
+ 'shim': {
20
+ description: 'Configure a webpack shimming feature',
21
+ type: 'object',
22
+ patternProperties: {
23
+ '.*': {
24
+ type: 'object'
25
+ }
26
+ }
27
+ },
28
+ 'map': {
29
+ description: 'The map option allows to substitute a module with the given ID with a different module.',
30
+ type: 'object',
31
+ patternProperties: {
32
+ '.*': {
33
+ type: 'object'
34
+ }
35
+ }
36
+ },
37
+ 'app-modules': {
38
+ description: 'Introduces a list of modules that should be initialized before application is launched.',
39
+ type: 'array',
40
+ items: {
41
+ type: 'string'
42
+ }
43
+ },
44
+ 'dynamic-imports': {
45
+ description: 'Gives possibility to import a module with the name that is determined at runtime.',
46
+ type: 'object',
47
+ patternProperties: {
48
+ '.*': {
49
+ type: 'array',
50
+ items: {
51
+ type: 'string'
52
+ }
53
+ }
54
+ }
55
+ },
56
+ 'configs': {
57
+ description: 'Runtime configuration for a the module.',
58
+ type: 'object',
59
+ patternProperties: {
60
+ '.*': {
61
+ type: 'object'
62
+ }
63
+ }
64
+ },
65
+ 'aliases': {
66
+ description: 'An alias is an alternative name.',
67
+ type: 'object',
68
+ patternProperties: {
69
+ '.*': {
70
+ type: 'string'
71
+ }
72
+ }
73
+ }
74
+ },
75
+ additionalProperties: false
76
+ };
@@ -19,8 +19,8 @@ class DynamicImportsFileWriter {
19
19
  write(dynamicImports, output) {
20
20
  const buildPath = path.join(output, 'dynamic-imports.js');
21
21
  let content = Object.entries(dynamicImports).map(([chunkName, moduleNames]) => {
22
- const notation = chunkName === 'commons' ?
23
- '/* webpackMode: "eager" */' : `/* webpackChunkName: "${chunkName}" */`;
22
+ const notation = chunkName === 'commons'
23
+ ? '/* webpackMode: "eager" */' : `/* webpackChunkName: "${chunkName}" */`;
24
24
  return moduleNames.map(moduleName =>`'${moduleName}': () => import(${notation}'${moduleName}')`);
25
25
  });
26
26
  content = `module.exports = {\n ${content.flat().join(',\n ')}\n};\n`;
@@ -25,7 +25,7 @@ class SCSSEntryPointFileWriter {
25
25
  input = input.replace(/\.[^/.]+$/, '');
26
26
  // don't add the base path to global node modules,
27
27
  // e.g. '~bootstrap/scss/bootstrap'
28
- const basePath = input.startsWith('~') ? '': baseInputPath;
28
+ const basePath = input.startsWith('~') ? '' : baseInputPath;
29
29
  let importModule = `@import "${basePath}${input}";\n`;
30
30
  if (ignoreRTL) {
31
31
  importModule = `/*rtl:begin:ignore*/\n${importModule}/*rtl:end:ignore*/\n`;
package/messages.js DELETED
@@ -1,29 +0,0 @@
1
- const doc = "Please, find more information in the documentation ";
2
- const CSSError = "Failed assembly styles.\n";
3
- const CSSDoc = "https://doc.oroinc.com/backend/bundles/platform/AssetBundle/#load-scss-or-css-files-from-the-bundle";
4
- const JSError = `Failed assembly JS.\n`;
5
- const JSDoc = "https://doc.oroinc.com/backend/bundles/platform/AssetBundle/#create-jsmodules-yml-configuration";
6
- const JSExtraBuildDoc = "https://doc.oroinc.com/master/frontend/storefront/how-to/how-to-create-extra-js-build-for-landing-page";
7
-
8
- module.exports = {
9
- assetsMissedOutput(group, theme) {
10
- const error = `The "output" for "${group}" entry point in "${theme}" theme is not defined.\n`;
11
-
12
- return `${CSSError}${error}${doc}${CSSDoc}`;
13
- },
14
- assetsMissedInput(group, theme) {
15
- const error = `The "output" for "${group}" entry point in "${theme}" theme is not defined.\n`;
16
-
17
- return `${CSSError}${error}${doc}${CSSDoc}`
18
- },
19
- jsModulesError(theme) {
20
- const error = `Failed assembly JS for the "${theme}" theme.\n`;
21
-
22
- return `${error}${doc}${JSDoc}`
23
- },
24
- jsExtraModulesError(parts) {
25
- const error = `Sections ["${parts.join('", "')}"] are not allowed in extra js build definition\n`;
26
-
27
- return `${JSError}${error}${doc}${JSExtraBuildDoc}`;
28
- }
29
- };