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
JavaScript
"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 };