UNPKG

8.55 kBPlain TextView Raw
1// When you're using Webpack, it's often convenient to be able to require modules from regular JavaScript
2// and have them transformed by Webpack. This is especially useful when doing ASP.NET server-side prerendering,
3// because it means your boot module can use whatever source language you like (e.g., TypeScript), and means
4// that your loader plugins (e.g., require('./mystyles.less')) work in exactly the same way on the server as
5// on the client.
6import 'es6-promise';
7import * as path from 'path';
8import * as webpack from 'webpack';
9import { requireNewCopy } from './RequireNewCopy';
10
11// Strange import syntax to work around https://github.com/Microsoft/TypeScript/issues/2719
12import { requirefromstring } from './typings/require-from-string';
13import { memoryfs } from './typings/memory-fs';
14const nodeExternals = require('webpack-node-externals');
15const requireFromString = require('require-from-string') as typeof requirefromstring.requireFromString;
16const MemoryFS = require('memory-fs') as typeof memoryfs.MemoryFS;
17
18// Ensure we only go through the compile process once per [config, module] pair
19const loadViaWebpackPromisesCache: { [key: string]: any } = {};
20
21export interface LoadViaWebpackCallback<T> {
22 (error: any, result: T): void;
23}
24
25export function loadViaWebpack<T>(webpackConfigPath: string, modulePath: string, callback: LoadViaWebpackCallback<T>) {
26 const cacheKey = JSON.stringify(webpackConfigPath) + JSON.stringify(modulePath);
27 if (!(cacheKey in loadViaWebpackPromisesCache)) {
28 loadViaWebpackPromisesCache[cacheKey] = loadViaWebpackNoCache(webpackConfigPath, modulePath);
29 }
30 loadViaWebpackPromisesCache[cacheKey].then(result => {
31 callback(null, result);
32 }, error => {
33 callback(error, null);
34 })
35}
36
37function setExtension(filePath: string, newExtension: string) {
38 const oldExtensionIfAny = path.extname(filePath);
39 const basenameWithoutExtension = path.basename(filePath, oldExtensionIfAny);
40 return path.join(path.dirname(filePath), basenameWithoutExtension) + newExtension;
41}
42
43function loadViaWebpackNoCache<T>(webpackConfigPath: string, modulePath: string) {
44 return new Promise<T>((resolve, reject) => {
45 // Load the Webpack config and make alterations needed for loading the output into Node
46 const webpackConfig: webpack.Configuration = requireNewCopy(webpackConfigPath);
47 webpackConfig.entry = modulePath;
48 webpackConfig.target = 'node';
49
50 // Make sure we preserve the 'path' and 'publicPath' config values if specified, as these
51 // can affect the build output (e.g., when using 'file' loader, the publicPath value gets
52 // set as a prefix on output paths).
53 webpackConfig.output = webpackConfig.output || {};
54 webpackConfig.output.path = webpackConfig.output.path || '/';
55 webpackConfig.output.filename = 'webpack-output.js';
56 webpackConfig.output.libraryTarget = 'commonjs';
57 const outputVirtualPath = path.join(webpackConfig.output.path, webpackConfig.output.filename);
58
59 // In Node, we want any JavaScript modules under /node_modules/ to be loaded natively and not bundled into the
60 // output (partly because it's faster, but also because otherwise there'd be different instances of modules
61 // depending on how they were loaded, which could lead to errors).
62 // ---
63 // NOTE: We have to use webpack-node-externals rather than webpack-externals-plugin because
64 // webpack-externals-plugin doesn't correctly resolve relative paths, which means you can't
65 // use css-loader, since tries to require('./../../node_modules/css-loader/lib/css-base.js') (see #132)
66 // ---
67 // So, ensure that webpackConfig.externals is an array, and push WebpackNodeExternals into it:
68 let externalsArray: any[] = (webpackConfig.externals as any[]) || [];
69 if (!(externalsArray instanceof Array)) {
70 externalsArray = [externalsArray];
71 }
72 webpackConfig.externals = externalsArray;
73 externalsArray.push(nodeExternals({
74 // However, we do *not* want to treat non-JS files under /node_modules/ as externals (i.e., things
75 // that should be loaded via regular CommonJS 'require' statements). For example, if you reference
76 // a .css file inside an NPM module (e.g., require('somepackage/somefile.css')), then we do need to
77 // load that via Webpack rather than as a regular CommonJS module.
78 //
79 // So, configure webpack-externals-plugin to 'whitelist' (i.e., not treat as external) any file
80 // that has an extension other than .js. Also, since some libraries such as font-awesome refer to
81 // their own files with cache-busting querystrings (e.g., (url('./something.css?v=4.1.2'))), we
82 // need to treat '?' as an alternative 'end of filename' marker.
83 //
84 // The complex, awkward regex can be eliminated once webpack-externals-plugin merges
85 // https://github.com/liady/webpack-node-externals/pull/12
86 //
87 // This regex looks for at least one dot character that is *not* followed by "js<end-or-questionmark>", but
88 // is followed by some series of non-dot characters followed by <end-or-questionmark>:
89 whitelist: [/\.(?!js(\?|$))([^.]+(\?|$))/]
90 }));
91
92 // The CommonsChunkPlugin is not compatible with a CommonJS environment like Node, nor is it needed in that case
93 webpackConfig.plugins = webpackConfig.plugins.filter(plugin => {
94 return !(plugin instanceof webpack.optimize.CommonsChunkPlugin);
95 });
96
97 // The typical use case for DllReferencePlugin is for referencing vendor modules. In a Node
98 // environment, it doesn't make sense to load them from a DLL bundle, nor would that even
99 // work, because then you'd get different module instances depending on whether a module
100 // was referenced via a normal CommonJS 'require' or via Webpack. So just remove any
101 // DllReferencePlugin from the config.
102 // If someone wanted to load their own DLL modules (not an NPM module) via DllReferencePlugin,
103 // that scenario is not supported today. We would have to add some extra option to the
104 // asp-prerender tag helper to let you specify a list of DLL bundles that should be evaluated
105 // in this context. But even then you'd need special DLL builds for the Node environment so that
106 // external dependencies were fetched via CommonJS requires, so it's unclear how that could work.
107 // The ultimate escape hatch here is just prebuilding your code as part of the application build
108 // and *not* using asp-prerender-webpack-config at all, then you can do anything you want.
109 webpackConfig.plugins = webpackConfig.plugins.filter(plugin => {
110 // DllReferencePlugin is missing from webpack.d.ts for some reason, hence referencing it
111 // as a key-value object property
112 return !(plugin instanceof webpack['DllReferencePlugin']);
113 });
114
115 // Create a compiler instance that stores its output in memory, then load its output
116 const compiler = webpack(webpackConfig);
117 compiler.outputFileSystem = new MemoryFS();
118 compiler.run((err, stats) => {
119 if (err) {
120 reject(err);
121 } else {
122 // We're in a callback, so need an explicit try/catch to propagate any errors up the promise chain
123 try {
124 if (stats.hasErrors()) {
125 throw new Error('Webpack compilation reported errors. Compiler output follows: '
126 + stats.toString({ chunks: false }));
127 }
128
129 // The dynamically-built module will only appear in node-inspector if it has some nonempty
130 // file path. The following value is arbitrary (since there's no real compiled file on disk)
131 // but is sufficient to enable debugging.
132 const fakeModulePath = setExtension(modulePath, '.js');
133
134 const fileContent = compiler.outputFileSystem.readFileSync(outputVirtualPath, 'utf8');
135 const moduleInstance = requireFromString<T>(fileContent, fakeModulePath);
136 resolve(moduleInstance);
137 } catch(ex) {
138 reject(ex);
139 }
140 }
141 });
142 });
143}
144
\No newline at end of file