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

iAPI Router: Handle styles assets on region-based navigation #67826

Draft
wants to merge 6 commits into
base: trunk
Choose a base branch
from
Draft
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
79 changes: 79 additions & 0 deletions packages/interactivity-router/src/assets/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const cssUrlRegEx =
/url\(\s*(?:(["'])((?:\\.|[^\n\\"'])+)\1|((?:\\.|[^\s,"'()\\])+))\s*\)/g;

const resolveUrl = ( relativeUrl: string, baseUrl: string ) => {
try {
return new URL( relativeUrl, baseUrl ).toString();
} catch ( e ) {
return relativeUrl;
}
};

const withAbsoluteUrls = ( cssText: string, baseUrl: string ) =>
cssText.replace(
cssUrlRegEx,
( _match, quotes = '', relUrl1, relUrl2 ) =>
`url(${ quotes }${ resolveUrl(
relUrl1 || relUrl2,
baseUrl
) }${ quotes })`
);

const styleSheetCache = new Map< string, Promise< CSSStyleSheet > >();

const getCachedSheet = async (
sheetId: string,
factory: () => Promise< CSSStyleSheet >
) => {
if ( ! styleSheetCache.has( sheetId ) ) {
styleSheetCache.set( sheetId, factory() );
}
return styleSheetCache.get( sheetId );
};

const sheetFromLink = async (
{ id, href, sheet: elementSheet }: HTMLLinkElement,
baseUrl: string
) => {
const sheetId = id || href;
const sheetUrl = resolveUrl( href, baseUrl );

if ( elementSheet ) {
return getCachedSheet( sheetId, () => {
const sheet = new CSSStyleSheet();
for ( const { cssText } of elementSheet.cssRules ) {
sheet.insertRule( withAbsoluteUrls( cssText, sheetUrl ) );
}
return Promise.resolve( sheet );
} );
}
return getCachedSheet( sheetId, async () => {
const response = await fetch( href );
const text = await response.text();
const sheet = new CSSStyleSheet();
await sheet.replace( withAbsoluteUrls( text, sheetUrl ) );
return sheet;
} );
};

const sheetFromStyle = async ( { id, textContent }: HTMLStyleElement ) => {
const sheetId = id || textContent;
return getCachedSheet( sheetId, async () => {
const sheet = new CSSStyleSheet();
await sheet.replace( textContent );
return sheet;
} );
};

export const generateCSSStyleSheets = (
doc: Document,
baseUrl: string = ( doc.location || window.location ).href
): Promise< CSSStyleSheet >[] =>
[ ...doc.querySelectorAll( 'style,link[rel=stylesheet]' ) ].map(
( element ) => {
if ( 'LINK' === element.nodeName ) {
return sheetFromLink( element as HTMLLinkElement, baseUrl );
}
return sheetFromStyle( element as HTMLStyleElement );
}
);
126 changes: 0 additions & 126 deletions packages/interactivity-router/src/head.ts

This file was deleted.

61 changes: 36 additions & 25 deletions packages/interactivity-router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { store, privateApis, getConfig } from '@wordpress/interactivity';
/**
* Internal dependencies
*/
import { fetchHeadAssets, updateHead, headElements } from './head';
import { generateCSSStyleSheets } from './assets/styles';

const {
directivePrefix,
Expand Down Expand Up @@ -37,16 +37,18 @@ interface PrefetchOptions {

interface VdomParams {
vdom?: typeof initialVdom;
baseUrl?: string;
}

interface Page {
regions: Record< string, any >;
head: HTMLHeadElement[];
styles: Promise< CSSStyleSheet >[];
scriptModules: string[];
title: string;
initialData: any;
}

type RegionsToVdom = ( dom: Document, params?: VdomParams ) => Promise< Page >;
type RegionsToVdom = ( dom: Document, params?: VdomParams ) => Page;

// Check if the navigation mode is full page or region based.
const navigationMode: 'regionBased' | 'fullPage' =
Expand All @@ -73,20 +75,25 @@ const fetchPage = async ( url: string, { html }: { html: string } ) => {
html = await res.text();
}
const dom = new window.DOMParser().parseFromString( html, 'text/html' );
return regionsToVdom( dom );
return regionsToVdom( dom, { baseUrl: url } );
} catch ( e ) {
return false;
}
};

// Return an object with VDOM trees of those HTML regions marked with a
// `router-region` directive.
const regionsToVdom: RegionsToVdom = async ( dom, { vdom } = {} ) => {
const regionsToVdom: RegionsToVdom = ( dom, { vdom, baseUrl } = {} ) => {
const regions = { body: undefined };
let head: HTMLElement[];
const styles = generateCSSStyleSheets( dom, baseUrl );
const scriptModules = [
...dom.querySelectorAll< HTMLScriptElement >(
'script[type=module][src]'
),
].map( ( s ) => s.src );

if ( globalThis.IS_GUTENBERG_PLUGIN ) {
if ( navigationMode === 'fullPage' ) {
head = await fetchHeadAssets( dom );
regions.body = vdom
? vdom.get( document.body )
: toVdom( dom.body );
Expand All @@ -103,15 +110,28 @@ const regionsToVdom: RegionsToVdom = async ( dom, { vdom } = {} ) => {
}
const title = dom.querySelector( 'title' )?.innerText;
const initialData = parseServerData( dom );
return { regions, head, title, initialData };
return { regions, styles, scriptModules, title, initialData };
};

// Render all interactive regions contained in the given page.
const renderRegions = async ( page: Page ) => {
// Whait for styles and modules to be ready.
await Promise.all( [
...page.styles,
...page.scriptModules.map(
( src ) => import( /* webpackIgnore: true */ src )
),
] );
// Replace style sheets.
const sheets = await Promise.all( page.styles );
window.document
.querySelectorAll( 'style,link[rel=stylesheet]' )
.forEach( ( element ) => element.remove() );
window.document.adoptedStyleSheets = sheets;

if ( globalThis.IS_GUTENBERG_PLUGIN ) {
if ( navigationMode === 'fullPage' ) {
// Once this code is tested and more mature, the head should be updated for region based navigation as well.
await updateHead( page.head );
// Update HTML.
const fragment = getRegionRootFragment( document.body );
batch( () => {
populateServerData( page.initialData );
Expand Down Expand Up @@ -169,23 +189,14 @@ window.addEventListener( 'popstate', async () => {
// Initialize the router and cache the initial page using the initial vDOM.
// Once this code is tested and more mature, the head should be updated for
// region based navigation as well.
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
if ( navigationMode === 'fullPage' ) {
// Cache the scripts. Has to be called before fetching the assets.
[].map.call(
document.querySelectorAll( 'script[type="module"][src]' ),
( script ) => {
headElements.set( script.getAttribute( 'src' ), {
tag: script,
} );
}
);
await fetchHeadAssets( document );
}
}
pages.set(
getPagePath( window.location.href ),
Promise.resolve( regionsToVdom( document, { vdom: initialVdom } ) )
Promise.resolve(
regionsToVdom( document, {
vdom: initialVdom,
baseUrl: window.location.href,
} )
)
);

// Check if the link is valid for client-side navigation.
Expand Down
Loading