UNPKG

isomorphic-loader

Version:

Webpack isomorphic loader tools to make Node require handle files like images for Server Side Rendering (SSR)

248 lines (214 loc) 6.81 kB
"use strict"; /* eslint-disable max-statements, max-len, complexity, prefer-template, no-magic-numbers */ const Path = require("path"); const Fs = require("fs"); const Module = require("module"); const Config = require("./config"); const { removeLoaders, removeCwd, getParentPath, replaceAppSrcDir } = require("./utils"); const Pkg = require("../package.json"); const logger = require("./logger"); const assert = require("assert"); const posixify = require("./posixify"); const EventEmitter = require("events"); const LOG_PREFIX = "isomorphic-loader"; class ExtendRequire extends EventEmitter { constructor(options) { super(); this.assetsCount = 0; this.clearData(); this.options = { cwd: process.cwd(), ...options }; this._urlMap = this.urlMap; this._originalLoad = undefined; this.activated = false; this.interceptLoad(); } /** * Clear config and assets to contain nothing * * @returns {void} nothing */ clearData() { this.assets = { marked: {} }; this.config = {}; this._markedNames = []; } /** * Determine if isomorphic data is generated by webpack running in dev mode * * @returns {boolean} true if webpack is running in dev mode */ isWebpackDev() { return Boolean(this.config.isWebpackDev); } /** * Stop extend node.js require, restore original require, and clear config and assets data. * * @returns {*} undefined */ reset() { this.deactivate(); this.clearData(); if (this._originalLoad) { Module._load = this._originalLoad; this._originalLoad = undefined; } this.emit("reset"); } /** * set base publicPath for creating the URL of the assets * * @param {*} p - public path (undefined to unset it) * @returns {void} none */ setPublicPath(p) { this._publicPath = (p !== undefined ? p : this.config.output.publicPath) || ""; } /** * Set a callback function that take an asset URL and map it to something else * @param {function} mapper - callback * @returns {void} none */ setUrlMapper(mapper) { this._urlMap = mapper; } /** * built-in URL mapper, which just prepend publicPath to it * * @param {string} url - url to map * @returns {string} updated URL */ urlMap(url) { return this._publicPath + url; } /** * Set the assets mapping to use for modified require behavior * * @param {*} config - isomorphic config generated from the webpack plugin * @returns {*} undefined */ initialize(config) { this.config = { output: {}, ...config }; if (this.options.processConfig) { this.config = this.options.processConfig(this.config); } if (this._publicPath === undefined) { this.setPublicPath(); } this.assets = { marked: {}, ...this.config.assets }; this._markedNames = Object.keys(this.assets.marked); assert( this.config.version === Pkg.version, `${LOG_PREFIX}: this module version ${Pkg.version} is different from config version ${this.config.version}` ); this.activate(); this.interceptLoad(); } /** * stop mapping non-standard JS module for node.js require * * @returns {*} undefined */ deactivate() { this.assetsCount = 0; this.activated = false; this.emit("deactivate"); } /** * Look for and load assets from `dist/isomorphic-assets.json` or the array of files passed in * @param {*} customFilePath - optional file to look for and load * @returns {string} the name of the file that's loaded */ loadAssets(customFilePath) { return [] .concat(customFilePath, this.options.assetsFile, Config.defaultAssetsFile) .filter(x => x) .find(file => { try { const config = JSON.parse(Fs.readFileSync(file, "utf-8")); this.initialize(config); return true; } catch (err) { if (err.code !== "ENOENT") { throw new Error(`${LOG_PREFIX}: fail to load config from ${file} - ${err.message}`); } return false; } }); } /** * start mapping non-standard JS module for node.js require. * * - assets must have been set with `setAssets` * @returns {*} undefined */ activate() { this.assetsCount = this._markedNames.length; this.activated = true; this.emit("activate"); } interceptLoad() { if (this._originalLoad) { return; } const appSrcDir = this.options.appSrcDir; const isAssetNotFound = request => { const x = Path.basename(request); return this._markedNames.find(name => Path.basename(name).indexOf(x) >= 0); }; const checkAsset = (moduleInstance, request, parent) => { const config = this.config; if (config.output && this.assetsCount > 0) { let requestMarkKey; const xRequest = removeLoaders(request); if (xRequest.startsWith(".")) { const parentPath = posixify(removeCwd(getParentPath(parent), true, this.options.cwd)); requestMarkKey = Path.posix.join(parentPath, xRequest); } else { try { requestMarkKey = posixify( removeCwd(moduleInstance._resolveFilename(xRequest, parent), false, this.options.cwd) ); } catch (e) { if (isAssetNotFound(xRequest)) { logger.error(LOG_PREFIX, "check asset " + xRequest + " exception", e); } return undefined; } } let assetUrl = this.assets.marked[replaceAppSrcDir(requestMarkKey, appSrcDir)]; if (assetUrl) { if (/\.(css|less|styl|sass|scss)$/.test(requestMarkKey)) { return assetUrl; } try { assetUrl = this._urlMap(assetUrl, request, appSrcDir); } catch (err) { logger.error(LOG_PREFIX, "urlMap thrown error", err); } if (config.isWebpackDev && config.webpackDev.addUrl && config.webpackDev.url) { const sep = !config.webpackDev.url.endsWith("/") && !assetUrl.startsWith("/") ? "/" : ""; assetUrl = config.webpackDev.url + sep + assetUrl; } return assetUrl; } } if (this.options.ignoreExtensions) { const ext = Path.extname(request); if (ext && this.options.ignoreExtensions.includes(ext)) { return {}; } } return undefined; }; const originalLoad = (this._originalLoad = Module._load); Module._load = function(request, parent, isMain) { const asset = checkAsset(this, request, parent); if (asset !== undefined) { return asset; } return originalLoad.apply(this, [request, parent, isMain]); }; } } module.exports = { ExtendRequire };