theme-utils.mjs 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373
  1. import { spawn } from 'child_process';
  2. import fs, { existsSync } from 'fs';
  3. import open from 'open';
  4. import inquirer from 'inquirer';
  5. import { RewritingStream } from 'parse5-html-rewriting-stream';
  6. import { table } from 'table';
  7. import progressbar from 'string-progressbar';
  8. const remoteSSH = 'wpcom-sandbox';
  9. const sandboxPublicThemesFolder = '/home/wpdev/public_html/wp-content/themes/pub';
  10. const sandboxPremiumThemesFolder = '/home/wpdev/public_html/wp-content/themes/premium';
  11. const sandboxRootFolder = '/home/wpdev/public_html/';
  12. const isWin = process.platform === 'win32';
  13. const premiumThemes = ['videomaker', 'videomaker-white'];
  14. const coreThemes = ['twentyten', 'twentyeleven', 'twentytwelve', 'twentythirteen', 'twentyfourteen', 'twentyfifteen', 'twentysixteen', 'twentyseventeen', 'twentynineteen', 'twentytwenty', 'twentytwentyone', 'twentytwentytwo'];
  15. const commands = {
  16. "push-button-deploy": {
  17. helpText: `
  18. * Gets the last deployed hash from the sandbox
  19. * Version bump all themes that have changes since the last deployment
  20. * Commit the version bump change to github
  21. * Clean the sandbox and ensure it is up - to - date
  22. * Push all changed files(including removal of deleted files) since the last deployment
  23. * Update the 'last deployed' hash on the sandbox
  24. * Create a phabricator diff based on the changes since the last deployment.The description including the commit messages since the last deployment.
  25. * Open the Phabricator Diff in your browser
  26. * Create a tag in the github repository at this point of change which includes the phabricator link in the description
  27. * After pausing to allow testing, land and deploy the changes
  28. `,
  29. run: pushButtonDeploy
  30. },
  31. "clean-sandbox": {
  32. helpText: 'Perform a hard reset, checkout trunk, and pull on the public themes working copy on your sandbox.',
  33. run: cleanSandbox
  34. },
  35. "clean-premium-sandbox": {
  36. helpText: 'Perform a hard reset, checkout trunk, and pull on the premium themes working copy on your sandbox.',
  37. run: cleanPremiumSandbox
  38. },
  39. "clean-all-sandbox": {
  40. helpText: 'Perform a hard reset, checkout trunk, and pull on both the public and premium themes working copies on your sandbox.',
  41. run: cleanAllSandbox
  42. },
  43. "push-to-sandbox": {
  44. helpText: 'Uses rsync to copy all modified files for all themes from the local machine to your sandbox.',
  45. run: pushToSandbox
  46. },
  47. "push-changes-to-sandbox": {
  48. helpText: 'Uses rsync to copy all modified files for any modified themes from the local machine to your sandbox.',
  49. run: pushChangesToSandbox
  50. },
  51. "push-theme-to-sandbox": {
  52. helpText: 'Uses rsync to copy all modified files for the specified theme from the local machine to your sandbox.',
  53. additionalArgs: '<theme-slug>',
  54. run: (args) => pushThemeToSandbox(args?.[1])
  55. },
  56. "push-premium-to-sandbox": {
  57. helpText: 'Uses rsync to copy all modified files for all premium themes from the local machine to your sandbox. For the blockbase theme, all instances of the string "blockbase" are replaced with "blockbase-premium".',
  58. run: pushPremiumToSandbox
  59. },
  60. "version-bump-themes": {
  61. 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.',
  62. run: versionBumpThemes
  63. },
  64. "land-diff": {
  65. helpText: 'Run arc land to merge in the specified diff id.',
  66. additionalArgs: '<arc diff id>',
  67. run: (args) => landChanges(args?.[1])
  68. },
  69. "deploy-preview": {
  70. helpText: 'Display a list of the changes to be deployed.',
  71. run: deployPreview
  72. },
  73. "deploy-theme": {
  74. helpText: 'This runs "deploy pub <theme>" on the provided list of themes.',
  75. additionalArgs: '<array of theme slugs>',
  76. run: (args) => deployThemes(args?.[1].split(/[ ,]+/))
  77. },
  78. "build-com-zip": {
  79. helpText: 'Build the production zip file for the specified theme.',
  80. additionalArgs: '<theme-slug>',
  81. run: (args) => buildComZips(args?.[1].split(/[ ,]+/))
  82. },
  83. "checkout-core-theme": {
  84. helpText: 'Use SVN to checkout the given core themes from the wpcom SVN repository.',
  85. additionalArgs: '<theme-slug>',
  86. run: (args) => checkoutCoreTheme(args?.[1])
  87. },
  88. "pull-core-themes": {
  89. helpText: 'Use rsync to copy all public CORE theme files from your sandbox to your local machine. CORE themes are any of the Twenty<whatever> themes.',
  90. run: pullCoreThemes
  91. },
  92. "push-core-themes": {
  93. helpText: 'Use rsync to copy all public CORE theme files from your local machine to your sandbox. CORE themes are any of the Twenty<whatever> themes.',
  94. run: pushCoreThemes
  95. },
  96. "sync-core-theme": {
  97. 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.',
  98. additionalArgs: '<theme-slug> <since-revision>',
  99. run: (args) => syncCoreTheme(args?.[1], args?.[2])
  100. },
  101. "deploy-sync-core-theme": {
  102. 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.',
  103. additionalArgs: '<theme-slug> <since-revision>',
  104. run: (args) => deploySyncCoreTheme(args?.[1], args?.[2])
  105. },
  106. "create-core-phabricator-diff": {
  107. helpText: 'Given a theme slug and specific revision create a Phabricator diff from the resources currently on the sandbox.',
  108. additionalArgs: '<theme-slug> <since-revision>',
  109. run: (args) => createCorePhabriactorDiff(args?.[1], args?.[2])
  110. },
  111. "update-theme-changelog": {
  112. 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.',
  113. additionalArgs: '<theme-slug> <add-changes, true/false>',
  114. run: (args) => updateThemeChangelog(args?.[1], false, args?.[2])
  115. },
  116. "rebuild-theme-changelog": {
  117. helpText: 'Rebuild the entire change long from the given starting hash.',
  118. additionalArgs: '<theme-slug> <since>',
  119. run: (args) => rebuildThemeChangelog(args?.[1], args?.[2])
  120. },
  121. "escape-patterns": {
  122. helpText: 'Escapes block patterns for pattern files that have changes (staged or unstaged).',
  123. run: () => escapePatterns()
  124. },
  125. "help": {
  126. helpText: 'Displays the main help message.',
  127. run: (args) => showHelp(args?.[1])
  128. },
  129. };
  130. (async function start() {
  131. let args = process.argv.slice(2);
  132. let command = args?.[0];
  133. if (!commands[command]) {
  134. return showHelp();
  135. }
  136. commands[command].run(args);
  137. })();
  138. function showHelp(command = '') {
  139. if (!command || !commands.hasOwnProperty(command)) {
  140. console.log(`
  141. node theme-utils.mjs [command]
  142. Available commands:
  143. _(theme-utils.mjs help [command] for more details)_
  144. \t${Object.keys(commands).join('\n\t')}
  145. `);
  146. return;
  147. }
  148. const { helpText, additionalArgs } = commands[command];
  149. console.log(`
  150. ${command} ${additionalArgs ?? ''}
  151. ${helpText}
  152. `);
  153. }
  154. /*
  155. Create list of changes from git logs
  156. Optionally pass in a deployed hash or default to calling getLastDeployedHash()
  157. Optionally pass in boolean bulletPoints to add bullet points to each commit log
  158. */
  159. async function getCommitLogs(hash, bulletPoints, theme) {
  160. if (!hash) {
  161. hash = await getLastDeployedHash();
  162. }
  163. let format = 'format:%s';
  164. let themeDir = '';
  165. if (bulletPoints) {
  166. format = 'format:"* %s"';
  167. }
  168. if (theme) {
  169. themeDir = `-- ./${theme}`;
  170. }
  171. let logs = await executeCommand(`git log --reverse --pretty=${format} ${hash}..HEAD ${themeDir}`);
  172. // Remove any double quotes from commit messages
  173. logs = logs.replace(/"/g, '');
  174. return logs;
  175. }
  176. /*
  177. Determine what changes would be deployed
  178. */
  179. async function deployPreview() {
  180. console.clear();
  181. console.log('To ensure accuracy clean your sandbox before previewing. (It is not automatically done).');
  182. let message = await checkForDeployability();
  183. if (message) {
  184. console.log(`\n${message}\n\n`);
  185. }
  186. let hash = await getLastDeployedHash();
  187. console.log(`Last deployed hash: ${hash}`);
  188. let changedThemes = await getChangedThemes(hash);
  189. console.log(`The following themes have changes:\n${changedThemes}`);
  190. let logs = await getCommitLogs(hash);
  191. console.log(`\n\nCommit log of changes to be deployed:\n\n${logs}\n\n`);
  192. }
  193. /*
  194. Execute the first phase of a deployment.
  195. * Gets the last deployed hash from the sandbox
  196. * Version bump all themes that have changes since the last deployment
  197. * Commit the version bump change to github
  198. * Clean the sandbox and ensure it is up-to-date
  199. * Push all changed files (including removal of deleted files) since the last deployment
  200. * Update the 'last deployed' hash on the sandbox
  201. * Create a phabricator diff based on the changes since the last deployment. The description including the commit messages since the last deployment.
  202. * Open the Phabricator Diff in your browser
  203. * Create a tag in the github repository at this point of change which includes the phabricator link in the description
  204. */
  205. async function pushButtonDeploy() {
  206. console.clear();
  207. let prompt = await inquirer.prompt([{
  208. type: 'confirm',
  209. message: 'You are about to deploy /trunk. Are you ready to continue?',
  210. name: "continue",
  211. default: false
  212. }]);
  213. if (!prompt.continue) {
  214. return;
  215. }
  216. let message = await checkForDeployability();
  217. if (message) {
  218. return console.log(`\n\n${message}\n\n`);
  219. }
  220. try {
  221. await cleanSandbox();
  222. let hash = await getLastDeployedHash();
  223. let thingsWentBump = await versionBumpThemes();
  224. if (thingsWentBump) {
  225. prompt = await inquirer.prompt([{
  226. type: 'confirm',
  227. message: 'Are you good with the version bump and changelog updates? Make any manual adjustments now if necessary.',
  228. name: "continue",
  229. default: false
  230. }]);
  231. if (!prompt.continue) {
  232. console.log(`Aborted Automated Deploy Process at version bump changes.`);
  233. return;
  234. }
  235. }
  236. let changedThemes = await getChangedThemes(hash);
  237. await pushChangesToSandbox();
  238. //push changes (from version bump)
  239. if (thingsWentBump) {
  240. prompt = await inquirer.prompt([{
  241. type: 'confirm',
  242. message: 'Are you ready to push this version bump change to the source repository (Github)?',
  243. name: "continue",
  244. default: false
  245. }]);
  246. if (!prompt.continue) {
  247. console.log(`Aborted Automated Deploy Process at version bump push change.`);
  248. return;
  249. }
  250. await executeCommand(`
  251. git commit -m "Version Bump";
  252. git push
  253. `, true);
  254. }
  255. await updateLastDeployedHash();
  256. let commitMessage = await buildPhabricatorCommitMessageSince(hash);
  257. let diffUrl = await createPhabricatorDiff(commitMessage);
  258. let diffId = diffUrl.split('a8c.com/')[1];
  259. await tagDeployment({
  260. hash: hash,
  261. diffId: diffId
  262. });
  263. console.log(`\n\nPhase One Complete\n\nYour sandbox has been updated and the diff 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`);
  264. prompt = await inquirer.prompt([{
  265. type: 'confirm',
  266. message: 'Are you ready to land these changes?',
  267. name: "continue",
  268. default: false
  269. }]);
  270. if (!prompt.continue) {
  271. console.log(`Aborted Automated Deploy Process Landing Phase\n\nYou will have to land these changes manually. The ID of the diff to land: ${diffId}`);
  272. return;
  273. }
  274. await landChanges(diffId);
  275. let changedPublicThemes = changedThemes.filter(item => !premiumThemes.includes(item));
  276. try {
  277. await deployThemes(changedPublicThemes);
  278. }
  279. catch (err) {
  280. prompt = await inquirer.prompt([{
  281. type: 'confirm',
  282. message: `There was an error deploying themes. ${err} Do you wish to continue to the next step?`,
  283. name: "continue",
  284. default: false
  285. }]);
  286. if (!prompt.continue) {
  287. console.log(`Aborted Automated Deploy during deploy phase.`);
  288. return;
  289. }
  290. }
  291. await buildComZips(changedPublicThemes);
  292. console.log(`The following themes have changed:\n${changedThemes.join('\n')}`)
  293. console.log('\n\nAll Done!!\n\n');
  294. }
  295. catch (err) {
  296. console.log("ERROR with deploy script: ", err);
  297. }
  298. }
  299. async function deploySyncCoreTheme(theme, sinceRevision) {
  300. if (!theme) {
  301. console.log('Must supply theme to sync and revision to start from');
  302. return;
  303. }
  304. await cleanSandbox();
  305. await checkoutCoreTheme(theme);
  306. await syncCoreTheme(theme, sinceRevision);
  307. let prompt = await inquirer.prompt([{
  308. type: 'confirm',
  309. message: `Changes have been synced locally. Please resolve any conflicts now. Are you ready to continue?`,
  310. name: "continue",
  311. default: false
  312. }]);
  313. if (!prompt.continue) {
  314. console.log(`Aborted Core Sync Deploy.`);
  315. return;
  316. }
  317. await pushThemeToSandbox(theme);
  318. let diffId = await createCorePhabriactorDiff(theme, sinceRevision);
  319. prompt = await inquirer.prompt([{
  320. type: 'confirm',
  321. message: 'Are you ready to land these changes?',
  322. name: "continue",
  323. default: false
  324. }]);
  325. if (!prompt.continue) {
  326. console.log(`Aborted Automated Deploy Sync Process Landing Phase\n\nYou will have to land these changes manually. The ID of the diff to land: ${diffId}`);
  327. return;
  328. }
  329. return;
  330. // await landChanges(diffId);
  331. // await deployThemes([theme]);
  332. // await buildComZips([theme]);
  333. }
  334. async function buildCorePhabricatorCommitMessageSince(theme, sinceRevision){
  335. let latestRevision = await executeCommand(`svn info -r HEAD https://develop.svn.wordpress.org/trunk | grep Revision | egrep -o "[0-9]+"`);
  336. let logs = await executeCommand(`svn log https://core.svn.wordpress.org/trunk/wp-content/themes/${theme} -r${sinceRevision}:HEAD`)
  337. // Remove any double or back quotes from commit messages
  338. logs = logs.replace(/"/g, '');
  339. logs = logs.replace(/`/g, "'");
  340. logs = logs.replace(/\$/g, "%24");
  341. return `${theme}: Merge latest core changes up to [wp${latestRevision}]
  342. Summary:
  343. ${logs}
  344. Test Plan: Activate ${theme} and ensure nothing is broken
  345. Reviewers:
  346. #themes_team
  347. Subscribers:
  348. `;
  349. }
  350. /**
  351. * Deploys the localy copy of a core theme to wpcom.
  352. */
  353. async function createCorePhabriactorDiff(theme, sinceRevision) {
  354. let commitMessage = await buildCorePhabricatorCommitMessageSince(theme, sinceRevision);
  355. let diffUrl = await createPhabricatorDiff(commitMessage);
  356. let diffId = diffUrl.split('a8c.com/')[1];
  357. return diffId;
  358. }
  359. /*
  360. Build .zip file for .com
  361. */
  362. async function buildComZip(themeSlug) {
  363. console.log(`Building ${themeSlug} .zip`);
  364. let styleCss = fs.readFileSync(`${themeSlug}/style.css`, 'utf8');
  365. // Gets the theme version (Version:) and minimum WP version (Tested up to:) from the theme's style.css
  366. let themeVersion = getThemeMetadata(styleCss, 'Version');
  367. let wpVersionCompat = getThemeMetadata(styleCss, 'Requires at least');
  368. if (themeVersion && wpVersionCompat) {
  369. await executeOnSandbox(`php ${sandboxRootFolder}bin/themes/theme-downloads/build-theme-zip.php --stylesheet=pub/${themeSlug} --themeversion=${themeVersion} --wpversioncompat=${wpVersionCompat};`, true);
  370. }
  371. else {
  372. console.log('Unable to build theme .zip.');
  373. if (!themeVersion) {
  374. console.log('Could not find theme version (Version:) in the theme style.css.');
  375. }
  376. if (!wpVersionCompat) {
  377. console.log('Could not find WP compat version (Tested up to:) in the theme style.css.');
  378. }
  379. console.log('Please build the .zip file for the theme manually.', themeSlug);
  380. open('https://mc.a8c.com/themes/downloads/');
  381. }
  382. }
  383. async function buildComZips(themes) {
  384. console.log(`Building dotcom .zip files`);
  385. const progress = startProgress(themes.length);
  386. const failedThemes = []
  387. for (let theme of themes) {
  388. try {
  389. await buildComZip(theme);
  390. } catch (err) {
  391. console.log(`There was an error building dotcom zip for ${theme}. ${err}`);
  392. failedThemes.push(theme);
  393. }
  394. progress.increment();
  395. }
  396. if (failedThemes.length) {
  397. const tableConfig = {
  398. columnDefault: {
  399. width: 40,
  400. },
  401. header: {
  402. alignment: 'center',
  403. content: `There was an error building dotcom zip for following themes.`,
  404. }
  405. };
  406. console.log(table(failedThemes.map(t => [t]), tableConfig));
  407. }
  408. }
  409. /*
  410. Check to ensure that:
  411. * The current branch is /trunk
  412. * That trunk is up-to-date with origin/trunk
  413. */
  414. async function checkForDeployability() {
  415. let branchName = await executeCommand('git symbolic-ref --short HEAD');
  416. if (branchName !== 'trunk') {
  417. return 'Only the /trunk branch can be deployed.';
  418. }
  419. await executeCommand('git remote update', true);
  420. let localMasterHash = await executeCommand('git rev-parse trunk')
  421. let remoteMasterHash = await executeCommand('git rev-parse origin/trunk')
  422. if (localMasterHash !== remoteMasterHash) {
  423. return 'Local /trunk is out-of-date. Pull changes to continue.'
  424. }
  425. return null;
  426. }
  427. /*
  428. Land the changes from the given diff ID. This is the "production merge".
  429. */
  430. async function landChanges(diffId) {
  431. return executeCommand(`ssh -tt -A ${remoteSSH} "cd ${sandboxPublicThemesFolder}; /usr/local/bin/arc patch ${diffId}; /usr/local/bin/arc land; exit;"`, true);
  432. }
  433. async function getChangedThemes(hash) {
  434. console.log('Determining all changed themes');
  435. let themes = await getActionableThemes();
  436. let changedThemes = [];
  437. for (let theme of themes) {
  438. let hasChanges = await checkThemeForChanges(theme, hash);
  439. if (hasChanges) {
  440. changedThemes.push(theme);
  441. }
  442. }
  443. return changedThemes;
  444. }
  445. /*
  446. Deploy a collection of themes.
  447. Part of the push-button-deploy process.
  448. Can also be triggered to deploy a single theme with the command:
  449. node ./theme-utils.mjs deploy-theme THEMENAME
  450. */
  451. async function deployThemes(themes) {
  452. let response;
  453. const failedThemes = [];
  454. const progress = startProgress(themes.length);
  455. for (let theme of themes) {
  456. console.log(`Deploying ${theme}`);
  457. let deploySuccess = false;
  458. let attempt = 0;
  459. while (!deploySuccess && attempt <= 2) {
  460. attempt++;
  461. console.log(`\nattempt #${attempt}\n\n`);
  462. try {
  463. response = await executeOnSandbox(`deploy pub ${theme};exit;`, true, true);
  464. deploySuccess = response.includes('successfully deployed to');
  465. } catch (error) {
  466. deploySuccess = false
  467. }
  468. if (!deploySuccess) {
  469. console.log('Deploy was not successful. Trying again in 10 seconds...');
  470. await new Promise(resolve => setTimeout(resolve, 10000));
  471. }
  472. else {
  473. console.log("Deploy successful.");
  474. }
  475. }
  476. if (!deploySuccess) {
  477. console.log(`${theme} was not sucessfully deployed and should be deployed manually.`);
  478. failedThemes.push(theme);
  479. }
  480. progress.increment();
  481. }
  482. if (failedThemes.length) {
  483. const tableConfig = {
  484. columnDefault: {
  485. width: 40,
  486. },
  487. header: {
  488. alignment: 'center',
  489. content: `Following themes are not deployed.`,
  490. }
  491. };
  492. console.log(table(failedThemes.map(t => [t]), tableConfig));
  493. }
  494. }
  495. /*
  496. Provide the hash of the last managed deployment.
  497. This hash is used to determine all the changes that have happened between that point and the current point.
  498. */
  499. async function getLastDeployedHash() {
  500. let result = await executeOnSandbox(`
  501. cat ${sandboxPublicThemesFolder}/.pub-git-hash
  502. `);
  503. return result;
  504. }
  505. /*
  506. Update the 'last deployed hash' on the server with the current hash.
  507. */
  508. async function updateLastDeployedHash() {
  509. let hash = await executeCommand(`git rev-parse HEAD`);
  510. await executeOnSandbox(`
  511. echo '${hash}' > ${sandboxPublicThemesFolder}/.pub-git-hash
  512. `);
  513. }
  514. /*
  515. Version bump (increment version patch) any theme project that has had changes since the last deployment.
  516. If a theme's version has already been changed since that last deployment then do not version bump it.
  517. If any theme projects have had a version bump also version bump the parent project.
  518. If a theme has changes also update its changelog.
  519. Commit the change.
  520. */
  521. async function versionBumpThemes() {
  522. console.log("Version Bumping");
  523. let themes = await getActionableThemes();
  524. let hash = await getLastDeployedHash();
  525. let changesWereMade = false;
  526. let versionBumpCount = 0;
  527. for (let theme of themes) {
  528. let hasChanges = await checkThemeForChanges(theme, hash);
  529. if (!hasChanges) {
  530. // console.log(`${theme} has no changes`);
  531. continue;
  532. }
  533. versionBumpCount++;
  534. let hasVersionBump = await checkThemeForVersionBump(theme, hash);
  535. if (hasVersionBump) {
  536. continue;
  537. }
  538. await versionBumpTheme(theme, true);
  539. await updateThemeChangelog(theme, true);
  540. changesWereMade = true;
  541. }
  542. //version bump the root project if there were changes to any of the themes
  543. let rootHasVersionBump = await checkProjectForVersionBump(hash);
  544. if (versionBumpCount > 0 && !rootHasVersionBump) {
  545. await executeCommand(`npm version patch --no-git-tag-version && git add package.json package-lock.json`);
  546. changesWereMade = true;
  547. }
  548. return changesWereMade;
  549. }
  550. export function getThemeMetadata(styleCss, attribute) {
  551. if (!styleCss || !attribute) {
  552. return null;
  553. }
  554. switch (attribute) {
  555. case 'Version':
  556. return styleCss
  557. .match(/(?<=Version:\s*).*?(?=\s*\r?\n|\rg)/gs)[0]
  558. .trim()
  559. .replace('-wpcom', '');
  560. case 'Requires at least':
  561. return styleCss
  562. .match(/(?<=Requires at least:\s*).*?(?=\s*\r?\n|\rg)/gs);
  563. }
  564. }
  565. /* Rebuild theme changelog from a given starting hash */
  566. async function rebuildThemeChangelog(theme, since) {
  567. console.log(`Rebuilding ${theme} changelog since ${since || 'forever'}`);
  568. if (since) {
  569. since = `${since}..HEAD`;
  570. } else {
  571. since = 'HEAD';
  572. }
  573. let hashes = await executeCommand(`git rev-list ${since} -- ./${theme}`);
  574. hashes = hashes.split('\n');
  575. let logs = '== Changelog ==\n';
  576. for (let hash of hashes) {
  577. let log = await executeCommand(`git log -n 1 --pretty=format:"* %s" ${hash}`);
  578. if (log.includes('Version Bump')) {
  579. let previousStyleString = await executeCommand(`git show ${hash}:${theme}/style.css 2>/dev/null`);
  580. let version = getThemeMetadata(previousStyleString, 'Version');
  581. logs += `\n= ${version} =\n`;
  582. } else {
  583. // Remove any double quotes from commit messages
  584. log = log.replace(/"/g, '');
  585. logs += log + '\n';
  586. }
  587. }
  588. // Get theme readme.txt
  589. let readmeFilePath = `${theme}/readme.txt`;
  590. // Update readme.txt
  591. fs.readFile(readmeFilePath, 'utf8', function (err, data) {
  592. let changelogSection = '== Changelog ==';
  593. let regex = new RegExp('^.*' + changelogSection + '.*$', 'gm');
  594. let formattedChangelog = data.replace(regex, logs);
  595. fs.writeFile(readmeFilePath, formattedChangelog, 'utf8', function (err) {
  596. if (err) return console.log(err);
  597. });
  598. });
  599. }
  600. /*
  601. Update theme changelog using current commit logs.
  602. Used by versionBumpThemes to update each theme changelog.
  603. */
  604. async function updateThemeChangelog(theme, addChanges) {
  605. console.log(`Updating ${theme} changelog`);
  606. // Get theme version
  607. let styleCss = fs.readFileSync(`${theme}/style.css`, 'utf8');
  608. let version = getThemeMetadata(styleCss, 'Version');
  609. // Get list of updates with bullet points
  610. let logs = await getCommitLogs('', true, theme);
  611. // Get theme readme.txt
  612. let readmeFilePath = `${theme}/readme.txt`;
  613. if (!existsSync(readmeFilePath)) {
  614. console.log(`Unable to find a readme.txt for ${theme}.`);
  615. return;
  616. }
  617. // Build changelog entry
  618. let newChangelogEntry = `== Changelog ==
  619. = ${version} =
  620. ${logs}`;
  621. // Update readme.txt
  622. fs.readFile(readmeFilePath, 'utf8', function (err, data) {
  623. let changelogSection = '== Changelog ==';
  624. let regex = new RegExp('^.*' + changelogSection + '.*$', 'gm');
  625. let formattedChangelog = data.replace(regex, newChangelogEntry);
  626. fs.writeFile(readmeFilePath, formattedChangelog, 'utf8', function (err) {
  627. if (err) return console.log(err);
  628. });
  629. });
  630. // Stage readme.txt
  631. if (addChanges) {
  632. await executeCommand(`git add ${readmeFilePath}`);
  633. }
  634. }
  635. /*
  636. Version Bump a Theme.
  637. Used by versionBumpThemes to do the work of version bumping.
  638. First increment the patch version in style.css
  639. Then update any of these files with the new version: [package.json, style.scss, style-child-theme.scss]
  640. */
  641. async function versionBumpTheme(theme, addChanges) {
  642. console.log(`${theme} needs a version bump`);
  643. await executeCommand(`perl -pi -e 's/Version: ((\\d+\\.)*)(\\d+)(.*)$/"Version: ".$1.($3+1).$4/ge' ${theme}/style.css`, true);
  644. await executeCommand(`git add ${theme}/style.css`);
  645. let styleCss = fs.readFileSync(`${theme}/style.css`, 'utf8');
  646. let currentVersion = getThemeMetadata(styleCss, 'Version');
  647. let filesToUpdate = await executeCommand(`find ${theme} -not \\( -path "*/node_modules/*" -prune \\) -and \\( -name package.json -or -name style.scss -or -name style-child-theme.scss \\) -maxdepth 3`);
  648. filesToUpdate = filesToUpdate.split('\n').filter(item => item != '');
  649. for (let file of filesToUpdate) {
  650. const isPackageJson = file === `${theme}/package.json`;
  651. if (isPackageJson) {
  652. // update theme/package.json and package-lock.json
  653. await executeCommand(`npm version ${currentVersion} --workspace=${theme} --silent`);
  654. } else {
  655. await executeCommand(`perl -pi -e 's/Version: (.*)$/"Version: '${currentVersion}'"/ge' ${file}`);
  656. }
  657. if (addChanges) {
  658. await executeCommand(`git add ${file}`);
  659. if (isPackageJson) {
  660. await executeCommand(`git add package-lock.json`);
  661. }
  662. }
  663. }
  664. }
  665. /*
  666. Determine if a theme has had a version bump since a given hash.
  667. Used by versionBumpThemes
  668. Compares the value of 'version' in style.css between the hash and current value
  669. */
  670. async function checkThemeForVersionBump(theme, hash) {
  671. return executeCommand(`
  672. git show ${hash}:${theme}/style.css 2>/dev/null
  673. `)
  674. .catch((error) => {
  675. //This is a new theme, no need to bump versions so we'll just say we've already done it
  676. return true;
  677. })
  678. .then((previousStyleString) => {
  679. if (previousStyleString === true) {
  680. return previousStyleString;
  681. }
  682. let previousVersion = getThemeMetadata(previousStyleString, 'Version');
  683. let styleCss = fs.readFileSync(`${theme}/style.css`, 'utf8');
  684. let currentVersion = getThemeMetadata(styleCss, 'Version');
  685. return previousVersion != currentVersion;
  686. });
  687. }
  688. /*
  689. Determine if the project has had a version bump since a given hash.
  690. Used by versionBumpThemes
  691. Compares the value of 'version' in package.json between the hash and current value
  692. */
  693. async function checkProjectForVersionBump(hash) {
  694. let previousPackageString = await executeCommand(`
  695. git show ${hash}:./package.json 2>/dev/null
  696. `);
  697. let previousPackage = JSON.parse(previousPackageString);
  698. let currentPackage = JSON.parse(fs.readFileSync(`./package.json`))
  699. return previousPackage.version != currentPackage.version;
  700. }
  701. /*
  702. Determine if a theme has had changes since a given hash.
  703. Used by versionBumpThemes
  704. */
  705. async function checkThemeForChanges(theme, hash) {
  706. let comittedChanges = await executeCommand(`git diff --name-only ${hash} HEAD -- ${theme}`);
  707. return comittedChanges != '';
  708. }
  709. /*
  710. Provide a list of 'actionable' themes (those themes that have style.css files)
  711. */
  712. async function getActionableThemes() {
  713. let result = await executeCommand(`for d in */; do
  714. if test -f "./$d/style.css"; then
  715. echo $d;
  716. fi
  717. done`);
  718. return result
  719. .split('\n')
  720. .map(item => item.replace('/', ''));
  721. }
  722. /*
  723. Clean the theme sandbox.
  724. checkout origin/trunk and ensure it's up-to-date.
  725. Remove any other changes.
  726. */
  727. async function cleanSandbox() {
  728. console.log('Cleaning the Themes Sandbox');
  729. await executeOnSandbox(`
  730. cd ${sandboxPublicThemesFolder};
  731. git reset --hard HEAD;
  732. git clean -fd;
  733. git checkout trunk;
  734. git pull;
  735. echo;
  736. git status
  737. `, true);
  738. console.log('All done cleaning.');
  739. }
  740. /*
  741. Clean the premium theme sandbox.
  742. checkout origin/trunk and ensure it's up-to-date.
  743. Remove any other changes.
  744. */
  745. async function cleanPremiumSandbox() {
  746. console.log('Cleaning the Themes Sandbox');
  747. await executeOnSandbox(`
  748. cd ${sandboxPremiumThemesFolder};
  749. git reset --hard HEAD;
  750. git clean -fd;
  751. git checkout trunk;
  752. git pull;
  753. echo;
  754. git status
  755. `, true);
  756. console.log('All done cleaning.');
  757. }
  758. /*
  759. Clean the entire sandbox.
  760. checkout origin/trunk and ensure it's up-to-date.
  761. Remove any other changes.
  762. */
  763. async function cleanAllSandbox() {
  764. console.log('Cleaning the Entire Sandbox');
  765. let response = await executeOnSandbox(`
  766. cd ${sandboxRootFolder};
  767. git reset --hard HEAD;
  768. git clean -fd;
  769. git checkout trunk;
  770. git pull;
  771. echo;
  772. git status
  773. `, true);
  774. console.log('All done cleaning.');
  775. }
  776. /*
  777. Push exactly what is here (all files) up to the sandbox (with the exclusion of files noted in .sandbox-ignore)
  778. */
  779. async function pushToSandbox() {
  780. console.log("Pushing All Themes to Sandbox.");
  781. let allThemes = await getActionableThemes();
  782. allThemes = allThemes.filter(item => !premiumThemes.includes(item));
  783. console.log(`Syncing ${allThemes.length} themes`);
  784. for (let theme of allThemes) {
  785. await pushThemeToSandbox(theme);
  786. }
  787. }
  788. async function pushThemeToSandbox(theme) {
  789. console.log(`Syncing ${theme}`);
  790. return executeCommand(`
  791. rsync -avR --no-p --no-times --delete -m --exclude-from='.sandbox-ignore' ./${theme}/ wpcom-sandbox:${sandboxPublicThemesFolder}/
  792. `, true);
  793. }
  794. /*
  795. Push exactly what is here (all files) up to the sandbox (with the exclusion of files noted in .sandbox-ignore)
  796. This pushes only the folders noted as "premiumThemes" into the premium themes directory.
  797. This is the only part of the deploy process that is automated; the rest must be done manually including:
  798. * Creating a Phabricator Diff
  799. * Landing (comitting) the change
  800. * Deploying the theme
  801. * Triggering the .zip builds
  802. */
  803. async function pushPremiumToSandbox() {
  804. //TODO: It would be nice to determine this list programatically
  805. const filesToModify = [
  806. 'style.css',
  807. 'block-templates/404.html',
  808. 'block-template-parts/header.html',
  809. 'block-template-parts/footer.html'
  810. ];
  811. // Change 'blockbase' to 'blockbase-premium' in the files noted
  812. for (let theme of premiumThemes) {
  813. for (let file of filesToModify) {
  814. await executeCommand(`perl -pi -e 's/blockbase/blockbase-premium/' ${theme}/${file}`, true);
  815. }
  816. }
  817. // Push the changes in the premium themes to the sandbox
  818. await executeCommand(`
  819. rsync -avR --no-p --no-times --delete -m --exclude-from='.sandbox-ignore' --exclude='sass' ./${premiumThemes.join(' ./')} wpcom-sandbox:${sandboxPremiumThemesFolder}/
  820. `, true);
  821. // revert the local blockbase-premium changes
  822. for (let theme of premiumThemes) {
  823. for (let file of filesToModify) {
  824. await executeCommand(`
  825. git restore --source=HEAD --staged --worktree ./${theme}/${file}
  826. `);
  827. }
  828. }
  829. }
  830. /*
  831. Push only (and every) change since the point-of-diversion from /trunk
  832. Remove files from the sandbox that have been removed since the last deployed hash
  833. */
  834. async function pushChangesToSandbox() {
  835. console.log("Pushing Changed Themes to Sandbox.");
  836. let hash = await getLastDeployedHash();
  837. let changedThemes = await getChangedThemes(hash);
  838. changedThemes = changedThemes.filter(item => !premiumThemes.includes(item));
  839. console.log(`Syncing ${changedThemes.length} themes`);
  840. for (let theme of changedThemes) {
  841. await pushThemeToSandbox(theme);
  842. }
  843. }
  844. async function checkoutCoreTheme(theme) {
  845. if (!theme) {
  846. console.log('Must supply theme to sync and revision to start from');
  847. return;
  848. }
  849. return executeCommand(`
  850. rm -rf ./${theme}
  851. svn checkout https://wpcom-themes.svn.automattic.com/${theme} ./${theme}
  852. `);
  853. }
  854. async function pullCoreThemes() {
  855. console.log("Pulling CORE themes from sandbox.");
  856. for (let theme of coreThemes) {
  857. await executeCommand(`
  858. rsync -avr --no-p --no-times --delete -m --exclude-from='.sandbox-ignore' wpcom-sandbox:${sandboxPublicThemesFolder}/${theme}/ ./${theme}/
  859. `, true);
  860. }
  861. }
  862. async function pushCoreThemes() {
  863. console.log("Pushing CORE themes to sandbox.");
  864. for (let theme of coreThemes) {
  865. await executeCommand(`
  866. rsync -avr --no-p --no-times --delete -m --exclude-from='.sandbox-ignore' ./${theme}/ wpcom-sandbox:${sandboxPublicThemesFolder}/${theme}/
  867. `, true);
  868. }
  869. }
  870. async function syncCoreTheme(theme, sinceRevision) {
  871. if (!theme) {
  872. console.log('Must supply theme to sync and revision to start from');
  873. return;
  874. }
  875. if (!sinceRevision) {
  876. sinceRevision = await executeCommand(`cat ./${theme}/.pub-svn-revision`);
  877. }
  878. let latestRevision = await executeCommand(`svn info -r HEAD https://develop.svn.wordpress.org/trunk | grep Revision | egrep -o "[0-9]+"`);
  879. console.log(`syncing core theme ${theme} from ${sinceRevision} to ${latestRevision}`);
  880. try {
  881. await executeCommand(`
  882. svn merge --accept postpone http://develop.svn.wordpress.org/trunk/src/wp-content/themes/${theme} ./${theme} -r${sinceRevision}:HEAD
  883. echo '${latestRevision}' > ./${theme}/.pub-svn-revision
  884. `, true);
  885. }
  886. catch (err) {
  887. console.log('Error merging:', err);
  888. }
  889. return latestRevision;
  890. }
  891. /*
  892. Build the Phabricator commit message.
  893. This message contains the logs from all of the commits since the given hash.
  894. Used by create*PhabricatorDiff
  895. */
  896. async function buildPhabricatorCommitMessageSince(hash) {
  897. let projectVersion = await executeCommand(`node -p "require('./package.json').version"`);
  898. let logs = await getCommitLogs(hash);
  899. return `Deploy Themes ${projectVersion} to wpcom
  900. Summary:
  901. ${logs}
  902. Test Plan: Execute Smoke Test
  903. Reviewers:
  904. Subscribers:
  905. `;
  906. }
  907. /*
  908. Create a Phabricator diff with the given message based on the contents currently in the sandbox.
  909. Open the phabricator diff in your browser.
  910. Provide the URL of the phabricator diff.
  911. */
  912. async function createPhabricatorDiff(commitMessage) {
  913. console.log('creating Phabricator Diff');
  914. let result = await executeOnSandbox(`
  915. cd ${sandboxPublicThemesFolder};
  916. git branch -D deploy
  917. git checkout -b deploy
  918. git add --all
  919. git commit -m "${commitMessage}"
  920. arc diff --create --verbatim
  921. `, true);
  922. let phabricatorUrl = getPhabricatorUrlFromResponse(result);
  923. console.log('Diff Created at: ', phabricatorUrl);
  924. if (phabricatorUrl) {
  925. open(phabricatorUrl);
  926. }
  927. return phabricatorUrl;
  928. }
  929. /*
  930. Utility to pull the Phabricator URL from the diff creation command.
  931. Used by createPhabricatorDiff
  932. */
  933. function getPhabricatorUrlFromResponse(response) {
  934. return response
  935. ?.split('\n')
  936. ?.find(item => {
  937. return item.includes('Revision URI: ');
  938. })
  939. ?.split("Revision URI: ")[1];
  940. }
  941. /*
  942. Create a git tag at the current hash.
  943. In the description include the commit logs since the given hash.
  944. Include the (cleansed) Phabricator link.
  945. */
  946. async function tagDeployment(options = {}) {
  947. console.log('tagging deployment');
  948. let hash = options.hash || await getLastDeployedHash();
  949. let workInTheOpenPhabricatorUrl = '';
  950. if (options.diffId) {
  951. workInTheOpenPhabricatorUrl = `Phabricator: ${options.diffId}-code`;
  952. }
  953. let projectVersion = await executeCommand(`node -p "require('./package.json').version"`);
  954. let logs = await getCommitLogs(hash);
  955. let tag = `v${projectVersion}`;
  956. let message = `Deploy Themes ${tag} to wpcom. \n\n${logs} \n\n${workInTheOpenPhabricatorUrl}`;
  957. await executeCommand(`
  958. git tag -a ${tag} -m "${message}"
  959. git push origin ${tag}
  960. `, true);
  961. }
  962. /*
  963. Execute a command on the sandbox.
  964. Expects the following to be configured in your ~/.ssh/config file:
  965. Host wpcom-sandbox
  966. User wpdev
  967. HostName SANDBOXURL.wordpress.com
  968. ForwardAgent yes
  969. */
  970. function executeOnSandbox(command, logResponse, enablePsudoterminal) {
  971. if (enablePsudoterminal) {
  972. return executeCommand(`ssh -tt -A ${remoteSSH} << EOF
  973. ${command}
  974. EOF`, logResponse);
  975. }
  976. return executeCommand(`ssh -TA ${remoteSSH} << EOF
  977. ${command}
  978. EOF`, logResponse);
  979. }
  980. /*
  981. Execute a command locally.
  982. */
  983. export async function executeCommand(command, logResponse) {
  984. const timeout = 2*60*1000; // 2 min
  985. return new Promise((resolove, reject) => {
  986. let child;
  987. let response = '';
  988. let errResponse = '';
  989. if (isWin) {
  990. child = spawn('cmd.exe', ['/s', '/c', '"' + command + '"'], {
  991. windowsVerbatimArguments: true,
  992. stdio: [process.stdin, 'pipe', 'pipe'],
  993. detached: true,
  994. })
  995. } else {
  996. child = spawn(process.env.SHELL, ['-c', command], {
  997. stdio: [process.stdin, 'pipe', 'pipe'],
  998. detached: true,
  999. });
  1000. }
  1001. var timer = setTimeout(() => {
  1002. try {
  1003. process.kill(-child.pid, 'SIGKILL');
  1004. } catch (e) {
  1005. console.log('Cannot kill process');
  1006. }
  1007. }, timeout);
  1008. child.stdout.on('data', (data) => {
  1009. response += data;
  1010. if (logResponse) {
  1011. console.log(data.toString());
  1012. }
  1013. });
  1014. child.stderr.on('data', (data) => {
  1015. errResponse += data;
  1016. if (logResponse) {
  1017. console.log(data.toString());
  1018. }
  1019. });
  1020. child.on('exit', (code) => {
  1021. clearTimeout(timer)
  1022. if (code !== 0) {
  1023. reject(errResponse.trim());
  1024. }
  1025. resolove(response.trim());
  1026. });
  1027. });
  1028. }
  1029. async function escapePatterns() {
  1030. // get staged files
  1031. const staged = (await executeCommand(`git diff --cached --name-only`)).split('\n');
  1032. // get unstaged, untracked files
  1033. const unstaged = (await executeCommand(`git ls-files -m -o --exclude-standard`)).split('\n');
  1034. // avoid duplicates and filter pattern files
  1035. const patterns = [...new Set([...staged, ...unstaged])].filter(file => file.match(/.*\/patterns\/.*.php/g));
  1036. // arrange patterns by theme
  1037. const themePatterns = patterns.reduce((acc, file) => {
  1038. const themeSlug = file.split('/').shift();
  1039. return {
  1040. ...acc,
  1041. [themeSlug]: (acc[themeSlug] || []).concat(file)
  1042. };
  1043. }, {});
  1044. Object.entries(themePatterns).forEach(async ([themeSlug, patterns]) => {
  1045. console.log(getPatternTable(themeSlug, patterns));
  1046. const prompt = await inquirer.prompt([{
  1047. type: 'input',
  1048. message: 'Verify the theme slug',
  1049. name: "themeSlug",
  1050. default: themeSlug
  1051. }]);
  1052. if (!prompt.themeSlug) {
  1053. return;
  1054. }
  1055. const rewriter = getReWriter(prompt.themeSlug);
  1056. patterns.forEach(file => {
  1057. const tmpFile = `${file}-tmp`;
  1058. const rstream = fs.createReadStream( file, { encoding: 'UTF-8' } );
  1059. const wstream = fs.createWriteStream( tmpFile, { encoding: 'UTF-8' } );
  1060. wstream.on('finish', () => {
  1061. fs.renameSync(tmpFile, file);
  1062. });
  1063. rstream.pipe(rewriter).pipe(wstream);
  1064. });
  1065. });
  1066. // Helper functions
  1067. function getReWriter(themeSlug) {
  1068. const rewriter = new RewritingStream();
  1069. rewriter.on('text', (_, raw) => {
  1070. rewriter.emitRaw(escapeText(raw, themeSlug));
  1071. });
  1072. rewriter.on('startTag', (startTag, rawHtml) => {
  1073. if (startTag.tagName === 'img') {
  1074. const attrs = startTag.attrs.filter(attr => ['src', 'alt'].includes(attr.name));
  1075. attrs.forEach(attr => {
  1076. if (attr.name === 'src') {
  1077. attr.value = escapeImagePath(attr.value);
  1078. } else if (attr.name === 'alt') {
  1079. attr.value = escapeText(attr.value, themeSlug, true);
  1080. }
  1081. });
  1082. }
  1083. rewriter.emitStartTag(startTag);
  1084. });
  1085. rewriter.on('comment', (comment, rawHtml) => {
  1086. if (comment.text.startsWith('?php')) {
  1087. rewriter.emitRaw(rawHtml);
  1088. return;
  1089. }
  1090. // escape the strings in block config (blocks that are represented as comments)
  1091. // ex: <!-- wp:search {label: "Search"} /-->
  1092. const block = escapeBlockAttrs(comment.text, themeSlug)
  1093. rewriter.emitComment({...comment, text: block})
  1094. });
  1095. return rewriter;
  1096. }
  1097. function escapeBlockAttrs(block, themeSlug) {
  1098. // Set isAttr to true if it is an attribute in the result HTML
  1099. // If set to true, it generates esc_attr_, otherwise it generates esc_html_
  1100. const allowedAttrs=[
  1101. { name: 'label' },
  1102. { name: 'placeholder', isAttr: true },
  1103. { name: 'buttonText' },
  1104. { name: 'content' }
  1105. ];
  1106. const start = block.indexOf('{');
  1107. const end = block.lastIndexOf('}');
  1108. const configPrefix = block.slice(0, start);
  1109. const config = block.slice(start, end+1);
  1110. const configSuffix = block.slice(end+1);
  1111. try {
  1112. const configJson = JSON.parse(config);
  1113. allowedAttrs.forEach((attr) => {
  1114. if (!configJson[attr.name]) return;
  1115. configJson[attr.name] = escapeText(configJson[attr.name], themeSlug, attr.isAttr)
  1116. })
  1117. return configPrefix + JSON.stringify(configJson) + configSuffix;
  1118. } catch (error) {
  1119. // do nothing
  1120. return block
  1121. }
  1122. }
  1123. function escapeText(text, themeSlug, isAttr = false) {
  1124. const trimmedText = text && text.trim();
  1125. if (!themeSlug || !trimmedText || trimmedText.startsWith(`<?php`)) return text;
  1126. const escFunction = isAttr ? 'esc_attr__' : 'esc_html__';
  1127. const spaceChar = text.startsWith(' ') ? '&nbsp;' : ''
  1128. const resultText = text.replace('\'', '\\\'').trim();
  1129. return `${spaceChar}<?php echo ${escFunction}( '${resultText}', '${themeSlug}' ); ?>`;
  1130. }
  1131. function escapeImagePath(src) {
  1132. if (!src || src.trim().startsWith('<?php')) return src;
  1133. const assetsDir = 'assets';
  1134. const parts = src.split('/');
  1135. const resultSrc = parts.slice(parts.indexOf(assetsDir)).join('/');
  1136. return `<?php echo esc_url( get_template_directory_uri() ); ?>/${resultSrc}`;
  1137. }
  1138. function getPatternTable(themeSlug, patterns) {
  1139. const tableConfig = {
  1140. columnDefault: {
  1141. width: 80,
  1142. },
  1143. header: {
  1144. alignment: 'center',
  1145. content: `THEME: ${themeSlug}\n\n Following patterns may get updated with escaped strings and/or image paths`,
  1146. }
  1147. };
  1148. return table([patterns], tableConfig);
  1149. }
  1150. }
  1151. function startProgress(length) {
  1152. let current = 0;
  1153. function render() {
  1154. const [progress, percentage] = progressbar.filledBar(length, current);
  1155. console.log('\nProgress:', [progress, Math.round(percentage*100)/100], `${current}/${length}\n`);
  1156. }
  1157. render();
  1158. return {
  1159. increment() {
  1160. current++;
  1161. render();
  1162. }
  1163. };
  1164. }