diff --git a/src/cli/createServer.ts b/src/cli/createServer.ts index 5c73b7a..d75a6f7 100644 --- a/src/cli/createServer.ts +++ b/src/cli/createServer.ts @@ -1,38 +1,25 @@ -import { createRequestHandler } from '@expo/server/adapter/express'; import compression from 'compression'; import express from 'express'; -import morgan from 'morgan'; import { type Options } from './resolveOptions'; import { StatsFileSource } from '../data/StatsFileSource'; -import { env } from '../utils/env'; -import { CLIENT_BUILD_DIR, SERVER_BUILD_DIR } from '../utils/middleware'; +import { createAtlasMiddleware } from '../utils/middleware'; export function createServer(options: Options) { - global.EXPO_ATLAS_SOURCE = new StatsFileSource(options.statsFile); process.env.NODE_ENV = 'production'; - const app = express(); + const source = new StatsFileSource(options.statsFile); + const middleware = createAtlasMiddleware(source); + const baseUrl = '/_expo/atlas'; // Keep in sync with webui `app.json` `baseUrl` - // http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header - app.disable('x-powered-by'); + const app = express(); + app.disable('x-powered-by'); // http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header app.use(compression()); + app.use(baseUrl, middleware); - if (env.EXPO_ATLAS_DEBUG) { - app.use(morgan('tiny')); - } - - // TODO(cedric): replace with middleware once we can - app.use( - express.static(CLIENT_BUILD_DIR, { - maxAge: '1h', - extensions: ['html'], - }) - ); - - // TODO(cedric): replace with middleware once we can - app.all('*', createRequestHandler({ build: SERVER_BUILD_DIR })); + // Add catch-all to redirect to the webui + app.use((req, res, next) => (!req.url.startsWith(baseUrl) ? res.redirect(baseUrl) : next())); return app; } diff --git a/src/utils/middleware.ts b/src/utils/middleware.ts index 5ff0246..219f659 100644 --- a/src/utils/middleware.ts +++ b/src/utils/middleware.ts @@ -9,9 +9,8 @@ import { type StatsSource } from '../data/types'; const WEBUI_ROOT = path.resolve(__dirname, '../../../webui'); -// TODO(cedric): drop these exports once we can use this as base for the standalone server -export const CLIENT_BUILD_DIR = path.join(WEBUI_ROOT, 'dist/client'); -export const SERVER_BUILD_DIR = path.join(WEBUI_ROOT, 'dist/server'); +const CLIENT_BUILD_DIR = path.join(WEBUI_ROOT, 'dist/client'); +const SERVER_BUILD_DIR = path.join(WEBUI_ROOT, 'dist/server'); /** * Initialize Expo Atlas to gather statistics from Metro during development. diff --git a/webui/app.json b/webui/app.json index d0e7e1a..1c1678a 100644 --- a/webui/app.json +++ b/webui/app.json @@ -8,6 +8,7 @@ "output": "server" }, "experiments": { + "baseUrl": "/_expo/atlas", "tsconfigPaths": true, "typedRoutes": true }, diff --git a/webui/bun.lockb b/webui/bun.lockb index 0b8e24d..d620a01 100755 Binary files a/webui/bun.lockb and b/webui/bun.lockb differ diff --git a/webui/package.json b/webui/package.json index 9a3f112..7c9010c 100644 --- a/webui/package.json +++ b/webui/package.json @@ -26,7 +26,7 @@ "echarts-for-react": "^3.0.2", "expo": "~50.0.6", "expo-linking": "~6.2.2", - "expo-router": "^3.4.8", + "expo-router": "0.0.1-canary-20240320-8a10e09", "expo-status-bar": "~1.11.1", "lucide-react": "^0.336.0", "nativewind": "^4.0.36", diff --git a/webui/src/app/folders/[path].tsx b/webui/src/app/folders/[path].tsx index a76c916..332cded 100644 --- a/webui/src/app/folders/[path].tsx +++ b/webui/src/app/folders/[path].tsx @@ -8,6 +8,7 @@ import { TreemapGraph } from '~/components/graphs/TreemapGraph'; import { useStatsEntryContext } from '~/providers/stats'; import { Skeleton } from '~/ui/Skeleton'; import { Tag } from '~/ui/Tag'; +import { fetchApi } from '~/utils/api'; import { formatFileSize } from '~/utils/formatString'; import { type PartialStatsEntry } from '~core/data/types'; @@ -88,7 +89,7 @@ function useFolderData(entryId: string, path: string) { queryKey: [`module`, entryId, path], queryFn: async ({ queryKey }) => { const [_key, entry, path] = queryKey as [string, string, string]; - return fetch(`/api/stats/${entry}/folders`, { + return fetchApi(`/api/stats/${entry}/folders`, { method: 'POST', body: JSON.stringify({ path }), }) diff --git a/webui/src/app/index.tsx b/webui/src/app/index.tsx index 0580b27..b3ab52c 100644 --- a/webui/src/app/index.tsx +++ b/webui/src/app/index.tsx @@ -11,6 +11,7 @@ import { } from '~/providers/modules'; import { useStatsEntryContext } from '~/providers/stats'; import { Tag } from '~/ui/Tag'; +import { fetchApi } from '~/utils/api'; import { formatFileSize } from '~/utils/formatString'; export default function GraphScreen() { @@ -66,7 +67,7 @@ function useBundleGraphData(entryId: string, filters?: ModuleFilters) { ? `/api/stats/${entry}/modules?${filtersToUrlParams(filters)}` : `/api/stats/${entry}/modules`; - return fetch(url) + return fetchApi(url) .then((res) => (res.ok ? res : Promise.reject(res))) .then((res) => res.json()); }, diff --git a/webui/src/app/modules/[path].tsx b/webui/src/app/modules/[path].tsx index 03b851f..5e54d94 100644 --- a/webui/src/app/modules/[path].tsx +++ b/webui/src/app/modules/[path].tsx @@ -6,6 +6,7 @@ import { useStatsEntryContext } from '~/providers/stats'; import { CodeBlock, CodeBlockSectionWithPrettier, guessLanguageFromPath } from '~/ui/CodeBlock'; import { Skeleton } from '~/ui/Skeleton'; import { Tag } from '~/ui/Tag'; +import { fetchApi } from '~/utils/api'; import { formatFileSize } from '~/utils/formatString'; import { type PartialStatsEntry, type StatsModule } from '~core/data/types'; @@ -114,7 +115,7 @@ function useModuleData(entryId: string, path: string) { queryKey: [`module`, entryId, path], queryFn: async ({ queryKey }) => { const [_key, entry, path] = queryKey as [string, string, string]; - return fetch(`/api/stats/${entry}/modules`, { + return fetchApi(`/api/stats/${entry}/modules`, { method: 'POST', body: JSON.stringify({ path }), }) diff --git a/webui/src/providers/stats.tsx b/webui/src/providers/stats.tsx index 81a1ee9..921e345 100644 --- a/webui/src/providers/stats.tsx +++ b/webui/src/providers/stats.tsx @@ -1,6 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import { type PropsWithChildren, createContext, useContext, useMemo, useState } from 'react'; +import { fetchApi } from '~/utils/api'; import { type PartialStatsEntry } from '~core/data/types'; type StatsEntryContext = { @@ -44,6 +45,6 @@ export function StatsEntryProvider({ children }: PropsWithChildren) { function useStatsEntriesData() { return useQuery({ queryKey: ['stats-entries'], - queryFn: () => fetch('/api/stats').then((res) => res.json()), + queryFn: () => fetchApi('/api/stats').then((res) => res.json()), }); } diff --git a/webui/src/utils/api.ts b/webui/src/utils/api.ts new file mode 100644 index 0000000..e84b678 --- /dev/null +++ b/webui/src/utils/api.ts @@ -0,0 +1,17 @@ +/** + * Keep this path in sync with `app.json`'s `baseUrl`. + * + * @see https://docs.expo.dev/versions/latest/config/app/#baseurl + */ +const baseUrl = '/_expo/atlas'; + +/** + * Fetch data from the API routes, adding the `baseUrl` to all requests. + */ +export function fetchApi(path: string, options?: RequestInit) { + if (path.startsWith(baseUrl)) { + return fetch(path, options); + } + + return fetch(path.startsWith('/') ? `${baseUrl}${path}` : `${baseUrl}/${path}`, options); +}