// When you're using Webpack, it's often convenient to be able to require modules from regular JavaScript // and have them transformed by Webpack. This is especially useful when doing ASP.NET server-side prerendering, // because it means your boot module can use whatever source language you like (e.g., TypeScript), and means // that your loader plugins (e.g., require('./mystyles.less')) work in exactly the same way on the server as // on the client. import 'es6-promise'; import * as path from 'path'; import * as webpack from 'webpack'; import { requireNewCopy } from './RequireNewCopy'; // Strange import syntax to work around https://github.com/Microsoft/TypeScript/issues/2719 import { requirefromstring } from './typings/require-from-string'; import { memoryfs } from './typings/memory-fs'; const nodeExternals = require('webpack-node-externals'); const requireFromString = require('require-from-string') as typeof requirefromstring.requireFromString; const MemoryFS = require('memory-fs') as typeof memoryfs.MemoryFS; // Ensure we only go through the compile process once per [config, module] pair const loadViaWebpackPromisesCache: { [key: string]: any } = {}; export interface LoadViaWebpackCallback { (error: any, result: T): void; } export function loadViaWebpack(webpackConfigPath: string, modulePath: string, callback: LoadViaWebpackCallback) { const cacheKey = JSON.stringify(webpackConfigPath) + JSON.stringify(modulePath); if (!(cacheKey in loadViaWebpackPromisesCache)) { loadViaWebpackPromisesCache[cacheKey] = loadViaWebpackNoCache(webpackConfigPath, modulePath); } loadViaWebpackPromisesCache[cacheKey].then(result => { callback(null, result); }, error => { callback(error, null); }) } function setExtension(filePath: string, newExtension: string) { const oldExtensionIfAny = path.extname(filePath); const basenameWithoutExtension = path.basename(filePath, oldExtensionIfAny); return path.join(path.dirname(filePath), basenameWithoutExtension) + newExtension; } function loadViaWebpackNoCache(webpackConfigPath: string, modulePath: string) { return new Promise((resolve, reject) => { // Load the Webpack config and make alterations needed for loading the output into Node const webpackConfig: webpack.Configuration = requireNewCopy(webpackConfigPath); webpackConfig.entry = modulePath; webpackConfig.target = 'node'; // Make sure we preserve the 'path' and 'publicPath' config values if specified, as these // can affect the build output (e.g., when using 'file' loader, the publicPath value gets // set as a prefix on output paths). webpackConfig.output = webpackConfig.output || {}; webpackConfig.output.path = webpackConfig.output.path || '/'; webpackConfig.output.filename = 'webpack-output.js'; webpackConfig.output.libraryTarget = 'commonjs'; const outputVirtualPath = path.join(webpackConfig.output.path, webpackConfig.output.filename); // In Node, we want any JavaScript modules under /node_modules/ to be loaded natively and not bundled into the // output (partly because it's faster, but also because otherwise there'd be different instances of modules // depending on how they were loaded, which could lead to errors). // --- // NOTE: We have to use webpack-node-externals rather than webpack-externals-plugin because // webpack-externals-plugin doesn't correctly resolve relative paths, which means you can't // use css-loader, since tries to require('./../../node_modules/css-loader/lib/css-base.js') (see #132) // --- // So, ensure that webpackConfig.externals is an array, and push WebpackNodeExternals into it: let externalsArray: any[] = (webpackConfig.externals as any[]) || []; if (!(externalsArray instanceof Array)) { externalsArray = [externalsArray]; } webpackConfig.externals = externalsArray; externalsArray.push(nodeExternals({ // However, we do *not* want to treat non-JS files under /node_modules/ as externals (i.e., things // that should be loaded via regular CommonJS 'require' statements). For example, if you reference // a .css file inside an NPM module (e.g., require('somepackage/somefile.css')), then we do need to // load that via Webpack rather than as a regular CommonJS module. // // So, configure webpack-externals-plugin to 'whitelist' (i.e., not treat as external) any file // that has an extension other than .js. Also, since some libraries such as font-awesome refer to // their own files with cache-busting querystrings (e.g., (url('./something.css?v=4.1.2'))), we // need to treat '?' as an alternative 'end of filename' marker. // // The complex, awkward regex can be eliminated once webpack-externals-plugin merges // https://github.com/liady/webpack-node-externals/pull/12 // // This regex looks for at least one dot character that is *not* followed by "js", but // is followed by some series of non-dot characters followed by : whitelist: [/\.(?!js(\?|$))([^.]+(\?|$))/] })); // The CommonsChunkPlugin is not compatible with a CommonJS environment like Node, nor is it needed in that case webpackConfig.plugins = webpackConfig.plugins.filter(plugin => { return !(plugin instanceof webpack.optimize.CommonsChunkPlugin); }); // The typical use case for DllReferencePlugin is for referencing vendor modules. In a Node // environment, it doesn't make sense to load them from a DLL bundle, nor would that even // work, because then you'd get different module instances depending on whether a module // was referenced via a normal CommonJS 'require' or via Webpack. So just remove any // DllReferencePlugin from the config. // If someone wanted to load their own DLL modules (not an NPM module) via DllReferencePlugin, // that scenario is not supported today. We would have to add some extra option to the // asp-prerender tag helper to let you specify a list of DLL bundles that should be evaluated // in this context. But even then you'd need special DLL builds for the Node environment so that // external dependencies were fetched via CommonJS requires, so it's unclear how that could work. // The ultimate escape hatch here is just prebuilding your code as part of the application build // and *not* using asp-prerender-webpack-config at all, then you can do anything you want. webpackConfig.plugins = webpackConfig.plugins.filter(plugin => { // DllReferencePlugin is missing from webpack.d.ts for some reason, hence referencing it // as a key-value object property return !(plugin instanceof webpack['DllReferencePlugin']); }); // Create a compiler instance that stores its output in memory, then load its output const compiler = webpack(webpackConfig); compiler.outputFileSystem = new MemoryFS(); compiler.run((err, stats) => { if (err) { reject(err); } else { // We're in a callback, so need an explicit try/catch to propagate any errors up the promise chain try { if (stats.hasErrors()) { throw new Error('Webpack compilation reported errors. Compiler output follows: ' + stats.toString({ chunks: false })); } // The dynamically-built module will only appear in node-inspector if it has some nonempty // file path. The following value is arbitrary (since there's no real compiled file on disk) // but is sufficient to enable debugging. const fakeModulePath = setExtension(modulePath, '.js'); const fileContent = compiler.outputFileSystem.readFileSync(outputVirtualPath, 'utf8'); const moduleInstance = requireFromString(fileContent, fakeModulePath); resolve(moduleInstance); } catch(ex) { reject(ex); } } }); }); }