diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js index bcab809464..d9c99b688e 100644 --- a/apps/mobile/metro.config.js +++ b/apps/mobile/metro.config.js @@ -18,6 +18,24 @@ config.resolver.nodeModulesPaths = [ path.resolve(__dirname, "../../node_modules"), ] +config.resolver.resolveRequest = (context, moduleName, platform) => { + const result = context.resolveRequest(context, moduleName, platform) + if (result.type === "sourceFile") { + const lastDotIndex = result.filePath.lastIndexOf(".") + const mobilePath = `${result.filePath.slice(0, lastDotIndex)}.mobile${result.filePath.slice(lastDotIndex)}` + const file = context.fileSystemLookup(mobilePath) + if (file.exists) { + return { + ...result, + filePath: mobilePath, + } + } else { + return result + } + } + return result +} + module.exports = wrapWithReanimatedMetroConfig( withNativeWind(config, { input: "./src/global.css" }), ) diff --git a/apps/mobile/src/atoms/env.ts b/apps/mobile/src/atoms/env.ts deleted file mode 100644 index dea86e6ba2..0000000000 --- a/apps/mobile/src/atoms/env.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createAtomHooks } from "@follow/utils" -import { atomWithStorage } from "jotai/utils" - -import { JotaiPersistSyncStorage } from "../lib/jotai" - -type Environment = "prod" | "dev" | "staging" -export const [, , useEnvironment, , getEnvironment, setEnvironment] = createAtomHooks( - atomWithStorage("debug-env", "prod", JotaiPersistSyncStorage, { - getOnInit: true, - }), -) diff --git a/apps/mobile/src/constants/env.ts b/apps/mobile/src/constants/env.ts deleted file mode 100644 index c96372730c..0000000000 --- a/apps/mobile/src/constants/env.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const appEndpointMap = { - prod: { - api: "https://api.follow.is", - web: "https://app.follow.is", - }, - dev: { - api: "https://api.dev.follow.is", - web: "https://dev.follow.is", - }, - staging: { - api: "https://api.follow.is", - web: "https://staging.follow.is", - }, -} diff --git a/apps/mobile/src/lib/api-fetch.ts b/apps/mobile/src/lib/api-fetch.ts index cc4db5a6bd..b8eeb05563 100644 --- a/apps/mobile/src/lib/api-fetch.ts +++ b/apps/mobile/src/lib/api-fetch.ts @@ -1,17 +1,17 @@ /* eslint-disable no-console */ import type { AppType } from "@follow/shared" +import { env } from "@follow/shared/src/env" import { FetchError, ofetch } from "ofetch" import { userActions } from "../store/user/store" import { getCookie } from "./auth" -import { getApiUrl } from "./env" const { hc } = require("hono/dist/cjs/client/client") as typeof import("hono/client") export const apiFetch = ofetch.create({ retry: false, - baseURL: getApiUrl(), + baseURL: env.VITE_API_URL, onRequest: async (ctx) => { const { options, request } = ctx if (__DEV__) { @@ -47,7 +47,7 @@ export const apiFetch = ofetch.create({ }, }) -export const apiClient = hc(getApiUrl(), { +export const apiClient = hc(env.VITE_API_URL, { fetch: async (input: any, options = {}) => apiFetch(input.toString(), options).catch((err) => { throw err diff --git a/apps/mobile/src/lib/auth.ts b/apps/mobile/src/lib/auth.ts index bc744d9988..68271ae7c3 100644 --- a/apps/mobile/src/lib/auth.ts +++ b/apps/mobile/src/lib/auth.ts @@ -1,4 +1,5 @@ import { expoClient } from "@better-auth/expo/client" +import { env } from "@follow/shared/src/env" import { useQuery } from "@tanstack/react-query" import { twoFactorClient } from "better-auth/client/plugins" import { createAuthClient } from "better-auth/react" @@ -6,7 +7,6 @@ import type * as better_call from "better-call" import * as SecureStore from "expo-secure-store" import { whoamiQueryKey } from "../store/user/hooks" -import { getApiUrl } from "./env" import { queryClient } from "./query-client" const storagePrefix = "follow_auth" @@ -14,7 +14,7 @@ export const cookieKey = `${storagePrefix}_cookie` export const sessionTokenKey = "__Secure-better-auth.session_token" const authClient = createAuthClient({ - baseURL: `${getApiUrl()}/better-auth`, + baseURL: `${env.VITE_API_URL}/better-auth`, plugins: [ twoFactorClient(), { diff --git a/apps/mobile/src/lib/env.ts b/apps/mobile/src/lib/env.ts deleted file mode 100644 index c5a0f0ddcc..0000000000 --- a/apps/mobile/src/lib/env.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { getEnvironment } from "../atoms/env" -import { appEndpointMap } from "../constants/env" - -export const getApiUrl = () => { - const env = getEnvironment() - return appEndpointMap[env].api -} - -export const getWebUrl = () => { - const env = getEnvironment() - return appEndpointMap[env].web -} diff --git a/apps/mobile/src/lib/image.ts b/apps/mobile/src/lib/image.ts index 4b6edc4b5d..8e8841c968 100644 --- a/apps/mobile/src/lib/image.ts +++ b/apps/mobile/src/lib/image.ts @@ -1,26 +1,4 @@ -const imageRefererMatches = [ - { - url: /^https:\/\/\w+\.sinaimg.cn/, - referer: "https://weibo.com", - }, - { - url: /^https:\/\/i\.pximg\.net/, - referer: "https://www.pixiv.net", - }, - { - url: /^https:\/\/cdnfile\.sspai\.com/, - referer: "https://sspai.com", - }, - { - url: /^https:\/\/(?:\w|-)+\.cdninstagram\.com/, - referer: "https://www.instagram.com", - }, - { - url: /^https:\/\/sp1\.piokok\.com/, - referer: "https://www.piokok.com", - force: true, - }, -] +import { imageRefererMatches } from "@follow/shared/src/image" const isValidUrl = (url: string) => { try { diff --git a/apps/mobile/src/modules/context-menu/lists.tsx b/apps/mobile/src/modules/context-menu/lists.tsx index 814ac27a0b..a257f60a5f 100644 --- a/apps/mobile/src/modules/context-menu/lists.tsx +++ b/apps/mobile/src/modules/context-menu/lists.tsx @@ -1,9 +1,9 @@ +import { env } from "@follow/shared/src/env" import type { FC, PropsWithChildren } from "react" import { useMemo } from "react" import { Alert, Clipboard } from "react-native" import { ContextMenu } from "@/src/components/ui/context-menu" -import { getWebUrl } from "@/src/lib/env" import { toast } from "@/src/lib/toast" import { getList } from "@/src/store/list/getters" import { useIsOwnList } from "@/src/store/list/hooks" @@ -31,7 +31,7 @@ export const SubscriptionListItemContextMenu: FC< const list = getList(id) if (!list) return toast.info("Link copied to clipboard") - Clipboard.setString(`${getWebUrl()}/share/lists/${list.id}`) + Clipboard.setString(`${env.VITE_WEB_URL}/share/lists/${list.id}`) }, }, { diff --git a/apps/mobile/src/modules/screen/TimelineSelectorProvider.tsx b/apps/mobile/src/modules/screen/TimelineSelectorProvider.tsx index ba9950a58e..02adb990ce 100644 --- a/apps/mobile/src/modules/screen/TimelineSelectorProvider.tsx +++ b/apps/mobile/src/modules/screen/TimelineSelectorProvider.tsx @@ -1,3 +1,4 @@ +import { env } from "@follow/shared/src/env" import { useLocalSearchParams } from "expo-router" import { useMemo } from "react" import { Share, useAnimatedValue, View } from "react-native" @@ -9,7 +10,6 @@ import { NavigationBlurEffectHeader } from "@/src/components/layouts/views/SafeN import { UIBarButton } from "@/src/components/ui/button/UIBarButton" import { TIMELINE_VIEW_SELECTOR_HEIGHT } from "@/src/constants/ui" import { Share3CuteReIcon } from "@/src/icons/share_3_cute_re" -import { getWebUrl } from "@/src/lib/env" import { HomeLeftAction, HomeSharedRightAction, @@ -101,8 +101,7 @@ function FeedShareAction({ params }: { params: any }) { onPress={() => { const feed = getFeed(feedId) if (!feed) return - const webUrl = getWebUrl() - const url = `${webUrl}/share/feeds/${feedId}` + const url = `${env.VITE_WEB_URL}/share/feeds/${feedId}` Share.share({ message: `Check out ${feed.title} on Follow: ${url}`, title: feed.title!, diff --git a/packages/shared/src/env.desktop.ts b/packages/shared/src/env.desktop.ts new file mode 100644 index 0000000000..9029ce3034 --- /dev/null +++ b/packages/shared/src/env.desktop.ts @@ -0,0 +1,67 @@ +import { createEnv } from "@t3-oss/env-core" +import { z } from "zod" + +export const isDev = + "process" in globalThis ? process.env.NODE_ENV === "development" : import.meta.env.DEV +export const env = createEnv({ + clientPrefix: "VITE_", + client: { + VITE_WEB_URL: z.string().url().default("https://app.follow.is"), + VITE_API_URL: z.string(), + VITE_DEV_PROXY: z.string().optional(), + VITE_SENTRY_DSN: z.string().optional(), + VITE_INBOXES_EMAIL: z.string().default("@follow.re"), + VITE_FIREBASE_CONFIG: z.string().optional(), + + VITE_OPENPANEL_CLIENT_ID: z.string().optional(), + VITE_OPENPANEL_API_URL: z.string().url().optional(), + + // For external, use api_url if you don't want to fill it in. + VITE_EXTERNAL_PROD_API_URL: z.string().optional(), + VITE_EXTERNAL_DEV_API_URL: z.string().optional(), + VITE_EXTERNAL_API_URL: z.string().optional(), + VITE_WEB_PROD_URL: z.string().optional(), + VITE_WEB_DEV_URL: z.string().optional(), + }, + + emptyStringAsUndefined: true, + runtimeEnv: getRuntimeEnv() as any, + + skipValidation: "process" in globalThis ? process.env.VITEST === "true" : false, +}) + +function metaEnvIsEmpty() { + try { + return Object.keys(import.meta.env || {}).length === 0 + } catch { + return true + } +} + +function getRuntimeEnv() { + try { + if (metaEnvIsEmpty()) { + return process.env + } + return injectExternalEnv(import.meta.env) + } catch { + return process.env + } +} + +declare const globalThis: any +function injectExternalEnv(originEnv: T): T { + if (!("document" in globalThis)) { + return originEnv + } + const prefix = "__followEnv" + const env = globalThis[prefix] + if (!env) { + return originEnv + } + + for (const key in env) { + originEnv[key as keyof T] = env[key] + } + return originEnv +} diff --git a/packages/shared/src/env.mobile.ts b/packages/shared/src/env.mobile.ts new file mode 100644 index 0000000000..23fea443ec --- /dev/null +++ b/packages/shared/src/env.mobile.ts @@ -0,0 +1,24 @@ +const profile = "prod" + +const appEndpointMap = { + prod: { + VITE_API_URL: "https://api.follow.is", + VITE_WEB_URL: "https://app.follow.is", + VITE_INBOXES_EMAIL: "@follow.re", + }, + dev: { + VITE_API_URL: "https://api.dev.follow.is", + VITE_WEB_URL: "https://dev.follow.is", + VITE_INBOXES_EMAIL: "__devdev@follow.re", + }, + staging: { + VITE_API_URL: "https://api.follow.is", + VITE_WEB_URL: "https://staging.follow.is", + VITE_INBOXES_EMAIL: "@follow.re", + }, +} + +export const env = { + VITE_WEB_URL: appEndpointMap[profile].VITE_WEB_URL, + VITE_API_URL: appEndpointMap[profile].VITE_API_URL, +} diff --git a/packages/shared/src/env.ts b/packages/shared/src/env.ts index 9029ce3034..7904410a31 100644 --- a/packages/shared/src/env.ts +++ b/packages/shared/src/env.ts @@ -1,67 +1,23 @@ -import { createEnv } from "@t3-oss/env-core" import { z } from "zod" -export const isDev = - "process" in globalThis ? process.env.NODE_ENV === "development" : import.meta.env.DEV -export const env = createEnv({ - clientPrefix: "VITE_", - client: { - VITE_WEB_URL: z.string().url().default("https://app.follow.is"), - VITE_API_URL: z.string(), - VITE_DEV_PROXY: z.string().optional(), - VITE_SENTRY_DSN: z.string().optional(), - VITE_INBOXES_EMAIL: z.string().default("@follow.re"), - VITE_FIREBASE_CONFIG: z.string().optional(), - - VITE_OPENPANEL_CLIENT_ID: z.string().optional(), - VITE_OPENPANEL_API_URL: z.string().url().optional(), - - // For external, use api_url if you don't want to fill it in. - VITE_EXTERNAL_PROD_API_URL: z.string().optional(), - VITE_EXTERNAL_DEV_API_URL: z.string().optional(), - VITE_EXTERNAL_API_URL: z.string().optional(), - VITE_WEB_PROD_URL: z.string().optional(), - VITE_WEB_DEV_URL: z.string().optional(), - }, - - emptyStringAsUndefined: true, - runtimeEnv: getRuntimeEnv() as any, - - skipValidation: "process" in globalThis ? process.env.VITEST === "true" : false, -}) - -function metaEnvIsEmpty() { - try { - return Object.keys(import.meta.env || {}).length === 0 - } catch { - return true - } +export const envSchema = { + VITE_WEB_URL: z.string().url().default("https://app.follow.is"), + VITE_API_URL: z.string().default("https://api.follow.is"), + VITE_DEV_PROXY: z.string().optional(), + VITE_SENTRY_DSN: z.string().optional(), + VITE_INBOXES_EMAIL: z.string().default("@follow.re"), + VITE_FIREBASE_CONFIG: z.string().optional(), + + VITE_OPENPANEL_CLIENT_ID: z.string().optional(), + VITE_OPENPANEL_API_URL: z.string().url().optional(), + + // For external, use api_url if you don't want to fill it in. + VITE_EXTERNAL_PROD_API_URL: z.string().optional(), + VITE_EXTERNAL_DEV_API_URL: z.string().optional(), + VITE_EXTERNAL_API_URL: z.string().optional(), + VITE_WEB_PROD_URL: z.string().optional(), + VITE_WEB_DEV_URL: z.string().optional(), } -function getRuntimeEnv() { - try { - if (metaEnvIsEmpty()) { - return process.env - } - return injectExternalEnv(import.meta.env) - } catch { - return process.env - } -} - -declare const globalThis: any -function injectExternalEnv(originEnv: T): T { - if (!("document" in globalThis)) { - return originEnv - } - const prefix = "__followEnv" - const env = globalThis[prefix] - if (!env) { - return originEnv - } - - for (const key in env) { - originEnv[key as keyof T] = env[key] - } - return originEnv -} +export const isDev = false +export const env = z.object(envSchema).parse({}) diff --git a/packages/shared/src/image.ts b/packages/shared/src/image.ts index a8635059d7..642bf52057 100644 --- a/packages/shared/src/image.ts +++ b/packages/shared/src/image.ts @@ -1,4 +1,4 @@ -import { env } from "@follow/shared/env" +import { env } from "./env" export const IMAGE_PROXY_URL = "https://webp.follow.is" @@ -8,7 +8,7 @@ export const selfRefererMatches = [env.VITE_OPENPANEL_API_URL, IMAGE_PROXY_URL]. export const imageRefererMatches = [ { - url: /^https:\/\/\w+\.sinaimg.cn/, + url: /^https:\/\/\w+\.sinaimg\.cn/, referer: "https://weibo.com", }, { diff --git a/plugins/vite/specific-import.ts b/plugins/vite/specific-import.ts index 2514311352..290e7d5c01 100644 --- a/plugins/vite/specific-import.ts +++ b/plugins/vite/specific-import.ts @@ -6,49 +6,31 @@ export function createPlatformSpecificImportPlugin(platform: Platform): Plugin { name: "platform-specific-import", enforce: "pre", async resolveId(source, importer) { - if (!importer) { - return null - } + const resolvedPath = await this.resolve(source, importer, { + skipSelf: true, + }) const allowExts = [".js", ".jsx", ".ts", ".tsx"] - if (!allowExts.some((ext) => importer.endsWith(ext))) return null - - if (importer.includes("node_modules")) return null - const [path, query] = source.split("?") - - if (path.startsWith(".") || path.startsWith("/")) { - let priorities: string[] = [] - switch (platform) { - case "electron": { - priorities = [".electron.ts", ".electron.tsx", ".electron.js", ".electron.jsx"] - - break - } - case "web": { - priorities = [".web.ts", ".web.tsx", ".web.js", ".web.jsx"] - - break - } - case "rn": { - priorities = [".rn.ts", ".rn.tsx", ".rn.js", ".rn.jsx"] - - break - } - // No default - } - - for (const ext of priorities) { - const resolvedPath = await this.resolve( - `${path}${ext}${query ? `?${query}` : ""}`, - importer, - { - skipSelf: true, - }, - ) - - if (resolvedPath) { - return resolvedPath.id + if ( + resolvedPath && + !resolvedPath.id.includes("node_modules") && + allowExts.some((ext) => importer?.endsWith(ext)) && + allowExts.some((ext) => resolvedPath.id?.endsWith(ext)) + ) { + const lastDotIndex = resolvedPath.id.lastIndexOf(".") + + const paths = [ + `${resolvedPath.id.slice(0, lastDotIndex)}.${platform}${resolvedPath.id.slice(lastDotIndex)}`, + `${resolvedPath.id.slice(0, lastDotIndex)}.desktop${resolvedPath.id.slice(lastDotIndex)}`, + ] + + for (const path of paths) { + const resolvedPlatform = await this.resolve(path, importer, { + skipSelf: true, + }) + if (resolvedPlatform) { + return resolvedPlatform.id } } }