// ==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',
'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 `
${ label }
`;
}
/**
* 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 `
${ options
.map( ( option ) => {
return getOption( option, columnWidth );
} )
.join( '' ) }
`;
}
/**
* 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.' );
}
}
} )();