From 1a49662d29dbfb5cfd35ce57054b07c0f742b6d5 Mon Sep 17 00:00:00 2001 From: Valery Bugakov Date: Mon, 19 Apr 2021 11:55:47 +0300 Subject: [PATCH 01/19] web: add web-app server for development and production builds --- client/web/.env.example | 6 ++ client/web/dev/server/development.server.ts | 59 +++++++++++++++++++ client/web/dev/server/production.server.ts | 49 +++++++++++++++ client/web/dev/utils/constants.ts | 14 +++++ client/web/dev/utils/create-js-context.ts | 55 +++++++++++++++++ .../utils/csrf/get-csrf-token-and-cookie.ts | 35 +++++++++++ .../csrf/get-csrf-token-cookie-middleware.ts | 6 ++ client/web/dev/utils/csrf/index.ts | 2 + client/web/dev/utils/environment-config.ts | 9 +++ .../web/dev/utils/get-api-proxy-settings.ts | 37 ++++++++++++ client/web/dev/utils/get-site-config.ts | 25 ++++++++ client/web/dev/utils/index.ts | 5 ++ .../dev/webpack/get-html-webpack-plugins.ts | 53 +++++++++++++++++ client/web/gulpfile.js | 1 + client/web/package.json | 2 + client/web/webpack.config.js | 10 +++- package.json | 4 ++ yarn.lock | 59 +++++++++++++++++-- 18 files changed, 425 insertions(+), 6 deletions(-) create mode 100644 client/web/.env.example create mode 100644 client/web/dev/server/development.server.ts create mode 100644 client/web/dev/server/production.server.ts create mode 100644 client/web/dev/utils/constants.ts create mode 100644 client/web/dev/utils/create-js-context.ts create mode 100644 client/web/dev/utils/csrf/get-csrf-token-and-cookie.ts create mode 100644 client/web/dev/utils/csrf/get-csrf-token-cookie-middleware.ts create mode 100644 client/web/dev/utils/csrf/index.ts create mode 100644 client/web/dev/utils/environment-config.ts create mode 100644 client/web/dev/utils/get-api-proxy-settings.ts create mode 100644 client/web/dev/utils/get-site-config.ts create mode 100644 client/web/dev/utils/index.ts create mode 100644 client/web/dev/webpack/get-html-webpack-plugins.ts diff --git a/client/web/.env.example b/client/web/.env.example new file mode 100644 index 0000000000000..8d4b381897f64 --- /dev/null +++ b/client/web/.env.example @@ -0,0 +1,6 @@ +WEBPACK_SERVE_INDEX=true +# SOURCEGRAPH_API_URL=https://sourcegraph.com +SOURCEGRAPH_API_URL=https://k8s.sgdev.org +SOURCEGRAPH_HTTPS_DOMAIN=sourcegraph.test +SOURCEGRAPH_HTTPS_PORT=3443 +NO_HOT=false diff --git a/client/web/dev/server/development.server.ts b/client/web/dev/server/development.server.ts new file mode 100644 index 0000000000000..ebb9035618c60 --- /dev/null +++ b/client/web/dev/server/development.server.ts @@ -0,0 +1,59 @@ +import 'dotenv/config' + +import createWebpackCompiler, { Configuration } from 'webpack' +import WebpackDevServer from 'webpack-dev-server' + +import { + getCSRFTokenCookieMiddleware, + PROXY_ROUTES, + environmentConfig, + getAPIProxySettings, + getCSRFTokenAndCookie, + STATIC_ASSETS_PATH, + STATIC_ASSETS_URL, + WEBPACK_STATS_OPTIONS, +} from '../utils' + +// TODO: migrate webpack.config.js to TS to fix this. +// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports +const webpackConfig = require('../../webpack.config') as Configuration +const { SOURCEGRAPH_API_URL, SOURCEGRAPH_HTTPS_PORT, IS_HOT_RELOAD_ENABLED } = environmentConfig + +export async function webpackDevelopmentServer(): Promise { + const { csrfContextValue, csrfCookieValue } = await getCSRFTokenAndCookie(SOURCEGRAPH_API_URL) + console.log('Starting development server...', { ...environmentConfig, csrfContextValue, csrfCookieValue }) + + const options: WebpackDevServer.Configuration = { + hot: IS_HOT_RELOAD_ENABLED, + // TODO: resolve https://github.com/webpack/webpack-dev-server/issues/2313 and enable HTTPS. + https: false, + historyApiFallback: true, + port: SOURCEGRAPH_HTTPS_PORT, + publicPath: STATIC_ASSETS_URL, + contentBase: STATIC_ASSETS_PATH, + contentBasePublicPath: [STATIC_ASSETS_URL, '/'], + stats: WEBPACK_STATS_OPTIONS, + noInfo: false, + disableHostCheck: true, + proxy: [ + { + context: PROXY_ROUTES, + ...getAPIProxySettings({ + csrfContextValue, + apiURL: SOURCEGRAPH_API_URL, + }), + }, + ], + before(app) { + app.use(getCSRFTokenCookieMiddleware(csrfCookieValue)) + }, + } + + WebpackDevServer.addDevServerEntrypoints(webpackConfig, options) + + const server = new WebpackDevServer(createWebpackCompiler(webpackConfig), options) + + server.listen(SOURCEGRAPH_HTTPS_PORT, '0.0.0.0') +} + +webpackDevelopmentServer().catch(error => console.error(error)) diff --git a/client/web/dev/server/production.server.ts b/client/web/dev/server/production.server.ts new file mode 100644 index 0000000000000..bd48b9eeddb29 --- /dev/null +++ b/client/web/dev/server/production.server.ts @@ -0,0 +1,49 @@ +import 'dotenv/config' + +import historyApiFallback from 'connect-history-api-fallback' +import express from 'express' +import { createProxyMiddleware } from 'http-proxy-middleware' +import open from 'open' + +import { + PROXY_ROUTES, + getAPIProxySettings, + getCSRFTokenCookieMiddleware, + environmentConfig, + getCSRFTokenAndCookie, + STATIC_ASSETS_PATH, +} from '../utils' + +const { SOURCEGRAPH_API_URL, SOURCEGRAPH_HTTPS_PORT, SOURCEGRAPH_HTTPS_DOMAIN } = environmentConfig +const PROD_SERVER_URL = `http://${SOURCEGRAPH_HTTPS_DOMAIN}:${SOURCEGRAPH_HTTPS_PORT}` + +async function startProductionServer(): Promise { + const { csrfContextValue, csrfCookieValue } = await getCSRFTokenAndCookie(SOURCEGRAPH_API_URL) + console.log('Starting production server...', { ...environmentConfig, csrfContextValue, csrfCookieValue }) + + const app = express() + + app.use(historyApiFallback()) + app.use(getCSRFTokenCookieMiddleware(csrfCookieValue)) + + app.use(express.static(STATIC_ASSETS_PATH)) + app.use('/.assets', express.static(STATIC_ASSETS_PATH)) + + app.use( + PROXY_ROUTES, + createProxyMiddleware( + getAPIProxySettings({ + csrfContextValue, + apiURL: SOURCEGRAPH_API_URL, + }) + ) + ) + + app.listen(SOURCEGRAPH_HTTPS_PORT, () => { + console.log(`[PROD] Server: ${PROD_SERVER_URL}`) + + return open(`${PROD_SERVER_URL}/search`) + }) +} + +startProductionServer().catch(error => console.error('Something went wrong :(', error)) diff --git a/client/web/dev/utils/constants.ts b/client/web/dev/utils/constants.ts new file mode 100644 index 0000000000000..261e2492e78cf --- /dev/null +++ b/client/web/dev/utils/constants.ts @@ -0,0 +1,14 @@ +import path from 'path' + +export const ROOT_PATH = path.resolve(__dirname, '../../../../') +export const STATIC_ASSETS_PATH = path.resolve(ROOT_PATH, 'ui/assets') +export const STATIC_ASSETS_URL = '/.assets/' + +// TODO: share with gulpfile.js +export const WEBPACK_STATS_OPTIONS = { + all: false, + timings: true, + errors: true, + warnings: true, + colors: true, +} diff --git a/client/web/dev/utils/create-js-context.ts b/client/web/dev/utils/create-js-context.ts new file mode 100644 index 0000000000000..34733b0ed2408 --- /dev/null +++ b/client/web/dev/utils/create-js-context.ts @@ -0,0 +1,55 @@ +import { SourcegraphContext } from '../../src/jscontext' + +import { getSiteConfig } from './get-site-config' + +// TODO: share with `client/web/src/integration/jscontext` which is not included into `tsconfig.json` now. +export const builtinAuthProvider = { + serviceType: 'builtin' as const, + serviceID: '', + clientID: '', + displayName: 'Builtin username-password authentication', + isBuiltin: true, + authenticationURL: '', +} + +export const createJsContext = ({ sourcegraphBaseUrl }: { sourcegraphBaseUrl: string }): SourcegraphContext => { + const siteConfig = getSiteConfig() + + if (siteConfig?.authProviders) { + siteConfig.authProviders.unshift(builtinAuthProvider) + } + + return { + externalURL: sourcegraphBaseUrl, + accessTokensAllow: 'all-users-create', + allowSignup: true, + batchChangesEnabled: true, + codeIntelAutoIndexingEnabled: false, + externalServicesUserModeEnabled: true, + productResearchPageEnabled: true, + csrfToken: 'qwerty', + assetsRoot: '/.assets', + deployType: 'dev', + debug: true, + emailEnabled: false, + experimentalFeatures: {}, + isAuthenticatedUser: true, + likelyDockerOnMac: false, + needServerRestart: false, + needsSiteInit: false, + resetPasswordEnabled: true, + sentryDSN: null, + site: { + 'update.channel': 'release', + }, + siteID: 'TestSiteID', + siteGQLID: 'TestGQLSiteID', + sourcegraphDotComMode: true, + userAgentIsBot: false, + version: '0.0.0', + xhrHeaders: {}, + authProviders: [builtinAuthProvider], + // Site-config overrides default JS context + ...siteConfig, + } +} diff --git a/client/web/dev/utils/csrf/get-csrf-token-and-cookie.ts b/client/web/dev/utils/csrf/get-csrf-token-and-cookie.ts new file mode 100644 index 0000000000000..c2f670560f32e --- /dev/null +++ b/client/web/dev/utils/csrf/get-csrf-token-and-cookie.ts @@ -0,0 +1,35 @@ +import fetch from 'node-fetch' + +const CSRF_CONTEXT_KEY = 'csrfToken' +const CSRF_CONTEXT_VALUE_REGEXP = new RegExp(`${CSRF_CONTEXT_KEY}":"(.*?)"`) + +const CSRF_COOKIE_NAME = 'sg_csrf_token' +const CSRF_COOKIE_VALUE_REGEXP = new RegExp(`${CSRF_COOKIE_NAME}=(.*?);`) + +interface CSFRTokenAndCookie { + csrfContextValue: string + csrfCookieValue: string +} + +export async function getCSRFTokenAndCookie(proxyUrl: string): Promise { + const response = await fetch(`${proxyUrl}/sign-in`) + + const html = await response.text() + const cookieHeader = response.headers.get('set-cookie') + + if (!cookieHeader) { + throw new Error(`"set-cookie" header not found in "${proxyUrl}/sign-in" response`) + } + + const csrfHeaderMatches = CSRF_CONTEXT_VALUE_REGEXP.exec(html) + const csrfCookieMatches = CSRF_COOKIE_VALUE_REGEXP.exec(cookieHeader) + + if (!csrfHeaderMatches || !csrfCookieMatches) { + throw new Error('CSRF value not found!') + } + + return { + csrfContextValue: csrfHeaderMatches[1], + csrfCookieValue: csrfCookieMatches[1], + } +} diff --git a/client/web/dev/utils/csrf/get-csrf-token-cookie-middleware.ts b/client/web/dev/utils/csrf/get-csrf-token-cookie-middleware.ts new file mode 100644 index 0000000000000..22cfb9ce45e63 --- /dev/null +++ b/client/web/dev/utils/csrf/get-csrf-token-cookie-middleware.ts @@ -0,0 +1,6 @@ +import { RequestHandler } from 'express' + +export const getCSRFTokenCookieMiddleware = (csrfCookieValue: string): RequestHandler => (_request, response, next) => { + response.cookie('sg_csrf_token', csrfCookieValue, { httpOnly: true }) + next() +} diff --git a/client/web/dev/utils/csrf/index.ts b/client/web/dev/utils/csrf/index.ts new file mode 100644 index 0000000000000..babb02186f1ab --- /dev/null +++ b/client/web/dev/utils/csrf/index.ts @@ -0,0 +1,2 @@ +export * from './get-csrf-token-and-cookie' +export * from './get-csrf-token-cookie-middleware' diff --git a/client/web/dev/utils/environment-config.ts b/client/web/dev/utils/environment-config.ts new file mode 100644 index 0000000000000..67bf44c8711ce --- /dev/null +++ b/client/web/dev/utils/environment-config.ts @@ -0,0 +1,9 @@ +export const environmentConfig = { + SOURCEGRAPH_API_URL: process.env.SOURCEGRAPH_API_URL || 'https://k8s.sgdev.org', + SOURCEGRAPH_HTTPS_DOMAIN: process.env.SOURCEGRAPH_HTTPS_DOMAIN || 'sourcegraph.test', + SOURCEGRAPH_HTTPS_PORT: Number(process.env.SOURCEGRAPH_HTTPS_PORT) || 3443, + WEBPACK_SERVE_INDEX: process.env.WEBPACK_SERVE_INDEX === 'true', + + // TODO: do we use process.env.NO_HOT anywhere? + IS_HOT_RELOAD_ENABLED: process.env.NO_HOT !== 'true', +} diff --git a/client/web/dev/utils/get-api-proxy-settings.ts b/client/web/dev/utils/get-api-proxy-settings.ts new file mode 100644 index 0000000000000..100ac7ead6e1d --- /dev/null +++ b/client/web/dev/utils/get-api-proxy-settings.ts @@ -0,0 +1,37 @@ +import { Options } from 'http-proxy-middleware' + +// One of the API routes: "/-/sign-in". +export const PROXY_ROUTES = ['/.api', '/search/stream', '/-', '/.auth'] + +interface GetAPIProxySettingsOptions { + csrfContextValue: string + apiURL: string +} + +export const getAPIProxySettings = ({ csrfContextValue, apiURL }: GetAPIProxySettingsOptions): Options => ({ + target: apiURL, + secure: false, + changeOrigin: true, + headers: { + 'x-csrf-token': csrfContextValue, + }, + cookieDomainRewrite: '', + onProxyRes: proxyResponse => { + if (proxyResponse.headers['set-cookie']) { + const cookies = proxyResponse.headers['set-cookie'].map(cookie => + cookie.replace(/; secure/gi, '').replace(/; samesite=.+/gi, '') + ) + + proxyResponse.headers['set-cookie'] = cookies + } + }, + // TODO: share with `client/web/gulpfile.js` + // Avoid crashing on "read ECONNRESET". + onError: () => undefined, + // Don't log proxy errors, these usually just contain + // ECONNRESET errors caused by the browser cancelling + // requests. This should not be needed to actually debug something. + logLevel: 'silent', + onProxyReqWs: (_proxyRequest, _request, socket) => + socket.on('error', error => console.error('WebSocket proxy error:', error)), +}) diff --git a/client/web/dev/utils/get-site-config.ts b/client/web/dev/utils/get-site-config.ts new file mode 100644 index 0000000000000..e38dff8e23f52 --- /dev/null +++ b/client/web/dev/utils/get-site-config.ts @@ -0,0 +1,25 @@ +import fs from 'fs' +import path from 'path' + +import { camelCase, mapKeys } from 'lodash' +import stripJsonComments from 'strip-json-comments' + +import { SourcegraphContext } from '../../src/jscontext' + +import { ROOT_PATH } from './constants' + +const SITE_CONFIG_PATH = path.resolve(ROOT_PATH, '../dev-private/enterprise/dev/site-config.json') + +export const getSiteConfig = (): Partial => { + try { + // eslint-disable-next-line no-sync + const siteConfig = JSON.parse(stripJsonComments(fs.readFileSync(SITE_CONFIG_PATH, 'utf-8'))) + + return mapKeys(siteConfig, (_value, key) => camelCase(key)) + } catch (error) { + console.log('Site config not found!', SITE_CONFIG_PATH) + console.error(error) + + return {} + } +} diff --git a/client/web/dev/utils/index.ts b/client/web/dev/utils/index.ts new file mode 100644 index 0000000000000..c1f0a0dbc45d8 --- /dev/null +++ b/client/web/dev/utils/index.ts @@ -0,0 +1,5 @@ +export * from './constants' +export * from './create-js-context' +export * from './environment-config' +export * from './get-api-proxy-settings' +export * from './csrf' diff --git a/client/web/dev/webpack/get-html-webpack-plugins.ts b/client/web/dev/webpack/get-html-webpack-plugins.ts new file mode 100644 index 0000000000000..14ab191d0cd4d --- /dev/null +++ b/client/web/dev/webpack/get-html-webpack-plugins.ts @@ -0,0 +1,53 @@ +import path from 'path' + +import HtmlWebpackHarddiskPlugin from 'html-webpack-harddisk-plugin' +import HtmlWebpackPlugin, { TemplateParameter, Options } from 'html-webpack-plugin' +import { Plugin } from 'webpack' + +import { createJsContext, environmentConfig, STATIC_ASSETS_PATH } from '../utils' + +const { SOURCEGRAPH_HTTPS_PORT, SOURCEGRAPH_API_URL } = environmentConfig + +export const getHTMLWebpackPlugins = (): Plugin[] => { + const jsContext = createJsContext({ sourcegraphBaseUrl: `http://localhost:${SOURCEGRAPH_HTTPS_PORT}` }) + + // TODO: use `cmd/frontend/internal/app/ui/app.html` template to be consistent with default production setup. + const templateContent = ({ htmlWebpackPlugin }: TemplateParameter): string => ` + + + Sourcegraph Development build + ${htmlWebpackPlugin.tags.headTags.toString()} + + +
+ + + + ` + + const htmlWebpackPlugin = new HtmlWebpackPlugin({ + // `TemplateParameter` can be mutated that's why we need to tell TS that we didn't touch it. + templateContent: templateContent as Options['templateContent'], + filename: path.resolve(STATIC_ASSETS_PATH, 'index.html'), + alwaysWriteToDisk: true, + }) + + return [htmlWebpackPlugin, new HtmlWebpackHarddiskPlugin()] +} diff --git a/client/web/gulpfile.js b/client/web/gulpfile.js index 6ff68100f163d..a0ed4aed4df1d 100644 --- a/client/web/gulpfile.js +++ b/client/web/gulpfile.js @@ -1,4 +1,5 @@ // @ts-check +require('ts-node').register({}) const log = require('fancy-log') const gulp = require('gulp') diff --git a/client/web/package.json b/client/web/package.json index 94f0e236d5bf7..e3229188860d1 100644 --- a/client/web/package.json +++ b/client/web/package.json @@ -15,6 +15,8 @@ "test:regression:onboarding": "mocha ./src/regression/onboarding.test.ts", "test:regression:search": "mocha ./src/regression/search.test.ts", "test-e2e-sgdev": "env SOURCEGRAPH_BASE_URL=https://sourcegraph.sgdev.org OVERRIDE_AUTH_SECRET=${SGDEV_OVERRIDE_AUTH_SECRET} mocha ./end-to-end/end-to-end.test.ts", + "serve:dev": "ts-node-transpile-only ./dev/server/development.server.ts", + "serve:prod": "ts-node-transpile-only ./dev/server/production.server.ts", "build": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" gulp build", "watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" gulp watch", "watch-webpack": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" gulp watchWebpack", diff --git a/client/web/webpack.config.js b/client/web/webpack.config.js index 300698ed743e5..7e53763751221 100644 --- a/client/web/webpack.config.js +++ b/client/web/webpack.config.js @@ -11,6 +11,8 @@ const webpack = require('webpack') const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') const { WebpackManifestPlugin } = require('webpack-manifest-plugin') +const { getHTMLWebpackPlugins } = require('./dev/webpack/get-html-webpack-plugins') + const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development' logger.info('Using mode', mode) @@ -18,6 +20,11 @@ const isDevelopment = mode === 'development' const isProduction = mode === 'production' const devtool = isProduction ? 'source-map' : 'cheap-module-eval-source-map' +const shouldServeIndexHTML = process.env.WEBPACK_SERVE_INDEX === 'true' +if (shouldServeIndexHTML) { + logger.info('Serving index.html with HTMLWebpackPlugin') +} + const shouldAnalyze = process.env.WEBPACK_ANALYZER === '1' if (shouldAnalyze) { logger.info('Running bundle analyzer') @@ -74,7 +81,7 @@ const config = { sourceMap: true, terserOptions: { compress: { - // // Don't inline functions, which causes name collisions with uglify-es: + // Don't inline functions, which causes name collisions with uglify-es: // https://github.com/mishoo/UglifyJS2/issues/2842 inline: 1, }, @@ -147,6 +154,7 @@ const config = { // Only output files that are required to run the application filter: ({ isInitial }) => isInitial, }), + ...(shouldServeIndexHTML ? getHTMLWebpackPlugins() : []), ...(shouldAnalyze ? [new BundleAnalyzerPlugin()] : []), ], resolve: { diff --git a/package.json b/package.json index 0ecf1960be6de..f5ab09971fbcb 100644 --- a/package.json +++ b/package.json @@ -219,6 +219,8 @@ "graphql": "^15.4.0", "graphql-schema-linter": "^2.0.1", "gulp": "^4.0.2", + "html-webpack-harddisk-plugin": "^2.0.0", + "http-proxy-middleware": "^1.1.2", "identity-obj-proxy": "^3.0.0", "jest": "^25.5.4", "jest-canvas-mock": "^2.3.0", @@ -297,6 +299,7 @@ "@sourcegraph/extension-api-classes": "^1.1.0", "@sourcegraph/react-loading-spinner": "0.0.7", "@sqs/jsonc-parser": "^1.0.3", + "@types/strip-json-comments": "^3.0.0", "@visx/annotation": "^1.7.2", "@visx/axis": "^1.7.0", "@visx/glyph": "^1.7.0", @@ -355,6 +358,7 @@ "semver": "^7.3.2", "shepherd.js": "^8.0.2", "string-score": "^1.0.1", + "strip-json-comments": "^3.1.1", "tabbable": "^5.1.5", "tagged-template-noop": "^2.1.1", "textarea-caret": "^3.1.0", diff --git a/yarn.lock b/yarn.lock index d33cccc1124c2..2fdfc7db6e116 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4105,6 +4105,13 @@ "@types/events" "*" "@types/node" "*" +"@types/http-proxy@^1.17.5": + version "1.17.5" + resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.5.tgz#c203c5e6e9dc6820d27a40eb1e511c70a220423d" + integrity sha512-GNkDE7bTv6Sf8JbV2GksknKOsk7OznNYHSdrtvPJXO0qJ9odZig6IZKUi5RFGi6d1bf6dgIAe4uXi3DBc7069Q== + dependencies: + "@types/node" "*" + "@types/is-absolute-url@3.0.0": version "3.0.0" resolved "https://registry.npmjs.org/@types/is-absolute-url/-/is-absolute-url-3.0.0.tgz#a9ec8e203051f9766f4f41e0f2d33f93d3624b9d" @@ -4623,6 +4630,13 @@ resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== +"@types/strip-json-comments@^3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-3.0.0.tgz#b1101c0a14305b589b271535a6a5ed7a5c2ed180" + integrity sha512-+vSlZeCCGm/1Q3WwBJDUSGHvADTQjRl4Fve0AG9H+A4+xgJ7yAkWKoc4g7I9UoVqtwCzjv2g/GZn4xipokMYyQ== + dependencies: + strip-json-comments "*" + "@types/stripe-v3@*": version "3.0.8" resolved "https://registry.npmjs.org/@types/stripe-v3/-/stripe-v3-3.0.8.tgz#9c2816acf34dcf0948236fc81ce99442344ebfb4" @@ -10664,6 +10678,11 @@ eventemitter3@^3.0.0, eventemitter3@^3.1.0: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + events@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" @@ -12780,6 +12799,11 @@ html-tags@^3.1.0: resolved "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg== +html-webpack-harddisk-plugin@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/html-webpack-harddisk-plugin/-/html-webpack-harddisk-plugin-2.0.0.tgz#2de78316554e7aa37d07066d3687901beca4d5ce" + integrity sha512-fWKH72FyaQ5K/j+kYy6LnQsQucSDnsEkghmB6g29TtpJ4sxHYFduEeUV1hfDqyDpCRW+bP7yacjQ+1ikeIDqeg== + html-webpack-plugin@^4.2.1: version "4.5.0" resolved "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.0.tgz#625097650886b97ea5dae331c320e3238f6c121c" @@ -12900,6 +12924,17 @@ http-proxy-middleware@0.19.1: lodash "^4.17.11" micromatch "^3.1.10" +http-proxy-middleware@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.1.2.tgz#38d062ce4182b2931442efc2d9a0c429cab634f8" + integrity sha512-YRFUeOG3q85FJjAaYVJUoNRW9a73SDlOtAyQOS5PHLr18QeZ/vEhxywNoOPiEO8BxCegz4RXzTHcvyLEGB78UA== + dependencies: + "@types/http-proxy" "^1.17.5" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + http-proxy@^1.17.0: version "1.17.0" resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" @@ -12909,6 +12944,15 @@ http-proxy@^1.17.0: follow-redirects "^1.0.0" requires-port "^1.0.0" +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -13729,6 +13773,11 @@ is-plain-obj@^2.0.0, is-plain-obj@^2.1.0: resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -21341,16 +21390,16 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" +strip-json-comments@*, strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + strip-json-comments@3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== -strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" From 477953a61c649d242320eb363a2a632976b4dc44 Mon Sep 17 00:00:00 2001 From: Valery Bugakov Date: Mon, 19 Apr 2021 12:02:15 +0300 Subject: [PATCH 02/19] web: use transpile-only everywhere --- client/web/gulpfile.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/web/gulpfile.js b/client/web/gulpfile.js index a0ed4aed4df1d..651db1b51fc4f 100644 --- a/client/web/gulpfile.js +++ b/client/web/gulpfile.js @@ -1,5 +1,7 @@ // @ts-check -require('ts-node').register({}) +require('ts-node').register({ + transpileOnly: true, +}) const log = require('fancy-log') const gulp = require('gulp') From c42b41c3059257723e79d92df27e66f422859e02 Mon Sep 17 00:00:00 2001 From: Valery Bugakov Date: Mon, 19 Apr 2021 15:11:53 +0300 Subject: [PATCH 03/19] web: yarn deduplicate --- yarn.lock | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2fdfc7db6e116..b0e997c8e888a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4097,15 +4097,7 @@ "@types/http-proxy" "*" "@types/node" "*" -"@types/http-proxy@*": - version "1.16.2" - resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.16.2.tgz#16cb373b52fff2aa2f389d23d940ed4a642349e5" - integrity sha512-GgqePmC3rlsn1nv+kx5OviPuUBU2omhnlXOaJSXFgOdsTcScNFap+OaCb2ip9Bm4m5L8EOehgT5d9M4uNB90zg== - dependencies: - "@types/events" "*" - "@types/node" "*" - -"@types/http-proxy@^1.17.5": +"@types/http-proxy@*", "@types/http-proxy@^1.17.5": version "1.17.5" resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.5.tgz#c203c5e6e9dc6820d27a40eb1e511c70a220423d" integrity sha512-GNkDE7bTv6Sf8JbV2GksknKOsk7OznNYHSdrtvPJXO0qJ9odZig6IZKUi5RFGi6d1bf6dgIAe4uXi3DBc7069Q== @@ -12935,16 +12927,7 @@ http-proxy-middleware@^1.1.2: is-plain-obj "^3.0.0" micromatch "^4.0.2" -http-proxy@^1.17.0: - version "1.17.0" - resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" - integrity sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g== - dependencies: - eventemitter3 "^3.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -http-proxy@^1.18.1: +http-proxy@^1.17.0, http-proxy@^1.18.1: version "1.18.1" resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== From 5400089905014e416335ba351fb277a97605ea71 Mon Sep 17 00:00:00 2001 From: Valery Bugakov Date: Tue, 27 Apr 2021 10:12:40 +0300 Subject: [PATCH 04/19] web: add README.md about local development --- client/web/README.md | 30 +++++++++++++++++++ client/web/dev/server/development.server.ts | 16 ++++++---- client/web/dev/server/production.server.ts | 22 +++++++++----- client/web/dev/tsconfig.json | 6 ++++ client/web/dev/utils/constants.ts | 6 ++++ client/web/dev/utils/create-js-context.ts | 1 + .../utils/csrf/get-csrf-token-and-cookie.ts | 12 ++++++-- .../csrf/get-csrf-token-cookie-middleware.ts | 5 +++- client/web/dev/utils/environment-config.ts | 1 + .../web/dev/utils/get-api-proxy-settings.ts | 5 ++++ client/web/dev/utils/get-site-config.ts | 5 ++-- .../dev/webpack/get-html-webpack-plugins.ts | 10 +++++-- client/web/gulpfile.js | 2 ++ client/web/package.json | 4 +-- client/web/webpack.config.js | 2 ++ 15 files changed, 104 insertions(+), 23 deletions(-) create mode 100644 client/web/README.md create mode 100644 client/web/dev/tsconfig.json diff --git a/client/web/README.md b/client/web/README.md new file mode 100644 index 0000000000000..6ab9e5f502835 --- /dev/null +++ b/client/web/README.md @@ -0,0 +1,30 @@ +# Web Application + +## Local development + +### Prerequisites + +1. Duplicate `client/web/.env.example` as `client/web/.env`. +2. Make sure that `WEBPACK_SERVE_INDEX` is set to `true`. +3. Make sure that `SOURCEGRAPH_API_URL` points to the accessible API url. + +### Development server + +```sh +yarn serve:dev +``` + +### Production server + +```sh +ENTERPRISE=1 NODE_ENV=production DISABLE_TYPECHECKING=true yarn run build +yarn serve:prod +``` + +Web app should be available at `http://${SOURCEGRAPH_HTTPS_DOMAIN}:${SOURCEGRAPH_HTTPS_PORT}`. +Build artifacts will be served from `/ui/assets`. + +### API proxy + +Both servers proxy API requests to `SOURCEGRAPH_API_URL` provided in `.env` file. To avoid `CSRF token is invalid` error +CSRF token is retrieved from the `SOURCEGRAPH_API_URL` before the server starts. Then this values is used for every subsequent request to the API. diff --git a/client/web/dev/server/development.server.ts b/client/web/dev/server/development.server.ts index ebb9035618c60..d371a098a01d8 100644 --- a/client/web/dev/server/development.server.ts +++ b/client/web/dev/server/development.server.ts @@ -1,5 +1,7 @@ import 'dotenv/config' +import chalk from 'chalk' +import signale from 'signale' import createWebpackCompiler, { Configuration } from 'webpack' import WebpackDevServer from 'webpack-dev-server' @@ -12,16 +14,18 @@ import { STATIC_ASSETS_PATH, STATIC_ASSETS_URL, WEBPACK_STATS_OPTIONS, + WEB_SERVER_URL, } from '../utils' -// TODO: migrate webpack.config.js to TS to fix this. +// TODO: migrate webpack.config.js to TS to use `import` in this file. // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports const webpackConfig = require('../../webpack.config') as Configuration const { SOURCEGRAPH_API_URL, SOURCEGRAPH_HTTPS_PORT, IS_HOT_RELOAD_ENABLED } = environmentConfig -export async function webpackDevelopmentServer(): Promise { +export async function startDevelopmentServer(): Promise { + // Get CSRF token value from the `SOURCEGRAPH_API_URL`. const { csrfContextValue, csrfCookieValue } = await getCSRFTokenAndCookie(SOURCEGRAPH_API_URL) - console.log('Starting development server...', { ...environmentConfig, csrfContextValue, csrfCookieValue }) + signale.await('Development server', { ...environmentConfig, csrfContextValue, csrfCookieValue }) const options: WebpackDevServer.Configuration = { hot: IS_HOT_RELOAD_ENABLED, @@ -53,7 +57,9 @@ export async function webpackDevelopmentServer(): Promise { const server = new WebpackDevServer(createWebpackCompiler(webpackConfig), options) - server.listen(SOURCEGRAPH_HTTPS_PORT, '0.0.0.0') + server.listen(SOURCEGRAPH_HTTPS_PORT, '0.0.0.0', () => { + signale.success(`Development server is ready at ${chalk.blue.bold(WEB_SERVER_URL)}`) + }) } -webpackDevelopmentServer().catch(error => console.error(error)) +startDevelopmentServer().catch(error => signale.error(error)) diff --git a/client/web/dev/server/production.server.ts b/client/web/dev/server/production.server.ts index bd48b9eeddb29..20ff1475f4778 100644 --- a/client/web/dev/server/production.server.ts +++ b/client/web/dev/server/production.server.ts @@ -1,9 +1,10 @@ import 'dotenv/config' +import chalk from 'chalk' import historyApiFallback from 'connect-history-api-fallback' import express from 'express' import { createProxyMiddleware } from 'http-proxy-middleware' -import open from 'open' +import signale from 'signale' import { PROXY_ROUTES, @@ -12,27 +13,34 @@ import { environmentConfig, getCSRFTokenAndCookie, STATIC_ASSETS_PATH, + WEB_SERVER_URL, } from '../utils' -const { SOURCEGRAPH_API_URL, SOURCEGRAPH_HTTPS_PORT, SOURCEGRAPH_HTTPS_DOMAIN } = environmentConfig -const PROD_SERVER_URL = `http://${SOURCEGRAPH_HTTPS_DOMAIN}:${SOURCEGRAPH_HTTPS_PORT}` +const { SOURCEGRAPH_API_URL, SOURCEGRAPH_HTTPS_PORT } = environmentConfig async function startProductionServer(): Promise { + // Get CSRF token value from the `SOURCEGRAPH_API_URL`. const { csrfContextValue, csrfCookieValue } = await getCSRFTokenAndCookie(SOURCEGRAPH_API_URL) - console.log('Starting production server...', { ...environmentConfig, csrfContextValue, csrfCookieValue }) + signale.await('Production server', { ...environmentConfig, csrfContextValue, csrfCookieValue }) const app = express() + // Serve index.html in place of any 404 responses. app.use(historyApiFallback()) + // Attach `CSRF_COOKIE_NAME` cookie to every response to avoid "CSRF token is invalid" API error. app.use(getCSRFTokenCookieMiddleware(csrfCookieValue)) + // Serve index.html. app.use(express.static(STATIC_ASSETS_PATH)) + // Serve build artifacts. app.use('/.assets', express.static(STATIC_ASSETS_PATH)) + // Proxy API requests to the `process.env.SOURCEGRAPH_API_URL`. app.use( PROXY_ROUTES, createProxyMiddleware( getAPIProxySettings({ + // Attach `x-csrf-token` header to every proxy request. csrfContextValue, apiURL: SOURCEGRAPH_API_URL, }) @@ -40,10 +48,8 @@ async function startProductionServer(): Promise { ) app.listen(SOURCEGRAPH_HTTPS_PORT, () => { - console.log(`[PROD] Server: ${PROD_SERVER_URL}`) - - return open(`${PROD_SERVER_URL}/search`) + signale.success(`Production server is ready at ${chalk.blue.bold(WEB_SERVER_URL)}`) }) } -startProductionServer().catch(error => console.error('Something went wrong :(', error)) +startProductionServer().catch(error => signale.error(error)) diff --git a/client/web/dev/tsconfig.json b/client/web/dev/tsconfig.json new file mode 100644 index 0000000000000..6da93044c319c --- /dev/null +++ b/client/web/dev/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "commonjs", + }, +} diff --git a/client/web/dev/utils/constants.ts b/client/web/dev/utils/constants.ts index 261e2492e78cf..e8d8b6c20ba01 100644 --- a/client/web/dev/utils/constants.ts +++ b/client/web/dev/utils/constants.ts @@ -1,9 +1,15 @@ import path from 'path' +import { environmentConfig } from './environment-config' + +const { SOURCEGRAPH_HTTPS_PORT, SOURCEGRAPH_HTTPS_DOMAIN } = environmentConfig + export const ROOT_PATH = path.resolve(__dirname, '../../../../') export const STATIC_ASSETS_PATH = path.resolve(ROOT_PATH, 'ui/assets') export const STATIC_ASSETS_URL = '/.assets/' +export const WEB_SERVER_URL = `http://${SOURCEGRAPH_HTTPS_DOMAIN}:${SOURCEGRAPH_HTTPS_PORT}` + // TODO: share with gulpfile.js export const WEBPACK_STATS_OPTIONS = { all: false, diff --git a/client/web/dev/utils/create-js-context.ts b/client/web/dev/utils/create-js-context.ts index 34733b0ed2408..d711741a728c5 100644 --- a/client/web/dev/utils/create-js-context.ts +++ b/client/web/dev/utils/create-js-context.ts @@ -12,6 +12,7 @@ export const builtinAuthProvider = { authenticationURL: '', } +// Create dummy JS context that will be added to index.html when `WEBPACK_SERVE_INDEX` is set to true. export const createJsContext = ({ sourcegraphBaseUrl }: { sourcegraphBaseUrl: string }): SourcegraphContext => { const siteConfig = getSiteConfig() diff --git a/client/web/dev/utils/csrf/get-csrf-token-and-cookie.ts b/client/web/dev/utils/csrf/get-csrf-token-and-cookie.ts index c2f670560f32e..1479b9e988ceb 100644 --- a/client/web/dev/utils/csrf/get-csrf-token-and-cookie.ts +++ b/client/web/dev/utils/csrf/get-csrf-token-and-cookie.ts @@ -1,9 +1,9 @@ import fetch from 'node-fetch' -const CSRF_CONTEXT_KEY = 'csrfToken' +export const CSRF_CONTEXT_KEY = 'csrfToken' const CSRF_CONTEXT_VALUE_REGEXP = new RegExp(`${CSRF_CONTEXT_KEY}":"(.*?)"`) -const CSRF_COOKIE_NAME = 'sg_csrf_token' +export const CSRF_COOKIE_NAME = 'sg_csrf_token' const CSRF_COOKIE_VALUE_REGEXP = new RegExp(`${CSRF_COOKIE_NAME}=(.*?);`) interface CSFRTokenAndCookie { @@ -11,6 +11,14 @@ interface CSFRTokenAndCookie { csrfCookieValue: string } +/** + * + * Fetch `${proxyUrl}/sign-in` and extract two values from the response: + * + * 1. `set-cookie` value for `CSRF_COOKIE_NAME`. + * 2. value from JS context under `CSRF_CONTEXT_KEY` key. + * + */ export async function getCSRFTokenAndCookie(proxyUrl: string): Promise { const response = await fetch(`${proxyUrl}/sign-in`) diff --git a/client/web/dev/utils/csrf/get-csrf-token-cookie-middleware.ts b/client/web/dev/utils/csrf/get-csrf-token-cookie-middleware.ts index 22cfb9ce45e63..1cdbc3da6e90c 100644 --- a/client/web/dev/utils/csrf/get-csrf-token-cookie-middleware.ts +++ b/client/web/dev/utils/csrf/get-csrf-token-cookie-middleware.ts @@ -1,6 +1,9 @@ import { RequestHandler } from 'express' +import { CSRF_COOKIE_NAME } from './get-csrf-token-and-cookie' + +// Attach `CSRF_COOKIE_NAME` cookie to every response to avoid "CSRF token is invalid" API error. export const getCSRFTokenCookieMiddleware = (csrfCookieValue: string): RequestHandler => (_request, response, next) => { - response.cookie('sg_csrf_token', csrfCookieValue, { httpOnly: true }) + response.cookie(CSRF_COOKIE_NAME, csrfCookieValue, { httpOnly: true }) next() } diff --git a/client/web/dev/utils/environment-config.ts b/client/web/dev/utils/environment-config.ts index 67bf44c8711ce..5cfa28748e471 100644 --- a/client/web/dev/utils/environment-config.ts +++ b/client/web/dev/utils/environment-config.ts @@ -1,4 +1,5 @@ export const environmentConfig = { + NODE_ENV: process.env.NODE_ENV || 'development', SOURCEGRAPH_API_URL: process.env.SOURCEGRAPH_API_URL || 'https://k8s.sgdev.org', SOURCEGRAPH_HTTPS_DOMAIN: process.env.SOURCEGRAPH_HTTPS_DOMAIN || 'sourcegraph.test', SOURCEGRAPH_HTTPS_PORT: Number(process.env.SOURCEGRAPH_HTTPS_PORT) || 3443, diff --git a/client/web/dev/utils/get-api-proxy-settings.ts b/client/web/dev/utils/get-api-proxy-settings.ts index 100ac7ead6e1d..a78e3b583eef1 100644 --- a/client/web/dev/utils/get-api-proxy-settings.ts +++ b/client/web/dev/utils/get-api-proxy-settings.ts @@ -10,14 +10,19 @@ interface GetAPIProxySettingsOptions { export const getAPIProxySettings = ({ csrfContextValue, apiURL }: GetAPIProxySettingsOptions): Options => ({ target: apiURL, + // Do not SSL certificate. secure: false, + // Change the origin of the host header to the target URL. changeOrigin: true, + // Attach `x-csrf-token` header to every request to avoid "CSRF token is invalid" API error. headers: { 'x-csrf-token': csrfContextValue, }, + // Rewrite domain of `set-cookie` headers for all cookies received. cookieDomainRewrite: '', onProxyRes: proxyResponse => { if (proxyResponse.headers['set-cookie']) { + // Remove `Secure` and `SameSite` from `set-cookie` headers. const cookies = proxyResponse.headers['set-cookie'].map(cookie => cookie.replace(/; secure/gi, '').replace(/; samesite=.+/gi, '') ) diff --git a/client/web/dev/utils/get-site-config.ts b/client/web/dev/utils/get-site-config.ts index e38dff8e23f52..6f9a8cfb267d9 100644 --- a/client/web/dev/utils/get-site-config.ts +++ b/client/web/dev/utils/get-site-config.ts @@ -1,7 +1,7 @@ import fs from 'fs' import path from 'path' -import { camelCase, mapKeys } from 'lodash' +import lodash from 'lodash' import stripJsonComments from 'strip-json-comments' import { SourcegraphContext } from '../../src/jscontext' @@ -10,12 +10,13 @@ import { ROOT_PATH } from './constants' const SITE_CONFIG_PATH = path.resolve(ROOT_PATH, '../dev-private/enterprise/dev/site-config.json') +// Get site-config from `SITE_CONFIG_PATH` as an object with camel cased keys. export const getSiteConfig = (): Partial => { try { // eslint-disable-next-line no-sync const siteConfig = JSON.parse(stripJsonComments(fs.readFileSync(SITE_CONFIG_PATH, 'utf-8'))) - return mapKeys(siteConfig, (_value, key) => camelCase(key)) + return lodash.mapKeys(siteConfig, (_value, key) => lodash.camelCase(key)) } catch (error) { console.log('Site config not found!', SITE_CONFIG_PATH) console.error(error) diff --git a/client/web/dev/webpack/get-html-webpack-plugins.ts b/client/web/dev/webpack/get-html-webpack-plugins.ts index 14ab191d0cd4d..ba03efb935d50 100644 --- a/client/web/dev/webpack/get-html-webpack-plugins.ts +++ b/client/web/dev/webpack/get-html-webpack-plugins.ts @@ -6,7 +6,7 @@ import { Plugin } from 'webpack' import { createJsContext, environmentConfig, STATIC_ASSETS_PATH } from '../utils' -const { SOURCEGRAPH_HTTPS_PORT, SOURCEGRAPH_API_URL } = environmentConfig +const { SOURCEGRAPH_HTTPS_PORT, SOURCEGRAPH_API_URL, NODE_ENV } = environmentConfig export const getHTMLWebpackPlugins = (): Plugin[] => { const jsContext = createJsContext({ sourcegraphBaseUrl: `http://localhost:${SOURCEGRAPH_HTTPS_PORT}` }) @@ -21,7 +21,10 @@ export const getHTMLWebpackPlugins = (): Plugin[] => {
From c7a44071c2f172d93afd928e4114ac733af50a1a Mon Sep 17 00:00:00 2001 From: Valery Bugakov Date: Wed, 28 Apr 2021 18:33:21 +0300 Subject: [PATCH 13/19] web: remove unused var --- client/web/dev/webpack/get-html-webpack-plugins.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/web/dev/webpack/get-html-webpack-plugins.ts b/client/web/dev/webpack/get-html-webpack-plugins.ts index 6dd776a555af9..ff1a705a629c0 100644 --- a/client/web/dev/webpack/get-html-webpack-plugins.ts +++ b/client/web/dev/webpack/get-html-webpack-plugins.ts @@ -6,7 +6,7 @@ import { Plugin } from 'webpack' import { createJsContext, environmentConfig, STATIC_ASSETS_PATH } from '../utils' -const { SOURCEGRAPH_HTTPS_PORT, SOURCEGRAPH_API_URL, NODE_ENV } = environmentConfig +const { SOURCEGRAPH_HTTPS_PORT, NODE_ENV } = environmentConfig export const getHTMLWebpackPlugins = (): Plugin[] => { const jsContext = createJsContext({ sourcegraphBaseUrl: `http://localhost:${SOURCEGRAPH_HTTPS_PORT}` }) From 81489e3d2a5969347268b70ad9b8201f4c4b2b2d Mon Sep 17 00:00:00 2001 From: Valery Bugakov Date: Wed, 28 Apr 2021 18:45:52 +0300 Subject: [PATCH 14/19] web: change readme.md --- client/web/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/web/README.md b/client/web/README.md index a466ef4e0d535..3d59eca733f09 100644 --- a/client/web/README.md +++ b/client/web/README.md @@ -2,7 +2,7 @@ ## Local development -### Prerequisites +### Configuration 1. Duplicate `client/web/.env.example` as `client/web/.env`. 2. Make sure that `WEBPACK_SERVE_INDEX` is set to `true` in the env file. From 582f9182a1b194e371d0a77e712d4a832a06f9a1 Mon Sep 17 00:00:00 2001 From: Valery Bugakov Date: Wed, 28 Apr 2021 18:53:58 +0300 Subject: [PATCH 15/19] web: move SITE_CONFIG_PATH to env config --- client/web/.env.example | 1 + client/web/dev/utils/constants.ts | 6 ------ client/web/dev/utils/environment-config.ts | 11 +++++++++++ client/web/dev/utils/get-site-config.ts | 5 ++--- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/client/web/.env.example b/client/web/.env.example index 8d4b381897f64..94678a2f73824 100644 --- a/client/web/.env.example +++ b/client/web/.env.example @@ -3,4 +3,5 @@ WEBPACK_SERVE_INDEX=true SOURCEGRAPH_API_URL=https://k8s.sgdev.org SOURCEGRAPH_HTTPS_DOMAIN=sourcegraph.test SOURCEGRAPH_HTTPS_PORT=3443 +# SITE_CONFIG_PATH=./site-config.json NO_HOT=false diff --git a/client/web/dev/utils/constants.ts b/client/web/dev/utils/constants.ts index e8d8b6c20ba01..261e2492e78cf 100644 --- a/client/web/dev/utils/constants.ts +++ b/client/web/dev/utils/constants.ts @@ -1,15 +1,9 @@ import path from 'path' -import { environmentConfig } from './environment-config' - -const { SOURCEGRAPH_HTTPS_PORT, SOURCEGRAPH_HTTPS_DOMAIN } = environmentConfig - export const ROOT_PATH = path.resolve(__dirname, '../../../../') export const STATIC_ASSETS_PATH = path.resolve(ROOT_PATH, 'ui/assets') export const STATIC_ASSETS_URL = '/.assets/' -export const WEB_SERVER_URL = `http://${SOURCEGRAPH_HTTPS_DOMAIN}:${SOURCEGRAPH_HTTPS_PORT}` - // TODO: share with gulpfile.js export const WEBPACK_STATS_OPTIONS = { all: false, diff --git a/client/web/dev/utils/environment-config.ts b/client/web/dev/utils/environment-config.ts index 5cfa28748e471..5790dca9f4cb9 100644 --- a/client/web/dev/utils/environment-config.ts +++ b/client/web/dev/utils/environment-config.ts @@ -1,10 +1,21 @@ +import path from 'path' + +import { ROOT_PATH } from './constants' + +const DEFAULT_SITE_CONFIG_PATH = path.resolve(ROOT_PATH, '../dev-private/enterprise/dev/site-config.json') + export const environmentConfig = { NODE_ENV: process.env.NODE_ENV || 'development', SOURCEGRAPH_API_URL: process.env.SOURCEGRAPH_API_URL || 'https://k8s.sgdev.org', SOURCEGRAPH_HTTPS_DOMAIN: process.env.SOURCEGRAPH_HTTPS_DOMAIN || 'sourcegraph.test', SOURCEGRAPH_HTTPS_PORT: Number(process.env.SOURCEGRAPH_HTTPS_PORT) || 3443, WEBPACK_SERVE_INDEX: process.env.WEBPACK_SERVE_INDEX === 'true', + SITE_CONFIG_PATH: process.env.SITE_CONFIG_PATH || DEFAULT_SITE_CONFIG_PATH, // TODO: do we use process.env.NO_HOT anywhere? IS_HOT_RELOAD_ENABLED: process.env.NO_HOT !== 'true', } + +const { SOURCEGRAPH_HTTPS_PORT, SOURCEGRAPH_HTTPS_DOMAIN } = environmentConfig + +export const WEB_SERVER_URL = `http://${SOURCEGRAPH_HTTPS_DOMAIN}:${SOURCEGRAPH_HTTPS_PORT}` diff --git a/client/web/dev/utils/get-site-config.ts b/client/web/dev/utils/get-site-config.ts index 7906043b74d86..fe3a90a0be91b 100644 --- a/client/web/dev/utils/get-site-config.ts +++ b/client/web/dev/utils/get-site-config.ts @@ -1,14 +1,13 @@ import fs from 'fs' -import path from 'path' import { parse } from '@sqs/jsonc-parser' import lodash from 'lodash' import { SourcegraphContext } from '../../src/jscontext' -import { ROOT_PATH } from './constants' +import { environmentConfig } from './environment-config' -const SITE_CONFIG_PATH = path.resolve(ROOT_PATH, '../dev-private/enterprise/dev/site-config.json') +const { SITE_CONFIG_PATH } = environmentConfig // Get site-config from `SITE_CONFIG_PATH` as an object with camel cased keys. export const getSiteConfig = (): Partial => { From 624fffaee81d163945f9cb2696ea89842d3a6c0a Mon Sep 17 00:00:00 2001 From: Valery Bugakov Date: Wed, 28 Apr 2021 18:55:35 +0300 Subject: [PATCH 16/19] web: add ENTERPRISE=1 to readme.md --- client/web/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/web/README.md b/client/web/README.md index 3d59eca733f09..b3b2d545a2317 100644 --- a/client/web/README.md +++ b/client/web/README.md @@ -11,7 +11,7 @@ ### Development server ```sh -yarn serve:dev +ENTERPRISE=1 yarn serve:dev ``` ### Production server From f8570549ba69baf10bda23e4585574f2885dca16 Mon Sep 17 00:00:00 2001 From: Valery Bugakov Date: Wed, 5 May 2021 10:45:43 +0300 Subject: [PATCH 17/19] web: add dev-web-server alert about proxied API --- client/web/src/global/GlobalAlerts.tsx | 15 +++++++++++++++ client/web/webpack.config.js | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/client/web/src/global/GlobalAlerts.tsx b/client/web/src/global/GlobalAlerts.tsx index 6de583140c858..9acacaddc5915 100644 --- a/client/web/src/global/GlobalAlerts.tsx +++ b/client/web/src/global/GlobalAlerts.tsx @@ -98,6 +98,21 @@ export class GlobalAlerts extends React.PureComponent { ))} + {process.env.SOURCEGRAPH_API_URL && ( + +
+ Warning! This build uses data from the proxied API:{' '} + + {process.env.SOURCEGRAPH_API_URL} + +
+ . +
+ )} Date: Wed, 5 May 2021 12:06:12 +0300 Subject: [PATCH 18/19] web: add web-standalone commands to the sg.config.yaml --- client/web/.env.example | 7 ---- client/web/README.md | 30 ++++++++++---- client/web/dev/server/development.server.ts | 2 - client/web/dev/server/production.server.ts | 2 - client/web/dev/utils/environment-config.ts | 1 + client/web/webpack.config.js | 2 - package.json | 1 - sg.config.yaml | 46 ++++++++++++++++++++- yarn.lock | 6 +-- 9 files changed, 71 insertions(+), 26 deletions(-) delete mode 100644 client/web/.env.example diff --git a/client/web/.env.example b/client/web/.env.example deleted file mode 100644 index 94678a2f73824..0000000000000 --- a/client/web/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -WEBPACK_SERVE_INDEX=true -# SOURCEGRAPH_API_URL=https://sourcegraph.com -SOURCEGRAPH_API_URL=https://k8s.sgdev.org -SOURCEGRAPH_HTTPS_DOMAIN=sourcegraph.test -SOURCEGRAPH_HTTPS_PORT=3443 -# SITE_CONFIG_PATH=./site-config.json -NO_HOT=false diff --git a/client/web/README.md b/client/web/README.md index b3b2d545a2317..2dad8d2d2cd02 100644 --- a/client/web/README.md +++ b/client/web/README.md @@ -2,23 +2,39 @@ ## Local development +Use `sg` CLI tool to configure and start local development server. For more information checkout `sg` [README]('../../dev/sg/README.md'). + ### Configuration -1. Duplicate `client/web/.env.example` as `client/web/.env`. -2. Make sure that `WEBPACK_SERVE_INDEX` is set to `true` in the env file. -3. Make sure that `SOURCEGRAPH_API_URL` points to the accessible API url in the env file. +Environment variables important for the web server: + +1. `WEBPACK_SERVE_INDEX` should be set to `true` to enable `HTMLWebpackPlugin`. +2. `SOURCEGRAPH_API_URL` is used as a proxied API url. By default it points to the [https://k8s.sgdev.org](https://k8s.sgdev.org). + +It's possible to overwrite these variables by creating `sg.config.overwrite.yaml` in the root folder and adjusting the `env` section of the relevant command. ### Development server ```sh -ENTERPRISE=1 yarn serve:dev +sg run web-standalone +``` + +For enterprise version: + +```sh +sg run enterprise-web-standalone ``` ### Production server ```sh -ENTERPRISE=1 NODE_ENV=production DISABLE_TYPECHECKING=true yarn run build -yarn serve:prod +sg run web-standalone-prod +``` + +For enterprise version: + +```sh +sg run enterprise-web-standalone-prod ``` Web app should be available at `http://${SOURCEGRAPH_HTTPS_DOMAIN}:${SOURCEGRAPH_HTTPS_PORT}`. @@ -26,6 +42,6 @@ Build artifacts will be served from `/ui/assets`. ### API proxy -In both environments server proxies API requests to `SOURCEGRAPH_API_URL` provided in the `.env` file. +In both environments, server proxies API requests to `SOURCEGRAPH_API_URL` provided as the `.env` variable. To avoid the `CSRF token is invalid` error CSRF token is retrieved from the `SOURCEGRAPH_API_URL` before the server starts. Then this value is used for every subsequent request to the API. diff --git a/client/web/dev/server/development.server.ts b/client/web/dev/server/development.server.ts index 4ebb2115bbb4d..b6fd14e940635 100644 --- a/client/web/dev/server/development.server.ts +++ b/client/web/dev/server/development.server.ts @@ -1,5 +1,3 @@ -import 'dotenv/config' - import chalk from 'chalk' import signale from 'signale' import createWebpackCompiler, { Configuration } from 'webpack' diff --git a/client/web/dev/server/production.server.ts b/client/web/dev/server/production.server.ts index 88a8d3ce80c4c..16de7545f1587 100644 --- a/client/web/dev/server/production.server.ts +++ b/client/web/dev/server/production.server.ts @@ -1,5 +1,3 @@ -import 'dotenv/config' - import chalk from 'chalk' import historyApiFallback from 'connect-history-api-fallback' import express, { RequestHandler } from 'express' diff --git a/client/web/dev/utils/environment-config.ts b/client/web/dev/utils/environment-config.ts index 5790dca9f4cb9..ff2e343519c48 100644 --- a/client/web/dev/utils/environment-config.ts +++ b/client/web/dev/utils/environment-config.ts @@ -11,6 +11,7 @@ export const environmentConfig = { SOURCEGRAPH_HTTPS_PORT: Number(process.env.SOURCEGRAPH_HTTPS_PORT) || 3443, WEBPACK_SERVE_INDEX: process.env.WEBPACK_SERVE_INDEX === 'true', SITE_CONFIG_PATH: process.env.SITE_CONFIG_PATH || DEFAULT_SITE_CONFIG_PATH, + ENTERPRISE: Boolean(process.env.ENTERPRISE), // TODO: do we use process.env.NO_HOT anywhere? IS_HOT_RELOAD_ENABLED: process.env.NO_HOT !== 'true', diff --git a/client/web/webpack.config.js b/client/web/webpack.config.js index f153f9bc6b10b..81110da50310a 100644 --- a/client/web/webpack.config.js +++ b/client/web/webpack.config.js @@ -1,7 +1,5 @@ // @ts-check -// Pick up configuration from `.env` file. -require('dotenv/config') const path = require('path') const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin') diff --git a/package.json b/package.json index 3b7c9e08cf8b1..5070b4e0985cb 100644 --- a/package.json +++ b/package.json @@ -211,7 +211,6 @@ "cross-env": "^7.0.2", "css-loader": "^5.2.4", "css-minimizer-webpack-plugin": "^1.3.0", - "dotenv": "^8.2.0", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.4", "enzyme-to-json": "^3.5.0", diff --git a/sg.config.yaml b/sg.config.yaml index e0568e958ca14..36b88dcce983d 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -177,7 +177,6 @@ commands: cmd: ./node_modules/.bin/gulp --silent --color dev install: yarn --no-progress env: - WEBPACK_DEV_SERVER: 1 NODE_ENV: development NODE_OPTIONS: "--max_old_space_size=4096" @@ -186,10 +185,53 @@ commands: install: yarn --no-progress env: ENTERPRISE: 1 - WEBPACK_DEV_SERVER: 1 NODE_ENV: development NODE_OPTIONS: "--max_old_space_size=4096" + web-standalone: + cmd: yarn workspace @sourcegraph/web serve:dev + env: + NODE_ENV: development + NODE_OPTIONS: "--max_old_space_size=4096" + WEBPACK_SERVE_INDEX: true + SOURCEGRAPH_API_URL: https://k8s.sgdev.org + SOURCEGRAPH_HTTPS_DOMAIN: sourcegraph.test + SOURCEGRAPH_HTTPS_PORT: 3443 + + enterprise-web-standalone: + cmd: yarn workspace @sourcegraph/web serve:dev + env: + ENTERPRISE: 1 + NODE_ENV: development + NODE_OPTIONS: "--max_old_space_size=4096" + WEBPACK_SERVE_INDEX: true + SOURCEGRAPH_API_URL: https://k8s.sgdev.org + SOURCEGRAPH_HTTPS_DOMAIN: sourcegraph.test + SOURCEGRAPH_HTTPS_PORT: 3443 + + web-standalone-prod: + cmd: yarn workspace @sourcegraph/web serve:prod + install: yarn workspace @sourcegraph/web run build + env: + NODE_ENV: production + NODE_OPTIONS: "--max_old_space_size=4096" + WEBPACK_SERVE_INDEX: true + SOURCEGRAPH_API_URL: https://k8s.sgdev.org + SOURCEGRAPH_HTTPS_DOMAIN: sourcegraph.test + SOURCEGRAPH_HTTPS_PORT: 3443 + + enterprise-web-standalone-prod: + cmd: yarn workspace @sourcegraph/web serve:prod + install: yarn workspace @sourcegraph/web run build + env: + ENTERPRISE: 1 + NODE_ENV: production + NODE_OPTIONS: "--max_old_space_size=4096" + WEBPACK_SERVE_INDEX: true + SOURCEGRAPH_API_URL: https://k8s.sgdev.org + SOURCEGRAPH_HTTPS_DOMAIN: sourcegraph.test + SOURCEGRAPH_HTTPS_PORT: 3443 + docsite: cmd: .bin/docsite_${VERSION} -config doc/docsite.json serve -http=localhost:5080 install: | diff --git a/yarn.lock b/yarn.lock index 847a95d9583a3..27355d846c10e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22063,9 +22063,9 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: integrity sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg== uglify-js@^3.1.4: - version "3.13.4" - resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.4.tgz#592588bb9f47ae03b24916e2471218d914955574" - integrity sha512-kv7fCkIXyQIilD5/yQy8O+uagsYIOt5cZvs890W40/e/rvjMSzJw81o9Bg0tkURxzZBROtDQhW2LFjOGoK3RZw== + version "3.13.5" + resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.5.tgz#5d71d6dbba64cf441f32929b1efce7365bb4f113" + integrity sha512-xtB8yEqIkn7zmOyS2zUNBsYCBRhDkvlNxMMY2smuJ/qA8NCHeQvKCF3i9Z4k8FJH4+PJvZRtMrPynfZ75+CSZw== unbox-primitive@^1.0.0: version "1.0.1" From 2dac0744cd2ec1db062b8e927fb8c52dce3fa69f Mon Sep 17 00:00:00 2001 From: Valery Bugakov Date: Wed, 5 May 2021 14:10:38 +0300 Subject: [PATCH 19/19] web: extract shared env variables to the top level `env` --- sg.config.yaml | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/sg.config.yaml b/sg.config.yaml index 36b88dcce983d..83fc19720d6e1 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -44,6 +44,15 @@ env: CODEINTEL_PGDATASOURCE: $PGDATASOURCE CODEINTEL_PG_ALLOW_SINGLE_DB: true + # Required for `frontend` and `web` commands + SOURCEGRAPH_HTTPS_DOMAIN: sourcegraph.test + SOURCEGRAPH_HTTPS_PORT: 3443 + + # Required for `web` commands + NODE_OPTIONS: "--max_old_space_size=4096" + # Default `NODE_ENV` to `development` + NODE_ENV: development + commands: frontend: cmd: ulimit -n 10000 && .bin/frontend @@ -170,55 +179,37 @@ commands: fi env: CADDY_VERSION: 2.3.0 - SOURCEGRAPH_HTTPS_DOMAIN: sourcegraph.test - SOURCEGRAPH_HTTPS_PORT: 3443 web: cmd: ./node_modules/.bin/gulp --silent --color dev install: yarn --no-progress - env: - NODE_ENV: development - NODE_OPTIONS: "--max_old_space_size=4096" enterprise-web: cmd: ./node_modules/.bin/gulp --silent --color dev install: yarn --no-progress env: ENTERPRISE: 1 - NODE_ENV: development - NODE_OPTIONS: "--max_old_space_size=4096" web-standalone: cmd: yarn workspace @sourcegraph/web serve:dev env: - NODE_ENV: development - NODE_OPTIONS: "--max_old_space_size=4096" WEBPACK_SERVE_INDEX: true SOURCEGRAPH_API_URL: https://k8s.sgdev.org - SOURCEGRAPH_HTTPS_DOMAIN: sourcegraph.test - SOURCEGRAPH_HTTPS_PORT: 3443 enterprise-web-standalone: cmd: yarn workspace @sourcegraph/web serve:dev env: ENTERPRISE: 1 - NODE_ENV: development - NODE_OPTIONS: "--max_old_space_size=4096" WEBPACK_SERVE_INDEX: true SOURCEGRAPH_API_URL: https://k8s.sgdev.org - SOURCEGRAPH_HTTPS_DOMAIN: sourcegraph.test - SOURCEGRAPH_HTTPS_PORT: 3443 web-standalone-prod: cmd: yarn workspace @sourcegraph/web serve:prod install: yarn workspace @sourcegraph/web run build env: NODE_ENV: production - NODE_OPTIONS: "--max_old_space_size=4096" WEBPACK_SERVE_INDEX: true SOURCEGRAPH_API_URL: https://k8s.sgdev.org - SOURCEGRAPH_HTTPS_DOMAIN: sourcegraph.test - SOURCEGRAPH_HTTPS_PORT: 3443 enterprise-web-standalone-prod: cmd: yarn workspace @sourcegraph/web serve:prod @@ -226,11 +217,8 @@ commands: env: ENTERPRISE: 1 NODE_ENV: production - NODE_OPTIONS: "--max_old_space_size=4096" WEBPACK_SERVE_INDEX: true SOURCEGRAPH_API_URL: https://k8s.sgdev.org - SOURCEGRAPH_HTTPS_DOMAIN: sourcegraph.test - SOURCEGRAPH_HTTPS_PORT: 3443 docsite: cmd: .bin/docsite_${VERSION} -config doc/docsite.json serve -http=localhost:5080