@caweb/css-audit-webpack-plugin 1.0.12 → 2.0.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
@@ -15,7 +15,7 @@ import chalk from 'chalk';
15
15
  import { fileURLToPath, URL } from 'url';
16
16
 
17
17
  // default configuration
18
- import {default as DefaultConfig} from './default.config.js';
18
+ import {default as DefaultConfig} from './css-audit.config.js';
19
19
 
20
20
  const boldWhite = chalk.bold.white;
21
21
  const boldGreen = chalk.bold.green;
@@ -24,50 +24,67 @@ 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
+ // the default publicPath is always the outputFolder
31
+ DefaultConfig.publicPath = DefaultConfig.outputFolder;
30
32
 
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
35
- }else if( ! path.isAbsolute(opts.outputFolder) ){
36
- opts.outputFolder = path.join(process.cwd(), opts.outputFolder );
37
- }
33
+ // the default output folder is always relative to the current working directory.
34
+ DefaultConfig.outputFolder = path.join( process.cwd(), DefaultConfig.outputFolder );
35
+
36
+ // if opts.outputFolder is defined
37
+ if( opts.outputFolder && ! path.isAbsolute(opts.outputFolder) ){
38
+ opts.publicPath = opts.outputFolder;
38
39
 
40
+ // we join the current working directory with the opts.outputFolder
41
+ opts.outputFolder = path.join( process.cwd(), opts.outputFolder );
42
+ }
43
+
39
44
  this.config = deepmerge(DefaultConfig, opts);
40
45
  }
41
46
 
42
47
  apply(compiler) {
43
48
  const staticDir = {
44
49
  directory: this.config.outputFolder,
50
+ publicPath: encodeURI(this.config.publicPath).replace(':', ''),
45
51
  watch: true
46
52
  }
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
- }
55
53
 
56
- // if dev server allows for multiple pages to be opened
57
- // add css-audit.html to open property.
58
- if( Array.isArray(devServer.open) ){
59
- devServer.open.push(`${hostUrl}/${this.config.rewrite ? this.config.rewrite : this.config.filename}.html`)
60
- }else if( 'object' === typeof devServer.open && Array.isArray(devServer.open.target) ){
61
- devServer.open.target.push(`${hostUrl}/${this.config.filename}.html`)
54
+ let { devServer } = compiler.options;
55
+ let auditUrl = `${devServer.server}://${devServer.host}:${devServer.port}`;
56
+ let nodeModulePath = encodeURI(this.config.publicPath).replace(':', '') + '/node_modules';
57
+ let pathRewrite = {};
58
+ pathRewrite[`^${nodeModulePath}`] = '';
59
+
60
+ let proxy = {
61
+ context: [ nodeModulePath ],
62
+ target: auditUrl,
63
+ pathRewrite,
64
+ };
65
+
66
+ // we add the proxy to the devServer
67
+ if( Array.isArray(devServer.proxy) ){
68
+ devServer.proxy.push(proxy)
69
+ }else{
70
+ devServer.proxy = [].concat(devServer.proxy, proxy );
62
71
  }
63
72
 
64
- // add our static directory
73
+ // add our static directory to the devServer
65
74
  if( Array.isArray(devServer.static) ){
66
75
  devServer.static.push(staticDir)
67
76
  }else{
68
77
  devServer.static = [].concat(devServer.static, staticDir );
69
78
  }
70
-
79
+
80
+ // if dev server allows for multiple pages to be opened
81
+ // add filename.html to open property.
82
+ if( Array.isArray(devServer.open) ){
83
+ devServer.open.push(`${staticDir.publicPath}/${this.config.filename}.html`)
84
+ }else if( 'object' === typeof devServer.open && Array.isArray(devServer.open.target) ){
85
+ devServer.open.target.push(`${staticDir.publicPath}/${this.config.filename}.html`)
86
+ }
87
+
71
88
  // we always make sure the output folder exists
72
89
  fs.mkdirSync( staticDir.directory, { recursive: true } );
73
90
 
@@ -134,7 +151,29 @@ class CSSAuditPlugin {
134
151
  })
135
152
  console.log(`<i> ${boldGreen('[webpack-dev-middleware] Running CSS Audit...')}`);
136
153
 
137
- let result = this.audit(files, this.config );
154
+ let audits = {};
155
+ this.config.audits.forEach( (audit) => {
156
+ let key = 'string' === typeof audit ? audit : audit[0];
157
+ let value = 'string' === typeof audit ? true : audit[1];
158
+
159
+ // fix key
160
+ key = key.replace(/-\w/g, (m) => m[1].toUpperCase());
161
+
162
+ // if key already exists, push value to array
163
+ if( audits.hasOwnProperty(key) ){
164
+ audits[key].push(value);
165
+ }else{
166
+ // otherwise, if the audit is an array create a new array with the value
167
+ audits[key] = ! Array.isArray(audit) ? value : [value];
168
+ }
169
+ });
170
+
171
+ let result = this.audit(files, {
172
+ format: this.config.format,
173
+ filename: this.config.filename,
174
+ outputFolder: this.config.outputFolder,
175
+ ...audits,
176
+ } );
138
177
 
