diff --git a/src/Microsoft.AspNet.SpaServices/Content/Node/prerenderer.js b/src/Microsoft.AspNet.SpaServices/Content/Node/prerenderer.js index 15d338f..0929822 100644 --- a/src/Microsoft.AspNet.SpaServices/Content/Node/prerenderer.js +++ b/src/Microsoft.AspNet.SpaServices/Content/Node/prerenderer.js @@ -1,188 +1,12 @@ -// ----------------------------------------------------------------------------------------- -// Loading via Webpack -// This is optional. You don't have to use Webpack. But if you are doing, it's extremely convenient -// to be able to load your boot module via Webpack compilation, so you can use whatever source language -// you like (e.g., TypeScript), and so that loader plugins (e.g., require('./mystyles.less')) work in -// exactly the same way on the server as you do on the client. -// If you don't use Webpack, then it's up to you to define a plain-JS boot module that in turn loads -// whatever other files you need (e.g., using some other compiler/bundler API, or maybe just having -// already precompiled to plain JS files on disk). -function loadViaWebpackNoCache(webpackConfigPath, modulePath) { - var ExternalsPlugin = require('webpack-externals-plugin'); - var requireFromString = require('require-from-string'); - var MemoryFS = require('memory-fs'); - var webpack = require('webpack'); - - return new Promise(function(resolve, reject) { - // Load the Webpack config and make alterations needed for loading the output into Node - var webpackConfig = 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(function(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(function(plugin) { - return !(plugin instanceof webpack.DllReferencePlugin); - }); - - // Create a compiler instance that stores its output in memory, then load its output - var compiler = webpack(webpackConfig); - compiler.outputFileSystem = new MemoryFS(); - compiler.run(function(err, stats) { - if (err) { - reject(err); - } else { - var fileContent = compiler.outputFileSystem.readFileSync('/webpack-output.js', 'utf8'); - var moduleInstance = requireFromString(fileContent); - resolve(moduleInstance); - } - }); - }); -} - -// Ensure we only go through the compile process once per [config, module] pair -var loadViaWebpackPromisesCache = {}; -function loadViaWebpack(webpackConfigPath, modulePath, callback) { - var cacheKey = JSON.stringify(webpackConfigPath) + JSON.stringify(modulePath); - if (!(cacheKey in loadViaWebpackPromisesCache)) { - loadViaWebpackPromisesCache[cacheKey] = loadViaWebpackNoCache(webpackConfigPath, modulePath); +// Pass through the invocation to the 'aspnet-prerendering' package, verifying that it can be loaded +module.exports.renderToString = function (callback) { + var aspNetPrerendering; + try { + aspNetPrerendering = require('aspnet-prerendering'); + } catch (ex) { + callback('To use prerendering, you must install the \'aspnet-prerendering\' NPM package.'); + return; } - loadViaWebpackPromisesCache[cacheKey].then(function(result) { - callback(null, result); - }, function(error) { - callback(error); - }) -} - -// ----------------------------------------------------------------------------------------- -// Rendering - -var url = require('url'); -var path = require('path'); -var domain = require('domain'); -var domainTask = require('domain-task'); -var baseUrl = require('domain-task/fetch').baseUrl; - -function findBootModule(applicationBasePath, bootModule, callback) { - var bootModuleNameFullPath = path.resolve(applicationBasePath, bootModule.moduleName); - if (bootModule.webpackConfig) { - var webpackConfigFullPath = path.resolve(applicationBasePath, bootModule.webpackConfig); - loadViaWebpack(webpackConfigFullPath, bootModuleNameFullPath, callback); - } else { - callback(null, require(bootModuleNameFullPath)); - } -} - -function findBootFunc(applicationBasePath, bootModule, callback) { - // First try to load the module (possibly via Webpack) - findBootModule(applicationBasePath, bootModule, function(findBootModuleError, foundBootModule) { - if (findBootModuleError) { - callback(findBootModuleError); - return; - } - - // Now try to pick out the function they want us to invoke - var bootFunc; - if (bootModule.exportName) { - // Explicitly-named export - bootFunc = foundBootModule[bootModule.exportName]; - } else if (typeof foundBootModule !== 'function') { - // TypeScript-style default export - bootFunc = foundBootModule.default; - } else { - // Native default export - bootFunc = foundBootModule; - } - - // Validate the result - if (typeof bootFunc !== 'function') { - if (bootModule.exportName) { - callback(new Error('The module at ' + bootModule.moduleName + ' has no function export named ' + bootModule.exportName + '.')); - } else { - callback(new Error('The module at ' + bootModule.moduleName + ' does not export a default function, and you have not specified which export to invoke.')); - } - } else { - callback(null, bootFunc); - } - }); -} - -function renderToString(callback, applicationBasePath, bootModule, absoluteRequestUrl, requestPathAndQuery) { - findBootFunc(applicationBasePath, bootModule, function (findBootFuncError, bootFunc) { - if (findBootFuncError) { - callback(findBootFuncError); - return; - } - - // Prepare a promise that will represent the completion of all domain tasks in this execution context. - // The boot code will wait for this before performing its final render. - var domainTaskCompletionPromiseResolve; - var domainTaskCompletionPromise = new Promise(function (resolve, reject) { - domainTaskCompletionPromiseResolve = resolve; - }); - var params = { - location: url.parse(requestPathAndQuery), - url: requestPathAndQuery, - absoluteUrl: absoluteRequestUrl, - domainTasks: domainTaskCompletionPromise - }; - - // Open a new domain that can track all the async tasks involved in the app's execution - domainTask.run(function() { - // Workaround for Node bug where native Promise continuations lose their domain context - // (https://github.com/nodejs/node-v0.x-archive/issues/8648) - bindPromiseContinuationsToDomain(domainTaskCompletionPromise, domain.active); - - // Make the base URL available to the 'domain-tasks/fetch' helper within this execution context - baseUrl(absoluteRequestUrl); - - // Actually perform the rendering - bootFunc(params).then(function(successResult) { - callback(null, { html: successResult.html, globals: successResult.globals }); - }, function(error) { - callback(error, null); - }); - }, function allDomainTasksCompleted(error) { - // There are no more ongoing domain tasks (typically data access operations), so we can resolve - // the domain tasks promise which notifies the boot code that it can do its final render. - if (error) { - callback(error, null); - } else { - domainTaskCompletionPromiseResolve(); - } - }); - }); -} - -function bindPromiseContinuationsToDomain(promise, domainInstance) { - var originalThen = promise.then; - promise.then = function then(resolve, reject) { - if (typeof resolve === 'function') { resolve = domainInstance.bind(resolve); } - if (typeof reject === 'function') { reject = domainInstance.bind(reject); } - return originalThen.call(this, resolve, reject); - }; -} - -module.exports.renderToString = renderToString; + + return aspNetPrerendering.renderToString.apply(this, arguments); +}; diff --git a/src/Microsoft.AspNet.SpaServices/Content/Node/webpack-dev-middleware.js b/src/Microsoft.AspNet.SpaServices/Content/Node/webpack-dev-middleware.js index 77932b9..8a0dd58 100644 --- a/src/Microsoft.AspNet.SpaServices/Content/Node/webpack-dev-middleware.js +++ b/src/Microsoft.AspNet.SpaServices/Content/Node/webpack-dev-middleware.js @@ -1,91 +1,12 @@ -var express = require('express'); -var webpack = require('webpack'); -var defaultPort = 0; // 0 means 'choose randomly'. Could allow an explicit value to be supplied instead. - -module.exports = { - createWebpackDevServer: function(callback, optionsJson) { - var options = JSON.parse(optionsJson); - var webpackConfig = require(options.webpackConfigPath); - var publicPath = (webpackConfig.output.publicPath || '').trim(); - if (!publicPath) { - throw new Error('To use the Webpack dev server, you must specify a value for \'publicPath\' on the \'output\' section of your webpack.config.'); - } - - var enableHotModuleReplacement = options.suppliedOptions.HotModuleReplacement; - var enableReactHotModuleReplacement = options.suppliedOptions.ReactHotModuleReplacement; - - var app = new express(); - var listener = app.listen(defaultPort, function() { - // Build the final Webpack config based on supplied options - if (enableHotModuleReplacement) { - webpackConfig.entry.main.unshift('webpack-hot-middleware/client'); - webpackConfig.plugins.push( - new webpack.HotModuleReplacementPlugin() - ); - - if (enableReactHotModuleReplacement) { - addReactHotModuleReplacementBabelTransform(webpackConfig); - } - } - - // Attach Webpack dev middleware and optional 'hot' middleware - var compiler = webpack(webpackConfig); - app.use(require('webpack-dev-middleware')(compiler, { - noInfo: true, - publicPath: publicPath - })); - - if (enableHotModuleReplacement) { - app.use(require('webpack-hot-middleware')(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) - }); - }); +// Pass through the invocation to the 'aspnet-webpack' package, verifying that it can be loaded +module.exports.createWebpackDevServer = function (callback) { + var aspNetWebpack; + try { + aspNetWebpack = require('aspnet-webpack'); + } catch (ex) { + callback('To use webpack dev middleware, you must install the \'aspnet-webpack\' NPM package.'); + return; } + + return aspNetWebpack.createWebpackDevServer.apply(this, arguments); }; - -function addReactHotModuleReplacementBabelTransform(webpackConfig) { - webpackConfig.module.loaders.forEach(function(loaderConfig) { - if (loaderConfig.loader && loaderConfig.loader.match(/\bbabel-loader\b/)) { - // Ensure the babel-loader options includes a 'query' - var query = loaderConfig.query = loaderConfig.query || {}; - - // Ensure Babel plugins includes 'react-transform' - var plugins = query.plugins = query.plugins || []; - if (!plugins.some(function(pluginConfig) { - return pluginConfig && pluginConfig[0] === 'react-transform'; - })) { - plugins.push(['react-transform', {}]); - } - - // Ensure 'react-transform' plugin is configured to use 'react-transform-hmr' - plugins.forEach(function(pluginConfig) { - if (pluginConfig && pluginConfig[0] === 'react-transform') { - var pluginOpts = pluginConfig[1] = pluginConfig[1] || {}; - var transforms = pluginOpts.transforms = pluginOpts.transforms || []; - if (!transforms.some(function(transform) { - return transform.transform === 'react-transform-hmr'; - })) { - transforms.push({ - transform: "react-transform-hmr", - imports: ["react"], - locals: ["module"] // Important for Webpack HMR - }); - } - } - }); - } - }); -} - -function removeTrailingSlash(str) { - if (str.lastIndexOf('/') === str.length - 1) { - str = str.substring(0, str.length - 1); - } - - return str; -}