diff --git a/theme-utils.mjs b/theme-utils.mjs index d601ed0e1e..e1bdbe46ea 100644 --- a/theme-utils.mjs +++ b/theme-utils.mjs @@ -14,14 +14,31 @@ import glob from 'fast-glob'; import chalk, { Chalk } from 'chalk'; const remoteSSH = 'wpcom-sandbox'; -const sandboxPublicThemesFolder = '/home/wpcom/public_html/wp-content/themes/pub'; +const sandboxPublicThemesFolder = + '/home/wpcom/public_html/wp-content/themes/pub'; const sandboxRootFolder = '/home/wpcom/public_html/'; -const glotPressScript = '~/public_html/bin/i18n/create-glotpress-project-for-theme.php'; +const glotPressScript = + '~/public_html/bin/i18n/create-glotpress-project-for-theme.php'; const isWin = process.platform === 'win32'; -const coreThemes = ['twentyten', 'twentyeleven', 'twentytwelve', 'twentythirteen', 'twentyfourteen', 'twentyfifteen', 'twentysixteen', 'twentyseventeen', 'twentynineteen', 'twentytwenty', 'twentytwentyone', 'twentytwentytwo', 'twentytwentythree', 'twentytwentyfour']; +const coreThemes = [ + 'twentyten', + 'twentyeleven', + 'twentytwelve', + 'twentythirteen', + 'twentyfourteen', + 'twentyfifteen', + 'twentysixteen', + 'twentyseventeen', + 'twentynineteen', + 'twentytwenty', + 'twentytwentyone', + 'twentytwentytwo', + 'twentytwentythree', + 'twentytwentyfour', +]; const commands = { - "push-button-deploy": { + 'push-button-deploy': { helpText: ` * Gets the last deployed hash from the sandbox * Version bump all themes that have changes since the last deployment @@ -34,97 +51,115 @@ const commands = { * Create a tag in the github repository at this point of change which includes the GitHub pull request link in the description * After pausing to allow testing, merge and deploy the changes `, - run: pushButtonDeploy + run: pushButtonDeploy, }, - "clean-sandbox": { - helpText: 'Perform a hard reset, checkout trunk, and pull on the public themes working copy on your sandbox.', - run: cleanSandbox + 'clean-sandbox': { + helpText: + 'Perform a hard reset, checkout trunk, and pull on the public themes working copy on your sandbox.', + run: cleanSandbox, }, - "push-to-sandbox": { - helpText: 'Uses rsync to copy all modified files for all themes from the local machine to your sandbox.', - run: pushToSandbox + 'push-to-sandbox': { + helpText: + 'Uses rsync to copy all modified files for all themes from the local machine to your sandbox.', + run: pushToSandbox, }, - "push-changes-to-sandbox": { - helpText: 'Uses rsync to copy all modified files for any modified themes from the local machine to your sandbox.', - run: pushChangesToSandbox + 'push-changes-to-sandbox': { + helpText: + 'Uses rsync to copy all modified files for any modified themes from the local machine to your sandbox.', + run: pushChangesToSandbox, }, - "push-theme-to-sandbox": { - helpText: 'Uses rsync to copy all modified files for the specified theme from the local machine to your sandbox.', + 'push-theme-to-sandbox': { + helpText: + 'Uses rsync to copy all modified files for the specified theme from the local machine to your sandbox.', additionalArgs: '', - run: (args) => pushThemeToSandbox(args?.[1]) + run: ( args ) => pushThemeToSandbox( args?.[ 1 ] ), }, - "version-bump-themes": { - helpText: 'Bump the version of any theme that has had changes since the last deployment. This includes bumping the version of any parent themes and updating the changelog for the theme.', - run: versionBumpThemes + 'version-bump-themes': { + helpText: + 'Bump the version of any theme that has had changes since the last deployment. This includes bumping the version of any parent themes and updating the changelog for the theme.', + run: versionBumpThemes, }, - "land-diff": { + 'land-diff': { helpText: 'Run gh pr merge to merge in the specified pull request id.', additionalArgs: '', - run: (args) => landChanges(args?.[1]) + run: ( args ) => landChanges( args?.[ 1 ] ), }, - "deploy-preview": { + 'deploy-preview': { helpText: 'Display a list of the changes to be deployed.', - run: deployPreview + run: deployPreview, }, - "deploy-theme": { - helpText: 'This runs "deploy pub " on the provided list of themes.', + 'deploy-theme': { + helpText: + 'This runs "deploy pub " on the provided list of themes.', additionalArgs: '', - run: (args) => deployThemes(args?.[1].split(/[ ,]+/)) + run: ( args ) => deployThemes( args?.[ 1 ].split( /[ ,]+/ ) ), }, - "add-strict-typing": { + 'add-strict-typing': { helpText: 'Adds strict typing to any changed themes.', - run: () => addStrictTypesToChangedThemes() + run: () => addStrictTypesToChangedThemes(), }, - "build-com-zip": { + 'build-com-zip': { helpText: 'Build the production zip file for the specified theme.', additionalArgs: '', - run: (args) => buildComZips(args?.[1].split(/[ ,]+/)) + run: ( args ) => buildComZips( args?.[ 1 ].split( /[ ,]+/ ) ), }, - "checkout-core-theme": { - helpText: 'Use SVN to checkout the given core themes from the wpcom SVN repository.', + 'checkout-core-theme': { + helpText: + 'Use SVN to checkout the given core themes from the wpcom SVN repository.', additionalArgs: '', - run: (args) => checkoutCoreTheme(args?.[1]) + run: ( args ) => checkoutCoreTheme( args?.[ 1 ] ), }, - "pull-all-themes": { - helpText: 'Use rsync to copy all public theme files from your sandbox to your local machine.', - run: pullAllThemes + 'pull-all-themes': { + helpText: + 'Use rsync to copy all public theme files from your sandbox to your local machine.', + run: pullAllThemes, }, - "pull-core-themes": { - helpText: 'Use rsync to copy all public CORE theme files from your sandbox to your local machine. CORE themes are any of the Twenty themes.', - run: pullCoreThemes + 'pull-core-themes': { + helpText: + 'Use rsync to copy all public CORE theme files from your sandbox to your local machine. CORE themes are any of the Twenty themes.', + run: pullCoreThemes, }, - "push-core-themes": { - helpText: 'Use rsync to copy all public CORE theme files from your local machine to your sandbox. CORE themes are any of the Twenty themes.', - run: pushCoreThemes + 'push-core-themes': { + helpText: + 'Use rsync to copy all public CORE theme files from your local machine to your sandbox. CORE themes are any of the Twenty themes.', + run: pushCoreThemes, }, - "sync-core-theme": { - helpText: 'Given a theme slug and SVN revision, sync the theme from the specified revision to the latest. This requires the core theme to be currently checked out from the wpcom svn repository.', + 'sync-core-theme': { + helpText: + 'Given a theme slug and SVN revision, sync the theme from the specified revision to the latest. This requires the core theme to be currently checked out from the wpcom svn repository.', additionalArgs: ' ', - run: (args) => syncCoreTheme(args?.[1], args?.[2]) + run: ( args ) => syncCoreTheme( args?.[ 1 ], args?.[ 2 ] ), }, - "deploy-sync-core-theme": { - helpText: 'Given a theme slug and SVN revision, sync the theme from the specified revision to the latest. This command contains additional prompts and error checking not provided by sync-core-theme.', + 'deploy-sync-core-theme': { + helpText: + 'Given a theme slug and SVN revision, sync the theme from the specified revision to the latest. This command contains additional prompts and error checking not provided by sync-core-theme.', additionalArgs: ' ', - run: (args) => deploySyncCoreTheme(args?.[1], args?.[2]) + run: ( args ) => deploySyncCoreTheme( args?.[ 1 ], args?.[ 2 ] ), }, - "create-core-github-pr": { - helpText: 'Given a theme slug and specific revision create a GitHub pull request from the resources currently on the sandbox.', + 'create-core-github-pr': { + helpText: + 'Given a theme slug and specific revision create a GitHub pull request from the resources currently on the sandbox.', additionalArgs: ' ', - run: (args) => createCoreGithubPR(args?.[1], args?.[2]) + run: ( args ) => createCoreGithubPR( args?.[ 1 ], args?.[ 2 ] ), }, - "update-theme-changelog": { - helpText: 'Use the commit log to build a list of recent changes and add them as a new changelog entry. If add-changes is true, the updated readme.txt will be staged.', + 'update-theme-changelog': { + helpText: + 'Use the commit log to build a list of recent changes and add them as a new changelog entry. If add-changes is true, the updated readme.txt will be staged.', additionalArgs: ' ', - run: (args) => updateThemeChangelog(args?.[1], false, args?.[2]) + run: ( args ) => + updateThemeChangelog( args?.[ 1 ], false, args?.[ 2 ] ), }, - "rebuild-theme-changelog": { - helpText: 'Rebuild the entire change long from the given starting hash.', + 'rebuild-theme-changelog': { + helpText: + 'Rebuild the entire change long from the given starting hash.', additionalArgs: ' ', - run: (args) => rebuildThemeChangelog(args?.[1], args?.[2]) + run: ( args ) => rebuildThemeChangelog( args?.[ 1 ], args?.[ 2 ] ), }, - "escape-patterns": { - helpText: 'Escapes block patterns for pattern files that have changes (staged or unstaged).', - run: () => escapePatterns() + 'escape-patterns': { + helpText: + 'Escapes block patterns for pattern files that have changes (staged or unstaged).', + additionalArgs: '[directory]', + run: ( args ) => escapePatterns( args?.[ 1 ] ), }, 'validate-theme': { helpText: [ @@ -157,23 +192,23 @@ const commands = { await validateThemes( themes, options ); }, }, - "help": { + help: { helpText: 'Displays the main help message.', - run: (args) => showHelp(args?.[1]) + run: ( args ) => showHelp( args?.[ 1 ] ), }, }; -(async function start() { - let args = process.argv.slice(2); - let command = args?.[0]; +( async function start() { + let args = process.argv.slice( 2 ); + let command = args?.[ 0 ]; - if (!commands[command]) { + if ( ! commands[ command ] ) { showHelp(); - process.exit(1); + process.exit( 1 ); } - await commands[command].run(args); -})(); + await commands[ command ].run( args ); +} )(); function wrapIndent( text, @@ -192,25 +227,25 @@ function wrapIndent( .join( newline ); } -function showHelp(command = '') { - if (!command || !commands.hasOwnProperty(command)) { - console.log(` +function showHelp( command = '' ) { + if ( ! command || ! commands.hasOwnProperty( command ) ) { + console.log( ` node theme-utils.mjs [command] Available commands: _(theme-utils.mjs help [command] for more details)_ -\t${Object.keys(commands).join('\n\t')} - `); +\t${ Object.keys( commands ).join( '\n\t' ) } + ` ); return; } - const { helpText, additionalArgs } = commands[command]; - console.log(` -${command} ${additionalArgs ?? ''} + const { helpText, additionalArgs } = commands[ command ]; + console.log( ` +${ command } ${ additionalArgs ?? '' } -${helpText} - `); +${ helpText } + ` ); } /* @@ -218,26 +253,28 @@ ${helpText} Optionally pass in a deployed hash or default to calling getLastDeployedHash() Optionally pass in boolean bulletPoints to add bullet points to each commit log */ -async function getCommitLogs(hash, bulletPoints, theme) { - if (!hash) { +async function getCommitLogs( hash, bulletPoints, theme ) { + if ( ! hash ) { hash = await getLastDeployedHash(); } let format = 'format:%s'; let themeDir = ''; - if (bulletPoints) { + if ( bulletPoints ) { format = 'format:"* %s"'; } - if (theme) { - themeDir = `-- ./${theme}`; + if ( theme ) { + themeDir = `-- ./${ theme }`; } - let logs = await executeCommand(`git log --reverse --pretty=${format} ${hash}..HEAD ${themeDir}`); + let logs = await executeCommand( + `git log --reverse --pretty=${ format } ${ hash }..HEAD ${ themeDir }` + ); // Remove any double quotes from commit messages - logs = logs.replace(/"/g, ''); + logs = logs.replace( /"/g, '' ); return logs; } @@ -247,34 +284,38 @@ async function getCommitLogs(hash, bulletPoints, theme) { */ async function deployPreview() { console.clear(); - console.log('To ensure accuracy clean your sandbox before previewing. (It is not automatically done).'); + console.log( + 'To ensure accuracy clean your sandbox before previewing. (It is not automatically done).' + ); let message = await checkForDeployability(); - if (message) { - console.log(`\n${message}\n\n`); + if ( message ) { + console.log( `\n${ message }\n\n` ); } let hash = await getLastDeployedHash(); - console.log(`Last deployed hash: ${hash}`); + console.log( `Last deployed hash: ${ hash }` ); - let changedThemes = await getChangedThemes(hash); - console.log(`The following themes have changes:\n${changedThemes}`); + let changedThemes = await getChangedThemes( hash ); + console.log( `The following themes have changes:\n${ changedThemes }` ); - let logs = await getCommitLogs(hash); - console.log(`\n\nCommit log of changes to be deployed:\n\n${logs}\n\n`); + let logs = await getCommitLogs( hash ); + console.log( `\n\nCommit log of changes to be deployed:\n\n${ logs }\n\n` ); } async function addStrictTypesToChangedThemes() { let hash = await getLastDeployedHash(); - const changedThemes = await getChangedThemes(hash); - - for (let theme of changedThemes) { - await executeCommand(` - bash -c "./add-strict-types.sh ${theme}" - `, true) - .catch((err) => { - console.log(`Error adding strict types to ${theme}: ${err}`); - }); + const changedThemes = await getChangedThemes( hash ); + + for ( let theme of changedThemes ) { + await executeCommand( + ` + bash -c "./add-strict-types.sh ${ theme }" + `, + true + ).catch( ( err ) => { + console.log( `Error adding strict types to ${ theme }: ${ err }` ); + } ); } } @@ -291,23 +332,24 @@ async function addStrictTypesToChangedThemes() { * Create a tag in the github repository at this point of change which includes the GitHub pull request link in the description */ async function pushButtonDeploy() { - console.clear(); - let prompt = await inquirer.prompt([{ - type: 'confirm', - message: 'You are about to deploy /trunk. Are you ready to continue?', - name: "continue", - default: false - }]); - - if (!prompt.continue) { + let prompt = await inquirer.prompt( [ + { + type: 'confirm', + message: + 'You are about to deploy /trunk. Are you ready to continue?', + name: 'continue', + default: false, + }, + ] ); + + if ( ! prompt.continue ) { return; } - let message = await checkForDeployability(); - if (message) { - return console.log(`\n\n${message}\n\n`); + if ( message ) { + return console.log( `\n\n${ message }\n\n` ); } try { @@ -317,178 +359,217 @@ async function pushButtonDeploy() { await addStrictTypesToChangedThemes(); const thingsWentBump = await versionBumpThemes(); - if (thingsWentBump) { - prompt = await inquirer.prompt([{ - type: 'confirm', - message: 'Are you good with the version bump and changelog updates? Make any manual adjustments now if necessary.', - name: "continue", - default: false - }]); - - if (!prompt.continue) { - console.log(`Aborted Automated Deploy Process at version bump changes.`); + if ( thingsWentBump ) { + prompt = await inquirer.prompt( [ + { + type: 'confirm', + message: + 'Are you good with the version bump and changelog updates? Make any manual adjustments now if necessary.', + name: 'continue', + default: false, + }, + ] ); + + if ( ! prompt.continue ) { + console.log( + `Aborted Automated Deploy Process at version bump changes.` + ); return; } } - const changedThemes = await getChangedThemes(hash); + const changedThemes = await getChangedThemes( hash ); - if (!changedThemes.length) { - console.log(`\n\nEverything is upto date. Nothing new to deploy.\n\n`); + if ( ! changedThemes.length ) { + console.log( + `\n\nEverything is upto date. Nothing new to deploy.\n\n` + ); return; } await pushChangesToSandbox(); - await createGlotPressProject(changedThemes); + await createGlotPressProject( changedThemes ); //push changes (from version bump) - if (thingsWentBump) { - prompt = await inquirer.prompt([{ - type: 'confirm', - message: 'Are you ready to push this version bump change to the source repository (GitHub.com)?', - name: "continue", - default: false - }]); - - if (!prompt.continue) { - console.log(`Aborted Automated Deploy Process at version bump push change.`); + if ( thingsWentBump ) { + prompt = await inquirer.prompt( [ + { + type: 'confirm', + message: + 'Are you ready to push this version bump change to the source repository (GitHub.com)?', + name: 'continue', + default: false, + }, + ] ); + + if ( ! prompt.continue ) { + console.log( + `Aborted Automated Deploy Process at version bump push change.` + ); return; } - await executeCommand(` + await executeCommand( + ` git commit -m "Version Bump"; git push --set-upstream origin trunk - `, true); + `, + true + ); } await updateLastDeployedHash(); - let commitMessage = await buildGithubCommitMessageSince(hash); + let commitMessage = await buildGithubCommitMessageSince( hash ); // Make sure the themes/pub repo in sandbox is ready to create a PR to the A8C GitHub Host - prompt = await inquirer.prompt([{ - type: 'confirm', - message: 'Before you can create the GitHub PR, login in A8C GitHub Enterprise Server from the themes/pub repo in your sandbox with the command "gh auth login" and using your SSH key.\nAre you logged in?', - name: "continue", - default: false - }]); + prompt = await inquirer.prompt( [ + { + type: 'confirm', + message: + 'Before you can create the GitHub PR, login in A8C GitHub Enterprise Server from the themes/pub repo in your sandbox with the command "gh auth login" and using your SSH key.\nAre you logged in?', + name: 'continue', + default: false, + }, + ] ); - if (!prompt.continue) { - console.log(`Aborted Automated Deploy Process at require to login in into A8C GitHub Enterprise Server in sandbox.`); + if ( ! prompt.continue ) { + console.log( + `Aborted Automated Deploy Process at require to login in into A8C GitHub Enterprise Server in sandbox.` + ); return; } - let prUrl = await createGithubPR(commitMessage); - let prId = prUrl.split('pull/')[1]; - + let prUrl = await createGithubPR( commitMessage ); + let prId = prUrl.split( 'pull/' )[ 1 ]; - await tagDeployment({ + await tagDeployment( { hash: hash, - prId: prId - }); + prId: prId, + } ); - console.log(`\n\nPhase One Complete\n\nYour sandbox has been updated and the PR is available for review.\nPlease give your sandbox a smoke test to determine that the changes work as expected.\nThe following themes have had changes: \n\n${changedThemes.join(' ')}\n\n\n`); + console.log( + `\n\nPhase One Complete\n\nYour sandbox has been updated and the PR is available for review.\nPlease give your sandbox a smoke test to determine that the changes work as expected.\nThe following themes have had changes: \n\n${ changedThemes.join( + ' ' + ) }\n\n\n` + ); - prompt = await inquirer.prompt([{ - type: 'confirm', - message: 'Are you ready to land these changes?', - name: "continue", - default: false - }]); + prompt = await inquirer.prompt( [ + { + type: 'confirm', + message: 'Are you ready to land these changes?', + name: 'continue', + default: false, + }, + ] ); - if (!prompt.continue) { - console.log(`Aborted Automated Deploy Process Landing Phase\n\nYou will have to land these changes manually. The ID of the PR to land: ${prId}`); + if ( ! prompt.continue ) { + console.log( + `Aborted Automated Deploy Process Landing Phase\n\nYou will have to land these changes manually. The ID of the PR to land: ${ prId }` + ); return; } - await landChanges(prId); + await landChanges( prId ); try { - await deployThemes(changedThemes); - } - catch (err) { - prompt = await inquirer.prompt([{ - type: 'confirm', - message: `There was an error deploying themes. ${err} Do you wish to continue to the next step?`, - name: "continue", - default: false - }]); - - if (!prompt.continue) { - console.log(`Aborted Automated Deploy during deploy phase.`); + await deployThemes( changedThemes ); + } catch ( err ) { + prompt = await inquirer.prompt( [ + { + type: 'confirm', + message: `There was an error deploying themes. ${ err } Do you wish to continue to the next step?`, + name: 'continue', + default: false, + }, + ] ); + + if ( ! prompt.continue ) { + console.log( `Aborted Automated Deploy during deploy phase.` ); return; } } - await buildComZips(changedThemes); + await buildComZips( changedThemes ); - console.log(`The following themes have changed:\n${changedThemes.join('\n')}`) - console.log('\n\nAll Done!!\n\n'); - } - catch (err) { - console.log("ERROR with deploy script: ", err); + console.log( + `The following themes have changed:\n${ changedThemes.join( + '\n' + ) }` + ); + console.log( '\n\nAll Done!!\n\n' ); + } catch ( err ) { + console.log( 'ERROR with deploy script: ', err ); } } -async function deploySyncCoreTheme(theme, sinceRevision) { -if (!theme) { -console.log('Must supply theme to sync and revision to start from'); -return; -} +async function deploySyncCoreTheme( theme, sinceRevision ) { + if ( ! theme ) { + console.log( 'Must supply theme to sync and revision to start from' ); + return; + } await cleanSandbox(); - await checkoutCoreTheme(theme); - await syncCoreTheme(theme, sinceRevision); + await checkoutCoreTheme( theme ); + await syncCoreTheme( theme, sinceRevision ); - let prompt = await inquirer.prompt([{ - type: 'confirm', - message: `Changes have been synced locally. Please resolve any conflicts now. Are you ready to continue?`, - name: "continue", - default: false - }]); + let prompt = await inquirer.prompt( [ + { + type: 'confirm', + message: `Changes have been synced locally. Please resolve any conflicts now. Are you ready to continue?`, + name: 'continue', + default: false, + }, + ] ); - if (!prompt.continue) { - console.log(`Aborted Core Sync Deploy.`); + if ( ! prompt.continue ) { + console.log( `Aborted Core Sync Deploy.` ); return; } - await pushThemeToSandbox(theme); - let prId = await createCoreGithubPR(theme, sinceRevision); + await pushThemeToSandbox( theme ); + let prId = await createCoreGithubPR( theme, sinceRevision ); - prompt = await inquirer.prompt([{ - type: 'confirm', - message: 'Are you ready to land these changes?', - name: "continue", - default: false - }]); + prompt = await inquirer.prompt( [ + { + type: 'confirm', + message: 'Are you ready to land these changes?', + name: 'continue', + default: false, + }, + ] ); - if (!prompt.continue) { - console.log(`Aborted Automated Deploy Sync Process Landing Phase\n\nYou will have to land these changes manually. The ID of the PR to land: ${prId}`); + if ( ! prompt.continue ) { + console.log( + `Aborted Automated Deploy Sync Process Landing Phase\n\nYou will have to land these changes manually. The ID of the PR to land: ${ prId }` + ); return; } - await landChanges(prId); - await deployThemes([theme]); - await buildComZips([theme]); + await landChanges( prId ); + await deployThemes( [ theme ] ); + await buildComZips( [ theme ] ); return; } - -async function buildCoreGithubCommitMessageSince(theme, sinceRevision){ - - let latestRevision = await executeCommand(`svn info -r HEAD https://develop.svn.wordpress.org/trunk | grep Revision | egrep -o "[0-9]+"`); - let logs = await executeCommand(`svn log https://core.svn.wordpress.org/trunk/wp-content/themes/${theme} -r${sinceRevision}:HEAD`) +async function buildCoreGithubCommitMessageSince( theme, sinceRevision ) { + let latestRevision = await executeCommand( + `svn info -r HEAD https://develop.svn.wordpress.org/trunk | grep Revision | egrep -o "[0-9]+"` + ); + let logs = await executeCommand( + `svn log https://core.svn.wordpress.org/trunk/wp-content/themes/${ theme } -r${ sinceRevision }:HEAD` + ); // Remove any double or back quotes from commit messages - logs = logs.replace(/"/g, ''); - logs = logs.replace(/`/g, "'"); - logs = logs.replace(/\$/g, "%24"); + logs = logs.replace( /"/g, '' ); + logs = logs.replace( /`/g, "'" ); + logs = logs.replace( /\$/g, '%24' ); - return `${theme}: Merge latest core changes up to [wp${latestRevision}] + return `${ theme }: Merge latest core changes up to [wp${ latestRevision }] Summary: -${logs} +${ logs } -Test Plan: Activate ${theme} and ensure nothing is broken +Test Plan: Activate ${ theme } and ensure nothing is broken Reviewers: #themes_team @@ -500,59 +581,71 @@ Subscribers: /** * Deploys the localy copy of a core theme to wpcom. */ -async function createCoreGithubPR(theme, sinceRevision) { - - let commitMessage = await buildCoreGithubCommitMessageSince(theme, sinceRevision); - - let prUrl = await createGithubPR(commitMessage); - let prId = prUrl.split('pull/')[1]; +async function createCoreGithubPR( theme, sinceRevision ) { + let commitMessage = await buildCoreGithubCommitMessageSince( + theme, + sinceRevision + ); + + let prUrl = await createGithubPR( commitMessage ); + let prId = prUrl.split( 'pull/' )[ 1 ]; return prId; } /* Build .zip file for .com */ -async function buildComZip(themeSlug) { - - console.log(`Building ${themeSlug} .zip`); +async function buildComZip( themeSlug ) { + console.log( `Building ${ themeSlug } .zip` ); - let styleCss = fs.readFileSync(`${themeSlug}/style.css`, 'utf8'); + let styleCss = fs.readFileSync( `${ themeSlug }/style.css`, 'utf8' ); // Gets the theme version (Version:) and minimum WP version (Tested up to:) from the theme's style.css - let themeVersion = getThemeMetadata(styleCss, 'Version'); - let wpVersionCompat = getThemeMetadata(styleCss, 'Requires at least'); + let themeVersion = getThemeMetadata( styleCss, 'Version' ); + let wpVersionCompat = getThemeMetadata( styleCss, 'Requires at least' ); - if (themeVersion && wpVersionCompat) { - await executeOnSandbox(`php ${sandboxRootFolder}bin/themes/theme-downloads/build-theme-zip.php --stylesheet=pub/${themeSlug} --themeversion=${themeVersion} --wpversioncompat=${wpVersionCompat};`, true); - } - else { - console.log('Unable to build theme .zip.'); - if (!themeVersion) { - console.log('Could not find theme version (Version:) in the theme style.css.'); + if ( themeVersion && wpVersionCompat ) { + await executeOnSandbox( + `php ${ sandboxRootFolder }bin/themes/theme-downloads/build-theme-zip.php --stylesheet=pub/${ themeSlug } --themeversion=${ themeVersion } --wpversioncompat=${ wpVersionCompat };`, + true + ); + } else { + console.log( 'Unable to build theme .zip.' ); + if ( ! themeVersion ) { + console.log( + 'Could not find theme version (Version:) in the theme style.css.' + ); } - if (!wpVersionCompat) { - console.log('Could not find WP compat version (Requires at least:) in the theme style.css.'); + if ( ! wpVersionCompat ) { + console.log( + 'Could not find WP compat version (Requires at least:) in the theme style.css.' + ); } - console.log('Please build the .zip file for the theme manually.', themeSlug); - open('https://mc.a8c.com/themes/downloads/'); + console.log( + 'Please build the .zip file for the theme manually.', + themeSlug + ); + open( 'https://mc.a8c.com/themes/downloads/' ); } } -async function buildComZips(themes) { - console.log(`Building dotcom .zip files`); - const progress = startProgress(themes.length); - const failedThemes = [] - for (let theme of themes) { +async function buildComZips( themes ) { + console.log( `Building dotcom .zip files` ); + const progress = startProgress( themes.length ); + const failedThemes = []; + for ( let theme of themes ) { try { - await buildComZip(theme); - } catch (err) { - console.log(`There was an error building dotcom zip for ${theme}. ${err}`); - failedThemes.push(theme); + await buildComZip( theme ); + } catch ( err ) { + console.log( + `There was an error building dotcom zip for ${ theme }. ${ err }` + ); + failedThemes.push( theme ); } progress.increment(); } - if (failedThemes.length) { + if ( failedThemes.length ) { const tableConfig = { columnDefault: { width: 40, @@ -560,9 +653,14 @@ async function buildComZips(themes) { header: { alignment: 'center', content: `There was an error building dotcom zip for following themes.`, - } + }, }; - console.log(table(failedThemes.map(t => [t]), tableConfig)); + console.log( + table( + failedThemes.map( ( t ) => [ t ] ), + tableConfig + ) + ); } } @@ -572,16 +670,16 @@ async function buildComZips(themes) { * That trunk is up-to-date with origin/trunk */ async function checkForDeployability() { - let branchName = await executeCommand('git symbolic-ref --short HEAD'); - if (branchName !== 'trunk') { + let branchName = await executeCommand( 'git symbolic-ref --short HEAD' ); + if ( branchName !== 'trunk' ) { return 'Only the /trunk branch can be deployed.'; } - await executeCommand('git remote update', true); - let localMasterHash = await executeCommand('git rev-parse trunk') - let remoteMasterHash = await executeCommand('git rev-parse origin/trunk') - if (localMasterHash !== remoteMasterHash) { - return 'Local /trunk is out-of-date. Pull changes to continue.' + await executeCommand( 'git remote update', true ); + let localMasterHash = await executeCommand( 'git rev-parse trunk' ); + let remoteMasterHash = await executeCommand( 'git rev-parse origin/trunk' ); + if ( localMasterHash !== remoteMasterHash ) { + return 'Local /trunk is out-of-date. Pull changes to continue.'; } return null; } @@ -589,18 +687,21 @@ async function checkForDeployability() { /* Land the changes from the given PR ID. This is the "production merge". */ -async function landChanges(prId) { - return executeCommand(`ssh -tt -A ${remoteSSH} "cd ${sandboxPublicThemesFolder} && gh pr merge ${prId} --squash; exit;"`, true); +async function landChanges( prId ) { + return executeCommand( + `ssh -tt -A ${ remoteSSH } "cd ${ sandboxPublicThemesFolder } && gh pr merge ${ prId } --squash; exit;"`, + true + ); } -async function getChangedThemes(hash) { - console.log('Determining all changed themes'); +async function getChangedThemes( hash ) { + console.log( 'Determining all changed themes' ); let themes = await getActionableThemes(); let changedThemes = []; - for (let theme of themes) { - let hasChanges = await checkThemeForChanges(theme, hash); - if (hasChanges) { - changedThemes.push(theme); + for ( let theme of themes ) { + let hasChanges = await checkThemeForChanges( theme, hash ); + if ( hasChanges ) { + changedThemes.push( theme ); } } return changedThemes; @@ -612,50 +713,55 @@ async function getChangedThemes(hash) { Can also be triggered to deploy a single theme with the command: node ./theme-utils.mjs deploy-theme THEMENAME */ -async function deployThemes(themes) { - +async function deployThemes( themes ) { let response; const failedThemes = []; - const progress = startProgress(themes.length); - - for (let theme of themes) { + const progress = startProgress( themes.length ); - console.log(`Deploying ${theme}`); + for ( let theme of themes ) { + console.log( `Deploying ${ theme }` ); let deploySuccess = false; let attempt = 0; - while (!deploySuccess && attempt <= 2) { - + while ( ! deploySuccess && attempt <= 2 ) { attempt++; - console.log(`\nattempt #${attempt}\n\n`); + console.log( `\nattempt #${ attempt }\n\n` ); try { - response = await executeOnSandbox(`deploy pub ${theme};exit;`, true, true); - deploySuccess = response.includes('successfully deployed to'); - } catch (error) { - deploySuccess = false + response = await executeOnSandbox( + `deploy pub ${ theme };exit;`, + true, + true + ); + deploySuccess = response.includes( 'successfully deployed to' ); + } catch ( error ) { + deploySuccess = false; } - if (!deploySuccess) { - console.log('Deploy was not successful. Trying again in 10 seconds...'); - await new Promise(resolve => setTimeout(resolve, 10000)); - } - else { - console.log("Deploy successful."); + if ( ! deploySuccess ) { + console.log( + 'Deploy was not successful. Trying again in 10 seconds...' + ); + await new Promise( ( resolve ) => + setTimeout( resolve, 10000 ) + ); + } else { + console.log( 'Deploy successful.' ); } - } - if (!deploySuccess) { - console.log(`${theme} was not sucessfully deployed and should be deployed manually.`); - failedThemes.push(theme); + if ( ! deploySuccess ) { + console.log( + `${ theme } was not sucessfully deployed and should be deployed manually.` + ); + failedThemes.push( theme ); } progress.increment(); } - if (failedThemes.length) { + if ( failedThemes.length ) { const tableConfig = { columnDefault: { width: 40, @@ -663,9 +769,14 @@ async function deployThemes(themes) { header: { alignment: 'center', content: `Following themes are not deployed.`, - } + }, }; - console.log(table(failedThemes.map(t => [t]), tableConfig)); + console.log( + table( + failedThemes.map( ( t ) => [ t ] ), + tableConfig + ) + ); } } @@ -674,9 +785,9 @@ async function deployThemes(themes) { This hash is used to determine all the changes that have happened between that point and the current point. */ async function getLastDeployedHash() { - let result = await executeOnSandbox(` - cat ${sandboxPublicThemesFolder}/.pub-git-hash - `); + let result = await executeOnSandbox( ` + cat ${ sandboxPublicThemesFolder }/.pub-git-hash + ` ); return result; } @@ -684,10 +795,10 @@ async function getLastDeployedHash() { Update the 'last deployed hash' on the server with the current hash. */ async function updateLastDeployedHash() { - let hash = await executeCommand(`git rev-parse HEAD`); - await executeOnSandbox(` - echo '${hash}' > ${sandboxPublicThemesFolder}/.pub-git-hash - `); + let hash = await executeCommand( `git rev-parse HEAD` ); + await executeOnSandbox( ` + echo '${ hash }' > ${ sandboxPublicThemesFolder }/.pub-git-hash + ` ); } /* @@ -698,152 +809,178 @@ async function updateLastDeployedHash() { Commit the change. */ async function versionBumpThemes() { - console.log("Version Bumping"); + console.log( 'Version Bumping' ); const themes = await getActionableThemes(); - const latestTag = execSync('git describe --tags --abbrev=0').toString().trim(); + const latestTag = execSync( 'git describe --tags --abbrev=0' ) + .toString() + .trim(); // Get the hash for the latest tag. - const hash = execSync(`git rev-parse ${latestTag}`).toString().trim(); + const hash = execSync( `git rev-parse ${ latestTag }` ).toString().trim(); let versionBumpCount = 0; let changesWereMade = false; - for (let theme of themes) { - const hasChanges = await checkThemeForChanges(theme, hash); + for ( let theme of themes ) { + const hasChanges = await checkThemeForChanges( theme, hash ); - if (!hasChanges) { + if ( ! hasChanges ) { continue; } versionBumpCount++; - const hasVersionBump = await checkThemeForVersionBump(theme, hash); + const hasVersionBump = await checkThemeForVersionBump( theme, hash ); - if (hasVersionBump) { + if ( hasVersionBump ) { continue; } - await versionBumpTheme(theme, true); - await updateThemeChangelog(theme, true); + await versionBumpTheme( theme, true ); + await updateThemeChangelog( theme, true ); changesWereMade = true; } //version bump the root project if there were changes to any of the themes - const rootHasVersionBump = await checkProjectForVersionBump(hash); + const rootHasVersionBump = await checkProjectForVersionBump( hash ); - if (versionBumpCount === 0) { - console.log('No changes detected; Root package version bump not required'); - } else if (!rootHasVersionBump) { - await executeCommand(`npm version patch --no-git-tag-version && git add package.json package-lock.json`); + if ( versionBumpCount === 0 ) { + console.log( + 'No changes detected; Root package version bump not required' + ); + } else if ( ! rootHasVersionBump ) { + await executeCommand( + `npm version patch --no-git-tag-version && git add package.json package-lock.json` + ); changesWereMade = true; } return changesWereMade; } -export function getThemeMetadata(styleCss, attribute, trimWPCom = true) { - if (!styleCss || !attribute) { +export function getThemeMetadata( styleCss, attribute, trimWPCom = true ) { + if ( ! styleCss || ! attribute ) { return null; } - switch (attribute) { + switch ( attribute ) { case 'Version': const version = styleCss - .match(/(?<=Version:\s*).*?(?=\s*\r?\n|\rg)/gs)?.[0] + .match( /(?<=Version:\s*).*?(?=\s*\r?\n|\rg)/gs )?.[ 0 ] ?.trim(); - return trimWPCom ? version.replace('-wpcom', '') : version; - default: + return trimWPCom ? version.replace( '-wpcom', '' ) : version; + case 'Requires at least': return styleCss - .match(new RegExp(`(?<=${attribute}:\\s*).*?(?=\\s*\\r?\\n|\\rg)`, 'gs'))?.[0] + .match( + /(?<=Requires at least:\s*).*?(?=\s*\r?\n|\rg)/gs + )?.[ 0 ] ?.trim(); } } /* Rebuild theme changelog from a given starting hash */ -async function rebuildThemeChangelog(theme, since) { - - console.log(`Rebuilding ${theme} changelog since ${since || 'forever'}`); +async function rebuildThemeChangelog( theme, since ) { + console.log( + `Rebuilding ${ theme } changelog since ${ since || 'forever' }` + ); - if (since) { - since = `${since}..HEAD`; + if ( since ) { + since = `${ since }..HEAD`; } else { since = 'HEAD'; } - let hashes = await executeCommand(`git rev-list ${since} -- ./${theme}`); - hashes = hashes.split('\n'); + let hashes = await executeCommand( + `git rev-list ${ since } -- ./${ theme }` + ); + hashes = hashes.split( '\n' ); let logs = '== Changelog ==\n'; - for (let hash of hashes) { - let log = await executeCommand(`git log -n 1 --pretty=format:"* %s" ${hash}`); - if (log.includes('Version Bump')) { - let previousStyleString = await executeCommand(`git show ${hash}:${theme}/style.css 2>/dev/null`); - let version = getThemeMetadata(previousStyleString, 'Version'); - logs += `\n= ${version} =\n`; + for ( let hash of hashes ) { + let log = await executeCommand( + `git log -n 1 --pretty=format:"* %s" ${ hash }` + ); + if ( log.includes( 'Version Bump' ) ) { + let previousStyleString = await executeCommand( + `git show ${ hash }:${ theme }/style.css 2>/dev/null` + ); + let version = getThemeMetadata( previousStyleString, 'Version' ); + logs += `\n= ${ version } =\n`; } else { // Remove any double quotes from commit messages - log = log.replace(/"/g, ''); + log = log.replace( /"/g, '' ); logs += log + '\n'; } } // Get theme readme.txt - let readmeFilePath = `${theme}/readme.txt`; + let readmeFilePath = `${ theme }/readme.txt`; // Update readme.txt - fs.readFile(readmeFilePath, 'utf8', function (err, data) { + fs.readFile( readmeFilePath, 'utf8', function ( err, data ) { let changelogSection = '== Changelog =='; - let regex = new RegExp('^.*' + changelogSection + '.*$', 'gm'); - let formattedChangelog = data.replace(regex, logs); - - fs.writeFile(readmeFilePath, formattedChangelog, 'utf8', function (err) { - if (err) return console.log(err); - }); - }); - + let regex = new RegExp( '^.*' + changelogSection + '.*$', 'gm' ); + let formattedChangelog = data.replace( regex, logs ); + + fs.writeFile( + readmeFilePath, + formattedChangelog, + 'utf8', + function ( err ) { + if ( err ) return console.log( err ); + } + ); + } ); } /* Update theme changelog using current commit logs. Used by versionBumpThemes to update each theme changelog. */ -async function updateThemeChangelog(theme, addChanges) { - console.log(`Updating ${theme} changelog`); +async function updateThemeChangelog( theme, addChanges ) { + console.log( `Updating ${ theme } changelog` ); // Get theme version - let styleCss = fs.readFileSync(`${theme}/style.css`, 'utf8'); - let version = getThemeMetadata(styleCss, 'Version'); + let styleCss = fs.readFileSync( `${ theme }/style.css`, 'utf8' ); + let version = getThemeMetadata( styleCss, 'Version' ); // Get list of updates with bullet points - const lastestTagHash = execSync('git describe --tags --abbrev=0').toString().trim(); - let logs = await getCommitLogs(lastestTagHash, true, theme); + const lastestTagHash = execSync( 'git describe --tags --abbrev=0' ) + .toString() + .trim(); + let logs = await getCommitLogs( lastestTagHash, true, theme ); // Get theme readme.txt - let readmeFilePath = `${theme}/readme.txt`; + let readmeFilePath = `${ theme }/readme.txt`; - if (!existsSync(readmeFilePath)) { - console.log(`Unable to find a readme.txt for ${theme}.`); + if ( ! existsSync( readmeFilePath ) ) { + console.log( `Unable to find a readme.txt for ${ theme }.` ); return; } // Build changelog entry let newChangelogEntry = `== Changelog == -= ${version} = -${logs}`; += ${ version } = +${ logs }`; // Update readme.txt - fs.readFile(readmeFilePath, 'utf8', function (err, data) { + fs.readFile( readmeFilePath, 'utf8', function ( err, data ) { let changelogSection = '== Changelog =='; - let regex = new RegExp('^.*' + changelogSection + '.*$', 'gm'); - let formattedChangelog = data.replace(regex, newChangelogEntry); - - fs.writeFile(readmeFilePath, formattedChangelog, 'utf8', function (err) { - if (err) return console.log(err); - }); - }); + let regex = new RegExp( '^.*' + changelogSection + '.*$', 'gm' ); + let formattedChangelog = data.replace( regex, newChangelogEntry ); + + fs.writeFile( + readmeFilePath, + formattedChangelog, + 'utf8', + function ( err ) { + if ( err ) return console.log( err ); + } + ); + } ); // Stage readme.txt - if (addChanges) { - await executeCommand(`git add ${readmeFilePath}`); + if ( addChanges ) { + await executeCommand( `git add ${ readmeFilePath }` ); } } @@ -853,29 +990,42 @@ ${logs}`; First increment the patch version in style.css Then update any of these files with the new version: [package.json, style.scss, style-child-theme.scss] */ -async function versionBumpTheme(theme, addChanges) { - - console.log(`${theme} needs a version bump`); - - await executeCommand(`perl -pi -e 's/Version: ((\\d+\\.)*)(\\d+)(.*)$/"Version: ".$1.($3+1).$4/ge' ${theme}/style.css`, true); - await executeCommand(`git add ${theme}/style.css`); - - let styleCss = fs.readFileSync(`${theme}/style.css`, 'utf8'); - let currentVersion = getThemeMetadata(styleCss, 'Version', false); - - let filesToUpdate = await executeCommand(`find ${theme} -not \\( -path "*/node_modules/*" -prune \\) -and \\( -name package.json -or -name style.scss -or -name style-rtl.css -or -name style-child-theme.scss \\) -maxdepth 3`); - filesToUpdate = filesToUpdate.split('\n').filter(item => item != ''); - - for (let file of filesToUpdate) { - const isPackageJson = file === `${theme}/package.json`; - if (isPackageJson) { +async function versionBumpTheme( theme, addChanges ) { + console.log( `${ theme } needs a version bump` ); + + await executeCommand( + `perl -pi -e 's/Version: ((\\d+\\.)*)(\\d+)(.*)$/"Version: ".$1.($3+1).$4/ge' ${ theme }/style.css`, + true + ); + await executeCommand( `git add ${ theme }/style.css` ); + + let styleCss = fs.readFileSync( `${ theme }/style.css`, 'utf8' ); + let currentVersion = getThemeMetadata( styleCss, 'Version', false ); + + let filesToUpdate = await executeCommand( + `find ${ theme } -not \\( -path "*/node_modules/*" -prune \\) -and \\( -name package.json -or -name style.scss -or -name style-rtl.css -or -name style-child-theme.scss \\) -maxdepth 3` + ); + filesToUpdate = filesToUpdate + .split( '\n' ) + .filter( ( item ) => item != '' ); + + for ( let file of filesToUpdate ) { + const isPackageJson = file === `${ theme }/package.json`; + if ( isPackageJson ) { // update theme/package.json and package-lock.json - await executeCommand(`npm version ${currentVersion.replace('-wpcom', '')} --workspace=${theme}`); + await executeCommand( + `npm version ${ currentVersion.replace( + '-wpcom', + '' + ) } --workspace=${ theme }` + ); } else { - await executeCommand(`perl -pi -e 's/Version: (.*)$/"Version: '${currentVersion}'"/ge' ${file}`); + await executeCommand( + `perl -pi -e 's/Version: (.*)$/"Version: '${ currentVersion }'"/ge' ${ file }` + ); } - if (addChanges) { - await executeCommand(`git add ${file}`); + if ( addChanges ) { + await executeCommand( `git add ${ file }` ); } } } @@ -885,23 +1035,26 @@ async function versionBumpTheme(theme, addChanges) { Used by versionBumpThemes Compares the value of 'version' in style.css between the hash and current value */ -async function checkThemeForVersionBump(theme, hash) { - return executeCommand(` - git show ${hash}:${theme}/style.css 2>/dev/null - `) - .catch((error) => { +async function checkThemeForVersionBump( theme, hash ) { + return executeCommand( ` + git show ${ hash }:${ theme }/style.css 2>/dev/null + ` ) + .catch( ( error ) => { //This is a new theme, no need to bump versions so we'll just say we've already done it return true; - }) - .then((previousStyleString) => { - if (previousStyleString === true) { + } ) + .then( ( previousStyleString ) => { + if ( previousStyleString === true ) { return previousStyleString; } - let previousVersion = getThemeMetadata(previousStyleString, 'Version'); - let styleCss = fs.readFileSync(`${theme}/style.css`, 'utf8'); - let currentVersion = getThemeMetadata(styleCss, 'Version'); + let previousVersion = getThemeMetadata( + previousStyleString, + 'Version' + ); + let styleCss = fs.readFileSync( `${ theme }/style.css`, 'utf8' ); + let currentVersion = getThemeMetadata( styleCss, 'Version' ); return previousVersion != currentVersion; - }); + } ); } /* @@ -909,12 +1062,12 @@ async function checkThemeForVersionBump(theme, hash) { Used by versionBumpThemes Compares the value of 'version' in package.json between the hash and current value */ -async function checkProjectForVersionBump(hash) { - let previousPackageString = await executeCommand(` - git show ${hash}:./package.json 2>/dev/null - `); - let previousPackage = JSON.parse(previousPackageString); - let currentPackage = JSON.parse(fs.readFileSync(`./package.json`)) +async function checkProjectForVersionBump( hash ) { + let previousPackageString = await executeCommand( ` + git show ${ hash }:./package.json 2>/dev/null + ` ); + let previousPackage = JSON.parse( previousPackageString ); + let currentPackage = JSON.parse( fs.readFileSync( `./package.json` ) ); return previousPackage.version != currentPackage.version; } @@ -922,8 +1075,10 @@ async function checkProjectForVersionBump(hash) { Determine if a theme has had changes since a given hash. Used by versionBumpThemes */ -async function checkThemeForChanges(theme, hash) { - let comittedChanges = await executeCommand(`git diff --name-only ${hash} HEAD -- ${theme}`); +async function checkThemeForChanges( theme, hash ) { + let comittedChanges = await executeCommand( + `git diff --name-only ${ hash } HEAD -- ${ theme }` + ); return comittedChanges != ''; } @@ -931,14 +1086,12 @@ async function checkThemeForChanges(theme, hash) { Provide a list of 'actionable' themes (those themes that have style.css files) */ async function getActionableThemes() { - let result = await executeCommand(`for d in */; do + let result = await executeCommand( `for d in */; do if test -f "./$d/style.css"; then echo $d; fi - done`); - return result - .split('\n') - .map(item => item.replace('/', '')); + done` ); + return result.split( '\n' ).map( ( item ) => item.replace( '/', '' ) ); } /* @@ -947,36 +1100,42 @@ async function getActionableThemes() { Remove any other changes. */ async function cleanSandbox() { - console.log('Cleaning the Themes Sandbox'); - await executeOnSandbox(` - cd ${sandboxPublicThemesFolder}; + console.log( 'Cleaning the Themes Sandbox' ); + await executeOnSandbox( + ` + cd ${ sandboxPublicThemesFolder }; git reset --hard HEAD; git clean -fd; git checkout trunk; git pull; echo; git status - `, true); - console.log('All done cleaning.'); + `, + true + ); + console.log( 'All done cleaning.' ); } /* Push exactly what is here (all files) up to the sandbox (with the exclusion of files noted in .sandbox-ignore) */ async function pushToSandbox() { - console.log("Pushing All Themes to Sandbox."); + console.log( 'Pushing All Themes to Sandbox.' ); let allThemes = await getActionableThemes(); - console.log(`Syncing ${allThemes.length} themes`); - for (let theme of allThemes) { - await pushThemeToSandbox(theme); + console.log( `Syncing ${ allThemes.length } themes` ); + for ( let theme of allThemes ) { + await pushThemeToSandbox( theme ); } } -async function pushThemeToSandbox(theme) { - console.log(`Syncing ${theme}`); - return executeCommand(` - rsync -avR --no-p --no-times --delete -m --exclude-from='.sandbox-ignore' ./${theme}/ wpcom-sandbox:${sandboxPublicThemesFolder}/ - `, true); +async function pushThemeToSandbox( theme ) { + console.log( `Syncing ${ theme }` ); + return executeCommand( + ` + rsync -avR --no-p --no-times --delete -m --exclude-from='.sandbox-ignore' ./${ theme }/ wpcom-sandbox:${ sandboxPublicThemesFolder }/ + `, + true + ); } /* @@ -984,97 +1143,112 @@ async function pushThemeToSandbox(theme) { Remove files from the sandbox that have been removed since the last deployed hash */ async function pushChangesToSandbox() { - - console.log("Pushing Changed Themes to Sandbox."); + console.log( 'Pushing Changed Themes to Sandbox.' ); let hash = await getLastDeployedHash(); - let changedThemes = await getChangedThemes(hash); - console.log(`Syncing ${changedThemes.length} themes`); + let changedThemes = await getChangedThemes( hash ); + console.log( `Syncing ${ changedThemes.length } themes` ); - for (let theme of changedThemes) { - await pushThemeToSandbox(theme); + for ( let theme of changedThemes ) { + await pushThemeToSandbox( theme ); } } -async function checkoutCoreTheme(theme) { - if (!theme) { - console.log('Must supply theme to sync and revision to start from'); +async function checkoutCoreTheme( theme ) { + if ( ! theme ) { + console.log( 'Must supply theme to sync and revision to start from' ); return; } - return executeCommand(` - rm -rf ./${theme} - svn checkout https://wpcom-themes.svn.automattic.com/${theme} ./${theme} - `); + return executeCommand( ` + rm -rf ./${ theme } + svn checkout https://wpcom-themes.svn.automattic.com/${ theme } ./${ theme } + ` ); } async function pullAllThemes() { - console.log("Pulling ALL themes from sandbox."); + console.log( 'Pulling ALL themes from sandbox.' ); let allThemes = await getActionableThemes(); - for (let theme of allThemes) { + for ( let theme of allThemes ) { try { - await executeCommand(` - rsync -avr --no-p --no-times --delete -m --exclude-from='.sandbox-ignore' wpcom-sandbox:${sandboxPublicThemesFolder}/${theme}/ ./${theme}/ - `, true); - } - catch (err) { - console.log('Error pulling:', err); + await executeCommand( + ` + rsync -avr --no-p --no-times --delete -m --exclude-from='.sandbox-ignore' wpcom-sandbox:${ sandboxPublicThemesFolder }/${ theme }/ ./${ theme }/ + `, + true + ); + } catch ( err ) { + console.log( 'Error pulling:', err ); } } } async function pullCoreThemes() { - console.log("Pulling CORE themes from sandbox."); - for (let theme of coreThemes) { - await executeCommand(` - rsync -avr --no-p --no-times --delete -m --exclude-from='.sandbox-ignore' wpcom-sandbox:${sandboxPublicThemesFolder}/${theme}/ ./${theme}/ - `, true); + console.log( 'Pulling CORE themes from sandbox.' ); + for ( let theme of coreThemes ) { + await executeCommand( + ` + rsync -avr --no-p --no-times --delete -m --exclude-from='.sandbox-ignore' wpcom-sandbox:${ sandboxPublicThemesFolder }/${ theme }/ ./${ theme }/ + `, + true + ); } } async function pushCoreThemes() { - console.log("Pushing CORE themes to sandbox."); - for (let theme of coreThemes) { - await executeCommand(` - rsync -avr --no-p --no-times --delete -m --exclude-from='.sandbox-ignore' ./${theme}/ wpcom-sandbox:${sandboxPublicThemesFolder}/${theme}/ - `, true); + console.log( 'Pushing CORE themes to sandbox.' ); + for ( let theme of coreThemes ) { + await executeCommand( + ` + rsync -avr --no-p --no-times --delete -m --exclude-from='.sandbox-ignore' ./${ theme }/ wpcom-sandbox:${ sandboxPublicThemesFolder }/${ theme }/ + `, + true + ); } } -async function syncCoreTheme(theme, sinceRevision) { - if (!theme) { - console.log('Must supply theme to sync and revision to start from'); +async function syncCoreTheme( theme, sinceRevision ) { + if ( ! theme ) { + console.log( 'Must supply theme to sync and revision to start from' ); return; } - if (!sinceRevision) { - sinceRevision = await executeCommand(`cat ./${theme}/.pub-svn-revision`); + if ( ! sinceRevision ) { + sinceRevision = await executeCommand( + `cat ./${ theme }/.pub-svn-revision` + ); } - let latestRevision = await executeCommand(`svn info -r HEAD https://develop.svn.wordpress.org/trunk | grep Revision | egrep -o "[0-9]+"`); - console.log(`syncing core theme ${theme} from ${sinceRevision} to ${latestRevision}`); + let latestRevision = await executeCommand( + `svn info -r HEAD https://develop.svn.wordpress.org/trunk | grep Revision | egrep -o "[0-9]+"` + ); + console.log( + `syncing core theme ${ theme } from ${ sinceRevision } to ${ latestRevision }` + ); try { - await executeCommand(` - svn merge --accept postpone http://develop.svn.wordpress.org/trunk/src/wp-content/themes/${theme} ./${theme} -r${sinceRevision}:HEAD - echo '${latestRevision}' > ./${theme}/.pub-svn-revision - `, true); - } - catch (err) { - console.log('Error merging:', err); + await executeCommand( + ` + svn merge --accept postpone http://develop.svn.wordpress.org/trunk/src/wp-content/themes/${ theme } ./${ theme } -r${ sinceRevision }:HEAD + echo '${ latestRevision }' > ./${ theme }/.pub-svn-revision + `, + true + ); + } catch ( err ) { + console.log( 'Error merging:', err ); } return latestRevision; } - /* Build the GitHub commit message. This message contains the logs from all of the commits since the given hash. Used by create*GithubPR */ -async function buildGithubCommitMessageSince(hash) { - - let projectVersion = await executeCommand(`node -p "require('./package.json').version"`); - let logs = await getCommitLogs(hash); - return `Deploy Themes ${projectVersion} to wpcom +async function buildGithubCommitMessageSince( hash ) { + let projectVersion = await executeCommand( + `node -p "require('./package.json').version"` + ); + let logs = await getCommitLogs( hash ); + return `Deploy Themes ${ projectVersion } to wpcom Summary: -${logs} +${ logs } Test Plan: Execute Smoke Test `; @@ -1085,26 +1259,28 @@ Test Plan: Execute Smoke Test Open the GitHub pull request in your browser. Provide the URL of the GitHub pull request. */ -async function createGithubPR(commitMessage) { +async function createGithubPR( commitMessage ) { + console.log( 'Creating GitHub Pull Request' ); - console.log('Creating GitHub Pull Request'); - - let result = await executeOnSandbox(` - cd ${sandboxPublicThemesFolder}; + let result = await executeOnSandbox( + ` + cd ${ sandboxPublicThemesFolder }; git branch -D deploy git checkout -b deploy git add --all - git commit -m "${commitMessage}" + git commit -m "${ commitMessage }" git push origin deploy gh pr create --fill --head deploy - `, true); + `, + true + ); - let githubUrl = getGithubUrlFromResponse(result); + let githubUrl = getGithubUrlFromResponse( result ); - console.log('PR Created at: ', githubUrl); + console.log( 'PR Created at: ', githubUrl ); - if (githubUrl) { - open(githubUrl); + if ( githubUrl ) { + open( githubUrl ); } return githubUrl; @@ -1114,10 +1290,10 @@ async function createGithubPR(commitMessage) { Utility to pull the GitHub URL from the PR creation command. Used by createGithubPR */ -function getGithubUrlFromResponse(response) { +function getGithubUrlFromResponse( response ) { return response - ?.split('\n') - ?.filter(item => item.includes('http')) // filter out lines that include 'http' + ?.split( '\n' ) + ?.filter( ( item ) => item.includes( 'http' ) ) // filter out lines that include 'http' ?.pop(); // get the last URL } @@ -1126,47 +1302,58 @@ function getGithubUrlFromResponse(response) { In the description include the commit logs since the given hash. Include the (cleansed) GitHub PR link. */ -async function tagDeployment(options = {}) { - - console.log('tagging deployment'); +async function tagDeployment( options = {} ) { + console.log( 'tagging deployment' ); - let hash = options.hash || await getLastDeployedHash(); + let hash = options.hash || ( await getLastDeployedHash() ); let workInTheOpenGithubUrl = ''; - if (options.prId) { - workInTheOpenGithubUrl = `GitHub: ${options.prId}`; + if ( options.prId ) { + workInTheOpenGithubUrl = `GitHub: ${ options.prId }`; } - let projectVersion = await executeCommand(`node -p "require('./package.json').version"`); - let logs = await getCommitLogs(hash); - let tag = `v${projectVersion}`; - let message = `Deploy Themes ${tag} to wpcom. \n\n${logs} \n\n${workInTheOpenGithubUrl}`; - - await executeCommand(` - git tag -a ${tag} -m "${message}" - git push origin ${tag} - `, true); + let projectVersion = await executeCommand( + `node -p "require('./package.json').version"` + ); + let logs = await getCommitLogs( hash ); + let tag = `v${ projectVersion }`; + let message = `Deploy Themes ${ tag } to wpcom. \n\n${ logs } \n\n${ workInTheOpenGithubUrl }`; + + await executeCommand( + ` + git tag -a ${ tag } -m "${ message }" + git push origin ${ tag } + `, + true + ); } -async function createGlotPressProject(changedThemes) { - for (const themeSlug of changedThemes) { - let styleCss = fs.readFileSync(`${themeSlug}/style.css`, 'utf8'); - let themeVersion = getThemeMetadata(styleCss, 'Version'); - +async function createGlotPressProject( changedThemes ) { + for ( const themeSlug of changedThemes ) { + let styleCss = fs.readFileSync( `${ themeSlug }/style.css`, 'utf8' ); + let themeVersion = getThemeMetadata( styleCss, 'Version' ); + // Check if theme version is correctly formatted. Temporarily coerce the value to a parseable semver version if it's not. - if (!semver.valid(themeVersion)) { - console.log(`\n[WARN] Invalid version in style.css for ${themeSlug}: ${themeVersion}\n`); - themeVersion = semver.coerce(themeVersion); + if ( ! semver.valid( themeVersion ) ) { + console.log( + `\n[WARN] Invalid version in style.css for ${ themeSlug }: ${ themeVersion }\n` + ); + themeVersion = semver.coerce( themeVersion ); } - if (semver.gte(themeVersion, '1.0.0')) { - console.log(`\nCreating GlotPress project for ${themeSlug}\n`); + if ( semver.gte( themeVersion, '1.0.0' ) ) { + console.log( `\nCreating GlotPress project for ${ themeSlug }\n` ); - await executeOnSandbox(` - cd ${sandboxPublicThemesFolder}; - php ${glotPressScript} ${sandboxPublicThemesFolder}/${themeSlug}; - `, true); + await executeOnSandbox( + ` + cd ${ sandboxPublicThemesFolder }; + php ${ glotPressScript } ${ sandboxPublicThemesFolder }/${ themeSlug }; + `, + true + ); } else { - console.log(`\nSkipped GlotPress project creation for ${themeSlug}.\nVersion bump to a major release to create GlotPress project.\n`); + console.log( + `\nSkipped GlotPress project creation for ${ themeSlug }.\nVersion bump to a major release to create GlotPress project.\n` + ); } } } @@ -1180,228 +1367,266 @@ Host wpcom-sandbox HostName SANDBOXURL.wordpress.com ForwardAgent yes */ -function executeOnSandbox(command, logResponse, enablePsudoterminal) { - - if (enablePsudoterminal) { - return executeCommand(`ssh -tt -A ${remoteSSH} << EOF -${command} -EOF`, logResponse); +function executeOnSandbox( command, logResponse, enablePsudoterminal ) { + if ( enablePsudoterminal ) { + return executeCommand( + `ssh -tt -A ${ remoteSSH } << EOF +${ command } +EOF`, + logResponse + ); } - return executeCommand(`ssh -TA ${remoteSSH} << EOF -${command} -EOF`, logResponse); + return executeCommand( + `ssh -TA ${ remoteSSH } << EOF +${ command } +EOF`, + logResponse + ); } /* Execute a command locally. */ -export async function executeCommand(command, logResponse) { - const timeout = 2*60*1000; // 2 min +export async function executeCommand( command, logResponse ) { + const timeout = 2 * 60 * 1000; // 2 min - return new Promise((resolove, reject) => { + return new Promise( ( resolove, reject ) => { let child; let response = ''; let errResponse = ''; - if (isWin) { - child = spawn('cmd.exe', ['/s', '/c', '"' + command + '"'], { + if ( isWin ) { + child = spawn( 'cmd.exe', [ '/s', '/c', '"' + command + '"' ], { windowsVerbatimArguments: true, - stdio: [process.stdin, 'pipe', 'pipe'], + stdio: [ process.stdin, 'pipe', 'pipe' ], detached: true, - }) + } ); } else { /* - * Determines the shell to execute the command. - * - Prioritizes using the user's default shell unless it's fish, a known problematic shell. - * - In this case, falls back to `/bin/bash` for better syntax compatibility. - */ + * Determines the shell to execute the command. + * - Prioritizes using the user's default shell unless it's fish, a known problematic shell. + * - In this case, falls back to `/bin/bash` for better syntax compatibility. + */ let shellPath = process.env.SHELL || '/bin/bash'; - if (shellPath.includes('fish') && fs.existsSync('/bin/bash')) { + if ( + shellPath.includes( 'fish' ) && + fs.existsSync( '/bin/bash' ) + ) { shellPath = '/bin/bash'; } - child = spawn(shellPath, ['-c', command], { - stdio: [process.stdin, 'pipe', 'pipe'], + child = spawn( shellPath, [ '-c', command ], { + stdio: [ process.stdin, 'pipe', 'pipe' ], detached: true, - }); + } ); } - var timer = setTimeout(() => { + var timer = setTimeout( () => { try { - process.kill(-child.pid, 'SIGKILL'); - } catch (e) { - console.log('Cannot kill process'); + process.kill( -child.pid, 'SIGKILL' ); + } catch ( e ) { + console.log( 'Cannot kill process' ); } - }, timeout); + }, timeout ); - child.stdout.on('data', (data) => { + child.stdout.on( 'data', ( data ) => { response += data; - if (logResponse) { - console.log(data.toString()); + if ( logResponse ) { + console.log( data.toString() ); } - }); + } ); - child.stderr.on('data', (data) => { + child.stderr.on( 'data', ( data ) => { errResponse += data; - if (logResponse) { - console.log(data.toString()); + if ( logResponse ) { + console.log( data.toString() ); } - }); + } ); - child.on('exit', (code) => { - clearTimeout(timer) - if (code !== 0) { - reject(errResponse.trim()); + child.on( 'exit', ( code ) => { + clearTimeout( timer ); + if ( code !== 0 ) { + reject( errResponse.trim() ); } - resolove(response.trim()); - }); - }); + resolove( response.trim() ); + } ); + } ); } -async function escapePatterns() { - // get staged files - const staged = (await executeCommand(`git diff --cached --name-only`)).split('\n'); - // get unstaged, untracked files - const unstaged = (await executeCommand(`git ls-files -m -o --exclude-standard`)).split('\n'); - - // avoid duplicates and filter pattern files - const patterns = [...new Set([...staged, ...unstaged])].filter(file => file.match(/.*\/patterns\/.*.php/g)); - - // arrange patterns by theme - const themePatterns = patterns.reduce((acc, file) => { - const themeSlug = file.split('/').shift(); +async function escapePatterns( themeSlug ) { + let patternFiles; + + if ( themeSlug ) { + // If a theme slug is provided, use fast-glob to find all PHP files in the specified theme's directory + patternFiles = await glob( `${ themeSlug }/patterns/**/*.php`, { + ignore: [ 'node_modules/**', 'vendor/**' ], // Exclude node_modules and vendor directories + } ); + } else { + // If no theme slug is provided, detect changed files via Git + const stagedFiles = await executeCommand( + 'git diff --name-only --cached' + ); + const unstagedFiles = await executeCommand( 'git diff --name-only' ); + + // Combine staged and unstaged files, remove duplicates, and filter for pattern files + patternFiles = [ + ...new Set( [ + ...stagedFiles.split( '\n' ), + ...unstagedFiles.split( '\n' ), + ] ), + ].filter( ( file ) => file.match( /.*\/patterns\/.*.php/g ) ); + } + + // Arrange patterns by theme + const themePatterns = patternFiles.reduce( ( acc, file ) => { + const themeSlug = file.split( '/' ).shift(); return { ...acc, - [themeSlug]: (acc[themeSlug] || []).concat(file) + [ themeSlug ]: ( acc[ themeSlug ] || [] ).concat( file ), }; - }, {}); - - Object.entries(themePatterns).forEach(async ([themeSlug, patterns]) => { - console.log(getPatternTable(themeSlug, patterns)); - - const prompt = await inquirer.prompt([{ - type: 'input', - message: 'Verify the theme slug', - name: "themeSlug", - default: themeSlug - }]); - - if (!prompt.themeSlug) { - return; - } + }, {} ); - patterns.forEach(file => { - const rewriter = getReWriter(prompt.themeSlug); - const tmpFile = `${file}-tmp`; - const readStream = fs.createReadStream( file, { encoding: 'UTF-8' } ); - const writeStream = fs.createWriteStream( tmpFile, { encoding: 'UTF-8' } ); - writeStream.on('finish', () => { - fs.renameSync(tmpFile, file); - }); + // Process each theme's patterns + for ( const [ themeSlug, patterns ] of Object.entries( themePatterns ) ) { + console.log( getPatternTable( themeSlug, patterns ) ); + + const prompt = await inquirer.prompt( [ + { + type: 'input', + message: 'Verify the theme slug', + name: 'themeSlug', + default: themeSlug, + }, + ] ); - readStream.pipe(rewriter).pipe(writeStream); - }); - }); + if ( ! prompt.themeSlug ) { + return; + } + for ( const file of patterns ) { + const rewriter = getReWriter( prompt.themeSlug ); + const tmpFile = `${ file }-tmp`; + const readStream = fs.createReadStream( file, { + encoding: 'UTF-8', + } ); + const writeStream = fs.createWriteStream( tmpFile, { + encoding: 'UTF-8', + } ); + writeStream.on( 'finish', () => { + fs.renameSync( tmpFile, file ); + } ); + + readStream.pipe( rewriter ).pipe( writeStream ); + } + } // Helper functions - function getReWriter(themeSlug) { + function getReWriter( themeSlug ) { const rewriter = new RewritingStream(); - rewriter.on('text', (_, raw) => { - rewriter.emitRaw(escapeText(raw, themeSlug)); - }); - - rewriter.on('startTag', (startTag, rawHtml) => { - if (startTag.tagName === 'img') { - const attrs = startTag.attrs.filter(attr => ['src', 'alt'].includes(attr.name)); - attrs.forEach(attr => { - if (attr.name === 'src') { - attr.value = escapeImagePath(attr.value); - } else if (attr.name === 'alt') { - attr.value = escapeText(attr.value, themeSlug, true); + rewriter.on( 'text', ( _, raw ) => { + rewriter.emitRaw( escapeText( raw, themeSlug ) ); + } ); + + rewriter.on( 'startTag', ( startTag, rawHtml ) => { + if ( startTag.tagName === 'img' ) { + const attrs = startTag.attrs.filter( ( attr ) => + [ 'src', 'alt' ].includes( attr.name ) + ); + attrs.forEach( ( attr ) => { + if ( attr.name === 'src' ) { + attr.value = escapeImagePath( attr.value ); + } else if ( attr.name === 'alt' ) { + attr.value = escapeText( attr.value, themeSlug, true ); } - }); + } ); } + rewriter.emitStartTag( startTag ); + } ); - rewriter.emitStartTag(startTag); - }); - - rewriter.on('comment', (comment, rawHtml) => { - if (comment.text.startsWith('?php')) { - rewriter.emitRaw(rawHtml); + rewriter.on( 'comment', ( comment, rawHtml ) => { + if ( comment.text.startsWith( '?php' ) ) { + rewriter.emitRaw( rawHtml ); return; } // escape the strings in block config (blocks that are represented as comments) // ex: - const block = escapeBlockAttrs(comment.text, themeSlug) - rewriter.emitComment({...comment, text: block}) - }); + const block = escapeBlockAttrs( comment.text, themeSlug ); + rewriter.emitComment( { ...comment, text: block } ); + } ); return rewriter; } - function escapeBlockAttrs(block, themeSlug) { + function escapeBlockAttrs( block, themeSlug ) { // Set isAttr to true if it is an attribute in the result HTML // If set to true, it generates esc_attr_, otherwise it generates esc_html_ - const allowedAttrs=[ + const allowedAttrs = [ { name: 'label' }, { name: 'placeholder', isAttr: true }, { name: 'buttonText' }, - { name: 'content' } + { name: 'content' }, ]; - const start = block.indexOf('{'); - const end = block.lastIndexOf('}'); - - const configPrefix = block.slice(0, start); - const config = block.slice(start, end+1); - const configSuffix = block.slice(end+1); + const start = block.indexOf( '{' ); + const end = block.lastIndexOf( '}' ); + + const configPrefix = block.slice( 0, start ); + const config = block.slice( start, end + 1 ); + const configSuffix = block.slice( end + 1 ); try { - const configJson = JSON.parse(config); - allowedAttrs.forEach((attr) => { - if (!configJson[attr.name]) return; - configJson[attr.name] = escapeText(configJson[attr.name], themeSlug, attr.isAttr) - }) - return configPrefix + JSON.stringify(configJson) + configSuffix; - } catch (error) { + const configJson = JSON.parse( config ); + allowedAttrs.forEach( ( attr ) => { + if ( ! configJson[ attr.name ] ) return; + configJson[ attr.name ] = escapeText( + configJson[ attr.name ], + themeSlug, + attr.isAttr + ); + } ); + return configPrefix + JSON.stringify( configJson ) + configSuffix; + } catch ( error ) { // do nothing - return block + return block; } } - function escapeText(text, themeSlug, isAttr = false) { + function escapeText( text, themeSlug, isAttr = false ) { const trimmedText = text && text.trim(); - if (!themeSlug || !trimmedText || trimmedText.startsWith(``; + const spaceChar = text.startsWith( ' ' ) ? ' ' : ''; + const resultText = text.replace( "'", "\\'" ).trim(); + return `${ spaceChar }`; } - function escapeImagePath(src) { - if (!src || src.trim().startsWith('/${resultSrc}`; + const parts = src.split( '/' ); + const resultSrc = parts.slice( parts.indexOf( assetsDir ) ).join( '/' ); + return `/${ resultSrc }`; } - function getPatternTable(themeSlug, patterns) { + function getPatternTable( themeSlug, patterns ) { const tableConfig = { columnDefault: { width: 40, }, header: { alignment: 'center', - content: `THEME: ${themeSlug}\n\n Following patterns may get updated with escaped strings and/or image paths`, - } + content: `THEME: ${ themeSlug }\n\n Following patterns may get updated with escaped strings and/or image paths`, + }, }; - return table(patterns.map(p => [p]), tableConfig); + return table( + patterns.map( ( p ) => [ p ] ), + tableConfig + ); } } @@ -1534,12 +1759,16 @@ async function validateThemes( themes, { format, color, tableWidth } ) { } const styleCss = await fs.promises.readFile( styleCssPath, 'utf-8' ); - const themeRequires = getThemeMetadata( styleCss, 'Requires at least', true ); + const themeRequires = getThemeMetadata( + styleCss, + 'Requires at least', + true + ); const wpVersion = themeRequires ? `${ themeRequires }.0`.split( '.', 2 ).join( '.' ) : undefined; - const hasThemeJsonSupport = wpVersion && semver.valid( `${ wpVersion }.0` ) && semver.gte( `${ wpVersion }.0`, '5.9.0' ) - const hasThemeJson = fs.existsSync( themeJsonPath ); + const isSupportedWpVersion = + wpVersion && semver.gte( `${ wpVersion }.0`, '5.9.0' ); if ( hasThemeJson && ! hasThemeJsonSupport ) { problems.push( @@ -1926,7 +2155,9 @@ async function validateThemes( themes, { format, color, tableWidth } ) { if ( process.stdout.isTTY ) { if ( problems.length ) { - console.log( chalk.yellow( '\n\nValidation passed with warnings.' ) ); + console.log( + chalk.yellow( '\n\nValidation passed with warnings.' ) + ); } else { console.log( chalk.green( '\n\nValidation passed.' ) ); } @@ -1963,7 +2194,8 @@ function createProblem( options ) { const separatorIndex = themeFile.indexOf( '/' ); const theme = themeOverride ? themeOverride.charAt( 0 ).toUpperCase() + themeOverride.slice( 1 ) - : themeFile.charAt( 0 ).toUpperCase() + themeFile.slice( 1, separatorIndex ); + : themeFile.charAt( 0 ).toUpperCase() + + themeFile.slice( 1, separatorIndex ); const file = themeFile.slice( separatorIndex + 1 ); const data = Array.isArray( problemData ) ? problemData : [ problemData ]; return { type, theme, file, metadata, data }; @@ -2134,23 +2366,30 @@ function problemsToTable( problems, options ) { return table( tableData, userConfig ).slice( 0, -1 ); // Remove trailing newline. } -function startProgress(length) { - if (!process.stdout.isTTY) { +function startProgress( length ) { + if ( ! process.stdout.isTTY ) { return { increment() {} }; } let current = 0; function render() { - const [progress, percentage] = progressbar.filledBar(length, current); - console.log('\nProgress:', [progress, Math.round(percentage*100)/100], `${current}/${length}\n`); + const [ progress, percentage ] = progressbar.filledBar( + length, + current + ); + console.log( + '\nProgress:', + [ progress, Math.round( percentage * 100 ) / 100 ], + `${ current }/${ length }\n` + ); } render(); return { increment() { current++; - process.stdout.moveCursor?.(0, -3); + process.stdout.moveCursor?.( 0, -3 ); render(); - } + }, }; }