Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block Directory: Use local assets with automatic asset detection #24117

Merged
merged 18 commits into from
Jul 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
dd32 marked this conversation as resolved.
Show resolved Hide resolved
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 () => {
dd32 marked this conversation as resolved.
Show resolved Hide resolved
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