@prerenderer/webpack-plugin
Version:
Flexible, framework-agnostic static site generation for apps built with webpack.
177 lines (176 loc) • 10.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const schema_utils_1 = require("schema-utils");
const Options_1 = require("./Options");
const webpack_1 = require("webpack");
const ts_deepmerge_1 = __importDefault(require("ts-deepmerge"));
const path_1 = __importDefault(require("path"));
const html_webpack_plugin_1 = __importDefault(require("html-webpack-plugin"));
class WebpackPrerenderSPAPlugin {
constructor(options = {}) {
(0, schema_utils_1.validate)(Options_1.schema, options, {
name: 'Prerender SPA Plugin',
baseDataPath: 'options',
});
this.options = ts_deepmerge_1.default.withOptions({ mergeArrays: false }, Options_1.defaultOptions, options);
}
prerender(compiler_1, compilation_1) {
return __awaiter(this, arguments, void 0, function* (compiler, compilation, alreadyRenderedRoutes = []) {
const indexPath = this.options.indexPath;
const entryPath = this.options.entryPath || indexPath;
if (!(entryPath in compilation.assets)) {
compilation.warnings.push(new webpack_1.WebpackError('[prerender-spa-plugin] Could not find the entryPath! Doing nothing.'));
return false;
}
const { default: Prerenderer } = yield Promise.resolve().then(() => __importStar(require('@prerenderer/prerenderer')));
const PrerendererInstance = new Prerenderer(Object.assign({ staticDir: compiler.options.output.path || '/' }, this.options));
// Modify the express server to serve the files from the webpack compiler because they are not written on disk yet
PrerendererInstance.hookServer((server) => {
const express = server.getExpressServer();
// Express doesn't have complete typings yet
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const routes = express._router.stack;
routes.forEach((route, i) => {
if (route.route && route.route.path === '*') {
routes.splice(i, 1);
}
}, 'post-fallback');
express.get('*', (req, res) => {
let url = req.path.slice(0, req.path.endsWith('/') ? -1 : undefined);
const publicPath = typeof compilation.outputOptions.publicPath === 'string' && compilation.outputOptions.publicPath !== 'auto'
? compilation.outputOptions.publicPath || '/'
: '/';
if (url.startsWith(publicPath)) {
url = url.slice(publicPath.length);
}
url = url in compilation.assets || url.includes('.') ? url : url + '/' + indexPath;
if (url.startsWith('/')) {
url = url.slice(1);
}
if (this.options.urlModifier) {
url = this.options.urlModifier(url);
}
if (url in compilation.assets) {
if (url.endsWith('.json')) {
const source = compilation.assets[url].source();
res.json(JSON.parse(typeof source === 'string' ? source : source.toString()));
}
else {
try {
res.type(path_1.default.extname(url));
res.send(compilation.assets[url].source());
}
catch (e) {
res.status(500);
compilation.errors.push(new webpack_1.WebpackError('[prerender-spa-plugin] Failed to deliver ' + url + ', is the type of the file correct?'));
}
}
}
else if (entryPath in compilation.assets) {
res.send(compilation.assets[entryPath].source());
}
else {
compilation.errors.push(new webpack_1.WebpackError('[prerender-spa-plugin] ' + url + ' not found during prerender'));
res.status(404);
}
});
});
try {
const routes = [...new Set(this.options.routes || [])];
if (routes.length) {
yield PrerendererInstance.initialize();
const renderedRoutes = yield PrerendererInstance.renderRoutes(routes);
alreadyRenderedRoutes.concat(routes);
// Calculate outputPath if it hasn't been set already.
renderedRoutes.forEach(processedRoute => {
// Create dirs and write prerendered files.
if (!processedRoute.outputPath) {
processedRoute.outputPath = path_1.default.join(processedRoute.route, indexPath);
if (processedRoute.outputPath.startsWith('/') || processedRoute.outputPath.startsWith('\\')) {
processedRoute.outputPath = processedRoute.outputPath.slice(1);
}
}
if (processedRoute.outputPath === entryPath && this.options.fallback) {
const fallback = typeof this.options.fallback === 'string' ? this.options.fallback : '_fallback';
const ext = path_1.default.extname(processedRoute.outputPath);
const fileName = processedRoute.outputPath.slice(0, -ext.length) + fallback + ext;
if (!(fileName in compilation.assets)) {
compilation.emitAsset(fileName, compilation.assets[processedRoute.outputPath]);
}
}
// false positive as calling call(compilation) right after
// eslint-disable-next-line @typescript-eslint/unbound-method
const fn = processedRoute.outputPath in compilation.assets ? compilation.updateAsset : compilation.emitAsset;
fn.call(compilation, processedRoute.outputPath, new compiler.webpack.sources.RawSource(processedRoute.html.trim(), false), {
prerendered: true,
});
});
}
}
catch (err) {
const msg = '[prerender-spa-plugin] Unable to prerender all routes!';
compilation.errors.push(new webpack_1.WebpackError(msg));
if (err instanceof Error) {
compilation.errors.push(new webpack_1.WebpackError(err.message));
}
else if (typeof err === 'object' && err) {
compilation.errors.push(new webpack_1.WebpackError(JSON.stringify(err)));
}
}
yield PrerendererInstance.destroy();
});
}
apply(compiler) {
const pluginName = this.constructor.name;
compiler.hooks.compilation.tap(pluginName, (compilation) => {
// Check if HtmlWebpackPlugin was used in the compilation
const isHtmlWebpackPluginUsed = compiler.options.plugins.some((plugin) => plugin instanceof html_webpack_plugin_1.default);
if (!isHtmlWebpackPluginUsed) {
compilation.warnings.push(new webpack_1.WebpackError('[prerender-spa-plugin] The HtmlWebpackPlugin is missing from webpack, the prerender plugin will do nothing.'));
}
else {
const hooks = html_webpack_plugin_1.default.getHooks(compilation);
hooks.afterEmit.tapPromise(pluginName, (out) => __awaiter(this, void 0, void 0, function* () {
yield this.prerender(compiler, compilation);
return out;
}));
}
});
}
}
exports.default = WebpackPrerenderSPAPlugin;