diff --git a/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/.gitignore b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/.gitignore new file mode 100644 index 0000000..a1df9a5 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/.gitignore @@ -0,0 +1,4 @@ +/typings/ +/node_modules/ +/*.js +/*.d.ts diff --git a/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/.npmignore b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/.npmignore new file mode 100644 index 0000000..858cdc4 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/.npmignore @@ -0,0 +1,3 @@ +!/*.js +!/*.d.ts +/typings/ diff --git a/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/LICENSE.txt b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/LICENSE.txt new file mode 100644 index 0000000..0bdc196 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/LICENSE.txt @@ -0,0 +1,12 @@ +Copyright (c) .NET Foundation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/README.md b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/README.md new file mode 100644 index 0000000..e459de7 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/README.md @@ -0,0 +1,6 @@ +# Not for general use + +This NPM package is an internal implementation detail of the `Microsoft.AspNet.SpaServices` NuGet package. + +You should not use this package directly in your own applications, because it is not supported, and there are no +guarantees about how its APIs will change in the future. diff --git a/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/package.json b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/package.json new file mode 100644 index 0000000..8f1afee --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/package.json @@ -0,0 +1,21 @@ +{ + "name": "aspnet-webpack", + "version": "1.0.0", + "description": "Helpers for using Webpack in ASP.NET projects. Works in conjunction with the Microsoft.AspNet.SpaServices NuGet package.", + "main": "index.js", + "scripts": { + "prepublish": "tsd update && tsc && echo 'Finished building NPM package \"aspnet-webpack\"'", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Microsoft", + "license": "Apache-2.0", + "dependencies": { + "es6-promise": "^3.1.2", + "express": "^4.13.4", + "memory-fs": "^0.3.0", + "require-from-string": "^1.1.0", + "webpack": "^1.12.14", + "webpack-dev-middleware": "^1.5.1", + "webpack-externals-plugin": "^1.0.0" + } +} diff --git a/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/DeepClone.ts b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/DeepClone.ts new file mode 100644 index 0000000..102b658 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/DeepClone.ts @@ -0,0 +1,3 @@ +export function deepClone(serializableObject: T): T { + return JSON.parse(JSON.stringify(serializableObject)); +} diff --git a/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/LoadViaWebpack.ts b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/LoadViaWebpack.ts new file mode 100644 index 0000000..c11514f --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/LoadViaWebpack.ts @@ -0,0 +1,82 @@ +// 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 ExternalsPlugin from 'webpack-externals-plugin'; +import requireFromString from 'require-from-string'; +import MemoryFS from 'memory-fs'; +import * as webpack from 'webpack'; +import { deepClone } from './DeepClone'; + +// 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 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 = deepClone(require(webpackConfigPath)); + webpackConfig.entry = modulePath; + webpackConfig.target = 'node'; + webpackConfig.output = { path: '/', filename: 'webpack-output.js', libraryTarget: 'commonjs' }; + + // In Node, we want anything 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) + webpackConfig.plugins = webpackConfig.plugins || []; + webpackConfig.plugins.push(new ExternalsPlugin({ type: 'commonjs', include: /node_modules/ })); + + // 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 { + const fileContent = compiler.outputFileSystem.readFileSync('/webpack-output.js', 'utf8'); + const moduleInstance = requireFromString(fileContent); + resolve(moduleInstance); + } + }); + }); +} diff --git a/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts new file mode 100644 index 0000000..66f40c4 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts @@ -0,0 +1,94 @@ +import * as express from 'express'; +import * as webpack from 'webpack'; +import { deepClone } from './DeepClone'; + +export interface CreateDevServerCallback { + (error: any, result: { Port: number, PublicPath: string }): void; +} + +// These are the options passed by WebpackDevMiddleware.cs +interface CreateDevServerOptions { + webpackConfigPath: string; + suppliedOptions: DevServerOptions; +} + +// These are the options configured in C# and then JSON-serialized, hence the C#-style naming +interface DevServerOptions { + HotModuleReplacement: boolean; + ReactHotModuleReplacement: boolean; +} + +export function createWebpackDevServer(callback: CreateDevServerCallback, optionsJson: string) { + const options: CreateDevServerOptions = JSON.parse(optionsJson); + const webpackConfig: webpack.Configuration = deepClone(require(options.webpackConfigPath)); + const publicPath = (webpackConfig.output.publicPath || '').trim(); + if (!publicPath) { + callback('To use the Webpack dev server, you must specify a value for \'publicPath\' on the \'output\' section of your webpack.config.', null); + return; + } + + const enableHotModuleReplacement = options.suppliedOptions.HotModuleReplacement; + const enableReactHotModuleReplacement = options.suppliedOptions.ReactHotModuleReplacement; + if (enableReactHotModuleReplacement && !enableHotModuleReplacement) { + callback('To use ReactHotModuleReplacement, you must also enable the HotModuleReplacement option.', null); + return; + } + + const app = express(); + const defaultPort = 0; // 0 means 'choose randomly'. Could allow an explicit value to be supplied instead. + const listener = app.listen(defaultPort, () => { + // Build the final Webpack config based on supplied options + if (enableHotModuleReplacement) { + // TODO: Stop assuming there's an entry point called 'main' + webpackConfig.entry['main'].unshift('webpack-hot-middleware/client'); + webpackConfig.plugins.push( + new webpack.HotModuleReplacementPlugin() + ); + + // Set up React HMR support if requested. This requires the 'aspnet-webpack-react' package. + if (enableReactHotModuleReplacement) { + let aspNetWebpackReactModule: any; + try { + aspNetWebpackReactModule = require('aspnet-webpack-react'); + } catch(ex) { + callback('To use ReactHotModuleReplacement, you must install the NPM package \'aspnet-webpack-react\'.', null); + return; + } + + aspNetWebpackReactModule.addReactHotModuleReplacementBabelTransform(webpackConfig); + } + } + + // Attach Webpack dev middleware and optional 'hot' middleware + const compiler = webpack(webpackConfig); + app.use(require('webpack-dev-middleware')(compiler, { + noInfo: true, + publicPath: publicPath + })); + + if (enableHotModuleReplacement) { + let webpackHotMiddlewareModule; + try { + webpackHotMiddlewareModule = require('webpack-hot-middleware'); + } catch (ex) { + callback('To use HotModuleReplacement, you must install the NPM package \'webpack-hot-middleware\'.', null); + return; + } + app.use(webpackHotMiddlewareModule(compiler)); + } + + // Tell the ASP.NET app what addresses we're listening on, so that it can proxy requests here + callback(null, { + Port: listener.address().port, + PublicPath: removeTrailingSlash(publicPath) + }); + }); +} + +function removeTrailingSlash(str: string) { + if (str.lastIndexOf('/') === str.length - 1) { + str = str.substring(0, str.length - 1); + } + + return str; +} diff --git a/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/index.ts b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/index.ts new file mode 100644 index 0000000..2d5ff4b --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/index.ts @@ -0,0 +1,2 @@ +export { createWebpackDevServer } from './WebpackDevMiddleware'; +export { loadViaWebpack } from './LoadViaWebpack'; diff --git a/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/typings/memory-fs.d.ts b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/typings/memory-fs.d.ts new file mode 100644 index 0000000..6ae2ee6 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/typings/memory-fs.d.ts @@ -0,0 +1,3 @@ +declare module 'memory-fs' { + export default class MemoryFS {} +} diff --git a/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/typings/require-from-string.d.ts b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/typings/require-from-string.d.ts new file mode 100644 index 0000000..9bfd268 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/typings/require-from-string.d.ts @@ -0,0 +1,3 @@ +declare module 'require-from-string' { + export default function requireFromString(fileContent: string): T; +} diff --git a/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/typings/webpack-externals-plugin.d.ts b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/typings/webpack-externals-plugin.d.ts new file mode 100644 index 0000000..c47e5f7 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/src/typings/webpack-externals-plugin.d.ts @@ -0,0 +1,12 @@ +declare module 'webpack-externals-plugin' { + import * as webpack from 'webpack'; + + export interface ExternalsPluginOptions { + type: string; + include: webpack.LoaderCondition; + } + + export default class ExternalsPlugin { + constructor(options: ExternalsPluginOptions); + } +} diff --git a/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/tsconfig.json b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/tsconfig.json new file mode 100644 index 0000000..de676e9 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "moduleResolution": "node", + "module": "commonjs", + "target": "es5", + "declaration": true, + "outDir": "." + }, + "exclude": [ + "node_modules" + ] +} diff --git a/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/tsd.json b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/tsd.json new file mode 100644 index 0000000..6644545 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/aspnet-webpack/tsd.json @@ -0,0 +1,33 @@ +{ + "version": "v4", + "repo": "borisyankov/DefinitelyTyped", + "ref": "master", + "path": "typings", + "bundle": "typings/tsd.d.ts", + "installed": { + "express/express.d.ts": { + "commit": "0144ad5a74053f2292424847259c4c8e1d0fecaa" + }, + "webpack/webpack.d.ts": { + "commit": "0144ad5a74053f2292424847259c4c8e1d0fecaa" + }, + "node/node.d.ts": { + "commit": "0144ad5a74053f2292424847259c4c8e1d0fecaa" + }, + "serve-static/serve-static.d.ts": { + "commit": "0144ad5a74053f2292424847259c4c8e1d0fecaa" + }, + "mime/mime.d.ts": { + "commit": "0144ad5a74053f2292424847259c4c8e1d0fecaa" + }, + "source-map/source-map.d.ts": { + "commit": "0144ad5a74053f2292424847259c4c8e1d0fecaa" + }, + "uglify-js/uglify-js.d.ts": { + "commit": "0144ad5a74053f2292424847259c4c8e1d0fecaa" + }, + "es6-promise/es6-promise.d.ts": { + "commit": "0144ad5a74053f2292424847259c4c8e1d0fecaa" + } + } +}