From 52b36d70191ef8af98e461901bebccb0298d030f Mon Sep 17 00:00:00 2001 From: nighca Date: Tue, 23 Jun 2026 16:47:43 +0800 Subject: [PATCH] Env by app --- spx-gui/.env | 19 ++++++- spx-gui/src/apis/account/oauth.ts | 17 +++--- spx-gui/src/apis/common/client.ts | 23 +++++--- spx-gui/src/apis/common/index.ts | 17 +++--- spx-gui/src/apis/file.ts | 18 ++++++- spx-gui/src/apps/account/env.ts | 19 +++++++ spx-gui/src/apps/account/main.ts | 5 +- spx-gui/src/apps/xbuilder/config.ts | 12 +++++ spx-gui/src/apps/xbuilder/env.ts | 35 +++++++++++++ spx-gui/src/apps/xbuilder/main.ts | 5 +- spx-gui/src/apps/xbuilder/pages/docs/api.vue | 4 +- .../asset/preprocessing/PreprocessModal.vue | 3 +- .../components/community/CommunityNavbar.vue | 3 +- .../community/footer/CommunityFooter.vue | 4 +- .../components/editor/navbar/EditorNavbar.vue | 4 +- .../project/runner/ProjectRunner.vue | 32 ++++++++---- spx-gui/src/models/common/cloud.ts | 17 +++++- spx-gui/src/setup/i18n.ts | 9 ++-- spx-gui/src/setup/index.ts | 26 +++++++--- spx-gui/src/setup/sentry.ts | 15 ++++-- spx-gui/src/stores/user/signed-in.ts | 40 ++++++++++---- spx-gui/src/utils/env.ts | 52 ------------------- spx-gui/src/widgets/spx-runner/index.ts | 5 +- spx-gui/src/widgets/xgo-code-editor/index.ts | 5 +- spx-gui/vercel-build.sh | 18 +++++++ 25 files changed, 280 insertions(+), 127 deletions(-) create mode 100644 spx-gui/src/apps/account/env.ts create mode 100644 spx-gui/src/apps/xbuilder/config.ts create mode 100644 spx-gui/src/apps/xbuilder/env.ts delete mode 100644 spx-gui/src/utils/env.ts diff --git a/spx-gui/.env b/spx-gui/.env index 66e1a83c74..dd85f69cea 100644 --- a/spx-gui/.env +++ b/spx-gui/.env @@ -3,6 +3,8 @@ # set in the corresponding `.env.[mode]` files. However, for the ones that # require default values, such as feature flags, they are set here. +# App xbuilder config. + # Base URL for the spx-backend API. # # Required. @@ -85,7 +87,22 @@ VITE_ACCOUNT_API_PROXY_TARGET="" # accordingly. VITE_ACCOUNT_WEB_ORIGIN="" -# Account OAuth configuration for the main-site hosted sign-in flow. +# Account OAuth configuration used by app xbuilder for the main-site hosted +# sign-in flow. # # Required when enabling the new Account OAuth flow. VITE_ACCOUNT_OAUTH_CLIENT_ID="" + +# App account config. +# +# These settings are intentionally separate from the app xbuilder settings +# above. App account should not inherit app xbuilder Sentry, language, or feature +# configuration just because both apps are built from this package. + +# Sentry configuration for app account. +VITE_ACCOUNT_SENTRY_DSN="" +VITE_ACCOUNT_SENTRY_TRACES_SAMPLE_RATE="0.8" +VITE_ACCOUNT_SENTRY_LSP_SAMPLE_RATE="0.1" + +# Default language for app account, e.g. `en`, `zh`. +VITE_ACCOUNT_DEFAULT_LANG="en" diff --git a/spx-gui/src/apis/account/oauth.ts b/spx-gui/src/apis/account/oauth.ts index a8b7657237..add8671a53 100644 --- a/spx-gui/src/apis/account/oauth.ts +++ b/spx-gui/src/apis/account/oauth.ts @@ -1,5 +1,4 @@ import { Client } from '@/apis/common/client' -import { apiBaseUrl } from '@/utils/env' import type { OAuthAPIs } from '@/utils/oauth' import { accountClient } from './common' @@ -85,11 +84,17 @@ class AccountOAuthApis implements OAuthAPIs { } /** Account OAuth APIs for use by app xbuilder. */ -export const accountOAuthApisForXBuilder = new AccountOAuthApis( - new Client({ - baseUrl: apiBaseUrl + '/account' - }) -) +const accountOAuthClientForXBuilder = new Client({}) + +export const accountOAuthApisForXBuilder = new AccountOAuthApis(accountOAuthClientForXBuilder) + +export type AccountOAuthAPIsForXBuilderConfig = { + apiBaseUrl: string +} + +export function configureAccountOAuthAPIsForXBuilder(config: AccountOAuthAPIsForXBuilderConfig) { + accountOAuthClientForXBuilder.setBaseUrl(config.apiBaseUrl + '/account') +} /** Account OAuth APIs for use by app account. */ export const accountOAuthApis = new AccountOAuthApis(accountClient) diff --git a/spx-gui/src/apis/common/client.ts b/spx-gui/src/apis/common/client.ts index 36b383db0e..e311fad727 100644 --- a/spx-gui/src/apis/common/client.ts +++ b/spx-gui/src/apis/common/client.ts @@ -65,13 +65,13 @@ export type JSONSSEEvent = { } export type ClientOptions = { - baseUrl: string + baseUrl?: string fetchFn?: typeof fetch } export class Client { constructor(options: ClientOptions) { - this.baseUrl = options.baseUrl + this.baseUrl = options.baseUrl ?? null this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis) } @@ -80,13 +80,22 @@ export class Client { this.tokenProvider = provider } - private baseUrl: string + setBaseUrl(baseUrl: string) { + this.baseUrl = baseUrl + } + + private baseUrl: string | null private fetchFn: typeof fetch private defaultTimeout = 10 * 1000 // 10 seconds + private getBaseUrl() { + if (this.baseUrl == null) throw new Error('API client base URL is not set') + return this.baseUrl + } + /** Get full URL for a given API path */ urlFor(path: string) { - const concated = this.baseUrl + path + const concated = this.getBaseUrl() + path return new URL(concated, window.location.origin) } @@ -103,7 +112,7 @@ export class Client { const traceData = Sentry.getTraceData() const sentryTraceHeader = traceData['sentry-trace'] const sentryBaggageHeader = traceData['baggage'] - const url = this.baseUrl + path + const url = this.getBaseUrl() + path const method = options?.method ?? 'GET' const body = payload != null ? JSON.stringify(payload) : null const headers = options?.headers ?? new Headers() @@ -119,7 +128,7 @@ export class Client { const traceData = Sentry.getTraceData() const sentryTraceHeader = traceData['sentry-trace'] const sentryBaggageHeader = traceData['baggage'] - const url = this.baseUrl + path + const url = this.getBaseUrl() + path const method = options?.method ?? 'POST' const body = new URLSearchParams() Object.entries(payload).forEach(([key, value]) => { @@ -178,7 +187,7 @@ export class Client { const traceData = Sentry.getTraceData() const sentryTraceHeader = traceData['sentry-trace'] const sentryBaggageHeader = traceData['baggage'] - const url = this.baseUrl + path + const url = this.getBaseUrl() + path const method = options?.method ?? 'GET' const headers = options?.headers ?? new Headers() await this.injectAuthorization(headers, options?.signal) diff --git a/spx-gui/src/apis/common/index.ts b/spx-gui/src/apis/common/index.ts index 34ac225ae4..8429024dc2 100644 --- a/spx-gui/src/apis/common/index.ts +++ b/spx-gui/src/apis/common/index.ts @@ -1,4 +1,3 @@ -import { apiBaseUrl } from '@/utils/env' import { Client } from './client' export type PaginationParams = { @@ -39,13 +38,17 @@ export function timeStringify(time: number) { /** * The default client instance for app XBuilder to make requests to spx-backend APIs. - * Requests made through this client will have the base URL set to `apiBaseUrl` from environment variables. - * The token provider is expected to be set separately on app initialization (see details in setup.ts) - * so credentials will be included in requests. + * The base URL and token provider are expected to be configured on app initialization. */ -export const client = new Client({ - baseUrl: apiBaseUrl -}) +export const client = new Client({}) + +export type CommonAPIConfig = { + apiBaseUrl: string +} + +export function configureCommonAPIs(config: CommonAPIConfig) { + client.setBaseUrl(config.apiBaseUrl) +} /** Art style indicates the visual style or aesthetic approach used in the creation of graphics */ export const enum ArtStyle { diff --git a/spx-gui/src/apis/file.ts b/spx-gui/src/apis/file.ts index a3bf3b522b..7d543c9220 100644 --- a/spx-gui/src/apis/file.ts +++ b/spx-gui/src/apis/file.ts @@ -2,7 +2,6 @@ * @desc File-related APIs of spx-backend */ -import { usercontentBaseUrl, usercontentBucket } from '@/utils/env' import { client, type UniversalUrl, type UniversalToWebUrlMap } from './common' import { UniversalUrlScheme, parseUniversalUrl } from '@/utils/universal-url' @@ -19,6 +18,22 @@ export type UploadSession = { region: string } +export type FileAPIConfig = { + usercontentBaseUrl: string + usercontentBucket: string +} + +let fileAPIConfig: FileAPIConfig | null = null + +export function configureFileAPIs(config: FileAPIConfig) { + fileAPIConfig = config +} + +function getFileAPIConfig() { + if (fileAPIConfig == null) throw new Error('File API config is not set') + return fileAPIConfig +} + export function createUploadSession() { return client.post('/upload-sessions') as Promise } @@ -33,6 +48,7 @@ export async function createFileURLSignatures(objects: UniversalUrl[]): Promise< /** Workaround for https://github.com/goplus/builder/issues/1598 */ function workAroundIssue1598(objects: UniversalUrl[]): UniversalToWebUrlMap { + const { usercontentBaseUrl, usercontentBucket } = getFileAPIConfig() return objects.reduce((map, universalUrl) => { const parsed = parseUniversalUrl(universalUrl) if (parsed.scheme === UniversalUrlScheme.Kodo) { diff --git a/spx-gui/src/apps/account/env.ts b/spx-gui/src/apps/account/env.ts new file mode 100644 index 0000000000..40979d125e --- /dev/null +++ b/spx-gui/src/apps/account/env.ts @@ -0,0 +1,19 @@ +import type { SentryConfig } from '@/setup/sentry' + +export type AccountEnv = { + defaultLang: string + sentry: SentryConfig +} + +function parseSampleRate(value: string, fallback: number) { + return parseFloat(value) || fallback +} + +export const accountEnv: AccountEnv = { + defaultLang: (import.meta.env.VITE_ACCOUNT_DEFAULT_LANG as string) || 'en', + sentry: { + dsn: (import.meta.env.VITE_ACCOUNT_SENTRY_DSN as string) || '', + tracesSampleRate: parseSampleRate(import.meta.env.VITE_ACCOUNT_SENTRY_TRACES_SAMPLE_RATE as string, 0.1), + lspSampleRate: parseSampleRate(import.meta.env.VITE_ACCOUNT_SENTRY_LSP_SAMPLE_RATE as string, 0.1) + } +} diff --git a/spx-gui/src/apps/account/main.ts b/spx-gui/src/apps/account/main.ts index b3fd95195a..71d48ed1a7 100644 --- a/spx-gui/src/apps/account/main.ts +++ b/spx-gui/src/apps/account/main.ts @@ -6,6 +6,7 @@ import { initDayjs } from '@/setup/dayjs' import { initI18n } from '@/setup/i18n' import { initSentry } from '@/setup/sentry' +import { accountEnv } from './env' import App from './App.vue' import router from './router' @@ -15,7 +16,7 @@ import router from './router' initDayjs() const app = createApp(App) -initSentry(app, router) -initI18n(app) +initSentry(app, router, accountEnv.sentry) +initI18n(app, accountEnv) app.use(router) app.mount('#app') diff --git a/spx-gui/src/apps/xbuilder/config.ts b/spx-gui/src/apps/xbuilder/config.ts new file mode 100644 index 0000000000..bca02377dc --- /dev/null +++ b/spx-gui/src/apps/xbuilder/config.ts @@ -0,0 +1,12 @@ +import type { XBuilderEnv } from './env' + +let xbuilderConfig: XBuilderEnv | null = null + +export function configureXBuilder(config: XBuilderEnv) { + xbuilderConfig = config +} + +export function getXBuilderConfig() { + if (xbuilderConfig == null) throw new Error('XBuilder config is not set') + return xbuilderConfig +} diff --git a/spx-gui/src/apps/xbuilder/env.ts b/spx-gui/src/apps/xbuilder/env.ts new file mode 100644 index 0000000000..390585bb71 --- /dev/null +++ b/spx-gui/src/apps/xbuilder/env.ts @@ -0,0 +1,35 @@ +import type { SentryConfig } from '@/setup/sentry' + +export type XBuilderEnv = { + apiBaseUrl: string + usercontentBaseUrl: string + usercontentBucket: string + disableAIGC: boolean + spxVersion: string + showLicense: boolean + showTutorialsEntry: boolean + defaultLang: string + accountOAuthClientId: string + sentry: SentryConfig +} + +function parseSampleRate(value: string, fallback: number) { + return parseFloat(value) || fallback +} + +export const xbuilderEnv: XBuilderEnv = { + apiBaseUrl: import.meta.env.VITE_API_BASE_URL as string, + usercontentBaseUrl: import.meta.env.VITE_USERCONTENT_BASE_URL as string, + usercontentBucket: import.meta.env.VITE_USERCONTENT_BUCKET as string, + disableAIGC: import.meta.env.VITE_DISABLE_AIGC === 'true', + spxVersion: import.meta.env.VITE_SPX_VERSION as string, + showLicense: import.meta.env.VITE_SHOW_LICENSE === 'true', + showTutorialsEntry: import.meta.env.VITE_SHOW_TUTORIALS_ENTRY === 'true', + defaultLang: (import.meta.env.VITE_DEFAULT_LANG as string) || 'en', + accountOAuthClientId: import.meta.env.VITE_ACCOUNT_OAUTH_CLIENT_ID as string, + sentry: { + dsn: (import.meta.env.VITE_SENTRY_DSN as string) || '', + tracesSampleRate: parseSampleRate(import.meta.env.VITE_SENTRY_TRACES_SAMPLE_RATE as string, 0.1), + lspSampleRate: parseSampleRate(import.meta.env.VITE_SENTRY_LSP_SAMPLE_RATE as string, 0.1) + } +} diff --git a/spx-gui/src/apps/xbuilder/main.ts b/spx-gui/src/apps/xbuilder/main.ts index 26e7cce983..3ed9e4b16e 100644 --- a/spx-gui/src/apps/xbuilder/main.ts +++ b/spx-gui/src/apps/xbuilder/main.ts @@ -2,11 +2,12 @@ import '@/polyfills' import '@/app.css' import { createApp } from 'vue' import { setup, configureApp } from '@/setup' +import { xbuilderEnv } from './env' import { initRouter } from './router' import App from './App.vue' -setup() +setup(xbuilderEnv) const app = createApp(App) const router = initRouter(app) -configureApp(app, router) +configureApp(app, router, xbuilderEnv) app.mount('#app') diff --git a/spx-gui/src/apps/xbuilder/pages/docs/api.vue b/spx-gui/src/apps/xbuilder/pages/docs/api.vue index e4fed10a29..60ed745486 100644 --- a/spx-gui/src/apps/xbuilder/pages/docs/api.vue +++ b/spx-gui/src/apps/xbuilder/pages/docs/api.vue @@ -4,7 +4,7 @@ import { ApiReference } from '@scalar/api-reference' import type { AnyApiReferenceConfiguration } from '@scalar/types/api-reference' import '@scalar/api-reference/style.css' import apiDocument from '@docs/openapi.yaml?raw' -import { apiBaseUrl } from '@/utils/env' +import { getXBuilderConfig } from '@/apps/xbuilder/config' import { usePageTitle } from '@/utils/utils' usePageTitle([ @@ -25,7 +25,7 @@ const configuration = reactive({ }, servers: [ { - url: apiBaseUrl + url: getXBuilderConfig().apiBaseUrl } ], hideClientButton: true, diff --git a/spx-gui/src/components/asset/preprocessing/PreprocessModal.vue b/spx-gui/src/components/asset/preprocessing/PreprocessModal.vue index 2948979fe7..d43a616eb2 100644 --- a/spx-gui/src/components/asset/preprocessing/PreprocessModal.vue +++ b/spx-gui/src/components/asset/preprocessing/PreprocessModal.vue @@ -105,7 +105,7 @@ import { computed, ref, shallowReactive, shallowRef, watch } from 'vue' import { stripExt } from '@/utils/path' import type { LocaleMessage } from '@/utils/i18n' -import { disableAIGC } from '@/utils/env' +import { getXBuilderConfig } from '@/apps/xbuilder/config' import { Costume } from '@/models/spx/costume' import { File } from '@/models/common/file' import { UIButton, UIFormModal } from '@/components/ui' @@ -124,6 +124,7 @@ import { useMessageHandle } from '@/utils/exception' import { useNetwork } from '@/utils/network' const { isOnline } = useNetwork() +const { disableAIGC } = getXBuilderConfig() const props = withDefaults( defineProps<{ diff --git a/spx-gui/src/components/community/CommunityNavbar.vue b/spx-gui/src/components/community/CommunityNavbar.vue index 4006cdea3a..ebb38c235c 100644 --- a/spx-gui/src/components/community/CommunityNavbar.vue +++ b/spx-gui/src/components/community/CommunityNavbar.vue @@ -43,11 +43,12 @@ import NavbarNewProjectItem from '@/components/navbar/NavbarNewProjectItem.vue' import NavbarOpenProjectItem from '@/components/navbar/NavbarOpenProjectItem.vue' import { searchKeywordQueryParamName } from '@/apps/xbuilder/pages/community/search.vue' import { getSearchRoute } from '@/apps/xbuilder/router' -import { showTutorialsEntry } from '@/utils/env' +import { getXBuilderConfig } from '@/apps/xbuilder/config' import NavbarLang from '../navbar/NavbarLang.vue' import NavbarTutorials from '../navbar/NavbarTutorials.vue' import { isSignedIn } from '@/stores/user' +const { showTutorialsEntry } = getXBuilderConfig() const router = useRouter() const searchInput = ref('') diff --git a/spx-gui/src/components/community/footer/CommunityFooter.vue b/spx-gui/src/components/community/footer/CommunityFooter.vue index f7cdb3e7dc..758f849d9d 100644 --- a/spx-gui/src/components/community/footer/CommunityFooter.vue +++ b/spx-gui/src/components/community/footer/CommunityFooter.vue @@ -1,6 +1,8 @@