UNPKG

isomorphic-loader

Version:

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

194 lines (162 loc) 6.36 kB
"use strict"; /* eslint-disable no-unused-expressions, prefer-template, no-magic-numbers, max-statements */ // https://webpack.github.io/docs/how-to-write-a-plugin.html const Path = require("path"); const Config = require("./config"); const { getWebpackRequest, removeLoaders, removeCwd, replaceAppSrcDir, getCssModuleGlobalMapping, getMyNodeModulesPath, requireFromString } = require("./utils"); const Pkg = require("../package.json"); const posixify = require("./posixify"); const EventEmitter = require("events"); const pluginName = "IsomorphicLoaderPlugin"; class IsomorphicLoaderPlugin extends EventEmitter { /** * * @param {*} options options * */ constructor(options) { super(); this.config = { valid: false }; this.options = { cwd: process.cwd(), assetsFile: "isomorphic-assets.json", ...options }; const directJs = Path.posix.join(getMyNodeModulesPath(), "index.js!"); this._loaderSigs = [directJs, `isomorphic-loader/lib/index.js!`, `isomorphic-loader!`]; } apply(compiler) { const createConfig = (compilerOpts, assets = { marked: {}, chunks: {} }) => { // // There is no easy way to detect that webpack-dev-server is running, but it // does change the output path to "/". Since that normally is "build" or "dist" // and it's unlikely anyone would use "/", assuming it's webpack-dev-server if // output path is "/". // const isWebpackDev = compilerOpts.output.path === "/" || require.main.filename.indexOf("webpack-dev-server") >= 0; const config = { valid: false, version: Pkg.version, timestamp: Date.now(), context: removeCwd(compilerOpts.context, false, this.options.cwd), output: { path: removeCwd(compilerOpts.output.path, false, this.options.cwd), filename: removeCwd(compilerOpts.output.filename, false, this.options.cwd), publicPath: compilerOpts.output.publicPath || "" }, assets }; if (isWebpackDev) { config.webpackDev = Object.assign( { skipSetEnv: false, url: "http://localhost:8080", // should extend require prepend webpack dev URL when returning URL for asset file addUrl: true }, this.options.webpackDev ); config.isWebpackDev = true; } return config; }; const updateConfigForDevMode = () => { if (!this.config.isWebpackDev) { return; } this.emit("update", { name: Config.configName, config: this.config }); }; const handleEmit = (compilation, webpackAssets) => { const stats = compilation.getStats().toJson(); const marked = stats.modules.reduce((acc, m) => { const ident = posixify(m.identifier); const foundSig = this._loaderSigs.find(sig => ident.includes(sig)); if (foundSig && m.assets && m.assets.length > 0) { const userRequest = removeLoaders(getWebpackRequest(m)); let requestMarkKey; // if userRequest starts with ., it's a relative path, so try to reconstruct its // full path by joining it with the issuer path // npm module name can't start with ., so it's not needed to check ./ or ../ if (userRequest.startsWith(".")) { const issuerPath = posixify( Path.dirname(removeCwd(removeLoaders(m.issuer), true, this.options.cwd)) ); // require paths always use / so use posix path requestMarkKey = Path.posix.join(issuerPath, userRequest); } else { // no need to worry about target's location relative to issuer's location, just // use full identifier path directly requestMarkKey = posixify( removeCwd(removeLoaders(m.identifier), false, this.options.cwd) ); } acc[replaceAppSrcDir(requestMarkKey, this.options.appSrcDir)] = m.assets[m.assets.length - 1]; } return acc; }, {}); const cssModuleMap = getCssModuleGlobalMapping(); Object.keys(cssModuleMap).forEach(userRequest => { const requestKey = posixify(removeCwd(userRequest, false, this.options.cwd)); const content = requireFromString(cssModuleMap[userRequest]); marked[replaceAppSrcDir(requestKey, this.options.appSrcDir)] = content; }); const assets = { marked, chunks: stats.assetsByChunkName }; const config = createConfig(compiler.options, assets); config.valid = true; const configStr = JSON.stringify(config, null, 2) + "\n"; webpackAssets[this.options.assetsFile] = { source: () => configStr, size: () => configStr.length }; this.config = config; }; const handleMake = (compilation, callback) => { this.config = createConfig(compiler.options); // If running in webpack dev server mode, then create a config file ASAP so not to leave // extend require waiting. updateConfigForDevMode(); /* istanbul ignore next */ if (compilation.hooks && compilation.hooks.processAssets) { // handle emit for webpack 5 /* istanbul ignore next */ compilation.hooks.processAssets.tap(pluginName, assets => handleEmit(compilation, assets)); } /* istanbul ignore next */ callback && callback(); }; const handleInvalid = () => { this.config.valid = false; updateConfigForDevMode(); }; /* istanbul ignore else */ if (compiler.hooks) { // use .hooks compiler.hooks.make.tap(pluginName, handleMake); compiler.hooks.emit.tap(pluginName, compilation => { // handle emit for webpack 4 /* istanbul ignore else */ if (!compilation.hooks.processAssets) { handleEmit(compilation, compilation.assets); } }); compiler.hooks.invalid.tap(pluginName, handleInvalid); compiler.hooks.done.tap(pluginName, updateConfigForDevMode); } else { compiler.plugin("make", handleMake); compiler.plugin("emit", handleEmit); compiler.plugin("invalid", handleInvalid); compiler.plugin("done", updateConfigForDevMode); } } } module.exports = { IsomorphicLoaderPlugin };