Add aspnet-webpack NPM package

This commit is contained in:
SteveSandersonMS
2016-03-10 23:20:24 +00:00
parent b519e58fc5
commit 2747aad66f
14 changed files with 290 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
/typings/
/node_modules/
/*.js
/*.d.ts

View File

@@ -0,0 +1,3 @@
!/*.js
!/*.d.ts
/typings/

View File

@@ -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.

View File

@@ -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.

View File

@@ -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"
}
}

View File

@@ -0,0 +1,3 @@
export function deepClone<T>(serializableObject: T): T {
return JSON.parse(JSON.stringify(serializableObject));
}

View File

@@ -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<T> {
(error: any, result: T): void;
}
export function loadViaWebpack<T>(webpackConfigPath: string, modulePath: string, callback: LoadViaWebpackCallback<T>) {
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<T>(webpackConfigPath: string, modulePath: string) {
return new Promise<T>((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<T>(fileContent);
resolve(moduleInstance);
}
});
});
}

View File

@@ -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;
}

View File

@@ -0,0 +1,2 @@
export { createWebpackDevServer } from './WebpackDevMiddleware';
export { loadViaWebpack } from './LoadViaWebpack';

View File

@@ -0,0 +1,3 @@
declare module 'memory-fs' {
export default class MemoryFS {}
}

View File

@@ -0,0 +1,3 @@
declare module 'require-from-string' {
export default function requireFromString<T>(fileContent: string): T;
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"moduleResolution": "node",
"module": "commonjs",
"target": "es5",
"declaration": true,
"outDir": "."
},
"exclude": [
"node_modules"
]
}

View File

@@ -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"
}
}
}