139
178
  if( result ){
140
179
  // we have to inject the css-audit.update.js file into the head in order for the webpack-dev-server scripts to load.
@@ -146,7 +185,7 @@ class CSSAuditPlugin {
146
185
  )
147
186
  }
148
187
 
149
- console.log(`<i> ${boldGreen('[webpack-dev-middleware] CSS Audit can be viewed at')} ${ boldBlue(new URL(`${hostUrl}/${this.config.filename}.html`).toString()) }`);
188
+ console.log(`<i> ${boldGreen('[webpack-dev-middleware] CSS Audit can be viewed at')} ${ boldBlue(new URL(`${auditUrl}${staticDir.publicPath}/${this.config.filename}.html`).toString()) }`);
150
189
 
151
190
  callback();
152
191
  }
@@ -156,6 +195,9 @@ class CSSAuditPlugin {
156
195
 
157
196
  }
158
197
 
198
+
199
+
200
+
159
201
  /**
160
202
  * Run WordPress CSS Audit
161
203
  *
@@ -191,10 +233,10 @@ class CSSAuditPlugin {
191
233
 
192
234
  let filesToBeAudited = [];
193
235
  let filesWithIssues = [];
194
-
236
+
195
237
  // the css audit tool always outputs to its own public directory
196
238
  let defaultOutputPath = path.join(currentPath, 'bin', 'auditor', 'public');
197
-
239
+
198
240
  // we always make sure the output folder exists
199
241
  fs.mkdirSync( outputFolder, { recursive: true } );
200
242
 
@@ -258,7 +300,7 @@ class CSSAuditPlugin {
258
300
  important && ! processArgs.includes('--no-important') ? '--important' : '',
259
301
  displayNone && ! processArgs.includes('--no-display-none') ? '--display-none' : '',
260
302
  selectors && ! processArgs.includes('--no-selectors') ? '--selectors' : '',
261
- mediaQueries && ! processArgs.includes('--no-media-queries') ? '--media-queries' : '',
303
+ // mediaQueries && ! processArgs.includes('--no-media-queries') ? '--media-queries' : '',
262
304
  typography && ! processArgs.includes('--no-typography') ? '--typography' : '',
263
305
  format ? `--format=${format}` : '',
264
306
  filename ? `--filename=${path.basename(process.cwd())}` : ''
@@ -269,25 +311,27 @@ class CSSAuditPlugin {
269
311
  auditArgs.push(`--property-values=${p.replace(' ',',')}`)
270
312
  })
271
313
  }
272
-
314
+
273
315
  let { stdout, stderr } = spawn.sync(
274
- 'node ' + resolveBin('@caweb/css-audit-webpack-plugin', {executable: 'auditor'}),
316
+ 'node',
275
317
  [
318
+ resolveBin('@caweb/css-audit-webpack-plugin', {executable: 'auditor'}),
319
+ // '--',
276
320
  ...filesToBeAudited,
277
321
  ...auditArgs
278
322
  ],
279
323
  {
324
+ shell: false,
280
325
  stdio: 'pipe',
281
326
  cwd: fs.existsSync(path.join(process.cwd(), 'css-audit.config.cjs')) ? process.cwd() : currentPath
282
327
  }
283
328
  )
284
-
329
+
285
330
  if( stderr && stderr.toString() ){
286
331
  console.log( stderr.toString() )
287
332
  }
288
333
 
289
334
  if( stdout && stdout.toString() ){
290
-
291
335
 
292
336
  // rename the file back to the intended file name instead of the project name
293
337
  let outputFile = path.join(outputFolder, `${filename}.html`);
@@ -305,7 +349,7 @@ class CSSAuditPlugin {
305
349
  )
306
350
  }
307
351
 
308
- let msg = stdout.toString().replace('undefined', '');
352
+ let msg = stdout.toString().replace('undefined', '').replace('template', 'css audit');
309
353
 
310
354
  // the command was ran via cli
311
355
  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": "2.0.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.101.0",
52
+ "webpack-cli": "^6.0.1"
52
53
  }
53
54
  }
@@ -1,5 +0,0 @@
1
- /**
2
- * External dependencies
3
- */
4
-
5
- module.exports = {};
package/default.config.js DELETED
@@ -1,19 +0,0 @@
1
- /**
2
- * External dependencies
3
- */
4
-
5
- export default {
6
- format: 'html',
7
- filename: 'css-audit',
8
- colors: true,
9
- important: true,
10
- displayNone: true,
11
- selectors: true,
12
- mediaQueries: true,
13
- typography: true,
14
- propertyValues: [
15
- 'font-size',
16
- 'padding,padding-top,padding-bottom,padding-right,padding-left' ,
17
- 'property-values', 'margin,margin-top,marin-bottom,marin-right,marin-left',
18
- ]
19
- };