UNPKG

html-bundler-webpack-plugin

Version:

Generates complete single-page or multi-page website from source assets. Built-in support for Markdown, Eta, EJS, Handlebars, Nunjucks, Pug. Alternative to html-webpack-plugin.

394 lines (328 loc) 14.4 kB
const path = require('path'); // the 'enhanced-resolve' package already used in webpack, don't need to define it in package.json const ResolverFactory = require('enhanced-resolve'); const { isUrl } = require('../Common/Helpers'); const PluginService = require('../Plugin/PluginService'); const Snapshot = require('../Plugin/Snapshot'); const { resolveException } = require('./Messages/Exeptions'); class Resolver { aliasRegexp = /^([~@])?(.*?)(?=\/)/; aliasFileRegexp = /^([~@])?(.*?)$/; hasAlias = false; fs = null; loaderContext = null; pluginCompiler = null; /** The list of ResolverType values, see types.d.ts */ static types = { default: 'default', script: 'script', style: 'style', asset: 'asset', include: 'include', }; /** * @param {Object} loaderContext */ constructor(loaderContext) { this.loaderContext = loaderContext; this.pluginCompiler = loaderContext._compilation.compiler; this.fs = loaderContext.fs.fileSystem; } /** * @param {Option} loaderOption */ init(loaderOption) { const options = loaderOption.getWebpackResolve(); this.basedir = loaderOption.getBasedir(); this.contextdir = loaderOption.getContextDir(); this.aliases = options.alias || {}; this.hasAlias = Object.keys(this.aliases).length > 0; const scriptResolverOptions = { ...options, preferRelative: options.preferRelative !== false, // resolve 'exports' field in package.json, default value is: ['webpack', 'production', 'browser'] conditionNames: ['require', 'node'], // restrict default extensions list '.js', '.json', '.wasm' for faster resolving of script files extensions: options.extensions.length ? options.extensions : ['.js'], }; const styleResolverOptions = { ...options, preferRelative: options.preferRelative !== false, byDependency: {}, conditionNames: ['style', 'sass'], // firstly, try to resolve 'browser' or 'style' fields in package.json to get compiled CSS bundle of a module, // e.g. bootstrap has the 'style' field, but material-icons has the 'browser' field for resolving the CSS file; // if a module has not a client specified field, then must be used path to client file of the module, // like `module-name/dist/bundle.css` mainFields: ['style', 'browser', 'sass', 'main'], mainFiles: ['_index', 'index'], extensions: ['.scss', '.sass', '.css'], restrictions: this.getStyleResolveRestrictions(), }; const fileResolverOptions = { ...options, preferRelative: options.preferRelative !== false, // resolve 'exports' field in package.json, default value is: ['webpack', 'production', 'browser'] conditionNames: ['require', 'node'], // restrict default extensions list '.js', '.json', '.wasm' for faster resolving of script files extensions: options.extensions.length ? options.extensions : ['.js'], // remove extensions for faster resolving of files different from styles and scripts //extensions: [], // don't work if required js file w/o an ext in template }; // resolver for scripts from the 'script' tag, npm modules, and other js files this.resolveScript = ResolverFactory.create.sync(scriptResolverOptions); // resolver for styles from the 'link' tag this.resolveStyle = ResolverFactory.create.sync(styleResolverOptions); // resolver for resources: scripts w/o an ext (e.g. in pug: require('./data')), images, fonts, etc. this.resolveFile = ResolverFactory.create.sync(fileResolverOptions); // TODO: use the resolver built-in in loaderContext, problem: this resolver is async //const resolveFileAsync = this.loaderContext.getResolve(fileResolverOptions); //this.resolveFile = async (context, request) => await resolveFileAsync(context, request); //const resolveStyleAsync = this.loaderContext.getResolve(styleResolverOptions); //this.resolveStyle = async (context, request) => await resolveStyleAsync(context, request); //const resolveScriptAsync = this.loaderContext.getResolve(scriptResolverOptions); //this.resolveScript = async (context, request) => await resolveScriptAsync(context, request); } /** * Return a list of resolve restrictions to restrict the paths that a request can be resolved on. * @see https://webpack.js.org/configuration/resolve/#resolverestrictions * @return {Array<RegExp|string>} */ getStyleResolveRestrictions() { const restrictions = PluginService.getOptions(this.pluginCompiler).getCss().test; return Array.isArray(restrictions) ? restrictions : [restrictions]; } /** * Resolve the request. * * @param {string} request The request to resolve. * @param {string} issuer The issuer of resource. * @param {ResolverType} [type = 'default'] The type of resolving request. * @return {string} */ resolve(request, issuer, type = Resolver.types.default) { const [issuerFile] = issuer.split('?', 1); const context = this.contextdir || path.dirname(issuerFile); const isScript = type === Resolver.types.script; const isStyle = type === Resolver.types.style; let isAliasArray = false; let resolvedRequest = null; // resolve an absolute path by prepending options.basedir if (this.basedir && request[0] === '/') { resolvedRequest = path.join(this.basedir, request); } // resolve a relative file if (resolvedRequest == null && request[0] === '.') { resolvedRequest = path.join(context, request); } // resolve a file by webpack `resolve.alias` if (resolvedRequest == null) { resolvedRequest = this.resolveAlias(request, type); isAliasArray = Array.isArray(resolvedRequest); } // resolved alias is an URL if (resolvedRequest && !isAliasArray && isUrl(resolvedRequest)) { return resolvedRequest; } // fallback to enhanced resolver if (resolvedRequest == null || isAliasArray) { let normalizedRequest = request; // remove optional prefix in request for enhanced resolver if (isAliasArray) normalizedRequest = this.removeAliasPrefix(normalizedRequest); try { resolvedRequest = isScript ? this.resolveScript(context, normalizedRequest) : isStyle ? this.resolveStyle(context, normalizedRequest) : this.resolveFile(context, normalizedRequest); } catch (error) { if (isScript) { // extract important ending of filename from the request // app.js => app.js // @alias/app.js => app.js // @alias/path/to/app.js => path/to/app.js // TODO: fix limitation, when a request is like `../../app.js` and used an array of aliases in the tsconfig let [requestFile] = normalizedRequest.split('?', 1); let beginPos = requestFile.indexOf('/'); if (beginPos > 0) requestFile = requestFile.slice(beginPos); Snapshot.addMissingFile(issuer, requestFile); } resolveException(error, request, path.relative(this.loaderContext.rootContext, issuer)); } } if (isScript) { resolvedRequest = this.resolveScriptExtension(resolvedRequest); } // request of the svg file can contain a fragment id, e.g., shapes.svg#circle const separator = resolvedRequest.indexOf('#') > 0 ? '#' : '?'; const [resolvedFile] = resolvedRequest.split(separator, 1); if (!require.resolve(resolvedFile)) { if (isScript) { Snapshot.addMissingFile(issuer, resolvedFile); } const error = new Error(`Can't resolve '${request}' in '${context}'`); resolveException(error, request, path.relative(this.loaderContext.rootContext, issuer)); } return resolvedRequest; } /** * Interpolate filename for `compile` mode. * * @note: the file is the argument of require() and can be any expression, like require('./' + file + '.jpg'). * See https://webpack.js.org/guides/dependency-management/#require-with-expression. * * @param {string} value The expression to resolve. * @param {string} templateFile The template file. * @param {types} [type = 'default'] The type of resolving request. * @return {string} */ interpolate(value, templateFile, type = Resolver.types.default) { value = value.trim(); const [, quote, file] = /(^"|'|`)(.+?)(?=`|'|")/.exec(value) || []; const isScript = type === 'script'; const isStyle = type === 'style'; let interpolatedValue = null; let valueFile = file; // the argument begin with a string quote const context = (this.contextdir || path.dirname(templateFile)) + '/'; if (!file) { // fix webpack require issue `Cannot find module` for the case: // - var file = './image.jpeg'; // require(file) <- error // require(file + '') <- solution return value + ` + ''`; } // resolve an absolute path by prepending options.basedir if (this.basedir && file[0] === '/') { interpolatedValue = quote + this.basedir + value.slice(2); } // resolve a file in current directory if (interpolatedValue == null && file.slice(0, 2) === './') { interpolatedValue = quote + context + value.slice(3); } // resolve a file in parent directory if (interpolatedValue == null && file.slice(0, 3) === '../') { interpolatedValue = quote + context + value.slice(1); } // resolve a webpack `resolve.alias` if (interpolatedValue == null) { interpolatedValue = this.resolveAlias(value.slice(1), type); if (typeof interpolatedValue === 'string') { interpolatedValue = quote + interpolatedValue; } else if (Array.isArray(interpolatedValue)) { interpolatedValue = null; valueFile = this.removeAliasPrefix(file); } } // try the enhanced resolver for alias from tsconfig or for alias as array of paths // the following examples work: // '@data/path/script' // '@data/path/script.js' // '@images/logo.jpg' // `${file}` if (interpolatedValue == null) { if (file.indexOf('{') < 0 && !file.endsWith('/')) { try { const resolvedValueFile = isStyle ? this.resolveStyle(context, valueFile) : this.resolveFile(context, valueFile); interpolatedValue = value.replace(file, resolvedValueFile); } catch (error) { resolveException(error, value, templateFile); } } } // @note: resolve of alias from tsconfig in interpolating string is not supported for `compile` method, // the following examples not work: // `@data/${pathname}/script` // `@data/${pathname}/script.js` // `@data/path/${filename}` // '@data/path/' + filename if (interpolatedValue == null) { return value; } // remove quotes: '/path/to/file.js' -> /path/to/file.js let resolvedValue = interpolatedValue.slice(1, -1); let resolvedFile; // detect only full resolved path, w/o interpolation like: '/path/to/' + file or `path/to/${file}` if (!/["'`$]/g.test(resolvedValue)) { resolvedFile = resolvedValue; } if (isScript || isStyle) { if (isScript) resolvedFile = this.resolveScriptExtension(resolvedFile); return resolvedFile; } // TODO: test add dependency to watch //if (resolvedFile) Dependency.add(resolvedFile); return interpolatedValue; } /** * Resolve script request w/o the extension. * The extension must be resolved to generate a correct unique JS filename in the plugin. * For example, `vendor.min?key=val` resolve to `vendor.min.js?key=val`. * * @param {string} request The request of script. * @return {string} */ resolveScriptExtension(request) { const [file, query] = request.split('?'); // TODO: get resolve extension from webpack config `resolve.extensions` and merge with the default (js|ts) const scriptExtensionRegexp = /\.(js|ts)$/; const resolvedFile = scriptExtensionRegexp.test(file) ? file : require.resolve(file); return query ? resolvedFile + '?' + query : resolvedFile; } /** * Resolve an alias in the argument of require() function. * * @param {string} request The value of extends/include/require(). * @return {string | [] | null} If found an alias return the resolved normalized path otherwise return null. * @param {ResolverType} type The type of resolving request. * @private */ resolveAlias(request, type) { if (this.hasAlias === false) return null; const fs = this.fs; const hasPath = request.indexOf('/') > -1; const aliasRegexp = hasPath ? this.aliasRegexp : this.aliasFileRegexp; // special use case for a templating engine, e.g. Pug, // when the webpack alias is a file and used by include e.g., `include FILE_ALIAS`, // the Pug compiler adds to the alias the `.pug` extension automatically, // this added extension must be removed from request if (type === Resolver.types.include && !hasPath && PluginService.getOptions(this.pluginCompiler).isEntry(request)) { let pos = request.lastIndexOf('.'); if (pos > 0) { request = request.slice(0, pos); } } const [, prefix, aliasName] = aliasRegexp.exec(request) || []; if (!prefix && !aliasName) return null; let alias = (prefix || '') + (aliasName || ''); let aliasPath = this.aliases[alias] || this.aliases[aliasName]; let resolvedFile = aliasPath; if (typeof aliasPath === 'string') { let file = request.slice(alias.length); let paths = [aliasPath, file]; if (isUrl(aliasPath)) { if (aliasPath.endsWith('/')) { aliasPath = aliasPath.slice(0, -1); } return aliasPath + file; } if (this.basedir && !fs.existsSync(aliasPath)) { paths.unshift(this.basedir); } resolvedFile = path.join(...paths); } return resolvedFile; } /** * @param {string} request * @return {string} */ removeAliasPrefix(request) { const [, prefix, aliasName] = this.aliasRegexp.exec(request) || []; const alias = (prefix || '') + (aliasName || ''); return prefix != null && aliasName != null && this.aliases[alias] == null ? request.slice(1) : request; } } module.exports = Resolver;