// ==UserScript== // @name WooCommerce Live Branches // @namespace https://wordpress.com/ // @version 1.1 // @description Adds links to PRs pointing to Jurassic Ninja sites for live-testing a changeset // @grant GM_xmlhttpRequest // @connect jurassic.ninja // @require https://code.jquery.com/jquery-3.3.1.min.js // @match https://github.com/woocommerce/woocommerce/pull/* // ==/UserScript== // Need to declare "jQuery" for linting within TamperMonkey, but in the monorepo it's already declared. // eslint-disable-next-line no-redeclare /* global jQuery */ ( function () { const $ = jQuery.noConflict(); const markdownBodySelector = '.pull-discussion-timeline .markdown-body'; const pluginsList = null; // Watch for relevant DOM changes that indicate we need to re-run `doit()`: // - Adding a new `.markdown-body`. // - Removing `#woocommerce-live-branches`. const observer = new MutationObserver( ( list ) => { for ( const m of list ) { for ( const n of m.addedNodes ) { if ( ( n.matches && n.matches( markdownBodySelector ) ) || ( n.querySelector && n.querySelector( markdownBodySelector ) ) ) { doit(); return; } } for ( const n of m.removedNodes ) { if ( n.id === 'woocommerce-live-branches' || ( n.querySelector && n.querySelector( '#woocommerce-live-branches' ) ) ) { doit(); return; } } } } ); observer.observe( document, { subtree: true, childList: true } ); // Run it on load too. doit(); /** * Determine the current repo. * * Currently looks at the URL, expecting it to match a `@match` pattern from the script header. * * @return {string|null} Repo name. */ function determineRepo() { const m = location.pathname.match( /^\/([^/]+\/[^/]+)\/pull\// ); return m && m[ 1 ] ? decodeURIComponent( m[ 1 ] ) : null; } /** Function. */ function doit() { const markdownBody = document.querySelectorAll( markdownBodySelector )[ 0 ]; if ( ! markdownBody || markdownBody.querySelector( '#woocommerce-live-branches' ) ) { // No body or Live Branches is already there, no need to do it again. return; } const host = 'https://jurassic.ninja'; const currentBranch = jQuery( '.head-ref:first' ).text(); const branchIsForked = currentBranch.includes( ':' ); const branchStatus = $( '.gh-header-meta .State' ).text().trim(); const repo = determineRepo(); if ( branchStatus === 'Merged' ) { const contents = `

This branch is already merged.

Test with trunk branch instead.

`; appendHtml( markdownBody, contents ); } else if ( branchStatus === 'Draft' ) { appendHtml( markdownBody, '

This branch is a draft. You can open live branches only from open pull requests.

' ); } else if ( branchIsForked ) { appendHtml( markdownBody, "

This branch can't be tested live because it comes from a forked version of this repo.

" ); } else if ( ! repo ) { appendHtml( markdownBody, '

Cannot determine the repository for this PR.

' ); } else { // TODO: Fetch the list of feature flags dynamically from the API or something. const featureFlags = [ 'async-product-editor-category-field', 'launch-your-store', 'minified-js', 'product-custom-fields', 'reactify-classic-payments-settings', 'settings', ]; const contents = `
Expand for JN site options:

Settings

${ getOptionsList( [ { label: 'A shortlived site', name: 'shortlived', }, { checked: true, label: 'WP_DEBUG and WP_DEBUG_LOG set to true', name: 'wp-debug-log', }, { label: 'Multisite based on subdomains', name: 'subdomain_multisite', }, { label: 'Multisite based on subdirectories', name: 'subdir_multisite', }, { label: 'Pre-generate content', name: 'content', }, { label: 'xmlrpc.php unavailable', name: 'blockxmlrpc', }, ], 100 ) }

Install additional plugins

