UNPKG

@regru/webpack-babel-multi-target-plugin

Version:

A Webpack plugin that works with Babel to allow deployment of ES2015 builds targeted to modern browsers, with an ES5 fallback for legacy browsers.

376 lines 18.4 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const path = require("path"); const babel_target_1 = require("./babel-target"); const blind_targeting_error_1 = require("./blind.targeting.error"); const excluded_packages_1 = require("./excluded.packages"); const babel_multi_target_loader_1 = require("./babel.multi.target.loader"); const plugin_name_1 = require("./plugin.name"); const NOT_TARGETED = [ /\.s?css$/, ]; // picks up where BabelTargetEntryPlugin leaves off and takes care of targeting all dependent modules // includes special case handling for Angular lazy routes /** * @internalapi */ class TargetingPlugin { constructor(targets, exclude, doNotTarget, externals) { this.targets = targets; this.exclude = exclude; this.externals = externals; this.babelLoaderPath = require.resolve('babel-loader'); this.babelLoaders = {}; this.remainingTargets = {}; this.doNotTarget = NOT_TARGETED.concat(doNotTarget || []); } apply(compiler) { // make sure our taps come after other plugins (particularly AngularCompilerPlugin) compiler.hooks.afterPlugins.tap(plugin_name_1.PLUGIN_NAME, () => { compiler.hooks.contextModuleFactory.tap(plugin_name_1.PLUGIN_NAME, (cmf) => { cmf.hooks.beforeResolve.tapPromise(plugin_name_1.PLUGIN_NAME, this.targetLazyModules.bind(this)); cmf.hooks.afterResolve.tapPromise(plugin_name_1.PLUGIN_NAME, this.wrapResolveDependencies.bind(this)); cmf.hooks.afterResolve.tapPromise(plugin_name_1.PLUGIN_NAME, this.afterResolve.bind(this)); }); compiler.hooks.normalModuleFactory.tap(plugin_name_1.PLUGIN_NAME, (nmf) => { nmf.hooks.module.tap(plugin_name_1.PLUGIN_NAME, this.targetModule.bind(this)); nmf.hooks.afterResolve.tapPromise(plugin_name_1.PLUGIN_NAME, this.afterResolve.bind(this)); }); compiler.hooks.compilation.tap(plugin_name_1.PLUGIN_NAME, (compilation) => { if (compilation.name) { return; } const ogAddModule = compilation.addModule.bind(compilation); compilation.addModule = (module, cacheGroup) => { this.targetModule(module); return ogAddModule(module, cacheGroup); }; }); compiler.hooks.watchRun.tapPromise(plugin_name_1.PLUGIN_NAME, () => __awaiter(this, void 0, void 0, function* () { this.remainingTargets = {}; })); }); } // HACK ALERT! // Sometimes, there just isn't a way to trace a request back to a targeted module or entry. This happens with // Angular's lazy loaded routes and ES6 dynamic imports. With dynamic imports, we'll get a pair of requests for each // time a module is dynamically referenced. The best we can do is just fake it - create an array for each request // that has a copy of the targets array, and assign a the first remaining target to each request getBlindTarget(issuer, key) { if (!this.remainingTargets) { this.remainingTargets = {}; } if (!this.remainingTargets[issuer]) { this.remainingTargets[issuer] = {}; } if (!this.remainingTargets[issuer][key]) { this.remainingTargets[issuer][key] = this.targets.slice(0); } if (!this.remainingTargets[issuer][key].length) { throw new blind_targeting_error_1.BlindTargetingError(key); } return this.remainingTargets[issuer][key].shift(); } targetLazyModules(resolveContext) { return __awaiter(this, void 0, void 0, function* () { // handle lazy modules from ES6 dynamic imports or Angular's AngularCompilerPlugin if (resolveContext.mode === 'lazy') { // FIXME: Mixing Harmony and CommonJs requires of @angular/core breaks lazy loading! // if this is happening, it's likely that a dependency has not correctly provided a true ES6 module and is // instead providing CommonJs module. const babelTarget = babel_target_1.BabelTarget.getTargetFromTag(resolveContext.request, this.targets) || this.getBlindTarget(resolveContext.context, resolveContext.resource); resolveContext.request = babelTarget.getTargetedRequest(resolveContext.request); // track a map of resources to targets if (!resolveContext.resolveOptions.babelTargetMap) { resolveContext.resolveOptions.babelTargetMap = {}; } resolveContext.resolveOptions.babelTargetMap[resolveContext.resource] = babelTarget; this.targetDependencies(babelTarget, resolveContext); return resolveContext; } }); } wrapResolveDependencies(resolveContext) { return __awaiter(this, void 0, void 0, function* () { if (resolveContext.mode === 'lazy') { const babelTarget = babel_target_1.BabelTarget.getTargetFromTag(resolveContext.request, this.targets); if (resolveContext.chunkName && babelTarget.tagAssetsWithKey) { resolveContext.chunkName = babelTarget.getTargetedAssetName(resolveContext.chunkName); } // this needs to happen in addition to request targeting in targetLazyModules, otherwise it breaks Angular routing // and makes all sorts of weird chunks resolveContext.resource = babelTarget.getTargetedRequest(resolveContext.resource); // piggy-back on the existing resolveDependencies function to target the dependencies. // for angular lazy routes, this wraps the resolveDependencies function defined in the compiler plugin const ogResolveDependencies = resolveContext.resolveDependencies; resolveContext.resolveDependencies = (_fs, _resource, cb) => { ogResolveDependencies(_fs, _resource, (err, dependencies) => { this.targetDependencies(babelTarget, { dependencies }); cb(null, dependencies); }); }; } }); } targetModule(module) { if (module.options && module.options.babelTarget) { // already targeted, no need to do it again return; } if (!this.isTargetedRequest(module, module.request)) { return; } let babelTarget; if (module.options && module.options.mode === 'lazy') { babelTarget = babel_target_1.BabelTarget.getTargetFromTag(module.options.request, this.targets); module.options.babelTarget = babelTarget; } else { babelTarget = babel_target_1.BabelTarget.getTargetFromTag(module.request, this.targets); if (!babelTarget) { return; } module.request = babelTarget.getTargetedRequest(module.request); if (!module.options) { module.options = {}; } module.options.babelTarget = babelTarget; } // wrap the module's addDependency function so that any dependencies added after this point are automatically // targeted const ogAddDependency = module.addDependency.bind(module); module.addDependency = (dep) => { this.targetDependency(dep, babelTarget); return ogAddDependency(dep); }; const ogAddBlock = module.addBlock.bind(module); module.addBlock = (block) => { // if a dynamic import has specified the [resource] tag in its chunk name, overwrite the computed // name with the request path, minus the extension if (module.options.mode === 'lazy' && module.options.chunkName && module.options.chunkName.includes('[resource]')) { const resource = block.request .replace(/\.\w+$/, '') // remove the extension .replace(/\W+/g, '-') // replace any non-alphanumeric characters with - .replace(/^-/, '') // trim leading - .replace(/-$/, ''); // trim following - block.groupOptions.name = module.options.chunkName.replace('[resource]', resource); } this.targetDependency(block, babelTarget); return ogAddBlock(block); }; } targetDependency(dep, babelTarget) { if (dep.options && dep.options.mode === 'lazy') { return this.targetDependency(dep.options, babelTarget); } if (!dep.request || !this.isTargetedRequest(dep, dep.request)) { return; } // update the dependency requests to be targeted // only tag dep.request, not tag dep.userRequest, it breaks lazy loading // userRequest basically maps the user-friendly name to the actual request // so if the code does require('some-lazy-route/lazy.module.ngfactory.js') <-- userRequest // it can be mapped to 'some-lazy-route/lazy.module.ngfactory.js?babelTarget=modern <-- request if (dep.request) { dep.request = babelTarget.getTargetedRequest(dep.request); } } targetDependencies(babelTarget, context) { context.dependencies.forEach((dep) => this.targetDependency(dep, babelTarget)); } afterResolve(resolveContext) { return __awaiter(this, void 0, void 0, function* () { const loaders = (resolveContext.loaders || []) .filter((loaderInfo) => loaderInfo.options && loaderInfo.options.isBabelMultiTargetLoader); this.checkResolveTarget(resolveContext, !!loaders.length); this.replaceLoaders(resolveContext, loaders); }); } checkResolveTarget(resolveContext, hasLoader) { if (!this.isTargetedRequest(resolveContext, resolveContext.request) || !this.isTranspiledRequest(resolveContext) || !hasLoader) { return; } let babelTarget = babel_target_1.BabelTarget.getTargetFromTag(resolveContext.request, this.targets); if (babelTarget) { // save babelTarget for quick lookup // makes it easier to get babelTarget for commonjs modules. resolveContext.contextInfo = { babelTarget }; this.targetChunkNames(resolveContext, babelTarget); return; } babelTarget = this.getTargetFromContext(resolveContext); if (babelTarget) { // this is probably a dynamic import, in which case the dependencies need to get targeted resolveContext.dependencies.forEach((dep) => this.targetDependency(dep, babelTarget)); } else { babelTarget = this.getBlindTarget(resolveContext.resourceResolveData.context.issuer, resolveContext.request); } this.targetChunkNames(resolveContext, babelTarget); resolveContext.request = babelTarget.getTargetedRequest(resolveContext.request); if (resolveContext.resource) { resolveContext.resource = babelTarget.getTargetedRequest(resolveContext.resource); } } targetChunkNames(resolveContext, babelTarget) { resolveContext.dependencies.forEach((dep) => { if (!dep.block || !dep.block.groupOptions || !dep.block.groupOptions.name) { return; } dep.block.groupOptions.name = babelTarget.getTargetedAssetName(dep.block.groupOptions.name); }); } replaceLoaders(resolveContext, loaders) { const babelTarget = this.isTranspiledRequest(resolveContext) && (babel_target_1.BabelTarget.getTargetFromTag(resolveContext.rawRequest, this.targets) || (resolveContext.resourceResolveData && this.getTargetFromContext(resolveContext))); loaders.forEach((loader) => { const index = resolveContext.loaders.indexOf(loader); if (!babelTarget) { resolveContext.loaders.splice(index, 1); return; } const effectiveLoader = { loader: loader.loader, options: loader.options.loaderOptions, ident: loader.ident, }; if (loader.loader === this.babelLoaderPath) { resolveContext.loaders.splice(index, 1, this.getTargetedBabelLoader(effectiveLoader, babelTarget)); } else { resolveContext.loaders.splice(index, 1, effectiveLoader); } }); } isTargetedRequest(context, request) { if (this.doNotTarget && this.doNotTarget.find(entry => entry.test(request))) { return false; } return !this.isExternalRequest(context, request, this.externals); } isExternalRequest(context, request, externals) { if (!externals) { return false; } if (Array.isArray(externals)) { for (const ext of externals) { if (this.isExternalRequest(context, request, ext)) { return true; } } return false; } if (typeof (externals) === 'function') { throw new Error('Using an ExternalsFunctionElement is not supported'); } if (typeof (externals) === 'string') { return request === externals; } if (externals instanceof RegExp) { return externals.test(request); } if (typeof (externals) === 'object') { return this.isExternalRequest(context, request, Object.keys(externals)); } return false; } isTranspiledRequest(resolveContext) { // ignore files/libs that are known to not need transpiling if (excluded_packages_1.STANDARD_EXCLUDED.find(pattern => pattern.test(resolveContext.resource))) { // TODO: report this somewhere? // console.info('not transpiling request from STANDARD_EXCLUDED', resolveContext.resource) return false; } if (excluded_packages_1.KNOWN_EXCLUDED.find(pattern => pattern.test(resolveContext.resource))) { // TODO: report this somewhere? // console.info('not transpiling request from KNOWN_EXCLUDED', resolveContext.resource) return false; } if (this.exclude.find(pattern => pattern.test(resolveContext.resolve))) { // TODO: report this somewhere? // console.info('not transpiling request from excluded patterns', resolveContext.resource) return false; } if (resolveContext.mode === 'lazy') { // ES6 dynamic import entry return true; } const pkgRoot = resolveContext.resourceResolveData.descriptionFileRoot; const pkg = resolveContext.resourceResolveData.descriptionFileData; // coming from a package's "main" or "browser" field? don't need to transpile if (pkg.main && resolveContext.resource === path.resolve(pkgRoot, pkg.main)) { // TODO: report this somewhere? // console.info('not transpiling request using package "main"', resolveContext.resource) return false; } if (pkg.browser) { // TODO: report this somewhere? // console.info('not transpiling request using package "browser"', resolveContext.resource) if (typeof (pkg.browser) === 'string' && resolveContext.resource === path.resolve(pkgRoot, pkg.browser)) { return false; } if (Array.isArray(pkg.browser) && pkg.browser.find((entry) => resolveContext.resource === path.resolve(pkgRoot, entry))) { return false; } if (typeof (pkg.browser === 'object') && Object.values(pkg.browser).find((entry) => { if (typeof entry !== 'string') { return false; } return resolveContext.resource === path.resolve(pkgRoot, entry); })) { return false; } } return true; } getTargetFromContext(context) { if (context.contextInfo && context.contextInfo.babelTarget) { return context.contextInfo.babelTarget; } if (context.resourceResolveData && context.resourceResolveData.context && context.resourceResolveData.context.babelTarget) { return context.resourceResolveData.context.babelTarget; } const dependencies = context.dependencies; for (const dep of dependencies) { if (dep.babelTarget) { return dep.babelTarget; } if (dep.originModule) { const target = babel_target_1.BabelTarget.findTarget(dep.originModule); if (target) { return target; } } } } getTargetedBabelLoader(loader, babelTarget) { if (!this.babelLoaders[babelTarget.key]) { this.babelLoaders[babelTarget.key] = Object.assign({}, loader, { loader: this.babelLoaderPath, options: babelTarget.options, }); } return this.babelLoaders[babelTarget.key]; } static loader(loader) { return new babel_multi_target_loader_1.BabelMultiTargetLoader(loader); } } exports.TargetingPlugin = TargetingPlugin; //# sourceMappingURL=targeting.plugin.js.map