@caweb/css-audit-webpack-plugin 1.0.12 → 1.1.0

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.
@@ -12,21 +12,19 @@
12
12
  "author": "WordPress CSS Contributors",
13
13
  "license": "GPL-2.0-or-later",
14
14
  "dependencies": {
15
- "@wordpress/eslint-plugin": "9.0.4",
16
- "@wordpress/prettier-config": "1.0.3",
17
- "chalk": "4.1.1",
18
- "cli-table3": "0.6.0",
19
- "cosmiconfig": "7.0.0",
20
- "css-tree": "1.1.3",
21
- "cssom": "0.4.4",
22
- "eslint": "7.26.0",
23
- "fs-extra": "10.0.0",
24
- "glob": "7.1.7",
25
- "minimist": "1.2.6",
26
- "postcss": "8.2.15",
27
- "postcss-values-parser": "5.0.0",
28
- "prettier": "npm:wp-prettier@^2.0.5",
29
- "tinycolor2": "1.4.2"
15
+ "@wordpress/eslint-plugin": "^22.0.0",
16
+ "chalk": "5.4.1",
17
+ "cli-table3": "0.6.5",
18
+ "cosmiconfig": "9.0.0",
19
+ "css-tree": "3.1.0",
20
+ "cssom": "0.5.0",
21
+ "eslint": "^8.57.1",
22
+ "glob": "11.0.3",
23
+ "minimist": "1.2.8",
24
+ "postcss": "8.5.6",
25
+ "postcss-values-parser": "6.0.2",
26
+ "prettier": "npm:wp-prettier@3.0.3",
27
+ "tinycolor2": "1.6.0"
30
28
  },
31
29
  "eslintConfig": {
32
30
  "extends": [
@@ -49,8 +47,8 @@
49
47
  },
50
48
  "prettier": "@wordpress/prettier-config",
51
49
  "devDependencies": {
52
- "handlebars": "4.7.7",
53
- "jest": "26.6.3",
54
- "twing": "5.0.2"
50
+ "handlebars": "4.7.8",
51
+ "jest": "30.0.3",
52
+ "twig": "^1.17.1"
55
53
  }
56
54
  }
@@ -63,4 +63,18 @@ describe( 'Audit: Selectors', () => {
63
63
  const { value } = results.find( ( { id } ) => 'count' === id );
64
64
  expect( value ).toBe( 2 );
65
65
  } );
66
+
67
+ it( 'should handle modern CSS', () => {
68
+ expect( () => {
69
+ audit( [
70
+ {
71
+ name: 'a.css',
72
+ content: `h1, h2 { color: green; }
73
+ div:not([hidden]) { color: black; }
74
+ body :is(h1, h2) { color: red; }
75
+ body :where(h1, h2) { color: orange; }`,
76
+ },
77
+ ] );
78
+ } ).not.toThrow( SyntaxError );
79
+ } );
66
80
  } );
