cartridge-resolver-plugin
Version:
Webpack Plugin: Provide the cartridge inheritance behavior same as Demandware server side scripts.
253 lines (222 loc) • 10.7 kB
JavaScript
const fs = require('fs');
const path = require('path');
/**
* Webpack: Cartridge Resolver Plugin
* Provide the cartridge inheritance behavior same as DW server side.
*
* @author Vinh Trinh <vinhtrinh.live@gmail.com>
*/
class CartridgeResolverPlugin {
/**
* Constructor
*
* @param {object} cartridges An associative object where the key is the cartridge name
* and the value is the local installed cartridge path
* @param {object} aliases An associative object where the key is the alias name
* and the value is the full cartridge name
*/
constructor(cartridges = {}, aliases = {}) {
this.cartridges = cartridges;
this.aliases = aliases;
this.cartridgeNames = Object.keys(cartridges);
this.aliasRegexp = new RegExp(`^(${Object.keys(aliases).join('|')})(\\\\|/)`);
this.absoluteRegexp = new RegExp(`^(${Object.keys(aliases).concat(this.cartridgeNames).join('|')}):`);
for (const alias in aliases) {
if (!this.cartridges.hasOwnProperty(aliases[alias])) {
throw new Error(`Unable find registered cartridge name "${aliases[alias]}" for alias "${alias}"`);
}
}
}
/**
* Applying the plugin
*
* @param {Resolver} resolver
*/
apply(resolver) {
resolver.hooks.resolve.tapAsync(this.constructor.name, (...args) => this.resolve.apply(this, [resolver].concat(args)));
}
/**
* Do the resolve asset for the given request
*
* Flags:
* *: solving asset from across registered cartridges
* ^: solving asset from registered cartridges which have lower priority with current cartridge
* ~: solving asset in current cartridge
*
* Absolute asset path using with cartridge name or alias {@see isAbsoluteCartridge}
* app_storefront_base:product/product === base:product/product
*
* @todo Refactor this function to reduce its Cognitive Complexity from 33 to the 15 allowed sonarjs/cognitive-complexity
*
* @param {Resolver} resolver
* @param {object} requestContext The asset request object
* @param {object} resolveContext
* @param {Function} callback The callback function
*/
resolve(resolver, requestContext, resolveContext, callback) {/* eslint-disable-line */
const currentCartridge = this.getCartridge(requestContext.context.issuer || requestContext.request);
if (currentCartridge) {
const flag = requestContext.request[0];
const currentCartridgeIndex = this.cartridgeNames.indexOf(currentCartridge);
let lookupCartrdiges;
let relativeRequest = requestContext.request.replace(this.aliasRegexp, () => '');
if (['*', '^', '~'].includes(flag)) {
relativeRequest = relativeRequest.replace(/^(\*|\^|~)(\\|\/|:)?/, '');
if (!relativeRequest) {
relativeRequest = this.getRelativeAsset(requestContext.context.issuer);
}
switch (flag) {
case '*':
lookupCartrdiges = this.cartridgeNames;
break;
case '^':
lookupCartrdiges = this.cartridgeNames.slice(currentCartridgeIndex + 1);
break;
case '~':
lookupCartrdiges = [currentCartridge];
break;
}
} else if (this.isAliasRequest(requestContext.request)) {
const relativeIssuer = this.getRelativeAsset(requestContext.context.issuer);
lookupCartrdiges = relativeRequest === relativeIssuer ? this.cartridgeNames.slice(currentCartridgeIndex + 1) : this.cartridgeNames;
} else if (this.isAbsoluteCartridge(requestContext.request)) {
const cartridgeName = requestContext.request.match(this.absoluteRegexp)[1];
relativeRequest = relativeRequest.replace(this.absoluteRegexp, '');
lookupCartrdiges = [this.cartridges[cartridgeName] ? cartridgeName : this.aliases[cartridgeName]];
} else if (!path.isAbsolute(requestContext.request)) {
if ('.' === requestContext.request) {
relativeRequest = this.getRelativeAsset(requestContext.context.issuer);
lookupCartrdiges = this.cartridgeNames.slice(currentCartridgeIndex + 1);
} else {
const relativeIssuer = this.getRelativeAsset(requestContext.context.issuer);
relativeRequest = this.getRelativeAsset(path.resolve(requestContext.path, requestContext.request));
lookupCartrdiges = relativeRequest === relativeIssuer ? this.cartridgeNames.slice(currentCartridgeIndex + 1) : this.cartridgeNames;
}
}
if (lookupCartrdiges && !relativeRequest.includes('/node_modules/')) {
const locale = this.getLocale(requestContext.context.issuer || requestContext.request);
const result = this.resolveFromCartrdiges(relativeRequest, lookupCartrdiges, locale);
if(result)
{
const target = resolver.ensureHook('resolved');
requestContext.path = result;
return resolver.doResolve(target, requestContext, `${this.constructor.name} found: ${result}`, resolveContext, callback);
}
}
}
callback();
}
/**
* Checks if the given asset is a cartrdige absolute path.
* A cartridge absolute path start with cartridge name or alias and separate with asset path a colon ":"
*
* @example
* base:checkout/address.js -> true
* app_storefront_base:orderHistory/orderHistory.js -> true
* wishlists:components/miniCart.js -> true
* plugin_wishlists:product/wishlist.js -> true
*
* not_registered_alias:product/details.js -> false
* not_registered_cartridge:product/details.js -> false
*
* @param {string} assetPath The asset path
*
* @returns {boolean} True if the given asset path is a cartridge absolute path
*/
isAbsoluteCartridge(assetPath) {
return this.absoluteRegexp.test(assetPath);
}
/**
* Checks if the given request asset using an aliased path
*
* @example
* base/components/consentTracking -> true
* base/components/footer -> true
* ./components/miniCart -> false
* base/components/collapsibleItem -> true
* base/components/search -> true
* base/components/clientSideValidation -> true
*
* @param {string} assetPath
*
* @returns {boolean} True if the given asset path start with an aliased
*/
isAliasRequest(assetPath) {
return this.aliasRegexp.test(assetPath);
}
/**
* Gets relative path for the given asset without extension follow pattern:
* /cartridge/client/{locale}/js/{relativeAsset}.js
*
* @example
* app_storefront_base/cartridge/client/default/js/product/base.js -> product/base
* app_storefront_base/cartridge/client/default/js/components/../cart/cart.js -> cart/cart
* plugin_wishlists/cartridge/client/default/js/components/miniCart.js -> components/miniCart
* plugin_wishlists/cartridge/client/default/js/product/wishlist.js -> product/wishlist
*
* @param {string} assetPath The asset path
*
* @returns {string} The relative path to cartridge asset path without extension
*/
getRelativeAsset(assetPath) {
return assetPath.replace(/\\/g, '/').replace(/(^.*\/cartridge\/client\/[\w-]+\/js\/|\.js$)/g, '');
}
/**
* Extract the cartridge name from the given asset path follow pattern:
* /{cartridgeName}/cartridge/client/
*
* @example
* project_root/cartridges/app_storefront_base/cartridge/client/default/js/cart/cart.js -> app_storefront_base
* project_root/vendors/plugin_wishlists/cartridge/client/default/js/main.js -> plugin_wishlists
* project_root/vendors/link_adyen/cartridges/int_adyen_SFRA/cartridge/client/default/js/checkout/billing.js -> int_adyen_SFRA
*
* @param {string} assetPath The absolute asset path
*
* @returns {string} The extracted cartridge name, null if no cartridge found
*/
getCartridge(assetPath) {
const match = assetPath.replace(/\\/g, '/').match(/\/(\w*?)\/cartridge\/client\//);
return match ? match[1] : null;
}
/**
* Extract locale from the given asset path follow pattern:
* /cartridge/client/{locale}/js/
*
* @example
* app_storefront_base/cartridge/client/default/js/profile/profile.js -> default
* app_storefront_base/cartridge/client/default/js/checkout/billing.js -> default
* app_storefront_base/cartridge/client/fr_FR/js/account/addressBook.js -> fr_FR
*
* @param {string} assetPath The absolute asset path
*
* @returns {string} The extracted locale, null if no cartridge found
*/
getLocale(assetPath) {
const match = assetPath.replace(/\\/g, '/').match(/\/cartridge\/client\/([\w-]+?)\/js\//);
return match ? match[1] : null;
}
/**
* Resolve the relative asset using the given cartridge names
*
* @param {string} relativeAsset The relative request asset without extension
* @param {string[]} cartridges List of cartridge names will be lookup for the given request asset
* @param {string} locale Asset locale
*
* @returns {string|null} The absolute path of solved asset. `null` will be returned if nothing found.
*/
resolveFromCartrdiges(relativeAsset, cartridges, locale = 'default') {
let resolvedAsset = null;
for (const cartridge of cartridges) {
resolvedAsset = path.resolve(this.cartridges[cartridge], locale, 'js', relativeAsset);
if (!(/\.js$/.test(resolvedAsset))) {
resolvedAsset += '.js';
}
if (fs.existsSync(resolvedAsset)) {
break;
}
resolvedAsset = null;
}
return resolvedAsset;
}
}
module.exports = CartridgeResolverPlugin;