From 83270ffdc9bc59ca4c72ca7c0b051a6e8a591ed6 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Mon, 18 Nov 2024 12:09:26 -0800 Subject: [PATCH] Add support for configurable domain sub-paths (#74) --- CHANGELOG.md | 4 ++ Dockerfile | 7 ++ entrypoint.sh | 74 +++++++++++++++++---- packages/web/next.config.mjs | 7 +- packages/web/src/app/api/(client)/client.ts | 21 +++++- packages/web/src/app/posthogProvider.tsx | 6 +- packages/web/src/lib/environment.client.ts | 1 + 7 files changed, 101 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65f65ce8..413f1591 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added `DOMAIN_SUB_PATH` environment variable to allow overriding the default domain subpath. ([#74](https://github.com/sourcebot-dev/sourcebot/pull/74)) + ## [2.4.3] - 2024-11-18 ### Changed diff --git a/Dockerfile b/Dockerfile index 47067d3e..79e3b452 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,8 @@ ENV NEXT_TELEMETRY_DISABLED=1 # @see: https://phase.dev/blog/nextjs-public-runtime-variables/ ARG NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED=BAKED_NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED ARG NEXT_PUBLIC_SOURCEBOT_VERSION=BAKED_NEXT_PUBLIC_SOURCEBOT_VERSION +# @note: leading "/" is required for the basePath property. @see: https://nextjs.org/docs/app/api-reference/next-config-js/basePath +ARG NEXT_PUBLIC_DOMAIN_SUB_PATH=/BAKED_NEXT_PUBLIC_DOMAIN_SUB_PATH RUN yarn workspace @sourcebot/web build # ------ Build Backend ------ @@ -54,6 +56,11 @@ RUN echo "Sourcebot Version: $SOURCEBOT_VERSION" # Valid values are: debug, info, warn, error ENV SOURCEBOT_LOG_LEVEL=info +# Configures the sub-path of the domain to serve Sourcebot from. +# For example, if DOMAIN_SUB_PATH is set to "/sb", Sourcebot +# will serve from http(s)://example.com/sb +ENV DOMAIN_SUB_PATH=/ + # @note: This is also set in .env ENV POSTHOG_KEY=phc_VFn4CkEGHRdlVyOOw8mfkoj1DKVoG6y1007EClvzAnS ENV NEXT_PUBLIC_POSTHOG_KEY=$POSTHOG_KEY diff --git a/entrypoint.sh b/entrypoint.sh index c2a0b0aa..c9663ef1 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -70,23 +70,69 @@ fi echo -e "\e[34m[Info] Using config file at: '$CONFIG_PATH'.\e[0m" -# Update nextjs public env variables w/o requiring a rebuild. +# Update NextJs public env variables w/o requiring a rebuild. # @see: https://phase.dev/blog/nextjs-public-runtime-variables/ +{ + # Infer NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED if it is not set + if [ -z "$NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED" ] && [ ! -z "$SOURCEBOT_TELEMETRY_DISABLED" ]; then + export NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED="$SOURCEBOT_TELEMETRY_DISABLED" + fi -# Infer NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED if it is not set -if [ -z "$NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED" ] && [ ! -z "$SOURCEBOT_TELEMETRY_DISABLED" ]; then - export NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED="$SOURCEBOT_TELEMETRY_DISABLED" -fi + # Infer NEXT_PUBLIC_SOURCEBOT_VERSION if it is not set + if [ -z "$NEXT_PUBLIC_SOURCEBOT_VERSION" ] && [ ! -z "$SOURCEBOT_VERSION" ]; then + export NEXT_PUBLIC_SOURCEBOT_VERSION="$SOURCEBOT_VERSION" + fi -# Infer NEXT_PUBLIC_SOURCEBOT_VERSION if it is not set -if [ -z "$NEXT_PUBLIC_SOURCEBOT_VERSION" ] && [ ! -z "$SOURCEBOT_VERSION" ]; then - export NEXT_PUBLIC_SOURCEBOT_VERSION="$SOURCEBOT_VERSION" -fi + # Iterate over all .js files in .next & public, making substitutions for the `BAKED_` sentinal values + # with their actual desired runtime value. + find /app/packages/web/public /app/packages/web/.next -type f -name "*.js" | + while read file; do + sed -i "s|BAKED_NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED|${NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED}|g" "$file" + sed -i "s|BAKED_NEXT_PUBLIC_SOURCEBOT_VERSION|${NEXT_PUBLIC_SOURCEBOT_VERSION}|g" "$file" + done +} -find /app/packages/web/public /app/packages/web/.next -type f -name "*.js" | -while read file; do - sed -i "s|BAKED_NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED|${NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED}|g" "$file" - sed -i "s|BAKED_NEXT_PUBLIC_SOURCEBOT_VERSION|${NEXT_PUBLIC_SOURCEBOT_VERSION}|g" "$file" -done +# Update specifically NEXT_PUBLIC_DOMAIN_SUB_PATH w/o requiring a rebuild. +# Ultimately, the DOMAIN_SUB_PATH sets the `basePath` param in the next.config.mjs. +# Similar to above, we pass in a `BAKED_` sentinal value into next.config.mjs at build +# time. Unlike above, the `basePath` configuration is set in files other than just javascript +# code (e.g., manifest files, css files, etc.), so this section has subtle differences. +# +# @see: https://nextjs.org/docs/app/api-reference/next-config-js/basePath +# @see: https://phase.dev/blog/nextjs-public-runtime-variables/ +{ + if [ ! -z "$DOMAIN_SUB_PATH" ]; then + # If the sub-path is "/", this creates problems with certain replacements. For example: + # /BAKED_NEXT_PUBLIC_DOMAIN_SUB_PATH/_next/image -> //_next/image (notice the double slash...) + # To get around this, we default to an empty sub-path, which is the default when no sub-path is defined. + if [ "$DOMAIN_SUB_PATH" = "/" ]; then + DOMAIN_SUB_PATH="" + + # Otherwise, we need to ensure that the sub-path starts with a slash, since this is a requirement + # for the basePath property. For example, assume DOMAIN_SUB_PATH=/bot, then: + # /BAKED_NEXT_PUBLIC_DOMAIN_SUB_PATH/_next/image -> /bot/_next/image + elif [[ ! "$DOMAIN_SUB_PATH" =~ ^/ ]]; then + DOMAIN_SUB_PATH="/$DOMAIN_SUB_PATH" + fi + fi + + if [ ! -z "$DOMAIN_SUB_PATH" ]; then + echo -e "\e[34m[Info] DOMAIN_SUB_PATH was set to "$DOMAIN_SUB_PATH". Overriding default path.\e[0m" + fi + + # Always set NEXT_PUBLIC_DOMAIN_SUB_PATH to DOMAIN_SUB_PATH (even if it is empty!!) + export NEXT_PUBLIC_DOMAIN_SUB_PATH="$DOMAIN_SUB_PATH" + + # Iterate over _all_ files in the web directory, making substitutions for the `BAKED_` sentinal values + # with their actual desired runtime value. + find /app/packages/web -type f | + while read file; do + # @note: the leading "/" is required here as it is included at build time. See Dockerfile. + sed -i "s|/BAKED_NEXT_PUBLIC_DOMAIN_SUB_PATH|${NEXT_PUBLIC_DOMAIN_SUB_PATH}|g" "$file" + done +} + + +# Run supervisord exec supervisord -c /etc/supervisor/conf.d/supervisord.conf \ No newline at end of file diff --git a/packages/web/next.config.mjs b/packages/web/next.config.mjs index a44d1189..fe7eda84 100644 --- a/packages/web/next.config.mjs +++ b/packages/web/next.config.mjs @@ -20,7 +20,12 @@ const nextConfig = { ]; }, // This is required to support PostHog trailing slash API requests - skipTrailingSlashRedirect: true, + skipTrailingSlashRedirect: true, + + // @note: this is evaluated at build time. + ...(process.env.NEXT_PUBLIC_DOMAIN_SUB_PATH ? { + basePath: process.env.NEXT_PUBLIC_DOMAIN_SUB_PATH, + } : {}) }; export default nextConfig; diff --git a/packages/web/src/app/api/(client)/client.ts b/packages/web/src/app/api/(client)/client.ts index a2345f72..840bee56 100644 --- a/packages/web/src/app/api/(client)/client.ts +++ b/packages/web/src/app/api/(client)/client.ts @@ -1,8 +1,12 @@ +'use client'; + +import { NEXT_PUBLIC_DOMAIN_SUB_PATH } from "@/lib/environment.client"; import { fileSourceResponseSchema, listRepositoriesResponseSchema, searchResponseSchema } from "@/lib/schemas"; import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "@/lib/types"; export const search = async (body: SearchRequest): Promise => { - const result = await fetch(`/api/search`, { + const path = resolveServerPath("/api/search"); + const result = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json", @@ -14,7 +18,8 @@ export const search = async (body: SearchRequest): Promise => { } export const fetchFileSource = async (body: FileSourceRequest): Promise => { - const result = await fetch(`/api/source`, { + const path = resolveServerPath("/api/source"); + const result = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json", @@ -26,7 +31,8 @@ export const fetchFileSource = async (body: FileSourceRequest): Promise => { - const result = await fetch('/api/repos', { + const path = resolveServerPath("/api/repos"); + const result = await fetch(path, { method: "GET", headers: { "Content-Type": "application/json", @@ -35,3 +41,12 @@ export const getRepos = async (): Promise => { return listRepositoriesResponseSchema.parse(result); } + +/** + * Given a subpath to a api route on the server (e.g., /api/search), + * returns the full path to that route on the server, taking into account + * the base path (if any). + */ +export const resolveServerPath = (path: string) => { + return `${NEXT_PUBLIC_DOMAIN_SUB_PATH}${path}`; +} \ No newline at end of file diff --git a/packages/web/src/app/posthogProvider.tsx b/packages/web/src/app/posthogProvider.tsx index c45736ab..1cee74f1 100644 --- a/packages/web/src/app/posthogProvider.tsx +++ b/packages/web/src/app/posthogProvider.tsx @@ -2,11 +2,15 @@ import { NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_UI_HOST, NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED } from '@/lib/environment.client' import posthog from 'posthog-js' import { PostHogProvider } from 'posthog-js/react' +import { resolveServerPath } from './api/(client)/client' if (typeof window !== 'undefined') { if (!NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED) { + // @see next.config.mjs for path rewrites to the "/ingest" route. + const posthogHostPath = resolveServerPath('/ingest'); + posthog.init(NEXT_PUBLIC_POSTHOG_KEY!, { - api_host: "/ingest", + api_host: posthogHostPath, ui_host: NEXT_PUBLIC_POSTHOG_UI_HOST, person_profiles: 'identified_only', capture_pageview: false, // Disable automatic pageview capture diff --git a/packages/web/src/lib/environment.client.ts b/packages/web/src/lib/environment.client.ts index fcb22b4b..85251888 100644 --- a/packages/web/src/lib/environment.client.ts +++ b/packages/web/src/lib/environment.client.ts @@ -8,3 +8,4 @@ export const NEXT_PUBLIC_POSTHOG_UI_HOST = getEnv(process.env.NEXT_PUBLIC_POSTHO export const NEXT_PUBLIC_POSTHOG_ASSET_HOST = getEnv(process.env.NEXT_PUBLIC_POSTHOG_ASSET_HOST); export const NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED = getEnvBoolean(process.env.NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED, false); export const NEXT_PUBLIC_SOURCEBOT_VERSION = getEnv(process.env.NEXT_PUBLIC_SOURCEBOT_VERSION, "unknown"); +export const NEXT_PUBLIC_DOMAIN_SUB_PATH = getEnv(process.env.NEXT_PUBLIC_DOMAIN_SUB_PATH, "");