@luban-cli/cli-plugin-service
Version:
A development runtime environment dependency
432 lines • 21.1 kB
JavaScript
"use strict";
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 path_1 = __importDefault(require("path"));
const webpack = require("webpack");
const portfinder_1 = __importDefault(require("portfinder"));
const WebpackDevServer = require("webpack-dev-server");
const chalk_1 = __importDefault(require("chalk"));
const cli_shared_utils_1 = require("@luban-cli/cli-shared-utils");
const express_1 = __importDefault(require("express"));
const react_loadable_1 = __importDefault(require("react-loadable"));
const memory_fs_1 = __importDefault(require("memory-fs"));
const http_proxy_middleware_1 = require("http-proxy-middleware");
const body_parser_1 = __importDefault(require("body-parser"));
const https_1 = __importDefault(require("https"));
const http_1 = __importDefault(require("http"));
const prepareURLs_1 = require("../utils/prepareURLs");
const setupMockServer_1 = require("../utils/setupMockServer");
const serverRender_1 = require("../utils/serverRender");
const generateInjectedHtmlTag_1 = require("../utils/generateInjectedHtmlTag");
const cleanDest_1 = require("../utils/cleanDest");
const getCertificate_1 = require("../utils/getCertificate");
const formatCompileError_1 = require("../utils/formatCompileError");
const generateDocument_1 = require("../utils/generateDocument");
const DEFAULT_SERVER_BUNDLE = { default: () => null, createStore: () => null };
const DEFAULT_HOST = "0.0.0.0";
const DEFAULT_ENABLED_HTTPS = false;
const defaultClientServerConfig = {
host: DEFAULT_HOST,
port: 8080,
https: DEFAULT_ENABLED_HTTPS,
};
const defaultSSRServerConfig = {
host: DEFAULT_HOST,
port: 3000,
https: DEFAULT_ENABLED_HTTPS,
};
class Serve {
constructor(api, projectConfig, args) {
this.pluginApi = api;
this.projectConfig = projectConfig;
this.commandArgs = args;
//TODO separate logic that prepare client side and server side
this.clientSideWebpackConfig = api.resolveWebpackConfig("client");
this.serverSideWebpackConfig = api.resolveWebpackConfig("server");
this.useHttps = args.https || defaultClientServerConfig.https;
this.protocol = this.useHttps ? "https" : "http";
this.serverSideHttpsOptions = { spdy: { protocols: ["h2", "http/1.1"] } };
this.serverSideApp = null;
this.serverSideServer = null;
}
preparePorts() {
//TODO separate logic that prepare client side and server side
const ports = [defaultClientServerConfig.port, defaultSSRServerConfig.port];
if (typeof this.commandArgs.port === "string") {
const commandPorts = this.commandArgs.port.split(" ");
if (typeof Number(commandPorts[0]) === "number") {
ports[0] = Number(commandPorts[0]);
}
if (typeof Number(commandPorts[1]) === "number") {
ports[1] = Number(commandPorts[1]) || defaultSSRServerConfig.port;
}
}
else if (typeof this.commandArgs.port === "number") {
ports[0] = Number(this.commandArgs.port || defaultClientServerConfig.port);
}
return ports;
}
init() {
return __awaiter(this, void 0, void 0, function* () {
//TODO separate logic that prepare client side and server side
const ports = this.preparePorts();
this.clientSideHost = this.commandArgs.host || defaultClientServerConfig.host;
const clientSidePort = ports[0];
this.serverSideHost = this.clientSideHost;
const serverSidePort = ports[1];
this.clientSidePort = yield portfinder_1.default.getPortPromise({ port: Number(clientSidePort) });
this.serverSidePort = yield portfinder_1.default.getPortPromise({ port: Number(serverSidePort) });
const rawPublicUrl = this.commandArgs.public;
this.publicUrl = rawPublicUrl
? /^[a-zA-Z]+:\/\//.test(rawPublicUrl)
? rawPublicUrl
: `${this.protocol}://${rawPublicUrl}`
: null;
this.CSRUrlList = prepareURLs_1.prepareUrls(this.protocol, this.clientSideHost, this.clientSidePort);
this.SSRUrlList = prepareURLs_1.prepareUrls(this.protocol, this.serverSideHost, this.serverSidePort);
});
}
setupServerSideApp() {
this.serverSideApp = express_1.default();
}
setupServerSideHttps() {
if (this.useHttps) {
const fakeCert = getCertificate_1.getCertificate(this.pluginApi.getContext());
this.serverSideHttpsOptions.key = fakeCert;
this.serverSideHttpsOptions.cert = fakeCert;
}
}
createServerSideServer() {
if (this.serverSideApp) {
if (this.useHttps) {
this.serverSideServer = https_1.default.createServer(this.serverSideHttpsOptions, this.serverSideApp);
}
else {
this.serverSideServer = http_1.default.createServer(this.serverSideApp);
}
}
}
startClientSide() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.clientSideWebpackConfig) {
throw new Error("client side webpack config unable resolved; command [serve]");
}
const compiler = webpack(this.clientSideWebpackConfig);
const webpackDevServerOptions = {
clientLogLevel: "info",
historyApiFallback: {
disableDotRule: true,
rewrites: [
{ from: /./, to: path_1.default.posix.join(this.projectConfig.publicPath || "/", "index.html") },
],
},
contentBase: this.pluginApi.resolve("public"),
watchContentBase: true,
hot: true,
compress: true,
publicPath: this.projectConfig.publicPath,
overlay: false,
https: this.protocol === "https",
open: false,
stats: {
version: false,
timings: true,
colors: true,
modules: false,
children: false,
chunks: false,
assets: false,
},
watchOptions: {
ignored: [
`${this.pluginApi.getContext()}/src/index.tsx`,
`${this.pluginApi.getContext()}/src/route.ts`,
],
},
before: (app) => {
const mockConfig = this.pluginApi.getMockConfig();
if (this.projectConfig.mock && mockConfig !== null) {
cli_shared_utils_1.info("setup development mock server...\n");
setupMockServer_1.setupMockServer(app, mockConfig || {});
}
},
};
this.csrServer = new WebpackDevServer(compiler, webpackDevServerOptions);
return new Promise((resolve, reject) => {
var _a;
let isFirstCompile = true;
compiler.hooks.done.tap("done", (stats) => {
var _a, _b, _c;
if (stats.hasErrors()) {
stats.toJson().errors.forEach((err) => {
cli_shared_utils_1.log(err);
});
return;
}
const networkUrl = this.publicUrl
? this.publicUrl.replace(/([^/])$/, "$1/")
: ((_a = this.CSRUrlList) === null || _a === void 0 ? void 0 : _a.lanUrlForTerminal) || "";
if (isFirstCompile) {
isFirstCompile = false;
console.log();
console.log(` App running at:`);
console.log(` - Local: ${chalk_1.default.cyan(((_b = this.CSRUrlList) === null || _b === void 0 ? void 0 : _b.localUrlForTerminal) || "")}`);
console.log(` - Network: ${chalk_1.default.cyan(networkUrl)}`);
console.log();
if (this.projectConfig.mock && this.pluginApi.getMockConfig() !== null) {
console.log(" Development mock server running at:");
console.log(` - Local: ${chalk_1.default.cyan(((_c = this.CSRUrlList) === null || _c === void 0 ? void 0 : _c.localUrlForTerminal) || "")}`);
console.log(` - Network: ${chalk_1.default.cyan(networkUrl)}`);
console.log();
}
const buildCommand = "npm run build";
console.log(` Note that the development build is not optimized.`);
console.log(` To create a production build, run ${chalk_1.default.cyan(buildCommand)}.`);
console.log(` To exit the server, use ${chalk_1.default.red("control + c")}`);
console.log();
resolve();
}
});
(_a = this.csrServer) === null || _a === void 0 ? void 0 : _a.listen(this.clientSidePort, this.clientSideHost, (err) => {
if (err) {
reject(err);
}
});
});
});
}
startServerSide() {
var _a;
if (!this.serverSideWebpackConfig) {
throw new Error("server side webpack config unable resolved; command [server]");
}
this.setupServerSideApp();
this.setupServerSideHttps();
this.createServerSideServer();
const mfs = new memory_fs_1.default();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const server = this.serverSideApp;
const compiler = webpack(this.serverSideWebpackConfig);
compiler.outputFileSystem = mfs;
let serverBundle = DEFAULT_SERVER_BUNDLE;
let isModuleCompileException = false;
let ModuleCompileMessage = null;
const watchCallback = (error, stats) => {
// never throw this error, just type narrow
if (!this.serverSideWebpackConfig) {
throw new Error("server side webpack config unable resolved; command [server]");
}
isModuleCompileException = false;
if (error) {
throw error;
}
const info = stats.toJson();
info.errors.forEach((error) => console.error(error));
info.warnings.forEach((warn) => console.warn(warn));
let bundlePath = "";
if (this.serverSideWebpackConfig.output) {
if (typeof this.serverSideWebpackConfig.output.filename === "string") {
bundlePath = path_1.default.join(this.serverSideWebpackConfig.output.path || "", this.serverSideWebpackConfig.output.filename);
}
}
const bundle = mfs.readFileSync(bundlePath, "utf-8");
let _module = { exports: DEFAULT_SERVER_BUNDLE };
try {
_module = serverRender_1.getModuleFromString(bundle, "server-entry.js", {
exports: DEFAULT_SERVER_BUNDLE,
});
}
catch (ignored) {
ModuleCompileMessage = ignored;
isModuleCompileException = true;
}
serverBundle = _module.exports;
};
compiler.watch({}, watchCallback);
// logger
// TODO configure it open or close
server.use((request, _, next) => {
cli_shared_utils_1.info(`${new Date().toLocaleTimeString()} ${request.method} ${request.originalUrl}`, "Server Side rendering");
next();
});
server.use(body_parser_1.default.json());
server.use(body_parser_1.default.urlencoded({ extended: false }));
const assetsProxy = http_proxy_middleware_1.createProxyMiddleware({
ws: true,
target: (_a = this.CSRUrlList) === null || _a === void 0 ? void 0 : _a.localUrlForBrowser,
logLevel: "silent",
secure: false,
});
server.use(this.projectConfig.publicPath, assetsProxy);
const cachedState = {};
let cachedLocation = {};
const shared = {};
// TODO handle /favicon.ico
server.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
var _b, _c;
if (!serverBundle) {
res.send("WAITING FOR SERVER SIDE BUILDING...");
return;
}
if (isModuleCompileException) {
res.send(formatCompileError_1.CompileErrorTrace((ModuleCompileMessage === null || ModuleCompileMessage === void 0 ? void 0 : ModuleCompileMessage.message) || "MODULE COMPILE EXCEPTION"));
return;
}
try {
const templateUrl = ((_b = this.CSRUrlList) === null || _b === void 0 ? void 0 : _b.localUrlForBrowser) + this.projectConfig.publicPath + "server.ejs";
const assetsManifestJsonUrl = ((_c = this.CSRUrlList) === null || _c === void 0 ? void 0 : _c.localUrlForBrowser) +
this.projectConfig.publicPath +
"asset-manifest.json";
const template = yield serverRender_1.getTemplate(templateUrl.replace(/(\d+)[(^/)](\/)+/, "$1$2"));
const assetsManifestJson = yield serverRender_1.getTemplate(assetsManifestJsonUrl.replace(/(\d+)[(^/)](\/)+/, "$1$2"));
const context = {
url: req.url,
path: req.path,
query: req.query,
initProps: {},
initState: {},
};
const staticRouterContext = {
location: Object.assign({ pathname: req.path }, cachedLocation),
};
const store = typeof serverBundle.createStore === "function"
? serverBundle.createStore(cachedState)
: null;
let App = null;
try {
App = yield serverBundle.default(context, staticRouterContext, store, shared);
}
catch (err) {
cli_shared_utils_1.error(`Execute server side entry exception: ${err}`, "Server side rendering");
}
const { injectedScripts, injectedStyles } = generateInjectedHtmlTag_1.generateInjectedTag(JSON.parse(assetsManifestJson), req.path);
const document = generateDocument_1.generateDocument(template, context, App, injectedScripts, injectedStyles);
if (staticRouterContext.url) {
cachedLocation = staticRouterContext.location || {};
res.status(302);
res.setHeader("Location", staticRouterContext.url);
res.end();
return;
}
else {
cachedLocation = {};
}
res.send(document);
}
catch (err) {
next(err);
}
}));
server.use([
function (err, _, res, _next) {
console.log(err.stack);
cli_shared_utils_1.error("Something broke!", "Server Side rendering");
res.status(500);
},
]);
return new Promise((resolve, reject) => {
let isFirstSSRCompile = true;
compiler.hooks.done.tap("done", (stats) => {
if (stats.hasErrors()) {
stats.toJson().errors.forEach((err) => {
cli_shared_utils_1.log(err);
});
return;
}
});
react_loadable_1.default.preloadAll().then(() => {
if (this.serverSideServer) {
this.ssrServer = this.serverSideServer.listen(this.serverSidePort, this.serverSideHost, () => {
var _a, _b;
if (isFirstSSRCompile) {
isFirstSSRCompile = false;
console.log();
console.log(` Server Side Rendering running at:`);
console.log(` - Local: ${chalk_1.default.cyan(((_a = this.SSRUrlList) === null || _a === void 0 ? void 0 : _a.localUrlForTerminal) || "")}`);
console.log(` - Network: ${chalk_1.default.cyan(((_b = this.SSRUrlList) === null || _b === void 0 ? void 0 : _b.lanUrlForTerminal) || "")}`);
console.log();
resolve();
}
});
this.ssrServer.on("upgrade", assetsProxy.upgrade);
this.ssrServer.on("error", (error) => reject(error));
}
});
});
}
start() {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
const context = this.pluginApi.getContext();
yield cleanDest_1.cleanDest(context, this.pluginApi.resolve(this.projectConfig.outputDir));
yield this.init();
yield serverRender_1.delay(1000);
console.log();
const queue = [this.startClientSide];
if (this.projectConfig.ssr) {
queue.push(this.startServerSide);
}
yield Promise.all(queue.map((q) => q.call(this)));
if (this.commandArgs.open) {
const openClientSide = cli_shared_utils_1.openBrowser(((_a = this.CSRUrlList) === null || _a === void 0 ? void 0 : _a.localUrlForBrowser) || "");
if (!openClientSide) {
cli_shared_utils_1.warn("open client side preview failed");
}
if (this.projectConfig.ssr) {
const openServerSide = cli_shared_utils_1.openBrowser(((_b = this.SSRUrlList) === null || _b === void 0 ? void 0 : _b.localUrlForBrowser) || "");
if (!openServerSide) {
cli_shared_utils_1.warn("open server side preview failed");
}
}
}
["SIGINT", "SIGTERM"].forEach((signal) => {
process.on(signal, () => {
var _a, _b;
(_a = this.ssrServer) === null || _a === void 0 ? void 0 : _a.close();
(_b = this.csrServer) === null || _b === void 0 ? void 0 : _b.close();
process.exit();
});
});
});
}
}
class ServeWrapper {
apply(params) {
const { api, projectConfig, args } = params;
api.registerCommand("serve", {
description: "start development server",
usage: "luban-cli-service serve [options]",
options: {
"--open": `open browser on server start`,
"--mode": `specify env mode (default: development)`,
"--host": `specify host (default: ${DEFAULT_HOST})`,
"--port": `specify port (default: Client: ${defaultClientServerConfig.port}); Server: ${defaultSSRServerConfig.port}`,
"--https": `use https (default: ${defaultClientServerConfig.https})`,
"--public": `specify the public network URL for the HMR client`,
},
}, () => __awaiter(this, void 0, void 0, function* () {
const serve = new Serve(api, projectConfig, args);
yield serve.start();
}));
}
addWebpackConfig(params) {
const { api, projectConfig } = params;
api.addWebpackConfig("client");
if (projectConfig.ssr) {
api.addWebpackConfig("server");
}
}
}
exports.default = ServeWrapper;
//# sourceMappingURL=serve.js.map