@wordpress/interactivity-router
Version:
Package that exposes state and actions from the `core/router` store, part of the Interactivity API.
293 lines (273 loc) • 7.49 kB
text/typescript
/**
* This code is derived from the following projects:
*
* 1. dynamic-importmap (https://github.com/keller-mark/dynamic-importmap)
* 2. es-module-shims (https://github.com/guybedford/es-module-shims)
*
* The original implementation was created by Guy Bedford in es-module-shims,
* then adapted by Mark Keller in dynamic-importmap, and further modified
* for use in this project.
*
* Both projects are licensed under the MIT license.
*
* MIT License: https://opensource.org/licenses/MIT
*/
const backslashRegEx = /\\/g;
function isURL( url: string ) {
if ( url.indexOf( ':' ) === -1 ) {
return false;
}
try {
new URL( url );
return true;
} catch {
return false;
}
}
function resolveIfNotPlainOrUrl( relUrl, parentUrl ) {
const hIdx = parentUrl.indexOf( '#' ),
qIdx = parentUrl.indexOf( '?' );
if ( hIdx + qIdx > -2 ) {
parentUrl = parentUrl.slice(
0,
// eslint-disable-next-line no-nested-ternary
hIdx === -1 ? qIdx : qIdx === -1 || qIdx > hIdx ? hIdx : qIdx
);
}
if ( relUrl.indexOf( '\\' ) !== -1 ) {
relUrl = relUrl.replace( backslashRegEx, '/' );
}
// protocol-relative
if ( relUrl[ 0 ] === '/' && relUrl[ 1 ] === '/' ) {
return parentUrl.slice( 0, parentUrl.indexOf( ':' ) + 1 ) + relUrl;
}
// relative-url
else if (
( relUrl[ 0 ] === '.' &&
( relUrl[ 1 ] === '/' ||
( relUrl[ 1 ] === '.' &&
( relUrl[ 2 ] === '/' ||
( relUrl.length === 2 && ( relUrl += '/' ) ) ) ) ||
( relUrl.length === 1 && ( relUrl += '/' ) ) ) ) ||
relUrl[ 0 ] === '/'
) {
const parentProtocol = parentUrl.slice(
0,
parentUrl.indexOf( ':' ) + 1
);
// Disabled, but these cases will give inconsistent results for deep backtracking
//if (parentUrl[parentProtocol.length] !== '/')
// throw new Error('Cannot resolve');
// read pathname from parent URL
// pathname taken to be part after leading "/"
let pathname;
if ( parentUrl[ parentProtocol.length + 1 ] === '/' ) {
// resolving to a :// so we need to read out the auth and host
if ( parentProtocol !== 'file:' ) {
pathname = parentUrl.slice( parentProtocol.length + 2 );
pathname = pathname.slice( pathname.indexOf( '/' ) + 1 );
} else {
pathname = parentUrl.slice( 8 );
}
} else {
// resolving to :/ so pathname is the /... part
pathname = parentUrl.slice(
parentProtocol.length +
( parentUrl[ parentProtocol.length ] === '/' )
);
}
if ( relUrl[ 0 ] === '/' ) {
return (
parentUrl.slice( 0, parentUrl.length - pathname.length - 1 ) +
relUrl
);
}
// join together and split for removal of .. and . segments
// looping the string instead of anything fancy for perf reasons
// '../../../../../z' resolved to 'x/y' is just 'z'
const segmented =
pathname.slice( 0, pathname.lastIndexOf( '/' ) + 1 ) + relUrl;
const output = [];
let segmentIndex = -1;
for ( let i = 0; i < segmented.length; i++ ) {
// busy reading a segment - only terminate on '/'
if ( segmentIndex !== -1 ) {
if ( segmented[ i ] === '/' ) {
output.push( segmented.slice( segmentIndex, i + 1 ) );
segmentIndex = -1;
}
continue;
}
// new segment - check if it is relative
else if ( segmented[ i ] === '.' ) {
// ../ segment
if (
segmented[ i + 1 ] === '.' &&
( segmented[ i + 2 ] === '/' || i + 2 === segmented.length )
) {
output.pop();
i += 2;
continue;
}
// ./ segment
else if (
segmented[ i + 1 ] === '/' ||
i + 1 === segmented.length
) {
i += 1;
continue;
}
}
// it is the start of a new segment
while ( segmented[ i ] === '/' ) {
i++;
}
segmentIndex = i;
}
// finish reading out the last segment
if ( segmentIndex !== -1 ) {
output.push( segmented.slice( segmentIndex ) );
}
return (
parentUrl.slice( 0, parentUrl.length - pathname.length ) +
output.join( '' )
);
}
}
function resolveUrl( relUrl, parentUrl ) {
return (
resolveIfNotPlainOrUrl( relUrl, parentUrl ) ||
( isURL( relUrl )
? relUrl
: resolveIfNotPlainOrUrl( './' + relUrl, parentUrl ) )
);
}
function getMatch( path, matchObj ) {
if ( matchObj[ path ] ) {
return path;
}
let sepIndex = path.length;
do {
const segment = path.slice( 0, sepIndex + 1 );
if ( segment in matchObj ) {
return segment;
}
} while ( ( sepIndex = path.lastIndexOf( '/', sepIndex - 1 ) ) !== -1 );
}
function applyPackages( id, packages ) {
const pkgName = getMatch( id, packages );
if ( pkgName ) {
const pkg = packages[ pkgName ];
if ( pkg === null ) {
return;
}
return pkg + id.slice( pkgName.length );
}
}
function resolveImportMap( importMap, resolvedOrPlain, parentUrl ) {
let scopeUrl = parentUrl && getMatch( parentUrl, importMap.scopes );
while ( scopeUrl ) {
const packageResolution = applyPackages(
resolvedOrPlain,
importMap.scopes[ scopeUrl ]
);
if ( packageResolution ) {
return packageResolution;
}
scopeUrl = getMatch(
scopeUrl.slice( 0, scopeUrl.lastIndexOf( '/' ) ),
importMap.scopes
);
}
return (
applyPackages( resolvedOrPlain, importMap.imports ) ||
( resolvedOrPlain.indexOf( ':' ) !== -1 && resolvedOrPlain )
);
}
function resolveAndComposePackages(
packages,
outPackages,
baseUrl,
parentMap
) {
for ( const p in packages ) {
const resolvedLhs = resolveIfNotPlainOrUrl( p, baseUrl ) || p;
const target = packages[ p ];
if ( typeof target !== 'string' ) {
continue;
}
const mapped = resolveImportMap(
parentMap,
resolveIfNotPlainOrUrl( target, baseUrl ) || target,
baseUrl
);
if ( mapped ) {
outPackages[ resolvedLhs ] = mapped;
continue;
}
// console.warn(
// `Mapping "${ p }" -> "${ packages[ p ] }" does not resolve`
// );
}
}
function resolveAndComposeImportMap( json, baseUrl, parentMap ) {
const outMap = {
imports: Object.assign( {}, parentMap.imports ),
scopes: Object.assign( {}, parentMap.scopes ),
};
if ( json.imports ) {
resolveAndComposePackages(
json.imports,
outMap.imports,
baseUrl,
parentMap
);
}
if ( json.scopes ) {
for ( const s in json.scopes ) {
const resolvedScope = resolveUrl( s, baseUrl );
resolveAndComposePackages(
json.scopes[ s ],
outMap.scopes[ resolvedScope ] ||
( outMap.scopes[ resolvedScope ] = {} ),
baseUrl,
parentMap
);
}
}
return outMap;
}
let importMap = { imports: {}, scopes: {} };
// TODO: check if this baseURI should change per document, and so
// it need to be passed as a parameter to methods like `resolve`.
const baseUrl = document.baseURI;
const pageBaseUrl = baseUrl;
/**
* Extends the internal dynamic import map with the passed one.
*
* @param importMapIn Import map.
* @param importMapIn.imports Imports declaration.
* @param importMapIn.scopes Scopes declaration.
*/
export function addImportMap( importMapIn: {
imports?: Record< string, string >;
scopes?: Record< string, any >;
} ) {
importMap = resolveAndComposeImportMap(
importMapIn,
pageBaseUrl,
importMap
);
}
/**
* Resolves the URL of the passed module ID against the current internal
* dynamic import map.
*
* @param id Module ID.
* @param parentUrl Parent URL, in case the module ID is relative.
* @return Resolved module URL.
*/
export function resolve( id: string, parentUrl: string ): string {
const urlResolved = resolveIfNotPlainOrUrl( id, parentUrl );
return resolveImportMap( importMap, urlResolved || id, parentUrl ) || id;
}