${ getOptionsList( [ { label: 'WooCommerce Smooth Generator', name: 'wc-smooth-generator', }, { label: 'Jetpack', name: 'nojetpack', invert: true, }, { label: 'WordPress Beta Tester', name: 'wordpress-beta-tester', }, { label: 'Gutenberg', name: 'gutenberg', }, { label: 'Classic Editor', name: 'classic-editor', }, { label: 'AMP', name: 'amp', }, { label: 'Config Constants', name: 'config-constants', }, { label: 'Code Snippets', name: 'code-snippets', }, { label: 'WP Rollback', name: 'wp-rollback', }, { label: 'WP Downgrade', name: 'wp-downgrade', }, { label: 'WP Super Cache', name: 'wp-super-cache', }, { label: 'WP Job Manager', name: 'wp-job-manager', }, ], 33 ) }

Enable additional feature flags

${ getOptionsList( featureFlags.map( ( flag ) => ( { label: flag, name: 'woocommerce-beta-tester-feature-flags', value: flag, } ) ), 50 ) }

`; appendHtml( markdownBody, contents ); updateLink(); } /** * Encode necessary HTML entities in a string. * * @param {string} s - String to encode. * @return {string} Encoded string. */ function encodeHtmlEntities( s ) { return s.replace( /[&<>"']/g, ( m ) => `&#${ m.charCodeAt( 0 ) };` ); } /** * Build the JN create URI. * * @return {string} URI. */ function getLink() { const query = [ 'woocommerce-beta-tester', `woocommerce-beta-tester-live-branch=${ currentBranch }`, ]; const enabledFeatureFlags = []; $( '#woocommerce-live-branches input[type=checkbox]:checked:not([data-invert]), #woocommerce-live-branches input[type=checkbox][data-invert]:not(:checked)' ).each( ( i, input ) => { if ( input.name === 'woocommerce-beta-tester-feature-flags' ) { enabledFeatureFlags.push( input.value ); return; } if ( input.value ) { query.push( encodeURIComponent( input.name ) + '=' + encodeURIComponent( input.value ) ); } else { query.push( encodeURIComponent( input.name ) ); } } ); if ( enabledFeatureFlags.length ) { query.push( `woocommerce-beta-tester-feature-flags=${ encodeURIComponent( enabledFeatureFlags.join( ',' ) ) }` ); } // prettier-ignore return `${ host }/create?${ query.join( '&' ).replace( /%(2F|5[BD])/g, m => decodeURIComponent( m ) ) }`; } /** * Build HTML for a single option checkbox. * * @param {Object} opts - Options. * @param {string} opts.label - Checkbox label HTML. * @param {string} opts.name - Checkbox name. * @param {string} [opts.value] - Checkbox value, if any. * @param {boolean} [opts.checked] - Whether the checkbox is default checked. * @param {boolean} [opts.disabled] - Whether the checkbox is disabled. * @param {boolean} [opts.invert] - Whether the sense of the checkbox is inverted. * @param {number} columnWidth - Column width. * @return {string} HTML. */ function getOption( { disabled = false, checked = false, invert = false, value = '', label, name, }, columnWidth ) { // prettier-ignore return `
  • `; } /** * Build HTML for a set of option checkboxes. * * @param {object[]} options - Array of options for `getOption()`. * @param {number} columnWidth - Column width. * @return {string} HTML. */ function getOptionsList( options, columnWidth ) { return ` `; } /** * Append HTML to the element. * * Also registers `onInputChanged()` as a change handler for all checkboxes in the HTML. * * @param {HTMLElement} el - Element. * @param {string} contents - HTML to append. */ function appendHtml( el, contents ) { const $el = $( el ); const liveBranches = $( '
    ' ).append( `

    WooCommerce Live Branches

    ${ contents }` ); $( '#woocommerce-live-branches' ).remove(); $el.append( liveBranches ); liveBranches .find( 'input[type=checkbox]' ) .each( () => this.addEventListener( 'change', onInputChanged ) ); } /** * Change handler. Updates the link. * * @param {Event} e - Event object. */ function onInputChanged( e ) { e.stopPropagation(); e.preventDefault(); if ( e.target.checked ) { e.target.setAttribute( 'checked', true ); } else { e.target.removeAttribute( 'checked' ); } updateLink(); } /** * Update the link. */ function updateLink() { const $link = $( '#woocommerce-beta-branch-link' ); const url = getLink(); $link .attr( 'href', url ) .text( 'Create Jurassic Ninja site for this branch.' ); } } } )();