From f22e84a143b66350f10850695b1ed86300355fcc Mon Sep 17 00:00:00 2001 From: madhusudhand Date: Wed, 4 Sep 2024 16:32:48 +0530 Subject: [PATCH 1/4] add style.css metadata validations to theme validation script --- package.json | 2 +- theme-utils.mjs | 291 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 265 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 25fd5fff36..a2c3e9f699 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "core:push": "node ./theme-utils.mjs push-core-themes", "core:sync": "node ./theme-utils.mjs sync-core-theme", "patterns:escape": "node ./theme-utils.mjs escape-patterns", - "validate:json": "node ./theme-utils.mjs validate-theme", + "validate:theme": "node ./theme-utils.mjs validate-theme", "prepare": "husky" }, "devDependencies": { diff --git a/theme-utils.mjs b/theme-utils.mjs index 523261c758..fcf70a8aaa 100644 --- a/theme-utils.mjs +++ b/theme-utils.mjs @@ -749,9 +749,9 @@ export function getThemeMetadata(styleCss, attribute, trimWPCom = true) { .match(/(?<=Version:\s*).*?(?=\s*\r?\n|\rg)/gs)?.[0] ?.trim(); return trimWPCom ? version.replace('-wpcom', '') : version; - case 'Requires at least': + default: return styleCss - .match(/(?<=Requires at least:\s*).*?(?=\s*\r?\n|\rg)/gs)?.[0] + .match(new RegExp(`(?<=${attribute}:\\s*).*?(?=\\s*\\r?\\n|\\rg)`, 'gs'))?.[0] ?.trim(); } } @@ -1542,33 +1542,270 @@ async function validateThemes( themes, { format, color, tableWidth } ) { : undefined; const isSupportedWpVersion = wpVersion && semver.gte( `${ wpVersion }.0`, '5.9.0' ) - if ( ! wpVersion ) { - problems.push( - createProblem( { - type: 'error', - file: styleCssPath, - data: { - // prettier-ignore - message: `missing ${ chalkStr.green( "'Requires at least'" ) } header metadata`, - }, - } ) - ); - } + const validators = { + isValidVersion( attr, value, validLengths = [ 3 ] ) { + const adjustedValue = + value && `${ value }.0`.split( '.', 3 ).join( '.' ); + if ( + ! value || + ! validLengths.includes( value.split( '.' ).length ) || + ! semver.valid( adjustedValue ) + ) { + return { + actual: `${ chalkStr.green( + attr + ) }: ${ chalkStr.yellow( value ) }`, + expected: `format ${ chalkStr.yellow( + Array.from( { length: Math.min( validLengths ) } ) + .fill( 'x' ) + .join( '.' ) + ) }`, + message: `${ value } is not a valid version`, + }; + } + }, + isGreaterOrEqual( attr, value, version ) { + const adjustedValue = + value && `${ value }.0`.split( '.', 3 ).join( '.' ); + const adjustedVersion = + version && `${ version }.0`.split( '.', 3 ).join( '.' ); + if ( + ! value || + ! version || + ! semver.valid( adjustedValue ) || + ! semver.valid( adjustedVersion ) || + ! semver.gte( adjustedValue, adjustedVersion ) + ) { + return { + actual: `${ chalkStr.green( + attr + ) }: ${ chalkStr.yellow( value ) }`, + expected: `${ chalkStr.yellow( version ) } or greater`, + message: `provide a valid version value`, + }; + } + }, + isUri: ( attr, value ) => { + if ( value && ! URL.canParse( value ) ) { + return { + actual: `${ chalkStr.green( + attr + ) }: ${ chalkStr.yellow( value ) }`, + expected: `a valid URI`, + message: `${ value } is not a valid URI`, + }; + } + }, + isValidSlug: ( attr, value ) => { + if ( value && ! /^[a-z0-9-]+$/.test( value ) ) { + return { + actual: `${ chalkStr.green( + attr + ) }: ${ chalkStr.yellow( value ) }`, + expected: `a valid value`, + message: `${ value } is not a valid value`, + }; + } + }, + // a8c validations + isA8CThemeUri: ( attr, value ) => { + if ( + value && + ! /^https:\/\/wordpress\.com\/themes\/[a-z0-9-]+\/?$/.test( + value + ) + ) { + return { + actual: `${ chalkStr.green( + attr + ) }: ${ chalkStr.yellow( value ) }`, + expected: `https://wordpress.com/themes/${ chalkStr.yellow( '{slug}' ) }/`, + message: `${ value } is not a valid WordPress.com theme URI`, + }; + } + }, + isA8CAuthor: ( attr, value ) => { + if ( value && ! /^Automattic$/.test( value ) ) { + return { + actual: `${ chalkStr.green( + attr + ) }: ${ chalkStr.yellow( value ) }`, + expected: `Automattic`, + message: `${ value } is not a valid author`, + }; + } + }, + isA8CAuthorUri: ( attr, value ) => { + if ( + value && + ! /^https:\/\/automattic\.com\/$/.test( value ) + ) { + return { + actual: `${ chalkStr.green( + attr + ) }: ${ chalkStr.yellow( value ) }`, + expected: `https://automattic.com/`, + message: `${ value } is not a valid Automattic author URI`, + }; + } + }, + }; - if ( ! isSupportedWpVersion ) { - problems.push( - createProblem( { - type: 'warning', - file: styleCssPath, - // prettier-ignore - data: { - actual: chalkStr.yellow( wpVersion ), - expected: `${ chalkStr.yellow( '5.9' ) } or greater`, - message: `the ${ chalkStr.green( "'Requires at least'" ) } version does not support theme.json`, + // validate style.css metadata + // Spec: https://developer.wordpress.org/themes/basics/main-stylesheet-style-css/ + const styleCssMetadata = [ + { attribute: 'Theme Name', required: true }, + { + attribute: 'Theme URI', + validators: [ + { + validate: validators.isUri, + type: 'warning', }, - } ) - ); - } + { + validate: validators.isA8CThemeUri, + type: 'warning', + }, + ], + }, + { + attribute: 'Author', + required: true, + validators: [ + { + validate: validators.isA8CAuthor, + type: 'warning', + }, + ], + }, + { + attribute: 'Author URI', + validators: [ + { + validate: validators.isUri, + type: 'warning', + }, + { + validate: validators.isA8CAuthorUri, + type: 'warning', + }, + ], + }, + { attribute: 'Description', required: true }, + { + attribute: 'Version', + required: true, + validators: [ + { + validate: ( attr, value ) => + validators.isValidVersion( attr, value, [ 3 ] ), + type: 'error', + }, + ], + }, + { + attribute: 'Requires at least', + required: true, + validators: [ + { + validate: ( attr, value ) => + validators.isValidVersion( attr, value, [ 2 ] ), + type: 'error', + }, + { + validate: ( attr, value ) => + validators.isGreaterOrEqual( + attr, + `${ value }.0`, + '5.9.0' + ), + type: 'error', + }, + ], + }, + { + attribute: 'Tested up to', + required: true, + validators: [ + { + validate: ( attr, value ) => + validators.isValidVersion( attr, value, [ 2, 3 ] ), + type: 'error', + }, + { + validate: ( attr, value ) => + validators.isGreaterOrEqual( + attr, + value, + themeRequires + ), + type: 'error', + }, + ], + }, + { + attribute: 'Requires PHP', + required: true, + validators: [ + { + validate: ( attr, value ) => + validators.isValidVersion( attr, value, [ 2 ] ), + type: 'error', + }, + ], + }, + { attribute: 'License', required: true }, + { + attribute: 'License URI', + required: true, + validators: [ + { + validate: validators.isUri, + type: 'warning', + }, + ], + }, + { + attribute: 'Text Domain', + required: true, + validators: [ + { + validate: validators.isValidSlug, + type: 'error', + }, + ], + }, + ]; + + styleCssMetadata.forEach( ( { attribute, required, validators } ) => { + const attributeValue = getThemeMetadata( styleCss, attribute ); + if ( ! attributeValue ) { + problems.push( + createProblem( { + type: required ? 'error' : 'warning', + file: styleCssPath, + data: { + message: `missing ${ chalkStr.green( + attribute + ) } header metadata`, + }, + } ) + ); + } else if ( validators ) { + validators.forEach( ( { validate, type } ) => { + const problem = validate( attribute, attributeValue ); + if ( problem ) { + problems.push( + createProblem( { + type: type, + file: styleCssPath, + data: problem, + } ) + ); + } + } ); + } + } ); const validations = await Promise.all( [ glob( `${ themeSlug }/styles/*.json` ).then( ( paths ) => ( { From 8e225d8cfcdc28e28e449f3480fde9e86d68fc03 Mon Sep 17 00:00:00 2001 From: madhusudhand Date: Mon, 9 Sep 2024 14:38:14 +0530 Subject: [PATCH 2/4] rename validator function names and return a consistent value --- theme-utils.mjs | 107 +++++++++++++++++++++++++++++------------------- 1 file changed, 64 insertions(+), 43 deletions(-) diff --git a/theme-utils.mjs b/theme-utils.mjs index fcf70a8aaa..01c5c4572a 100644 --- a/theme-utils.mjs +++ b/theme-utils.mjs @@ -1543,7 +1543,8 @@ async function validateThemes( themes, { format, color, tableWidth } ) { const isSupportedWpVersion = wpVersion && semver.gte( `${ wpVersion }.0`, '5.9.0' ) const validators = { - isValidVersion( attr, value, validLengths = [ 3 ] ) { + validateVersion( attr, value, validLengths = [ 3 ] ) { + const problems = []; const adjustedValue = value && `${ value }.0`.split( '.', 3 ).join( '.' ); if ( @@ -1551,7 +1552,7 @@ async function validateThemes( themes, { format, color, tableWidth } ) { ! validLengths.includes( value.split( '.' ).length ) || ! semver.valid( adjustedValue ) ) { - return { + problems.push( { actual: `${ chalkStr.green( attr ) }: ${ chalkStr.yellow( value ) }`, @@ -1561,10 +1562,12 @@ async function validateThemes( themes, { format, color, tableWidth } ) { .join( '.' ) ) }`, message: `${ value } is not a valid version`, - }; + } ); } + return { isValid: ! problems.length, problems }; }, - isGreaterOrEqual( attr, value, version ) { + validateVersionGte( attr, value, version ) { + const problems = []; const adjustedValue = value && `${ value }.0`.split( '.', 3 ).join( '.' ); const adjustedVersion = @@ -1576,78 +1579,91 @@ async function validateThemes( themes, { format, color, tableWidth } ) { ! semver.valid( adjustedVersion ) || ! semver.gte( adjustedValue, adjustedVersion ) ) { - return { + problems.push( { actual: `${ chalkStr.green( attr ) }: ${ chalkStr.yellow( value ) }`, expected: `${ chalkStr.yellow( version ) } or greater`, message: `provide a valid version value`, - }; + } ); } + return { isValid: ! problems.length, problems }; }, - isUri: ( attr, value ) => { + validateUri: ( attr, value ) => { + const problems = []; if ( value && ! URL.canParse( value ) ) { - return { + problems.push( { actual: `${ chalkStr.green( attr ) }: ${ chalkStr.yellow( value ) }`, expected: `a valid URI`, message: `${ value } is not a valid URI`, - }; + } ); } + return { isValid: ! problems.length, problems }; }, - isValidSlug: ( attr, value ) => { + validateThemeSlug: ( attr, value ) => { + const problems = []; if ( value && ! /^[a-z0-9-]+$/.test( value ) ) { - return { + problems.push( { actual: `${ chalkStr.green( attr ) }: ${ chalkStr.yellow( value ) }`, expected: `a valid value`, message: `${ value } is not a valid value`, - }; + } ); } + return { isValid: ! problems.length, problems }; }, // a8c validations - isA8CThemeUri: ( attr, value ) => { + validateA8CThemeUri: ( attr, value ) => { + const problems = []; if ( value && ! /^https:\/\/wordpress\.com\/themes\/[a-z0-9-]+\/?$/.test( value ) ) { - return { + problems.push( { actual: `${ chalkStr.green( attr ) }: ${ chalkStr.yellow( value ) }`, - expected: `https://wordpress.com/themes/${ chalkStr.yellow( '{slug}' ) }/`, + expected: `https://wordpress.com/themes/${ chalkStr.yellow( + '{slug}' + ) }/`, message: `${ value } is not a valid WordPress.com theme URI`, - }; + } ); } + return { isValid: ! problems.length, problems }; }, - isA8CAuthor: ( attr, value ) => { + validateA8CAuthor: ( attr, value ) => { + const problems = []; if ( value && ! /^Automattic$/.test( value ) ) { - return { + problems.push( { actual: `${ chalkStr.green( attr ) }: ${ chalkStr.yellow( value ) }`, expected: `Automattic`, message: `${ value } is not a valid author`, - }; + } ); } + return { isValid: ! problems.length, problems }; }, - isA8CAuthorUri: ( attr, value ) => { + validateA8CAuthorUri: ( attr, value ) => { + const problems = []; if ( value && ! /^https:\/\/automattic\.com\/$/.test( value ) ) { - return { + problems.push( { actual: `${ chalkStr.green( attr ) }: ${ chalkStr.yellow( value ) }`, expected: `https://automattic.com/`, message: `${ value } is not a valid Automattic author URI`, - }; + } ); } + return { isValid: ! problems.length, problems }; }, }; @@ -1659,11 +1675,11 @@ async function validateThemes( themes, { format, color, tableWidth } ) { attribute: 'Theme URI', validators: [ { - validate: validators.isUri, + validate: validators.validateUri, type: 'warning', }, { - validate: validators.isA8CThemeUri, + validate: validators.validateA8CThemeUri, type: 'warning', }, ], @@ -1673,7 +1689,7 @@ async function validateThemes( themes, { format, color, tableWidth } ) { required: true, validators: [ { - validate: validators.isA8CAuthor, + validate: validators.validateA8CAuthor, type: 'warning', }, ], @@ -1682,11 +1698,11 @@ async function validateThemes( themes, { format, color, tableWidth } ) { attribute: 'Author URI', validators: [ { - validate: validators.isUri, + validate: validators.validateUri, type: 'warning', }, { - validate: validators.isA8CAuthorUri, + validate: validators.validateA8CAuthorUri, type: 'warning', }, ], @@ -1698,7 +1714,7 @@ async function validateThemes( themes, { format, color, tableWidth } ) { validators: [ { validate: ( attr, value ) => - validators.isValidVersion( attr, value, [ 3 ] ), + validators.validateVersion( attr, value, [ 3 ] ), type: 'error', }, ], @@ -1709,12 +1725,12 @@ async function validateThemes( themes, { format, color, tableWidth } ) { validators: [ { validate: ( attr, value ) => - validators.isValidVersion( attr, value, [ 2 ] ), + validators.validateVersion( attr, value, [ 2 ] ), type: 'error', }, { validate: ( attr, value ) => - validators.isGreaterOrEqual( + validators.validateVersionGte( attr, `${ value }.0`, '5.9.0' @@ -1729,12 +1745,12 @@ async function validateThemes( themes, { format, color, tableWidth } ) { validators: [ { validate: ( attr, value ) => - validators.isValidVersion( attr, value, [ 2, 3 ] ), + validators.validateVersion( attr, value, [ 2, 3 ] ), type: 'error', }, { validate: ( attr, value ) => - validators.isGreaterOrEqual( + validators.validateVersionGte( attr, value, themeRequires @@ -1749,7 +1765,7 @@ async function validateThemes( themes, { format, color, tableWidth } ) { validators: [ { validate: ( attr, value ) => - validators.isValidVersion( attr, value, [ 2 ] ), + validators.validateVersion( attr, value, [ 2 ] ), type: 'error', }, ], @@ -1760,7 +1776,7 @@ async function validateThemes( themes, { format, color, tableWidth } ) { required: true, validators: [ { - validate: validators.isUri, + validate: validators.validateUri, type: 'warning', }, ], @@ -1770,7 +1786,7 @@ async function validateThemes( themes, { format, color, tableWidth } ) { required: true, validators: [ { - validate: validators.isValidSlug, + validate: validators.validateThemeSlug, type: 'error', }, ], @@ -1793,14 +1809,19 @@ async function validateThemes( themes, { format, color, tableWidth } ) { ); } else if ( validators ) { validators.forEach( ( { validate, type } ) => { - const problem = validate( attribute, attributeValue ); - if ( problem ) { - problems.push( - createProblem( { - type: type, - file: styleCssPath, - data: problem, - } ) + const { isValid, problems: validationProblems } = validate( + attribute, + attributeValue + ); + if ( ! isValid ) { + problems = problems.concat( + validationProblems.map( ( problem ) => + createProblem( { + type: type, + file: styleCssPath, + data: problem, + } ) + ) ); } } ); From eb67d528c5596664a549154403e09eb2702e6f4f Mon Sep 17 00:00:00 2001 From: madhusudhand Date: Mon, 9 Sep 2024 15:03:31 +0530 Subject: [PATCH 3/4] restore requires at least validation --- theme-utils.mjs | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/theme-utils.mjs b/theme-utils.mjs index 01c5c4572a..89a95c9554 100644 --- a/theme-utils.mjs +++ b/theme-utils.mjs @@ -1507,6 +1507,7 @@ async function validateThemes( themes, { format, color, tableWidth } ) { let hasError = false; for ( const themeSlug of themes ) { const styleCssPath = `${ themeSlug }/style.css`; + const themeJsonPath = `${ themeSlug }/theme.json`; if ( ! fs.existsSync( themeSlug ) ) { hasError = true; @@ -1536,11 +1537,27 @@ async function validateThemes( themes, { format, color, tableWidth } ) { } const styleCss = await fs.promises.readFile( styleCssPath, 'utf-8' ); - const themeRequires = getThemeMetadata( styleCss, 'Requires at least' ); + const themeRequires = getThemeMetadata( styleCss, 'Requires at least', true ); const wpVersion = themeRequires ? `${ themeRequires }.0`.split( '.', 2 ).join( '.' ) : undefined; - const isSupportedWpVersion = wpVersion && semver.gte( `${ wpVersion }.0`, '5.9.0' ) + const isSupportedWpVersion = wpVersion && semver.valid( `${ wpVersion }.0` ) && semver.gte( `${ wpVersion }.0`, '5.9.0' ) + const hasThemeJson = fs.existsSync( themeJsonPath ); + + if ( hasThemeJson && ! isSupportedWpVersion ) { + problems.push( + createProblem( { + type: 'warning', + file: styleCssPath, + // prettier-ignore + data: { + actual: chalkStr.yellow( wpVersion ), + expected: `${ chalkStr.yellow( '5.9' ) } or greater`, + message: `the ${ chalkStr.green( "'Requires at least'" ) } version does not support theme.json`, + }, + } ) + ); + } const validators = { validateVersion( attr, value, validLengths = [ 3 ] ) { @@ -1728,15 +1745,6 @@ async function validateThemes( themes, { format, color, tableWidth } ) { validators.validateVersion( attr, value, [ 2 ] ), type: 'error', }, - { - validate: ( attr, value ) => - validators.validateVersionGte( - attr, - `${ value }.0`, - '5.9.0' - ), - type: 'error', - }, ], }, { From 75fd4e9f2b2a9ad3802814050e10b41ce372a783 Mon Sep 17 00:00:00 2001 From: madhusudhand Date: Tue, 10 Sep 2024 13:07:51 +0530 Subject: [PATCH 4/4] allow theme uri to be both themes and theme sufix --- theme-utils.mjs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/theme-utils.mjs b/theme-utils.mjs index 89a95c9554..26198f451e 100644 --- a/theme-utils.mjs +++ b/theme-utils.mjs @@ -1541,10 +1541,10 @@ async function validateThemes( themes, { format, color, tableWidth } ) { const wpVersion = themeRequires ? `${ themeRequires }.0`.split( '.', 2 ).join( '.' ) : undefined; - const isSupportedWpVersion = wpVersion && semver.valid( `${ wpVersion }.0` ) && semver.gte( `${ wpVersion }.0`, '5.9.0' ) + const hasThemeJsonSupport = wpVersion && semver.valid( `${ wpVersion }.0` ) && semver.gte( `${ wpVersion }.0`, '5.9.0' ) const hasThemeJson = fs.existsSync( themeJsonPath ); - if ( hasThemeJson && ! isSupportedWpVersion ) { + if ( hasThemeJson && ! hasThemeJsonSupport ) { problems.push( createProblem( { type: 'warning', @@ -1637,7 +1637,7 @@ async function validateThemes( themes, { format, color, tableWidth } ) { const problems = []; if ( value && - ! /^https:\/\/wordpress\.com\/themes\/[a-z0-9-]+\/?$/.test( + ! /^https:\/\/wordpress\.com\/themes?\/[a-z0-9-]+\/?$/.test( value ) ) { @@ -1645,7 +1645,7 @@ async function validateThemes( themes, { format, color, tableWidth } ) { actual: `${ chalkStr.green( attr ) }: ${ chalkStr.yellow( value ) }`, - expected: `https://wordpress.com/themes/${ chalkStr.yellow( + expected: `https://wordpress.com/theme/${ chalkStr.yellow( '{slug}' ) }/`, message: `${ value } is not a valid WordPress.com theme URI`, @@ -1670,7 +1670,7 @@ async function validateThemes( themes, { format, color, tableWidth } ) { const problems = []; if ( value && - ! /^https:\/\/automattic\.com\/$/.test( value ) + ! /^https:\/\/automattic\.com\/?$/.test( value ) ) { problems.push( { actual: `${ chalkStr.green( @@ -1851,7 +1851,7 @@ async function validateThemes( themes, { format, color, tableWidth } ) { for ( const file of paths ) { try { const data = await readJson( file ); - const schemaUri = isSupportedWpVersion + const schemaUri = hasThemeJsonSupport ? `https://schemas.wp.org/wp/${ wpVersion }/${ schemaType }.json` : data.$schema;