@oroinc/oro-webpack-config-builder 5.1.0-dev006 → 5.1.0-dev007

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.
@@ -22,6 +22,29 @@ class LayoutModulesConfigLoader extends ModulesConfigLoader {
22
22
  return themeConfig;
23
23
  }
24
24
 
25
+ /**
26
+ * @param {string} theme Theme name
27
+ * @param {string} dirPath path to the directory were to search
28
+ * @param {string} fileExtName file extension
29
+ * @return {Object} Merged Configs loaded from all the bundles Yaml files matched by dirPath
30
+ */
31
+ getFilesPaths(theme, dirPath = '/public', fileExtName = '.txt') {
32
+ let iconsPaths = super.getFilesPaths(theme, dirPath, fileExtName);
33
+
34
+ // recursive process parent theme
35
+ const {parent: parentTheme} = this.themes[theme];
36
+ if (typeof parentTheme === 'string') {
37
+ const processedFiles = this.processedFiles;
38
+ const parentIconsPaths = this.getFilesPaths(parentTheme, dirPath, fileExtName);
39
+
40
+ iconsPaths = [...iconsPaths, ...parentIconsPaths];
41
+ // processedFiles from parent theme is added to processedFiles of current theme
42
+ this._processedFiles = [...processedFiles, ...this.processedFiles];
43
+ }
44
+
45
+ return iconsPaths;
46
+ }
47
+
25
48
  /**
26
49
  * All build names:
27
50
  * - based on theme names
@@ -119,6 +119,37 @@ class ModulesConfigLoader {
119
119
 
120
120
  return configs;
121
121
  }
122
+
123
+ /**
124
+ * @param {string} theme Theme name
125
+ * @param {string} dirPath path to the directory were to search
126
+ * @param {string} fileExtName file extension
127
+ * @return {Object} Merged Configs loaded from all the bundles Yaml files matched by dirPath
128
+ */
129
+ getFilesPaths(theme, dirPath = '/public', fileExtName = '.txt') {
130
+ const result = [];
131
+
132
+ this._processedFiles = [];
133
+ this._bundles.forEach(bundle => {
134
+ const absolutePath = path.resolve(bundle, dirPath, `${theme}/svg-icons`);
135
+
136
+ if (fs.existsSync(absolutePath)) {
137
+ this._processedFiles.push(absolutePath);
138
+
139
+ const files = fs.readdirSync(absolutePath);
140
+
141
+ for (const file of files) {
142
+ const fileExt = path.extname(file);
143
+
144
+ if (fileExt === fileExtName) {
145
+ result.push(`${absolutePath}/${file}`);
146
+ }
147
+ }
148
+ }
149
+ });
150
+
151
+ return result;
152
+ }
122
153
  }
123
154
 
124
155
  module.exports = ModulesConfigLoader;
@@ -26,9 +26,8 @@ const RtlCssWebpackPlugin = require('rtlcss-webpack-plugin');
26
26
  const validation = require('./validation');
27
27
  const EventEmitter = require('events');
28
28
  const ErrorHandler = require('./error-handler');
29
-
29
+ const SVGSprite = require('./svg-sprite');
30
30
  require('resolve-url-loader');
31
- require('lezer-loader');
32
31
 
