UNPKG

@luban-cli/cli-plugin-service

Version:
432 lines 21.1 kB
"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