@@ -17,11 +17,9 @@ module.exports = function ( files = [] ) {
17
17
  csstree.walk( ast, {
18
18
  visit: 'MediaQuery',
19
19
  enter( node ) {
20
- if ( node.children ) {
21
- allQueries.push( csstree.generate( node ) );
22
- }
20
+ allQueries.push( csstree.generate( node ) );
23
21
  csstree.walk( node, {
24
- visit: 'MediaFeature',
22
+ visit: 'Feature',
25
23
  enter( sizeNode ) {
26
24
  if (
27
25
  sizeNode.name === 'max-width' ||
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
+ const csstree = require( 'css-tree' );
4
5
  const { parse } = require( 'postcss' );
5
6
 
6
7
  const { getSpecificityArray } = require( '../utils/get-specificity' );
@@ -12,10 +13,11 @@ module.exports = function ( files = [] ) {
12
13
  files.forEach( ( { name, content } ) => {
13
14
  const root = parse( content, { from: name } );
14
15
  root.walkRules( function ( { selector } ) {
15
- const selectorList = selector.split( ',' );
16
- selectorList.forEach( ( selectorName ) => {
17
- // Remove excess whitespace from selectors.
18
- selectorName = selectorName.replace( /\s+/g, ' ' ).trim();
16
+ const selectorList = csstree.parse( selector, {
17
+ context: 'selectorList',
18
+ } );
19
+ selectorList.children.forEach( ( _selector ) => {
20
+ const selectorName = csstree.generate( _selector );
19
21
  const [ a, b, c ] = getSpecificityArray( selectorName );
20
22
  const sum = 100 * a + 10 * b + c; // eslint-disable-line no-mixed-operators
21
23
  selectors.push( {
@@ -1,6 +1,6 @@
1
- const fs = require( 'fs-extra' );
1
+ const fs = require( 'node:fs' );
2
2
  const path = require( 'path' );
3
- const { TwingEnvironment, TwingLoaderFilesystem } = require( 'twing' );
3
+ const Twig = require( 'twig' );
4
4
 
5
5
  /**
6
6
  * Internal dependencies
@@ -23,9 +23,6 @@ function getTemplateFile( name ) {
23
23
  }
24
24
 
25
25
  module.exports = function ( reports ) {
26
- const loader = new TwingLoaderFilesystem( templatePath );
27
- const twing = new TwingEnvironment( loader, { debug: true } );
28
-
29
26
  const reportName = getArg( '--filename' );
30
27
  const reportTemplate = getTemplateFile( reportName );
31
28
  const reportDestDir = path.join( __dirname, '..', '..', 'public' );
@@ -38,15 +35,18 @@ module.exports = function ( reports ) {
38
35
  // Copy CSS src to /public
39
36
  const cssSrc = path.join( __dirname, 'html', 'style.css' );
40
37
  const cssDest = path.join( reportDestDir, 'style.css' );
41
- fs.copyFile( cssSrc, cssDest );
38
+ fs.copyFileSync( cssSrc, cssDest );
42
39
 
43
- twing
44
- .render( reportTemplate, context )
45
- .then( ( output ) => {
46
- console.log( `Generated template for ${ reportName }.` );
47
- fs.writeFileSync( reportDest, output );
48
- } )
49
- .catch( ( e ) => {
50
- console.error( e );
51
- } );
40
+ Twig.renderFile(
41
+ path.resolve( __dirname, 'html', reportTemplate ),
42
+ context,
43
+ ( error, output ) => {
44
+ if ( error ) {
45
+ console.error( e );
46
+ } else {
47
+ console.log( `Generated template for ${ reportName }.` );
48
+ fs.writeFileSync( reportDest, output );
49
+ }
50
+ }
51
+ );
52
52
  };
@@ -14,6 +14,8 @@ describe( 'Calculate Specificity', () => {
14
14
  it( 'should calculate for pseudo-classes', () => {
15
15
  expect( getSpecificity( ':checked' ) ).toBe( 10 );
16
16
  expect( getSpecificity( 'a:link' ) ).toBe( 11 );
17
+ expect( getSpecificity( 'body:lang(en)' ) ).toBe( 11 );
18
+ expect( getSpecificity( 'body:lang(en,ja)' ) ).toBe( 11 );
17
19
  } );
18
20
 
19
21
  it( 'should calculate for class selectors', () => {
@@ -36,4 +38,25 @@ describe( 'Calculate Specificity', () => {
36
38
  getSpecificity( 'li > a[href*="en-US"] > .inline-warning' )
37
39
  ).toBe( 22 );
38
40
  } );
41
+
42
+ it( 'should calculate for :is selectors', () => {
43
+ expect( getSpecificity( ':is(h1)' ) ).toBe( 1 );
44
+ expect( getSpecificity( ':is(h1, .class)' ) ).toBe( 10 );
45
+ expect( getSpecificity( ':is(h1, .class, #id)' ) ).toBe( 100 );
46
+ expect( getSpecificity( 'span:is(h1)' ) ).toBe( 2 );
47
+ } );
48
+
49
+ it( 'should calculate for :where selectors', () => {
50
+ expect( getSpecificity( ':where(h1)' ) ).toBe( 0 );
51
+ expect( getSpecificity( ':where(h1, .class)' ) ).toBe( 0 );
52
+ expect( getSpecificity( ':where(h1, .class, #id)' ) ).toBe( 0 );
53
+ expect( getSpecificity( 'span:where(h1)' ) ).toBe( 1 );
54
+ } );
55
+
56
+ it( 'should calculate for :not selectors', () => {
57
+ expect( getSpecificity( ':not(h1)' ) ).toBe( 1 );
58
+ expect( getSpecificity( ':not(h1, .class)' ) ).toBe( 10 );
59
+ expect( getSpecificity( ':not(h1, .class, #id)' ) ).toBe( 100 );
60
+ expect( getSpecificity( 'span:not(h1)' ) ).toBe( 2 );
61
+ } );
39
62
  } );
@@ -14,10 +14,28 @@ function calculateSpecificity( [ a, b, c ], selector ) {
14
14
  if ( ! selector.type ) {
15
15
  return;
16
16
  }
17
- if ( 'lang' !== selector.name && selector.children ) {
18
- return selector.children
19
- .toArray()
20
- .reduce( calculateSpecificity, [ a, b, c ] );
17
+
18
+ if ( 'PseudoClassSelector' === selector.type && 'lang' !== selector.name ) {
19
+ if ( 'where' === selector.name ) {
20
+ return [ a, b, c ];
21
+ }
22
+ if ( selector.children ) {
23
+ let maxSpec = [ 0, 0, 0 ];
24
+ let max = -1;
25
+ selector.children.forEach( ( list ) => {
26
+ if ( 'SelectorList' === list.type ) {
27
+ list.children.forEach( ( s ) => {
28
+ const _selector = csstree.generate( s );
29
+ const result = getSpecificity( _selector );
30
+ if ( result > max ) {
31
+ maxSpec = getSpecificityArray( _selector );
32
+ max = result;
33
+ }
34
+ } );
35
+ }
36
+ } );
37
+ return [ a + maxSpec[ 0 ], b + maxSpec[ 1 ], c + maxSpec[ 2 ] ];
38
+ }
21
39
  }
22
40
 
23
41
  switch ( selector.type ) {
@@ -65,11 +83,10 @@ function calculateSpecificity( [ a, b, c ], selector ) {
65
83
  function getSpecificity( selector ) {
66
84
  const node = csstree.parse( selector, { context: 'selector' } );
67
85
  const selectorList = node.children.toArray();
68
- const [ a, b, c ] = selectorList.reduce( calculateSpecificity, [
69
- 0,
70
- 0,
71
- 0,
72
- ] );
86
+ const [ a, b, c ] = selectorList.reduce(
87
+ calculateSpecificity,
88
+ [ 0, 0, 0 ]
89
+ );
73
90
  return 100 * a + 10 * b + c;
74
91
  }
75
92
 
@@ -82,11 +99,10 @@ function getSpecificity( selector ) {
82
99
  function getSpecificityArray( selector ) {
83
100
  const node = csstree.parse( selector, { context: 'selector' } );
84
101
  const selectorList = node.children.toArray();
85
- const [ a, b, c ] = selectorList.reduce( calculateSpecificity, [
86
- 0,
87
- 0,
88
- 0,
89
- ] );
102
+ const [ a, b, c ] = selectorList.reduce(
103
+ calculateSpecificity,
104
+ [ 0, 0, 0 ]
105
+ );
90
106
  return [ a, b, c ];
91
107
  }
92
108
 
package/index.js CHANGED
@@ -24,14 +24,14 @@ const currentPath = path.dirname(fileURLToPath(import.meta.url));
24
24
 
25
25
  // CSS Audit Plugin
26
26
  class CSSAuditPlugin {
27
- config = {}
27
+ config = {};
28
28
 
29
29
  constructor(opts = {}) {
30
-
31
- // if no outputFolder is defined fallback to the default path
32
- if( ! opts.outputFolder ){
33
- opts.outputFolder = path.join(currentPath, 'bin', 'auditor', 'public')
34
- // path must be absolute
30
+
31
+ // if no outputFolder is defined fallback to the default path
32
+ if( ! opts.outputFolder ){
33
+ opts.outputFolder = path.join(process.cwd(), 'audits' )
34
+ // if outputfolder is set and not absolute
35
35
  }else if( ! path.isAbsolute(opts.outputFolder) ){
36
36
  opts.outputFolder = path.join(process.cwd(), opts.outputFolder );
37
37
  }
@@ -44,21 +44,15 @@ class CSSAuditPlugin {
44
44
  directory: this.config.outputFolder,
45
45
  watch: true
46
46
  }
47
- let { devServer, output } = compiler.options;
48
- let hostUrl = 'localhost' === devServer.host ? `http://${devServer.host}`: devServer.host;
49
- let hostPort = devServer.port;
50
-
51
- if( hostPort && 80 !== hostPort )
52
- {
53
- hostUrl = `${hostUrl}:${hostPort}`;
54
- }
47
+ let { devServer } = compiler.options;
48
+ let auditUrl = `${devServer.server}://${devServer.host}:${devServer.port}`;
55
49
 
56
50
  // if dev server allows for multiple pages to be opened
57
51
  // add css-audit.html to open property.
58
52
  if( Array.isArray(devServer.open) ){
59
- devServer.open.push(`${hostUrl}/${this.config.rewrite ? this.config.rewrite : this.config.filename}.html`)
53
+ devServer.open.push(`${auditUrl}/${this.config.filename}.html`)
60
54
  }else if( 'object' === typeof devServer.open && Array.isArray(devServer.open.target) ){
61
- devServer.open.target.push(`${hostUrl}/${this.config.filename}.html`)
55
+ devServer.open.target.push(`${auditUrl}/${this.config.filename}.html`)
62
56
  }
63
57
 
64
58
  // add our static directory
@@ -146,7 +140,7 @@ class CSSAuditPlugin {
146
140
  )
147
141
  }
148
142
 
149
- console.log(`<i> ${boldGreen('[webpack-dev-middleware] CSS Audit can be viewed at')} ${ boldBlue(new URL(`${hostUrl}/${this.config.filename}.html`).toString()) }`);
143
+ console.log(`<i> ${boldGreen('[webpack-dev-middleware] CSS Audit can be viewed at')} ${ boldBlue(new URL(`${auditUrl}/${this.config.filename}.html`).toString()) }`);
150
144
 
151
145
  callback();
152
146
  }
@@ -156,6 +150,9 @@ class CSSAuditPlugin {
156
150
 
157
151
  }
158
152
 
153
+
154
+
155
+
159
156
  /**
160
157
  * Run WordPress CSS Audit
161
158
  *
@@ -191,10 +188,10 @@ class CSSAuditPlugin {
191
188
 
192
189
  let filesToBeAudited = [];
193
190
  let filesWithIssues = [];
194
-
191
+
195
192
  // the css audit tool always outputs to its own public directory
196
193
  let defaultOutputPath = path.join(currentPath, 'bin', 'auditor', 'public');
197
-
194
+
198
195
  // we always make sure the output folder exists
199
196
  fs.mkdirSync( outputFolder, { recursive: true } );
200
197
 
@@ -258,7 +255,7 @@ class CSSAuditPlugin {
258
255
  important && ! processArgs.includes('--no-important') ? '--important' : '',
259
256
  displayNone && ! processArgs.includes('--no-display-none') ? '--display-none' : '',
260
257
  selectors && ! processArgs.includes('--no-selectors') ? '--selectors' : '',
261
- mediaQueries && ! processArgs.includes('--no-media-queries') ? '--media-queries' : '',
258
+ // mediaQueries && ! processArgs.includes('--no-media-queries') ? '--media-queries' : '',
262
259
  typography && ! processArgs.includes('--no-typography') ? '--typography' : '',
263
260
  format ? `--format=${format}` : '',
264
261
  filename ? `--filename=${path.basename(process.cwd())}` : ''
@@ -269,25 +266,27 @@ class CSSAuditPlugin {
269
266
  auditArgs.push(`--property-values=${p.replace(' ',',')}`)
270
267
  })
271
268
  }
272
-
269
+
273
270
  let { stdout, stderr } = spawn.sync(
274
- 'node ' + resolveBin('@caweb/css-audit-webpack-plugin', {executable: 'auditor'}),
271
+ 'node',
275
272
  [
273
+ resolveBin('@caweb/css-audit-webpack-plugin', {executable: 'auditor'}),
274
+ // '--',
276
275
  ...filesToBeAudited,
277
276
  ...auditArgs
278
277
  ],
279
278
  {
279
+ shell: false,
280
280
  stdio: 'pipe',
281
281
  cwd: fs.existsSync(path.join(process.cwd(), 'css-audit.config.cjs')) ? process.cwd() : currentPath
282
282
  }
283
283
  )
284
-
284
+
285
285
  if( stderr && stderr.toString() ){
286
286
  console.log( stderr.toString() )
287
287
  }
288
288
 
289
289
  if( stdout && stdout.toString() ){
290
-
291
290
 
292
291
  // rename the file back to the intended file name instead of the project name
293
292
  let outputFile = path.join(outputFolder, `${filename}.html`);
@@ -305,7 +304,7 @@ class CSSAuditPlugin {
305
304
  )
306
305
  }
307
306
 
308
- let msg = stdout.toString().replace('undefined', '');
307
+ let msg = stdout.toString().replace('undefined', '').replace('template', 'css audit');
309
308
 
310
309
  // the command was ran via cli
311
310
  if( 'audit' === process.argv[2] ){
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caweb/css-audit-webpack-plugin",
3
- "version": "1.0.12",
3
+ "version": "1.1.0",
4
4
  "description": "CAWebPublishing Webpack Plugin to run WordPress CSS Audit",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -40,14 +40,15 @@
40
40
  "test": "echo \"Error: run tests from root\" && exit 0"
41
41
  },
42
42
  "dependencies": {
43
- "chalk": "^5.3.0",
44
- "cross-spawn": "^7.0.3",
43
+ "chalk": "^5.4.1",
44
+ "cross-spawn": "^7.0.6",
45
45
  "deepmerge": "^4.3.1",
46
46
  "get-all-files": "^5.0.0",
47
- "resolve-bin": "^1.0.1"
47
+ "resolve-bin": "^1.0.1",
48
+ "twig": "^1.17.1"
48
49
  },
49
50
  "devDependencies": {
50
- "webpack": "^5.96.1",
51
- "webpack-cli": "^5.1.4"
51
+ "webpack": "^5.99.9",
52
+ "webpack-cli": "^6.0.1"
52
53
  }
53
54
  }