/* MIT License http://www.opensource.org/licenses/mit-license.php Author Natsu @xiaoxiaojx */ "use strict"; const { getContext } = require("loader-runner"); const ModuleNotFoundError = require("../ModuleNotFoundError"); const NormalModule = require("../NormalModule"); const { isAbsolute, join } = require("../util/fs"); const { parseResourceWithoutFragment } = require("../util/identifier"); const DEFAULT_SCHEME = "virtual"; const PLUGIN_NAME = "VirtualUrlPlugin"; /** * Defines the compiler type used by this module. * @typedef {import("../Compiler")} Compiler * @typedef {import("../../declarations/plugins/schemes/VirtualUrlPlugin").VirtualModule} VirtualModuleConfig * @typedef {import("../../declarations/plugins/schemes/VirtualUrlPlugin").VirtualModuleContent} VirtualModuleInput * @typedef {import("../../declarations/plugins/schemes/VirtualUrlPlugin").VirtualUrlOptions} VirtualUrlOptions */ /** @typedef {(loaderContext: LoaderContext) => Promise | string | Buffer} SourceFn */ /** @typedef {() => string} VersionFn */ /** @typedef {{ [key: string]: VirtualModuleInput }} VirtualModules */ /** * Defines the loader context type used by this module. * @template T * @typedef {import("../../declarations/LoaderContext").LoaderContext} LoaderContext */ /** * Normalizes a virtual module definition into a standard format * @param {VirtualModuleInput} virtualConfig The virtual module to normalize * @returns {VirtualModuleConfig} The normalized virtual module */ function normalizeModule(virtualConfig) { if (typeof virtualConfig === "string") { return { type: "", source() { return virtualConfig; } }; } else if (typeof virtualConfig === "function") { return { type: "", source: virtualConfig }; } return virtualConfig; } /** @typedef {{ [key: string]: VirtualModuleConfig }} NormalizedModules */ /** * Normalizes all virtual modules with the given scheme * @param {VirtualModules} virtualConfigs The virtual modules to normalize * @param {string} scheme The URL scheme to use * @returns {NormalizedModules} The normalized virtual modules */ function normalizeModules(virtualConfigs, scheme) { return Object.keys(virtualConfigs).reduce((pre, id) => { pre[toVid(id, scheme)] = normalizeModule(virtualConfigs[id]); return pre; }, /** @type {NormalizedModules} */ ({})); } /** * Converts a module id and scheme to a virtual module id * @param {string} id The module id * @param {string} scheme The URL scheme * @returns {string} The virtual module id */ function toVid(id, scheme) { return `${scheme}:${id}`; } /** * Converts a virtual module id to a module id * @param {string} vid The virtual module id * @param {string} scheme The URL scheme * @returns {string} The module id */ function fromVid(vid, scheme) { return vid.replace(`${scheme}:`, ""); } const VALUE_DEP_VERSION = `webpack/${PLUGIN_NAME}/version`; /** * Converts a module id and scheme to a cache key * @param {string} id The module id * @param {string} scheme The URL scheme * @returns {string} The cache key */ function toCacheKey(id, scheme) { return `${VALUE_DEP_VERSION}/${toVid(id, scheme)}`; } class VirtualUrlPlugin { /** * Creates an instance of VirtualUrlPlugin. * @param {VirtualModules} modules The virtual modules * @param {Omit | string=} schemeOrOptions The URL scheme to use */ constructor(modules, schemeOrOptions) { /** @type {VirtualUrlOptions} */ this.options = { modules, ...(typeof schemeOrOptions === "string" ? { scheme: schemeOrOptions } : schemeOrOptions || {}) }; /** @type {string} */ this.scheme = this.options.scheme || DEFAULT_SCHEME; /** @type {VirtualUrlOptions["context"]} */ this.context = this.options.context || "auto"; /** @type {NormalizedModules} */ this.modules = normalizeModules(this.options.modules, this.scheme); } /** * Applies the plugin by registering its hooks on the compiler. * @param {Compiler} compiler the compiler instance * @returns {void} */ apply(compiler) { compiler.hooks.validate.tap(PLUGIN_NAME, () => { compiler.validate( () => require("../../schemas/plugins/schemes/VirtualUrlPlugin.json"), this.options, { name: "Virtual Url Plugin", baseDataPath: "options" }, (options) => require("../../schemas/plugins/schemes/VirtualUrlPlugin.check")( options ) ); }); const scheme = this.scheme; const cachedParseResourceWithoutFragment = parseResourceWithoutFragment.bindCache(compiler.root); compiler.hooks.compilation.tap( PLUGIN_NAME, (compilation, { normalModuleFactory }) => { compilation.hooks.assetPath.tap( { name: PLUGIN_NAME, before: "TemplatedPathPlugin" }, (path, data) => { if (data.filename && this.modules[data.filename]) { /** * Returns safe path. * @param {string} str path * @returns {string} safe path */ const toSafePath = (str) => `__${str .replace(/:/g, "__") .replace(/^[^a-z0-9]+|[^a-z0-9]+$/gi, "") .replace(/[^a-z0-9._-]+/gi, "_")}`; // filename: virtual:logo.svg -> __virtual__logo.svg data.filename = toSafePath(data.filename); } return path; } ); normalModuleFactory.hooks.resolveForScheme .for(scheme) .tap(PLUGIN_NAME, (resourceData) => { const virtualConfig = this.findVirtualModuleConfigById( resourceData.resource ); const url = cachedParseResourceWithoutFragment( resourceData.resource ); const path = url.path; const type = virtualConfig.type || ""; const context = virtualConfig.context || this.context; resourceData.path = path + type; resourceData.resource = path; if (context === "auto") { const context = getContext(path); if (context === path) { resourceData.context = compiler.context; } else { const resolvedContext = fromVid(context, scheme); resourceData.context = isAbsolute(resolvedContext) ? resolvedContext : join( /** @type {import("..").InputFileSystem} */ (compiler.inputFileSystem), compiler.context, resolvedContext ); } } else if (context && typeof context === "string") { resourceData.context = context; } else { resourceData.context = compiler.context; } if (virtualConfig.version) { const cacheKey = toCacheKey(resourceData.resource, scheme); const cacheVersion = this.getCacheVersion(virtualConfig.version); compilation.valueCacheVersions.set( cacheKey, /** @type {string} */ (cacheVersion) ); } return true; }); const hooks = NormalModule.getCompilationHooks(compilation); hooks.readResource .for(scheme) .tapAsync(PLUGIN_NAME, async (loaderContext, callback) => { const { resourcePath } = loaderContext; const module = /** @type {NormalModule} */ (loaderContext._module); const cacheKey = toCacheKey(resourcePath, scheme); const addVersionValueDependency = () => { if (!module || !module.buildInfo) return; const buildInfo = module.buildInfo; if (!buildInfo.valueDependencies) { buildInfo.valueDependencies = new Map(); } const cacheVersion = compilation.valueCacheVersions.get(cacheKey); if (compilation.valueCacheVersions.has(cacheKey)) { buildInfo.valueDependencies.set( cacheKey, /** @type {string} */ (cacheVersion) ); } }; try { const virtualConfig = this.findVirtualModuleConfigById(resourcePath); const content = await virtualConfig.source(loaderContext); addVersionValueDependency(); callback(null, content); } catch (err) { callback(/** @type {Error} */ (err)); } }); } ); } /** * Finds virtual module config by id. * @param {string} id The module id * @returns {VirtualModuleConfig} The virtual module config */ findVirtualModuleConfigById(id) { const config = this.modules[id]; if (!config) { throw new ModuleNotFoundError( null, new Error(`Can't resolve virtual module ${id}`), { name: `virtual module ${id}` } ); } return config; } /** * Get the cache version for a given version value * @param {VersionFn | true | string} version The version value or function * @returns {string | undefined} The cache version */ getCacheVersion(version) { return version === true ? undefined : (typeof version === "function" ? version() : version) || "unset"; } } VirtualUrlPlugin.DEFAULT_SCHEME = DEFAULT_SCHEME; module.exports = VirtualUrlPlugin;