33
32
  class ConfigBuilder {
34
33
  constructor() {
@@ -188,7 +187,7 @@ class ConfigBuilder {
188
187
  }
189
188
 
190
189
  get resolvedProjectPath() {
191
- if (this._resolvedProjectPath == undefined) {
190
+ if (this._resolvedProjectPath === undefined) {
192
191
  this._resolvedProjectPath = path.resolve(this._projectPath);
193
192
  }
194
193
  return this._resolvedProjectPath;
@@ -332,7 +331,7 @@ class ConfigBuilder {
332
331
  }, {
333
332
  loader: 'sass-loader',
334
333
  options: {
335
- implementation: require("sass"),
334
+ implementation: require('sass'),
336
335
  sassOptions: {
337
336
  includePaths: [
338
337
  path.join(this.resolvedPublicPath, '/bundles')
@@ -353,10 +352,6 @@ class ConfigBuilder {
353
352
  publicPath: '../../_static/',
354
353
  name: this._getVersionedPath('[path][name].[ext]', this.assetVersion)
355
354
  }
356
- },
357
- {
358
- test: /\.grammar$/,
359
- use: 'lezer-loader',
360
355
  }
361
356
  ]
362
357
  },
@@ -430,7 +425,7 @@ class ConfigBuilder {
430
425
  }
431
426
 
432
427
  _getThemeWebpackConfig(buildName, args, env) {
433
- let {skipCSS, skipJS, skipRTL} = env;
428
+ let {skipCSS, skipJS, skipRTL, skipSVG} = env;
434
429
  let themeDefinition;
435
430
  let jsBuildConfig;
436
431
  let buildPublicPath;
@@ -444,7 +439,8 @@ class ConfigBuilder {
444
439
  'Resources/config/jsmodules.yml',
445
440
  'config/oro/jsmodules.yml'
446
441
  ]
447
- ); validation.jsmodulesValidator.checkFullSchema(
442
+ );
443
+ validation.jsmodulesValidator.checkFullSchema(
448
444
  jsModulesConfig,
449
445
  this._themeConfigFactory?._configLoader.processedFiles,
450
446
  buildName
@@ -474,6 +470,15 @@ class ConfigBuilder {
474
470
  buildName
475
471
  );
476
472
  jsBuildConfig = this._layoutThemeConfigFactory.create(buildPublicPath, jsModulesConfig);
473
+
474
+ if (!skipSVG) {
475
+ new SVGSprite(
476
+ this._layoutModulesConfigLoader,
477
+ buildName,
478
+ this._publicPath,
479
+ buildPublicPath
480
+ );
481
+ }
477
482
  }
478
483
  const {rtl_support: rtlSupport = false} = themeDefinition;
479
484
  const resolvedBuildPath = path.join(this.resolvedPublicPath, buildPublicPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oroinc/oro-webpack-config-builder",
3
- "version": "5.1.0-dev006",
3
+ "version": "5.1.0-dev007",
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,8 @@
34
34
  "sass-loader": "~13.2.0",
35
35
  "schema-utils": "^4.0.0",
36
36
  "style-loader": "~3.3.1",
37
+ "svgo": "^3.0.2",
38
+ "svgstore": "^3.0.1",
37
39
  "terser": "~5.17.1",
38
40
  "text-loader": "0.0.1",
39
41
  "underscore": "1.13.*",
@@ -43,7 +45,6 @@
43
45
  "webpack-cli": "~5.0.0",
44
46
  "webpack-dev-server": "^4.11.1",
45
47
  "webpack-merge": "~5.8.0",
46
- "wildcard": "~2.0.0",
47
- "lezer-loader": "^0.3.0"
48
+ "wildcard": "~2.0.0"
48
49
  }
49
50
  }
@@ -0,0 +1,135 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const {svgIconsValidator} = require('../validation');
4
+ const svgoConfig = require('./svgo-config');
5
+ const svgSpriteConfig = require('./svg-sprite-config');
6
+ const {optimize} = require('svgo');
7
+ const svgstore = require('svgstore');
8
+ class SvgSprite {
9
+ /**
10
+ * @param {ModulesConfigLoader} configLoader
11
+ * @param {string} theme
12
+ * @param {string} publicPath
13
+ * @param {string} buildPublicPath
14
+ */
15
+ constructor(configLoader, theme, publicPath, buildPublicPath) {
16
+ this.SPRITE_NAME = 'theme-icons';
17
+ this._configLoader = configLoader;
18
+ this._theme = theme;
19
+ this._publicPath = publicPath;
20
+ this._buildPublicPath = buildPublicPath;
21
+
22
+ this.optimize();
23
+ }
24
+
25
+ /**
26
+ * Starting optimization
27
+ */
28
+ optimize() {
29
+ const svgConfig = this._configLoader.loadConfig(
30
+ this._theme,
31
+ 'config/svg-icons.yml'
32
+ );
33
+
34
+ svgIconsValidator.checkFullSchema(svgConfig, this._configLoader.processedFiles, this._theme);
35
+
36
+ const svgPaths = this._configLoader.getFilesPaths(
37
+ this._theme,
38
+ 'Resources/public/',
39
+ '.svg'
40
+ );
41
+ const exclude = svgConfig.exclude ?? [];
42
+ const filesToOptimize = [];
43
+ const filesNames = {};
44
+
45
+ svgPaths.forEach(icon => {
46
+ const svgName = path.parse(icon).name;
47
+
48
+ if (svgName === this.SPRITE_NAME) {
49
+ throw new Error(
50
+ `The "${this.SPRITE_NAME}" is a reserved word and cannot be used as a svg name for building sprite.`
51
+ );
52
+ }
53
+
54
+ const toExclude = exclude.includes(svgName);
55
+ const fileIsChosenToMove = filesNames[svgName] === svgName;
56
+
57
+ if (toExclude === false && fileIsChosenToMove === false) {
58
+ filesToOptimize.push(icon);
59
+ filesNames[svgName] = svgName;
60
+ }
61
+ });
62
+
63
+ if (filesToOptimize.length === 0) {
64
+ return;
65
+ }
66
+
67
+ this.moveFiles(filesToOptimize);
68
+ }
69
+
70
+ /**
71
+ * Preparing files for optimization
72
+ * @param {Array} files
73
+ * @returns {Promise<void>}
74
+ */
75
+ async moveFiles(files) {
76
+ const buildPath = path.join(this._buildPublicPath, 'svg-icons');
77
+ const dirPath = path.resolve(`${this._publicPath}${buildPath}`);
78
+
79
+ if (!fs.existsSync(dirPath)) {
80
+ fs.mkdirSync(dirPath, {recursive: true});
81
+ }
82
+
83
+ try {
84
+ fs.readdirSync(dirPath).forEach(file => {
85
+ fs.unlinkSync(path.join(dirPath, file));
86
+ });
87
+ } catch (error) {
88
+ throw new Error(error);
89
+ }
90
+
91
+ await Promise.all(
92
+ files.map(file => this.optimizeFile(file, dirPath))
93
+ );
94
+ this.createSprite(fs.readdirSync(dirPath), dirPath);
95
+ }
96
+
97
+ /**
98
+ * Optimizing svg using svgo
99
+ * @param {Object} file
100
+ * @param {string} dirPath
101
+ * @returns {Promise<void>}
102
+ */
103
+ async optimizeFile(file, dirPath) {
104
+ const {name, ext} = path.parse(file);
105
+ const filepath = path.join(dirPath, `${name}${ext}`);
106
+
107
+ const originalSvg = fs.readFileSync(file, 'utf8');
108
+ const {data: optimizedSvg} = await optimize(originalSvg, {
109
+ path: filepath,
110
+ ...svgoConfig
111
+ });
112
+
113
+ // svgo will always add a final newline when in pretty mode
114
+ const resultSvg = optimizedSvg.trim().replace(/(\n\r?|\r\n?)/g, '');
115
+ fs.writeFileSync(filepath, resultSvg, 'utf8');
116
+ }
117
+
118
+ /**
119
+ * Creating sprite
120
+ * @param {Array} files
121
+ * @param {string} dirPath
122
+ */
123
+ createSprite(files, dirPath) {
124
+ const sprites = svgstore(svgSpriteConfig);
125
+
126
+ for (const file of files) {
127
+ const svgName = path.parse(file).name;
128
+ sprites.add(svgName, fs.readFileSync(path.join(dirPath, file), 'utf8'));
129
+ }
130
+
131
+ fs.writeFileSync(path.join(dirPath, `${this.SPRITE_NAME}.svg`), sprites, 'utf8');
132
+ }
133
+ }
134
+
135
+ module.exports = SvgSprite;
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ cleanDefs: false,
3
+ cleanSymbols: false,
4
+ inline: false
5
+ };
@@ -0,0 +1,32 @@
1
+ module.exports = {
2
+ multipass: true,
3
+ js2svg: {
4
+ pretty: true,
5
+ indent: 0,
6
+ eol: 'lf'
7
+ },
8
+ plugins: [{
9
+ name: 'preset-default',
10
+ params: {
11
+ overrides: {
12
+ removeUnknownsAndDefaults: {
13
+ keepRoleAttr: true
14
+ },
15
+ removeViewBox: false
16
+ }
17
+ }
18
+ },
19
+ // The next plugins are included in svgo but are not part of preset-default,
20
+ // so we need to enable them separately
21
+ 'cleanupListOfValues',
22
+ {
23
+ name: 'removeAttrs',
24
+ params: {
25
+ attrs: [
26
+ 'clip-rule',
27
+ 'data-name',
28
+ 'fill'
29
+ ]
30
+ }
31
+ }]
32
+ };
@@ -0,0 +1,36 @@
1
+ const BaseError = require('./base-error');
2
+
3
+ class SvgIconsSchemaError extends BaseError {
4
+ /**
5
+ * @example
6
+ * Invalid SVG config: the "svg-icons.yml" files in the "default" theme do not match the API schema.
7
+ * has an unknown property 'rest'.
8
+ * at default/config/svg-icons.yml
9
+ *
10
+ * @example
11
+ * Invalid SVG config: the "svg-icons.yml" files in the "default" theme do not match the API schema.
12
+ * misses the property 'exclude'.
13
+ * List of processed files for the "default" theme:
14
+ * /default/config/svg-icons.yml
15
+ * /default/config/svg-icons.yml
16
+ *
17
+ * @param {string} reason
18
+ * @param {Array} files
19
+ * @param {string} theme
20
+ */
21
+ constructor(reason, files, theme) {
22
+ let msg = `the "svg-icons.yml" files in the "${theme}" theme do not match the API schema.\n${reason}`;
23
+
24
+ const filesListMsg = BaseError.generateFilesListMsg(files, theme);
25
+
26
+ if (filesListMsg.length) {
27
+ msg = `${msg}\n${filesListMsg}`;
28
+ }
29
+
30
+ super(msg);
31
+
32
+ this.name = 'Invalid SVG config';
33
+ }
34
+ }
35
+
36
+ module.exports = SvgIconsSchemaError;
@@ -1,12 +1,15 @@
1
1
  const assetsValidation = require('./assets-validator');
2
2
  const jsmodulesValidator = require('./jsmodules-validator');
3
+ const svgIconsValidator = require('./svg-icons-validator');
3
4
 
4
5
  const isJSModulesPath = path => /jsmodules([-a-zA-Z\d]*)\.yml$/.test(path);
5
6
  const isAssetsPath = path => /assets\.yml$/.test(path);
7
+ const isSvgIconsPath = path => /svg-icons\.yml$/.test(path);
6
8
 
7
9
  module.exports = {
8
10
  assetsValidation,
9
11
  jsmodulesValidator,
12
+ svgIconsValidator,
10
13
 
11
14
  /**
12
15
  * set public path for all validators
@@ -31,6 +34,8 @@ module.exports = {
31
34
  if (validSchema) {
32
35
  assetsValidation.checkInputsExist(filePath, doc, theme);
33
36
  }
37
+ } else if (isSvgIconsPath(filePath)) {
38
+ svgIconsValidator.checkSchema(filePath, doc, theme);
34
39
  }
35
40
  }
36
41
  };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Schema to validate svg-icons.yml files
3
+ *
4
+ * @description
5
+ * Full scheme complements "svg-icons-schema" one.
6
+ * It should have only rules which are not defined in "svg-icons-schema" schema due to avoid duplicates in error messages
7
+ */
8
+
9
+ module.exports = {
10
+ type: 'object',
11
+ properties: {
12
+ exclude: {
13
+ description: 'The "exclude" property is an array of svg files to exclude from svg sprite.',
14
+ type: 'array',
15
+ minItems: 0
16
+ }
17
+ }
18
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Schema to validate svg-icons.yml files
3
+ */
4
+
5
+ module.exports = {
6
+ type: 'object',
7
+ properties: {
8
+ exclude: {
9
+ description: 'List of SVG files to exclude a from svg sprite.',
10
+ type: 'array',
11
+ items: {
12
+ type: 'string'
13
+ }
14
+ }
15
+ },
16
+ additionalProperties: false
17
+ };
@@ -0,0 +1,53 @@
1
+ const schema = require('./schemas/svg-icons-schema');
2
+ const fullSchema = require('./schemas/svg-icons-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 SvgIsonsSchemaError = require('./errors/svg-icons-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 SvgIsonsSchemaError(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 SvgIsonsSchemaError(result.formattedError, files, theme);
47
+
48
+ this.emitter.emit('error', error);
49
+ }
50
+
51
+ return result.valid;
52
+ }
53
+ });