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.
|
6 | import 'es6-promise';
|
7 | import * as path from 'path';
|
8 | import * as webpack from 'webpack';
|
9 | import { requireNewCopy } from './RequireNewCopy';
|
10 |
|
11 | // Strange import syntax to work around https://github.com/Microsoft/TypeScript/issues/2719
|
12 | import { requirefromstring } from './typings/require-from-string';
|
13 | import { memoryfs } from './typings/memory-fs';
|
14 | const nodeExternals = require('webpack-node-externals');
|
15 | const requireFromString = require('require-from-string') as typeof requirefromstring.requireFromString;
|
16 | const MemoryFS = require('memory-fs') as typeof memoryfs.MemoryFS;
|
17 |
|
18 | // Ensure we only go through the compile process once per [config, module] pair
|
19 | const loadViaWebpackPromisesCache: { [key: string]: any } = {};
|
20 |
|
21 | export interface LoadViaWebpackCallback<T> {
|
22 | (error: any, result: T): void;
|
23 | }
|
24 |
|
25 | export 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 |
|
37 | function 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 |
|
43 | function 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 |