@oroinc/oro-webpack-config-builder 6.1.0-lts06 → 6.1.0-lts08

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.
package/error-handler.js CHANGED
@@ -1,7 +1,15 @@
1
1
  const BaseError = require('./validation/errors/base-error');
2
- const {isVerboseMode, multiline} = require('./utils');
2
+ const {isVerboseMode} = require('./utils');
3
3
  const {red, yellow, green, bgRed} = require('colorette');
4
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
+
5
13
  class ErrorHandler {
6
14
  constructor() {
7
15
  this.failedThemes = [];
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Converts JS object to SCSS list variable for theme fonts
3
+ * @example {
4
+ * main: {
5
+ * family: 'Jakarta',
6
+ * variants: [{
7
+ * path: '/font.woff2',
8
+ * style: 'normal',
9
+ * weight: '700'
10
+ * }]
11
+ * formats: ['woff2']
12
+ * }
13
+ * } => (
14
+ * 'main': (
15
+ * 'family': 'Jakarta',
16
+ * 'variants': (
17
+ * (
18
+ * 'path': '/font.woff2',
19
+ * 'style': 'normal',
20
+ * 'weight': '700',
21
+ * )
22
+ * )
23
+ * 'formats': ('woff2')
24
+ * )
25
+ * )
26
+ *
27
+ * @param {object} fonts
28
+ * @param {string }scssVarName
29
+ * @returns {string}
30
+ */
31
+ const fontObjToSCSS = ({fonts, scssVarName = 'global-theme-fonts'} = {}) => {
32
+ if (!Object.keys(fonts).length) {
33
+ return `$${scssVarName}: ()`;
34
+ }
35
+ const fontsVariantsVars = Object.entries(fonts).map(([name, font]) => {
36
+ return `
37
+ '${name}': (
38
+ 'family': '${font.family}',
39
+ 'variants': (
40
+ ${font.variants.map(item => `(
41
+ 'path': '${item.path}',
42
+ ${item.weight !== void 0 ? `'weight': ${item.weight},` : ''}
43
+ ${item.style !== void 0 ? `'style': ${item.style}` : ''}
44
+ )`).join(`, `)}
45
+ ),
46
+ 'formats': (${font.formats.map(format => `'${format}'`).join(', ')})
47
+ )
48
+ `;
49
+ });
50
+
51
+ return `
52
+ $${scssVarName}: (
53
+ ${fontsVariantsVars.join(`, `)}
54
+ );
55
+ `;
56
+ };
57
+
58
+ module.exports = fontObjToSCSS;
@@ -0,0 +1,5 @@
1
+ const createFontList = require('./create-font-list');
2
+
3
+ module.exports = {
4
+ createFontList
5
+ };
@@ -71,6 +71,8 @@ class ModulesConfigLoader {
71
71
  * @private
72
72
  */
73
73
  _collectThemes(themes, themesLocation, themeInfoFileName) {
74
+ this._processedFiles = [];
75
+
74
76
  this._bundles.forEach(bundle => {
75
77
  const source = bundle + themesLocation;
76
78
 
@@ -81,11 +83,20 @@ class ModulesConfigLoader {
81
83
  if (!fs.lstatSync(themePath).isDirectory()) {
82
84
  return;
83
85
  }
86
+
84
87
  const themeFile = path.resolve(themePath, themeInfoFileName);
85
- if (!fs.existsSync(themeFile)) return;
86
88
 
87
- const theme = yaml.load(fs.readFileSync(themeFile, 'utf8'));
88
- themes[name] = merge(themes[name] || {}, theme, {arrayMerge});
89
+ if (!fs.existsSync(themeFile)) {
90
+ return;
91
+ }
92
+
93
+ this._processedFiles.push(themeFile);
94
+
95
+ const doc = yaml.load(fs.readFileSync(themeFile, 'utf8'));
96
+
97
+ validation.checkSchema(themeFile, doc, name);
98
+
99
+ themes[name] = merge(themes[name] || {}, doc, {arrayMerge});
89
100
  });
90
101
  });
91
102
  }
@@ -19,7 +19,6 @@ const ThemeConfigFactory = require('./theme-config-factory');
19
19
  const path = require('path');
20
20
  const fs = require('fs');
21
21
  const prepareModulesMap = require('./plugin/map/prepare-modules-map');
22
- const CssToJsonPlugin = require('./plugin/css-to-json');
23
22
  const resolve = require('enhanced-resolve');
24
23
  const {merge: webpackMerge} = require('webpack-merge');
25
24
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
@@ -29,8 +28,10 @@ const EventEmitter = require('events');
29
28
  const ErrorHandler = require('./error-handler');
30
29
  const SVGSprite = require('./svg-sprite');
31
30
  const TerserPlugin = require('terser-webpack-plugin');
31
+ const jsToSCSS = require('./js-to-scss');
32
32
  require('resolve-url-loader');
33
33
  require('lezer-loader');
34
+
34
35
  class ConfigBuilder {
35
36
  constructor() {
36
37
  this._projectPath = '';
@@ -274,6 +275,7 @@ class ConfigBuilder {
274
275
 
275
276
  const webpackConfig = {
276
277
  watchOptions: {
278
+ followSymlinks: true,
277
279
  aggregateTimeout: 200,
278
280
  ignored: /[\/\\]node_modules[\/\\].*\.js$/
279
281
  },
@@ -319,44 +321,6 @@ class ConfigBuilder {
319
321
  /[\/\\]bundles[\/\\]\.*[\/\\]lib[\/\\](?!chaplin|bootstrap|jquery\.dialog).*\.js$/
320
322
  ],
321
323
  rules: [
322
- {
323
- test: /\.s?css$/,
324
- use: [{
325
- loader: args.hot ? 'style-loader' : MiniCssExtractPlugin.loader
326
- }, {
327
- loader: 'css-loader',
328
- options: {
329
- importLoaders: 1,
330
- sourceMap: true,
331
- // can't use esModule since resolve-url-loader needs file path to start with '~'
332
- esModule: false
333
- }
334
- }, {
335
- loader: 'resolve-url-loader'
336
- }, {
337
- loader: 'postcss-loader',
338
- options: {
339
- sourceMap: true,
340
- postcssOptions: {
341
- plugins: [
342
- require('autoprefixer')
343
- ]
344
- }
345
- }
346
- }, {
347
- loader: 'sass-loader',
348
- options: {
349
- implementation: require('sass'),
350
- sassOptions: {
351
- includePaths: [
352
- path.join(this.resolvedPublicPath, '/bundles')
353
- ],
354
- outputStyle: 'expanded'
355
- },
356
- sourceMap: true
357
- }
358
- }]
359
- },
360
324
  {
361
325
  test: /\.(eot|ttf|woff|woff2|cur|ico|svg|png|jpg|gif)$/,
362
326
  loader: 'url-loader',
@@ -502,7 +466,10 @@ class ConfigBuilder {
502
466
  );
503
467
  }
504
468
  }
505
- let {rtl_support: rtlSupport = false, resolve_extra_paths: resolveExtraPaths = []} = themeDefinition;
469
+ let {
470
+ rtl_support: rtlSupport = false,
471
+ resolve_extra_paths: resolveExtraPaths = []
472
+ } = themeDefinition;
506
473
  const resolvedBuildPath = path.join(this.resolvedPublicPath, buildPublicPath);
507
474
 
508
475
  if (resolveExtraPaths.length) {
@@ -534,10 +501,6 @@ class ConfigBuilder {
534
501
  }));
535
502
  }
536
503
 
537
- if (!skipCSS) {
538
- plugins.push(new CssToJsonPlugin());
539
- }
540
-
541
504
  const cssEntryPoints = !skipCSS ? this._getCssEntryPoints(buildName, buildPublicPath) : {};
542
505
  const jsEntryPoints = !skipJS ? jsBuildConfig.entry : {};
543
506
 
@@ -563,6 +526,45 @@ class ConfigBuilder {
563
526
  plugins,
564
527
  module: {
565
528
  rules: [
529
+ {
530
+ test: /\.s?css$/,
531
+ use: [{
532
+ loader: args.hot ? 'style-loader' : MiniCssExtractPlugin.loader
533
+ }, {
534
+ loader: 'css-loader',
535
+ options: {
536
+ importLoaders: 1,
537
+ sourceMap: true,
538
+ // can't use esModule since resolve-url-loader needs file path to start with '~'
539
+ esModule: false
540
+ }
541
+ }, {
542
+ loader: 'resolve-url-loader'
543
+ }, {
544
+ loader: 'postcss-loader',
545
+ options: {
546
+ sourceMap: true,
547
+ postcssOptions: {
548
+ plugins: [
549
+ require('autoprefixer')
550
+ ]
551
+ }
552
+ }
553
+ }, {
554
+ loader: 'sass-loader',
555
+ options: {
556
+ implementation: require('sass'),
557
+ sassOptions: {
558
+ includePaths: [
559
+ path.join(this.resolvedPublicPath, '/bundles')
560
+ ],
561
+ outputStyle: 'expanded'
562
+ },
563
+ sourceMap: true,
564
+ additionalData: this.prepareAdditionalSCSS.bind(this, themeDefinition)
565
+ }
566
+ }]
567
+ },
566
568
  {
567
569
  test: /[\/\\]configs\.json$/,
568
570
  loader: 'config-loader',
@@ -577,6 +579,18 @@ class ConfigBuilder {
577
579
  };
578
580
  }
579
581
 
582
+ prepareAdditionalSCSS(theme, content, loaderContext) {
583
+ if (typeof theme.fonts === 'object') {
584
+ const fonts = {};
585
+ for (const [name, {family, formats, variants}] of Object.entries(theme.fonts)) {
586
+ fonts[name] = {family, formats, variants};
587
+ }
588
+ return jsToSCSS.createFontList({fonts}) + content;
589
+ }
590
+
591
+ return content;
592
+ }
593
+
580
594
  _initialize(args, env) {
581
595
  const entryPointFileWriter = new EntryPointFileWriter(this._publicPath);
582
596
 
@@ -608,6 +622,14 @@ class ConfigBuilder {
608
622
  this.resolvedProjectPath,
609
623
  this._publicPath
610
624
  );
625
+
626
+ for (const [theme, config] of Object.entries(this._layoutModulesConfigLoader.themes)) {
627
+ validation.themeValidator.checkFullSchema(
628
+ config,
629
+ this._layoutModulesConfigLoader.processedFiles,
630
+ theme
631
+ );
632
+ }
611
633
  this._layoutStyleLoader = new LayoutStyleLoader(this._layoutModulesConfigLoader, entryPointFileWriter);
612
634
  this._layoutThemeConfigFactory = new ThemeConfigFactory(
613
635
  this._layoutModulesConfigLoader,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oroinc/oro-webpack-config-builder",
3
- "version": "6.1.0-lts06",
3
+ "version": "6.1.0-lts08",
4
4
  "author": "Oro, Inc. (https://oroinc.com)",
5
5
  "license": "MIT",
6
6
  "description": "An integration of OroPlatform based applications with the Webpack.",
@@ -16,9 +16,9 @@
16
16
  "css-loader": "^6.8.1",
17
17
  "css-minimizer-webpack-plugin": "~5.0.0",
18
18
  "deepmerge": "~4.3.1",
19
- "esbuild-loader": "^4.3.0",
20
19
  "exports-loader": "~4.0.0",
21
20
  "expose-loader": "~4.1.0",
21
+ "esbuild-loader": "^4.3.0",
22
22
  "file-loader": "~6.2.0",
23
23
  "html-webpack-plugin": "~5.5.0",
24
24
  "imports-loader": "~4.0.1",
@@ -30,7 +30,6 @@
30
30
  "path": "0.12.7",
31
31
  "postcss": "<8.4.33",
32
32
  "postcss-loader": "~7.3.4",
33
- "postcss-safe-parser": "^7.0.1",
34
33
  "printf": "~0.6.0",
35
34
  "resolve-url-loader": "^5.0.0",
36
35
  "rtlcss-webpack-plugin": "~4.0.6",
package/utils.js CHANGED
@@ -1,7 +1,7 @@
1
1
  let prodMode;
2
2
  let verboseMode;
3
3
 
4
- const utils = {
4
+ module.exports = {
5
5
  isVerboseMode() {
6
6
  if (verboseMode === void 0) {
7
7
  verboseMode = process.argv.filter(arg => {
@@ -13,7 +13,6 @@ const utils = {
13
13
 
14
14
  return verboseMode;
15
15
  },
16
-
17
16
  isProdMode() {
18
17
  if (prodMode !== void 0) {
19
18
  return prodMode;
@@ -27,18 +26,5 @@ const utils = {
27
26
  });
28
27
 
29
28
  return prodMode;
30
- },
31
-
32
- emptyLine(length) {
33
- return new Array(length).fill(' ').join('');
34
- },
35
-
36
- multiline(color, msg) {
37
- msg = ` ${msg}`;
38
- msg = msg + utils.emptyLine(120 - msg.length);
39
-
40
- return `\n${color(utils.emptyLine(msg.length))}\n${color(msg)}\n${color(utils.emptyLine(msg.length))}`;
41
29
  }
42
30
  };
43
-
44
- module.exports = utils;
@@ -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 "theme.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 Theme config';
33
+ }
34
+ }
35
+
36
+ module.exports = SvgIconsSchemaError;
@@ -1,15 +1,18 @@
1
1
  const assetsValidation = require('./assets-validator');
2
2
  const jsmodulesValidator = require('./jsmodules-validator');
3
3
  const svgIconsValidator = require('./svg-icons-validator');
4
+ const themeValidator = require('./theme-validator');
4
5
 
5
6
  const isJSModulesPath = path => /jsmodules([-a-zA-Z\d]*)\.yml$/.test(path);
6
7
  const isAssetsPath = path => /assets\.yml$/.test(path);
7
8
  const isSvgIconsPath = path => /svg-icons\.yml$/.test(path);
9
+ const isThemePath = path => /theme\.yml$/.test(path);
8
10
 
9
11
  module.exports = {
10
12
  assetsValidation,
11
13
  jsmodulesValidator,
12
14
  svgIconsValidator,
15
+ themeValidator,
13
16
 
14
17
  /**
15
18
  * set public path for all validators
@@ -36,6 +39,8 @@ module.exports = {
36
39
  }
37
40
  } else if (isSvgIconsPath(filePath)) {
38
41
  svgIconsValidator.checkSchema(filePath, doc, theme);
42
+ } else if (isThemePath(filePath)) {
43
+ themeValidator.checkSchema(filePath, doc, theme);
39
44
  }
40
45
  }
41
46
  };
@@ -10,7 +10,7 @@ module.exports = {
10
10
  type: 'object',
11
11
  properties: {
12
12
  exclude: {
13
- description: 'The "exclude" property is an array of svg files to exclude from svg sprite.',
13
+ description: 'The "exclude" property is an array of svg files to exclude from svg sprite.',
14
14
  type: 'array',
15
15
  minItems: 0
16
16
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Schema to validate theme.yml files
3
+ *
4
+ * @description
5
+ * Full scheme complements "theme-schema" one.
6
+ * It should have only rules which are not defined in "theme-schema" schema due to avoid duplicates in error messages
7
+ */
8
+ module.exports = {
9
+ type: 'object',
10
+ required: ['label']
11
+ };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Schema to validate theme.yml files
3
+ */
4
+ module.exports = {
5
+ type: 'object',
6
+ properties: {
7
+ label: {
8
+ type: 'string'
9
+ },
10
+ description: {
11
+ type: 'string'
12
+ },
13
+ groups: {
14
+ type: 'array',
15
+ items: {
16
+ type: 'string'
17
+ }
18
+ },
19
+ icon: {
20
+ type: 'string'
21
+ },
22
+ logo: {
23
+ type: 'string'
24
+ },
25
+ logo_small: {
26
+ type: 'string'
27
+ },
28
+ rtl_support: {
29
+ type: 'boolean'
30
+ },
31
+ svg_icons_support: {
32
+ type: 'boolean'
33
+ },
34
+ fonts: {
35
+ type: 'object',
36
+ patternProperties: {
37
+ '.*': {
38
+ type: 'object',
39
+ required: ['family', 'variants', 'formats'],
40
+ additionalProperties: false,
41
+ properties: {
42
+ family: {
43
+ type: 'string'
44
+ },
45
+ variants: {
46
+ type: 'array',
47
+ items: {
48
+ type: 'object',
49
+ required: ['path'],
50
+ additionalProperties: false,
51
+ properties: {
52
+ path: {
53
+ type: 'string'
54
+ },
55
+ weight: {
56
+ anyOf: [{
57
+ type: 'string'
58
+ }, {
59
+ type: 'integer'
60
+ }]
61
+ },
62
+ style: {
63
+ type: 'string'
64
+ }
65
+ }
66
+ }
67
+ },
68
+ formats: {
69
+ type: 'array',
70
+ items: {
71
+ type: 'string'
72
+ }
73
+ },
74
+ preload: {
75
+ type: 'boolean'
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+ };
83
+
@@ -4,7 +4,7 @@ const schemaValidator = require('./schema-validator');
4
4
  const {isProdMode} = require('../utils');
5
5
  const EventEmitter = require('events');
6
6
  const emitter = new EventEmitter();
7
- const SvgIsonsSchemaError = require('./errors/svg-icons-schema-error');
7
+ const SvgIconsSchemaError = require('./errors/svg-icons-schema-error');
8
8
 
9
9
  module.exports = Object.assign({}, schemaValidator, {
10
10
  emitter,
@@ -22,7 +22,7 @@ module.exports = Object.assign({}, schemaValidator, {
22
22
  const result = this.validateSchema(schema, doc);
23
23
 
24
24
  if (!result.valid) {
25
- const error = new SvgIsonsSchemaError(result.formattedError, [filePath], theme);
25
+ const error = new SvgIconsSchemaError(result.formattedError, [filePath], theme);
26
26
 
27
27
  this.emitter.emit('error', error);
28
28
  }
@@ -43,7 +43,7 @@ module.exports = Object.assign({}, schemaValidator, {
43
43
  const result = this.validateSchema(fullSchema, doc);
44
44
 
45
45
  if (!result.valid) {
46
- const error = new SvgIsonsSchemaError(result.formattedError, files, theme);
46
+ const error = new SvgIconsSchemaError(result.formattedError, files, theme);
47
47
 
48
48
  this.emitter.emit('error', error);
49
49
  }
@@ -0,0 +1,53 @@
1
+ const schema = require('./schemas/theme-schema');
2
+ const fullSchema = require('./schemas/theme-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 ThemeSchemaError = require('./errors/theme-scheme-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 ThemeSchemaError(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 ThemeSchemaError(result.formattedError, files, theme);
47
+
48
+ this.emitter.emit('error', error);
49
+ }
50
+
51
+ return result.valid;
52
+ }
53
+ });
@@ -1,66 +0,0 @@
1
- const path = require('path');
2
- const webpack = require('webpack');
3
- const jsonTransform = require('./json-transformer');
4
- const {multiline} = require('./../../utils');
5
- const {bgRed} = require('colorette');
6
-
7
- const isExportCssFile = filename => {
8
- return path.extname(filename) === '.css' && /_json(\.rtl)?$/.test(path.basename(filename, '.css'));
9
- };
10
-
11
- class CssToJsonPlugin {
12
- constructor(options) {}
13
-
14
- processAssets = (compilation, callback) => {
15
- const chunks = Array.from(compilation.chunks);
16
-
17
- chunks.forEach(chunk => {
18
- const files = Array.from(chunk.files);
19
-
20
- files.filter(isExportCssFile).forEach(filename => {
21
- const src = compilation.assets[filename].source();
22
- const json = jsonTransform(src);
23
-
24
- try {
25
- JSON.parse(json);
26
- } catch (e) {
27
- console.error(
28
- multiline(bgRed, `[ERROR] The JSON generated from the CSS file "${filename}" is invalid.`)
29
- );
30
- console.error(e.message);
31
- return;
32
- }
33
-
34
- let jsonFileName = '[name].json';
35
-
36
- if (filename.includes('rtl')) {
37
- jsonFileName = '[name].rtl.json';
38
- }
39
-
40
- const dstFileName = compilation.getPath(jsonFileName, {
41
- chunk,
42
- cssFileName: filename
43
- });
44
-
45
- compilation.assets[dstFileName] = new webpack.sources.RawSource(json);
46
- chunk.files.add(dstFileName);
47
- });
48
- });
49
-
50
- callback();
51
- };
52
-
53
- apply(compiler) {
54
- compiler.hooks.compilation.tap('CssToJsonPlugin', compilation => {
55
- compilation.hooks.processAssets.tapAsync(
56
- {
57
- name: 'CssToJsonPlugin',
58
- stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE
59
- },
60
- (chunks, callback) => this.processAssets(compilation, callback)
61
- );
62
- });
63
- }
64
- }
65
-
66
- module.exports = CssToJsonPlugin;
@@ -1,42 +0,0 @@
1
- const postcss = require('postcss');
2
- const safeParser = require('postcss-safe-parser');
3
-
4
- const normalizeSelector = selector => {
5
- return selector
6
- .replace(/^\./g, '')
7
- .replace(/:(\w+)/, (matched, group) => {
8
- return group.charAt(0).toUpperCase() + group.slice(1);
9
- })
10
- /*
11
- * #main > .item => _main__item
12
- * input[type="text"] => input_type_text
13
- * .button + .icon => button__icon
14
- * .p:has(.btn) => p_has__btn_
15
- */
16
- .replace(/[:\.\[\]\(\)#\s>+~]/g, '_');
17
- };
18
-
19
- const transform = src => {
20
- const classMap = {};
21
- const root = postcss.parse(src, {parser: safeParser});
22
- root.walkRules(rule => {
23
- const selector = rule.selector;
24
-
25
- if (!selector.startsWith('.')) {
26
- return;
27
- }
28
-
29
- const normalizedKey = normalizeSelector(selector);
30
- const props = [];
31
- rule.walkDecls(decl => {
32
- const important = decl.important ? ' !important' : '';
33
- props.push(`${decl.prop}: ${decl.value}${important}`);
34
- });
35
-
36
- classMap[normalizedKey] = props.join('; ') + (props.length ? ';' : '');
37
- });
38
-
39
- return JSON.stringify(classMap, null, 2);
40
- };
41
-
42
- module.exports = transform;