Skip to content

Commit

Permalink
Block Directory: Use local assets with automatic asset detection (#24117
Browse files Browse the repository at this point in the history
)
  • Loading branch information
dd32 authored Jul 27, 2020
1 parent 6954857 commit 952ad64
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 102 deletions.
3 changes: 0 additions & 3 deletions packages/block-directory/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,6 @@ export function* installBlockType( block ) {
let success = false;
yield clearErrorNotice( id );
try {
if ( ! Array.isArray( assets ) || ! assets.length ) {
throw new Error( __( 'Block has no assets.' ) );
}
yield setIsInstalling( block.id, true );

// If we have a wp:plugin link, the plugin is installed but inactive.
Expand Down
116 changes: 74 additions & 42 deletions packages/block-directory/src/store/controls.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,49 @@
/**
* WordPress dependencies
*/
import { getPath } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';

/**
* Loads a JavaScript file.
* Load an asset for a block.
*
* @param {string} asset The url for this file.
* This function returns a Promise that will resolve once the asset is loaded,
* or in the case of Stylesheets and Inline Javascript, will resolve immediately.
*
* @param {HTMLElement} el A HTML Element asset to inject.
*
* @return {Promise} Promise which will resolve when the asset is loaded.
*/
export const loadScript = ( asset ) => {
if ( ! asset || ! /\.js$/.test( getPath( asset ) ) ) {
return Promise.reject( new Error( 'No script found.' ) );
}
export const loadAsset = ( el ) => {
return new Promise( ( resolve, reject ) => {
const existing = document.querySelector( `script[src="${ asset }"]` );
if ( existing ) {
existing.parentNode.removeChild( existing );
/*
* Reconstruct the passed element, this is required as inserting the Node directly
* won't always fire the required onload events, even if the asset wasn't already loaded.
*/
const newNode = document.createElement( el.nodeName );

[ 'id', 'rel', 'src', 'href', 'type' ].forEach( ( attr ) => {
if ( el[ attr ] ) {
newNode[ attr ] = el[ attr ];
}
} );

// Append inline <script> contents.
if ( el.innerHTML ) {
newNode.appendChild( document.createTextNode( el.innerHTML ) );
}
const script = document.createElement( 'script' );
script.src = asset;
script.onload = () => resolve( true );
script.onerror = () => reject( new Error( 'Error loading script.' ) );
document.body.appendChild( script );
} );
};

/**
* Loads a CSS file.
*
* @param {string} asset The url for this file.
*
* @return {Promise} Promise which will resolve when the asset is added.
*/
export const loadStyle = ( asset ) => {
if ( ! asset || ! /\.css$/.test( getPath( asset ) ) ) {
return Promise.reject( new Error( 'No style found.' ) );
}
return new Promise( ( resolve, reject ) => {
const link = document.createElement( 'link' );
link.rel = 'stylesheet';
link.href = asset;
link.onload = () => resolve( true );
link.onerror = () => reject( new Error( 'Error loading style.' ) );
document.body.appendChild( link );
newNode.onload = () => resolve( true );
newNode.onerror = () => reject( new Error( 'Error loading asset.' ) );

document.body.appendChild( newNode );

// Resolve Stylesheets and Inline JavaScript immediately.
if (
'link' === newNode.nodeName.toLowerCase() ||
( 'script' === newNode.nodeName.toLowerCase() && ! newNode.src )
) {
resolve();
}
} );
};

Expand All @@ -63,14 +62,47 @@ export function loadAssets( assets ) {
}

const controls = {
LOAD_ASSETS( { assets } ) {
const scripts = assets.map( ( asset ) =>
getPath( asset ).match( /\.js$/ ) !== null
? loadScript( asset )
: loadStyle( asset )
);
LOAD_ASSETS() {
/*
* Fetch the current URL (post-new.php, or post.php?post=1&action=edit) and compare the
* Javascript and CSS assets loaded between the pages. This imports the required assets
* for the block into the current page while not requiring that we know them up-front.
* In the future this can be improved by reliance upon block.json and/or a script-loader
* dependancy API.
*/
return apiFetch( {
url: document.location.href,
parse: false,
} )
.then( ( response ) => response.text() )
.then( ( data ) => {
const doc = new window.DOMParser().parseFromString(
data,
'text/html'
);

const newAssets = Array.from(
doc.querySelectorAll( 'link[rel="stylesheet"],script' )
).filter(
( asset ) =>
asset.id && ! document.getElementById( asset.id )
);

return Promise.all( scripts );
return new Promise( async ( resolve, reject ) => {
for ( const i in newAssets ) {
try {
/*
* Load each asset in order, as they may depend upon an earlier loaded script.
* Stylesheets and Inline Scripts will resolve immediately upon insertion.
*/
await loadAsset( newAssets[ i ] );
} catch ( e ) {
reject( e );
}
}
resolve();
} );
} );
},
};

Expand Down
25 changes: 0 additions & 25 deletions packages/block-directory/src/store/test/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,31 +158,6 @@ describe( 'actions', () => {
} );
} );

it( 'should set an error if the plugin has no assets', () => {
const generator = installBlockType( { ...block, assets: [] } );

expect( generator.next().value ).toEqual( {
type: 'CLEAR_ERROR_NOTICE',
blockId: block.id,
} );

expect( generator.next().value ).toMatchObject( {
type: 'SET_ERROR_NOTICE',
blockId: block.id,
} );

expect( generator.next().value ).toEqual( {
type: 'SET_INSTALLING_BLOCK',
blockId: block.id,
isInstalling: false,
} );

expect( generator.next() ).toEqual( {
value: false,
done: true,
} );
} );

it( "should set an error if the plugin can't install", () => {
const generator = installBlockType( block );

Expand Down
36 changes: 6 additions & 30 deletions packages/block-directory/src/store/test/controls.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,21 @@
/**
* Internal dependencies
*/
import { loadScript, loadStyle } from '../controls';
import { loadAsset } from '../controls';

describe( 'controls', () => {
const scriptAsset = 'http://www.wordpress.org/plugins/fakeasset.js';
const styleAsset = 'http://www.wordpress.org/plugins/fakeasset.css';
describe( 'loadAsset', () => {
const script = document.createElement( 'script' );
const style = document.createElement( 'link' );

describe( 'loadScript', () => {
it( 'should return a Promise when loading a script', () => {
const result = loadScript( scriptAsset );
const result = loadAsset( script );
expect( typeof result.then ).toBe( 'function' );
} );

it( 'should reject when no script is given', async () => {
expect.assertions( 1 );
const result = loadScript( '' );
await expect( result ).rejects.toThrow( Error );
} );

it( 'should reject when a non-js file is given', async () => {
const result = loadScript( styleAsset );
await expect( result ).rejects.toThrow( Error );
} );
} );

describe( 'loadStyle', () => {
it( 'should return a Promise when loading a style', () => {
const result = loadStyle( styleAsset );
const result = loadAsset( style );
expect( typeof result.then ).toBe( 'function' );
} );

it( 'should reject when no style is given', async () => {
expect.assertions( 1 );
const result = loadStyle( '' );
await expect( result ).rejects.toThrow( Error );
} );

it( 'should reject when a non-css file is given', async () => {
const result = loadStyle( scriptAsset );
await expect( result ).rejects.toThrow( Error );
} );
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ const MOCK_BLOCKS_RESPONSES = [
'application/javascript; charset=utf-8'
),
},
{
// Mock the post-new page as requested via apiFetch for determining new CSS/JS assets.
match: ( request ) => request.url().includes( '/post-new.php' ),
onRequestMatch: createResponse(
`<html><head><script id="mock-block-js" src="${ MOCK_BLOCK1.assets[ 0 ] }"></script></head><body/></html>`,
'text/html; charset=UTF-8'
),
},
];

function getResponseObject( obj, contentType ) {
Expand Down Expand Up @@ -180,8 +188,7 @@ describe( 'adding blocks from block directory', () => {
// Add the block
await addBtn.click();

// Delay to let block script load
await new Promise( ( resolve ) => setTimeout( resolve, 100 ) );
await page.waitForSelector( `div[data-type="${ MOCK_BLOCK1.name }"]` );

// The block will auto select and get added, make sure we see it in the content
expect( await getEditedPostContent() ).toMatchSnapshot();
Expand Down

0 comments on commit 952ad64

Please sign in to comment.