import http from 'http'; import express from 'express'; import path from 'path'; import fse from 'fs-extra'; import logger from '../services/logger'; import routes from './routes'; import proxy from './proxy'; import Runner from '../runner'; import consts from '../consts'; import config from '../config'; import socketService from '@stackbit/dev-common/dist/services/socket-service'; import { injectScript } from '@stackbit/dev-common/dist/utils/proxy-utils'; interface ServerOptions { noProxy?: boolean; serverPort: number; ssgPort: number; ssgHost: string; rootDir: string; apiSecret: string; runnableDir?: string; localDetails: any; cmsType?: string; csiEnabled?: boolean; csiWebhookUrl?: string; contentfulAccessToken?: string; contentfulSpaceIds?: [string]; contentfulPreviewTokens?: [string]; isDevServer: boolean; repoUrl?: string; repoBranch?: string; repoPublishBranch?: string; deployKey?: string; } export async function start({ serverPort, ssgPort, ssgHost, rootDir, runnableDir, apiSecret, cmsType, localDetails, isDevServer, ...options }: ServerOptions) { logger.debug('Starting Stackbit dev server...'); const runner = new Runner({ rootDir, runnableDir, cmsType, isLocalDev: !isDevServer, isDevServer, apiSecret, ...options }); await runner.install(); logger.debug('Done runner init'); if (!options.noProxy && ssgHost?.toLowerCase().startsWith('https')) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; } const server = express(); const httpServer = http.createServer(server); socketService.use(httpServer, logger); // manual handling for private network CORS is needed until cors library supports this natively // https://github.com/expressjs/cors/pull/274 server.use((req, res, next) => { if (req.headers['access-control-request-private-network'] === 'true' || req.headers['access-control-request-local-network'] === 'true') { res.setHeader('access-control-allow-private-network', 'true'); res.setHeader('access-control-allow-local-network', 'true'); } next(); }); server.use(require('cors')()); server.use((req, res, next) => { // log requests logger.debug(`got request ${req.method} ${req.path}`, { method: req.method, originalUrl: req.originalUrl, url: req.url, path: req.path }); next(); }); if (process.env.PERF) { const responseTime = require('response-time'); server.use(responseTime()); } server.use(`${consts.STATIC_ASSETS_PUBLIC_PATH}/*`, async (req, res, next) => { const assetFileName: any = (req.params as any)[0]; if (!assetFileName) { return next(); } const filePath = path.join(rootDir, decodeURIComponent(assetFileName)); if (!(await fse.pathExists(filePath))) { return next(); } return res.sendFile(filePath); }); server.use(consts.STATIC_ASSETS_PUBLIC_PATH, express.static(path.join(rootDir, consts.STATIC_ASSETS_FILE_PATH))); server.use(`${consts.STATIC_THEME_ASSETS_PUBLIC_PATH}/*`, async (req, res, next) => { const assetFileName: any = (req.params as any)[0]; if (!assetFileName) { return next(); } const filePath = path.join(rootDir, decodeURIComponent(assetFileName)); if (!(await fse.pathExists(filePath))) { return next(); } const extension = path.extname(filePath).substring(1); const contentType = express.static.mime.lookup(extension); if (contentType !== 'text/html') { return res.sendFile(filePath); } res.setHeader('content-type', contentType); let data = await fse.readFile(filePath, 'utf-8'); data = injectScript(data, config.controlUrl); return res.send(data); }); server.use(consts.STATIC_THEME_ASSETS_PUBLIC_PATH, express.static(path.join(rootDir, consts.STATIC_THEME_ASSETS_FILE_PATH))); server.use('/_static', express.static('static')); const openUrl = `${config.appUrl}/local/${Buffer.from( JSON.stringify({ ...localDetails, csi: runner.isCsiEnabled() }) ).toString('base64')}`; routes(server, runner, apiSecret, openUrl); if (!options.noProxy) { proxy(httpServer, server, ssgHost, ssgPort, runner); } return new Promise((resolve) => { httpServer.listen(serverPort, () => { logger.debug('Server running on port ' + serverPort); resolve(runner); }); }); }