UNPKG

cartridge-resolver-plugin

Version:

Webpack Plugin: Provide the cartridge inheritance behavior same as Demandware server side scripts.

253 lines (222 loc) 10.7 kB
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;