From 4542fbffd3ad66c105221b9ee241ec31ab8bf69e Mon Sep 17 00:00:00 2001 From: Cedric van Putten Date: Fri, 5 Apr 2024 16:26:29 +0200 Subject: [PATCH] refactor: drop exessive use of `stats` naming --- src/cli/createServer.ts | 4 +- src/cli/resolveOptions.ts | 6 +- src/data/AtlasFileSource.ts | 128 ++++++++++++++++++ src/data/MetroGraphSource.ts | 32 ++--- src/data/StatsFileSource.ts | 84 ------------ .../__tests__/atlas.test.ts} | 37 ++--- src/data/types.ts | 16 +-- src/index.ts | 9 +- src/metro.ts | 10 +- src/utils/env.ts | 4 +- src/utils/global.ts | 4 +- src/utils/middleware.ts | 6 +- src/utils/stats.ts | 46 ------- .../[entry]/folders/[path].tsx | 36 ++--- .../app/{stats => (atlas)}/[entry]/index.tsx | 38 ++++-- .../[entry]/modules/[path].tsx | 21 +-- .../stats => --/entries}/[entry]/index+api.ts | 0 .../entries}/[entry]/modules/graph+api.ts | 8 +- .../entries}/[entry]/modules/index+api.ts | 8 +- .../{api/stats => --/entries}/index+api.ts | 0 webui/src/app/_layout.tsx | 6 +- webui/src/app/index.tsx | 6 +- webui/src/components/BundleGraph.tsx | 8 +- webui/src/components/Page.tsx | 17 ++- .../{StatsEntrySelect.tsx => EntrySelect.tsx} | 6 +- ...StatsModuleFilter.tsx => ModuleFilter.tsx} | 4 +- webui/src/providers/entries.tsx | 73 ++++++++++ webui/src/providers/stats.tsx | 73 ---------- webui/src/utils/api.ts | 2 +- webui/src/utils/{stats.ts => entry.ts} | 4 +- webui/src/utils/filters.ts | 4 +- webui/src/utils/treemap.ts | 4 +- 32 files changed, 367 insertions(+), 337 deletions(-) create mode 100644 src/data/AtlasFileSource.ts delete mode 100644 src/data/StatsFileSource.ts rename src/{utils/__tests__/stats.test.ts => data/__tests__/atlas.test.ts} (80%) delete mode 100644 src/utils/stats.ts rename webui/src/app/{stats => (atlas)}/[entry]/folders/[path].tsx (71%) rename webui/src/app/{stats => (atlas)}/[entry]/index.tsx (69%) rename webui/src/app/{stats => (atlas)}/[entry]/modules/[path].tsx (89%) rename webui/src/app/{api/stats => --/entries}/[entry]/index+api.ts (100%) rename webui/src/app/{api/stats => --/entries}/[entry]/modules/graph+api.ts (89%) rename webui/src/app/{api/stats => --/entries}/[entry]/modules/index+api.ts (92%) rename webui/src/app/{api/stats => --/entries}/index+api.ts (100%) rename webui/src/components/forms/{StatsEntrySelect.tsx => EntrySelect.tsx} (94%) rename webui/src/components/forms/{StatsModuleFilter.tsx => ModuleFilter.tsx} (97%) create mode 100644 webui/src/providers/entries.tsx delete mode 100644 webui/src/providers/stats.tsx rename webui/src/utils/{stats.ts => entry.ts} (65%) diff --git a/src/cli/createServer.ts b/src/cli/createServer.ts index d75a6f7..27f409b 100644 --- a/src/cli/createServer.ts +++ b/src/cli/createServer.ts @@ -2,13 +2,13 @@ import compression from 'compression'; import express from 'express'; import { type Options } from './resolveOptions'; -import { StatsFileSource } from '../data/StatsFileSource'; +import { AtlasFileSource } from '../data/AtlasFileSource'; import { createAtlasMiddleware } from '../utils/middleware'; export function createServer(options: Options) { process.env.NODE_ENV = 'production'; - const source = new StatsFileSource(options.statsFile); + const source = new AtlasFileSource(options.statsFile); const middleware = createAtlasMiddleware(source); const baseUrl = '/_expo/atlas'; // Keep in sync with webui `app.json` `baseUrl` diff --git a/src/cli/resolveOptions.ts b/src/cli/resolveOptions.ts index 45528a7..76cae0a 100644 --- a/src/cli/resolveOptions.ts +++ b/src/cli/resolveOptions.ts @@ -2,7 +2,7 @@ import freeport from 'freeport-async'; import path from 'path'; import { type Input } from './bin'; -import { getStatsPath, validateStatsFile } from '../utils/stats'; +import { getAtlasPath, validateAtlasFile } from '../utils/stats'; export type Options = Awaited>; @@ -13,8 +13,8 @@ export async function resolveOptions(input: Input) { } async function resolveStatsFile(input: Input) { - const statsFile = input._[0] ?? getStatsPath(process.cwd()); - await validateStatsFile(statsFile); + const statsFile = input._[0] ?? getAtlasPath(process.cwd()); + await validateAtlasFile(statsFile); return path.resolve(statsFile); } diff --git a/src/data/AtlasFileSource.ts b/src/data/AtlasFileSource.ts new file mode 100644 index 0000000..0272d44 --- /dev/null +++ b/src/data/AtlasFileSource.ts @@ -0,0 +1,128 @@ +import assert from 'assert'; +import fs from 'fs'; +import path from 'path'; + +import type { PartialAtlasEntry, AtlasEntry, AtlasSource } from './types'; +import { name, version } from '../../package.json'; +import { env } from '../utils/env'; +import { AtlasValidationError } from '../utils/errors'; +import { appendJsonLine, forEachJsonLines, parseJsonLine } from '../utils/jsonl'; + +export type AtlasMetadata = { name: string; version: string }; + +export class AtlasFileSource implements AtlasSource { + constructor(public readonly statsPath: string) { + // + } + + listEntries() { + return listAtlasEntries(this.statsPath); + } + + getEntry(id: string) { + const numeric = parseInt(id, 10); + assert(!Number.isNaN(numeric) && numeric > 1, `Invalid entry ID: ${id}`); + return readAtlasEntry(this.statsPath, Number(id)); + } +} + +/** + * List all stats entries without parsing the data. + * This only reads the bundle name, and adds a line number as ID. + */ +export async function listAtlasEntries(statsPath: string) { + const bundlePattern = /^\["([^"]+)","([^"]+)","([^"]+)/; + const entries: PartialAtlasEntry[] = []; + + await forEachJsonLines(statsPath, (contents, line) => { + // Skip the stats metadata line + if (line === 1) return; + + const [_, platform, projectRoot, entryPoint] = contents.match(bundlePattern) ?? []; + if (platform && projectRoot && entryPoint) { + entries.push({ + id: String(line), + platform: platform as any, + projectRoot, + entryPoint, + }); + } + }); + + return entries; +} + +/** + * Get the stats entry by id or line number, and parse the data. + */ +export async function readAtlasEntry(statsPath: string, id: number): Promise { + const statsEntry = await parseJsonLine(statsPath, id); + return { + id: String(id), + platform: statsEntry[0], + projectRoot: statsEntry[1], + entryPoint: statsEntry[2], + runtimeModules: statsEntry[3], + modules: new Map(statsEntry[4].map((module) => [module.path, module])), + transformOptions: statsEntry[5], + serializeOptions: statsEntry[6], + }; +} + +/** Simple promise to avoid mixing appended data */ +let writeStatsQueue: Promise = Promise.resolve(); + +/** + * Add a new stats entry to the stats file. + * This is appended on a new line, so we can load the stats selectively. + */ +export function writeAtlasEntry(statsPath: string, stats: AtlasEntry) { + const entry = [ + stats.platform, + stats.projectRoot, + stats.entryPoint, + stats.runtimeModules, + Array.from(stats.modules.values()), + stats.transformOptions, + stats.serializeOptions, + ]; + + return (writeStatsQueue = writeStatsQueue.then(() => appendJsonLine(statsPath, entry))); +} + +/** The default location of the metro stats file */ +export function getAtlasPath(projectRoot: string) { + return path.join(projectRoot, '.expo/atlas.jsonl'); +} + +/** The information to validate if a stats file is compatible with this library version */ +export function getAtlasMetdata(): AtlasMetadata { + return { name, version }; +} + +/** Validate if the stats file is compatible with this library version */ +export async function validateAtlasFile(statsFile: string, metadata = getAtlasMetdata()) { + if (!fs.existsSync(statsFile)) { + throw new AtlasValidationError('STATS_FILE_NOT_FOUND', statsFile); + } + + if (env.EXPO_ATLAS_NO_STATS_VALIDATION) { + return; + } + + const data = await parseJsonLine(statsFile, 1); + + if (data.name !== metadata.name || data.version !== metadata.version) { + throw new AtlasValidationError('STATS_FILE_INCOMPATIBLE', statsFile, data.version); + } +} + +/** + * Create or overwrite the stats file with basic metadata. + * This metdata is used by the API to determine version compatibility. + */ +export async function createAtlasFile(filePath: string) { + await fs.promises.mkdir(path.dirname(filePath), { recursive: true }); + await fs.promises.rm(filePath, { force: true }); + await appendJsonLine(filePath, getAtlasMetdata()); +} diff --git a/src/data/MetroGraphSource.ts b/src/data/MetroGraphSource.ts index 544a902..c273d85 100644 --- a/src/data/MetroGraphSource.ts +++ b/src/data/MetroGraphSource.ts @@ -1,14 +1,14 @@ import type metro from 'metro'; import path from 'path'; -import type { StatsEntry, StatsModule, StatsSource } from './types'; +import type { AtlasEntry, AtlasModule, AtlasSource } from './types'; import { bufferIsUtf8 } from '../utils/buffer'; import { getPackageNameFromPath } from '../utils/package'; type MetroGraph = metro.Graph | metro.ReadOnlyGraph; type MetroModule = metro.Module; -type ConvertGraphToStatsOptions = { +type ConvertGraphToAtlasOptions = { projectRoot: string; entryPoint: string; preModules: Readonly; @@ -20,9 +20,9 @@ type ConvertGraphToStatsOptions = { }; }; -export class MetroGraphSource implements StatsSource { +export class MetroGraphSource implements AtlasSource { /** All known stats entries, stored by ID */ - protected entries: Map = new Map(); + protected entries: Map = new Map(); listEntries() { return Array.from(this.entries.values()).map((entry) => ({ @@ -45,7 +45,7 @@ export class MetroGraphSource implements StatsSource { * Event handler when a new graph instance is ready to serialize. * This converts all relevant data stored in the graph to stats objects. */ - onSerializeGraph(options: ConvertGraphToStatsOptions) { + onSerializeGraph(options: ConvertGraphToAtlasOptions) { const entry = convertGraph(options); this.entries.set(entry.id, entry); return entry; @@ -53,7 +53,7 @@ export class MetroGraphSource implements StatsSource { } /** Convert a Metro graph instance to a JSON-serializable stats entry */ -export function convertGraph(options: ConvertGraphToStatsOptions): StatsEntry { +export function convertGraph(options: ConvertGraphToAtlasOptions): AtlasEntry { const serializeOptions = convertSerializeOptions(options); const transformOptions = convertTransformOptions(options); const platform = @@ -75,9 +75,9 @@ export function convertGraph(options: ConvertGraphToStatsOptions): StatsEntry { /** Find and collect all dependnecies related to the entrypoint within the graph */ export function collectEntryPointModules( - options: Pick + options: Pick ) { - const modules = new Map(); + const modules = new Map(); function discover(modulePath: string) { const module = options.graph.dependencies.get(modulePath); @@ -94,9 +94,9 @@ export function collectEntryPointModules( /** Convert a Metro module to a JSON-serializable stats module */ export function convertModule( - options: Pick, + options: Pick, module: MetroModule -): StatsModule { +): AtlasModule { return { path: module.path, package: getPackageNameFromPath(module.path), @@ -118,7 +118,7 @@ export function convertModule( * If a file is an asset, it returns `[binary file]` instead. */ function getModuleSourceContent( - options: Pick, + options: Pick, module: MetroModule ) { const fileExtension = path.extname(module.path).replace('.', ''); @@ -144,16 +144,16 @@ function getModuleSourceContent( /** Convert Metro transform options to a JSON-serializable object */ export function convertTransformOptions( - options: Pick -): StatsEntry['transformOptions'] { + options: Pick +): AtlasEntry['transformOptions'] { return options.graph.transformOptions ?? {}; } /** Convert Metro serialize options to a JSON-serializable object */ export function convertSerializeOptions( - options: Pick -): StatsEntry['serializeOptions'] { - const serializeOptions: StatsEntry['serializeOptions'] = { ...options.options }; + options: Pick +): AtlasEntry['serializeOptions'] { + const serializeOptions: AtlasEntry['serializeOptions'] = { ...options.options }; // Delete all filters delete serializeOptions['processModuleFilter']; diff --git a/src/data/StatsFileSource.ts b/src/data/StatsFileSource.ts deleted file mode 100644 index a003a48..0000000 --- a/src/data/StatsFileSource.ts +++ /dev/null @@ -1,84 +0,0 @@ -import assert from 'assert'; - -import type { PartialStatsEntry, StatsEntry, StatsSource } from './types'; -import { appendJsonLine, forEachJsonLines, parseJsonLine } from '../utils/jsonl'; - -export class StatsFileSource implements StatsSource { - constructor(public readonly statsPath: string) { - // - } - - listEntries() { - return listStatsEntries(this.statsPath); - } - - getEntry(id: string) { - const numeric = parseInt(id, 10); - assert(!Number.isNaN(numeric) && numeric > 1, `Invalid stats entry ID: ${id}`); - return readStatsEntry(this.statsPath, Number(id)); - } -} - -/** - * List all stats entries without parsing the data. - * This only reads the bundle name, and adds a line number as ID. - */ -export async function listStatsEntries(statsPath: string) { - const bundlePattern = /^\["([^"]+)","([^"]+)","([^"]+)/; - const entries: PartialStatsEntry[] = []; - - await forEachJsonLines(statsPath, (contents, line) => { - // Skip the stats metadata line - if (line === 1) return; - - const [_, platform, projectRoot, entryPoint] = contents.match(bundlePattern) ?? []; - if (platform && projectRoot && entryPoint) { - entries.push({ - id: String(line), - platform: platform as any, - projectRoot, - entryPoint, - }); - } - }); - - return entries; -} - -/** - * Get the stats entry by id or line number, and parse the data. - */ -export async function readStatsEntry(statsPath: string, id: number): Promise { - const statsEntry = await parseJsonLine(statsPath, id); - return { - id: String(id), - platform: statsEntry[0], - projectRoot: statsEntry[1], - entryPoint: statsEntry[2], - runtimeModules: statsEntry[3], - modules: new Map(statsEntry[4].map((module) => [module.path, module])), - transformOptions: statsEntry[5], - serializeOptions: statsEntry[6], - }; -} - -/** Simple promise to avoid mixing appended data */ -let writeStatsQueue: Promise = Promise.resolve(); - -/** - * Add a new stats entry to the stats file. - * This is appended on a new line, so we can load the stats selectively. - */ -export function writeStatsEntry(statsPath: string, stats: StatsEntry) { - const entry = [ - stats.platform, - stats.projectRoot, - stats.entryPoint, - stats.runtimeModules, - Array.from(stats.modules.values()), - stats.transformOptions, - stats.serializeOptions, - ]; - - return (writeStatsQueue = writeStatsQueue.then(() => appendJsonLine(statsPath, entry))); -} diff --git a/src/utils/__tests__/stats.test.ts b/src/data/__tests__/atlas.test.ts similarity index 80% rename from src/utils/__tests__/stats.test.ts rename to src/data/__tests__/atlas.test.ts index 31b6ba4..5c0d85e 100644 --- a/src/utils/__tests__/stats.test.ts +++ b/src/data/__tests__/atlas.test.ts @@ -3,25 +3,30 @@ import fs from 'fs'; import path from 'path'; import { name, version } from '../../../package.json'; -import { AtlasValidationError } from '../errors'; -import { getStatsPath, getStatsMetdata, createStatsFile, validateStatsFile } from '../stats'; - -describe('getStatsPath', () => { +import { AtlasValidationError } from '../../utils/errors'; +import { + getAtlasPath, + getAtlasMetdata, + createAtlasFile, + validateAtlasFile, +} from '../AtlasFileSource'; + +describe('getAtlasPath', () => { it('returns default path `/.expo/atlas.jsonl`', () => { - expect(getStatsPath('')).toBe('/.expo/atlas.jsonl'); + expect(getAtlasPath('')).toBe('/.expo/atlas.jsonl'); }); }); describe('getStatsMetadata', () => { it('returns package name and version', () => { - expect(getStatsMetdata()).toMatchObject({ name, version }); + expect(getAtlasMetdata()).toMatchObject({ name, version }); }); }); -describe('createStatsFile', () => { +describe('createAtlasFile', () => { it('creates a stats file with the correct metadata', async () => { const file = fixture('create-metadata', { temporary: true }); - await createStatsFile(file); + await createAtlasFile(file); await expect(fs.promises.readFile(file, 'utf8')).resolves.toBe( JSON.stringify({ name, version }) + '\n' ); @@ -30,7 +35,7 @@ describe('createStatsFile', () => { it('overwrites invalid stats file', async () => { const file = fixture('create-invalid', { temporary: true }); await fs.promises.writeFile(file, JSON.stringify({ name, version: '0.0.0' }) + '\n'); - await createStatsFile(file); + await createAtlasFile(file); await expect(fs.promises.readFile(file, 'utf8')).resolves.toBe( JSON.stringify({ name, version }) + '\n' ); @@ -39,22 +44,22 @@ describe('createStatsFile', () => { it('reuses valid stats file', async () => { const file = fixture('create-valid', { temporary: true }); await fs.promises.writeFile(file, JSON.stringify({ name, version }) + '\n'); - await createStatsFile(file); + await createAtlasFile(file); await expect(fs.promises.readFile(file, 'utf-8')).resolves.toBe( JSON.stringify({ name, version }) + '\n' ); }); }); -describe('validateStatsFile', () => { +describe('validateAtlasFile', () => { it('passes for valid stats file', async () => { const file = fixture('validate-valid', { temporary: true }); - await createStatsFile(file); - await expect(validateStatsFile(file)).resolves.pass(); + await createAtlasFile(file); + await expect(validateAtlasFile(file)).resolves.pass(); }); it('fails for non-existing stats file', async () => { - await expect(validateStatsFile('./this-file-does-not-exists')).rejects.toThrow( + await expect(validateAtlasFile('./this-file-does-not-exists')).rejects.toThrow( AtlasValidationError ); }); @@ -62,14 +67,14 @@ describe('validateStatsFile', () => { it('fails for invalid stats file', async () => { const file = fixture('validate-invalid', { temporary: true }); await fs.promises.writeFile(file, JSON.stringify({ name, version: '0.0.0' }) + '\n'); - await expect(validateStatsFile(file)).rejects.toThrow(AtlasValidationError); + await expect(validateAtlasFile(file)).rejects.toThrow(AtlasValidationError); }); it('skips validation when EXPO_ATLAS_NO_STATS_VALIDATION is true-ish', async () => { using _env = env('EXPO_ATLAS_NO_STATS_VALIDATION', 'true'); const file = fixture('validate-skip-invalid', { temporary: true }); await fs.promises.writeFile(file, JSON.stringify({ name, version: '0.0.0' }) + '\n'); - await expect(validateStatsFile(file)).resolves.pass(); + await expect(validateAtlasFile(file)).resolves.pass(); }); }); diff --git a/src/data/types.ts b/src/data/types.ts index 1d63bc7..e081f1e 100644 --- a/src/data/types.ts +++ b/src/data/types.ts @@ -1,15 +1,15 @@ import type { MixedOutput } from 'metro'; -export interface StatsSource { +export interface AtlasSource { /** List all available stats entries */ - listEntries(): PartialStatsEntry[] | Promise; + listEntries(): PartialAtlasEntry[] | Promise; /** Load the full stats entry, by reference */ - getEntry(ref: string): StatsEntry | Promise; + getEntry(ref: string): AtlasEntry | Promise; } -export type PartialStatsEntry = Pick; +export type PartialAtlasEntry = Pick; -export type StatsEntry = { +export type AtlasEntry = { /** The unique reference or ID to this stats entry */ id: string; /** The platform for which the bundle was created */ @@ -19,16 +19,16 @@ export type StatsEntry = { /** The absolute path to the entry point used when creating the bundle */ entryPoint: string; /** All known modules that are prepended for the runtime itself */ - runtimeModules: StatsModule[]; + runtimeModules: AtlasModule[]; /** All known modules imported within the bundle, stored by absolute path */ - modules: Map; + modules: Map; /** The sarialization options used for this bundle */ serializeOptions?: Record; /** The transformation options used for this bundle */ transformOptions?: Record; }; -export type StatsModule = { +export type AtlasModule = { /** The absoluate path of this module */ path: string; /** The name of the package this module belongs to, if from an external package */ diff --git a/src/index.ts b/src/index.ts index 00357b0..45d57b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,8 +2,13 @@ import './utils/global'; export type * from './data/types'; export { MetroGraphSource } from './data/MetroGraphSource'; -export { StatsFileSource } from './data/StatsFileSource'; +export { + AtlasFileSource, + createAtlasFile, + validateAtlasFile, + getAtlasMetdata, + getAtlasPath, +} from './data/AtlasFileSource'; export { AtlasError, AtlasValidationError } from './utils/errors'; export { createAtlasMiddleware } from './utils/middleware'; -export { createStatsFile, validateStatsFile, getStatsMetdata, getStatsPath } from './utils/stats'; diff --git a/src/metro.ts b/src/metro.ts index b4eea86..aa13092 100644 --- a/src/metro.ts +++ b/src/metro.ts @@ -1,8 +1,8 @@ import { type MetroConfig } from 'metro-config'; import { convertGraph } from './data/MetroGraphSource'; -import { writeStatsEntry } from './data/StatsFileSource'; -import { createStatsFile, getStatsPath } from './utils/stats'; +import { writeAtlasEntry } from './data/AtlasFileSource'; +import { createAtlasFile, getAtlasPath } from './utils/stats'; type ExpoAtlasOptions = Partial<{ /** The output of the stats file, defaults to `.expo/stats.json` */ @@ -33,19 +33,19 @@ export function withExpoAtlas(config: MetroConfig, options: ExpoAtlasOptions = { throw new Error('No "projectRoot" configured in Metro config.'); } - const statsFile = options?.statsFile ?? getStatsPath(projectRoot); + const statsFile = options?.statsFile ?? getAtlasPath(projectRoot); const extensions = { source: config.resolver?.sourceExts, asset: config.resolver?.assetExts, }; // Note(cedric): we don't have to await this, Metro would never bundle before this is finisheds - createStatsFile(statsFile); + createAtlasFile(statsFile); // @ts-expect-error config.serializer.customSerializer = (entryPoint, preModules, graph, options) => { // Note(cedric): we don't have to await this, it has a built-in write queue - writeStatsEntry( + writeAtlasEntry( statsFile, convertGraph({ projectRoot, entryPoint, preModules, graph, options, extensions }) ); diff --git a/src/utils/env.ts b/src/utils/env.ts index f22a8aa..073bee9 100644 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -1,8 +1,8 @@ import { boolish } from 'getenv'; export const env = { - get EXPO_ATLAS_DEBUG() { - return boolish('EXPO_ATLAS_DEBUG', false); + get EXPO_DEBUG() { + return boolish('EXPO_DEBUG', false); }, get EXPO_ATLAS_NO_STATS_VALIDATION() { return boolish('EXPO_ATLAS_NO_STATS_VALIDATION', false); diff --git a/src/utils/global.ts b/src/utils/global.ts index 3e09141..ac3939b 100644 --- a/src/utils/global.ts +++ b/src/utils/global.ts @@ -1,9 +1,9 @@ -import { StatsSource } from '../data/types'; +import { AtlasSource } from '../data/types'; declare global { /** * The globally initialized data source for Atlas. * This is set in a global to access the data from the bundled webui API routes. */ - var EXPO_ATLAS_SOURCE: StatsSource; // eslint-disable-line no-var + var EXPO_ATLAS_SOURCE: AtlasSource; // eslint-disable-line no-var } diff --git a/src/utils/middleware.ts b/src/utils/middleware.ts index 219f659..c114e54 100644 --- a/src/utils/middleware.ts +++ b/src/utils/middleware.ts @@ -5,7 +5,7 @@ import path from 'path'; import serveStaticHandler from 'serve-static'; import { env } from './env'; -import { type StatsSource } from '../data/types'; +import { type AtlasSource } from '../data/types'; const WEBUI_ROOT = path.resolve(__dirname, '../../../webui'); @@ -27,12 +27,12 @@ const SERVER_BUILD_DIR = path.join(WEBUI_ROOT, 'dist/server'); * app.use('/_expo/atlas', middleware); * ``` */ -export function createAtlasMiddleware(source: StatsSource) { +export function createAtlasMiddleware(source: AtlasSource) { global.EXPO_ATLAS_SOURCE = source; const middleware = connect(); - if (env.EXPO_ATLAS_DEBUG) { + if (env.EXPO_DEBUG) { middleware.use(morgan('tiny')); } diff --git a/src/utils/stats.ts b/src/utils/stats.ts deleted file mode 100644 index fd746f6..0000000 --- a/src/utils/stats.ts +++ /dev/null @@ -1,46 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -import { appendJsonLine, parseJsonLine } from './jsonl'; -import { name, version } from '../../package.json'; -import { env } from '../utils/env'; -import { AtlasValidationError } from '../utils/errors'; - -export type StatsMetadata = { name: string; version: string }; - -/** The default location of the metro stats file */ -export function getStatsPath(projectRoot: string) { - return path.join(projectRoot, '.expo/atlas.jsonl'); -} - -/** The information to validate if a stats file is compatible with this library version */ -export function getStatsMetdata(): StatsMetadata { - return { name, version }; -} - -/** Validate if the stats file is compatible with this library version */ -export async function validateStatsFile(statsFile: string, metadata = getStatsMetdata()) { - if (!fs.existsSync(statsFile)) { - throw new AtlasValidationError('STATS_FILE_NOT_FOUND', statsFile); - } - - if (env.EXPO_ATLAS_NO_STATS_VALIDATION) { - return; - } - - const data = await parseJsonLine(statsFile, 1); - - if (data.name !== metadata.name || data.version !== metadata.version) { - throw new AtlasValidationError('STATS_FILE_INCOMPATIBLE', statsFile, data.version); - } -} - -/** - * Create or overwrite the stats file with basic metadata. - * This metdata is used by the API to determine version compatibility. - */ -export async function createStatsFile(filePath: string) { - await fs.promises.mkdir(path.dirname(filePath), { recursive: true }); - await fs.promises.rm(filePath, { force: true }); - await appendJsonLine(filePath, getStatsMetdata()); -} diff --git a/webui/src/app/stats/[entry]/folders/[path].tsx b/webui/src/app/(atlas)/[entry]/folders/[path].tsx similarity index 71% rename from webui/src/app/stats/[entry]/folders/[path].tsx rename to webui/src/app/(atlas)/[entry]/folders/[path].tsx index 4dcc0aa..a42fff6 100644 --- a/webui/src/app/stats/[entry]/folders/[path].tsx +++ b/webui/src/app/(atlas)/[entry]/folders/[path].tsx @@ -1,20 +1,20 @@ import { keepPreviousData, useQuery } from '@tanstack/react-query'; import { useLocalSearchParams } from 'expo-router'; -import type { ModuleGraphResponse } from '~/app/api/stats/[entry]/modules/graph+api'; +import type { ModuleGraphResponse } from '~/app/--/entries/[entry]/modules/graph+api'; import { BundleGraph } from '~/components/BundleGraph'; -import { Page, PageHeader, PageTitle } from '~/components/Page'; -import { StatsModuleFilter } from '~/components/forms/StatsModuleFilter'; -import { useStatsEntry } from '~/providers/stats'; +import { Page, PageContent, PageHeader, PageTitle } from '~/components/Page'; +import { ModuleFiltersForm } from '~/components/forms/ModuleFilter'; +import { useEntry } from '~/providers/entries'; import { Tag } from '~/ui/Tag'; import { fetchApi } from '~/utils/api'; import { type ModuleFilters, useModuleFilters, moduleFiltersToParams } from '~/utils/filters'; import { formatFileSize } from '~/utils/formatString'; -import { relativeEntryPath } from '~/utils/stats'; +import { relativeEntryPath } from '~/utils/entry'; export default function FolderPage() { const { path: absolutePath } = useLocalSearchParams<{ path: string }>(); - const { entry } = useStatsEntry(); + const { entry } = useEntry(); const { filters, filtersEnabled } = useModuleFilters(); const modules = useModuleGraphDataInFolder(entry.id, absolutePath!, filters); const treeHasData = !!modules.data?.data?.children?.length; @@ -29,21 +29,23 @@ export default function FolderPage() { {!!modules.data && } - + {modules.isError ? ( -
- Could not load the graph, try reloading this page -
+ + Try restarting Expo Atlas. If this error keeps happening, open a bug report. + ) : treeHasData ? ( ) : ( !modules.isPending && ( -
- {!filtersEnabled - ? 'No data available' - : 'No data available, try resetting the filters'} -
+ +

+ {filtersEnabled + ? 'Try adjusting or clearing the filters' + : 'Try another bundle entry'} +

+
) )} @@ -87,8 +89,8 @@ function useModuleGraphDataInFolder(entryId: string, path: string, filters: Modu ModuleFilters | undefined, ]; const url = filters - ? `/api/stats/${entry}/modules/graph?path=${encodeURIComponent(path)}&${moduleFiltersToParams(filters)}` - : `/api/stats/${entry}/modules/graph?path=${encodeURIComponent(path)}`; + ? `/entries/${entry}/modules/graph?path=${encodeURIComponent(path)}&${moduleFiltersToParams(filters)}` + : `/entries/${entry}/modules/graph?path=${encodeURIComponent(path)}`; return fetchApi(url) .then((res) => (res.ok ? res : Promise.reject(res))) diff --git a/webui/src/app/stats/[entry]/index.tsx b/webui/src/app/(atlas)/[entry]/index.tsx similarity index 69% rename from webui/src/app/stats/[entry]/index.tsx rename to webui/src/app/(atlas)/[entry]/index.tsx index 7f2a2f7..5ef9275 100644 --- a/webui/src/app/stats/[entry]/index.tsx +++ b/webui/src/app/(atlas)/[entry]/index.tsx @@ -1,10 +1,10 @@ import { keepPreviousData, useQuery } from '@tanstack/react-query'; -import type { ModuleGraphResponse } from '~/app/api/stats/[entry]/modules/graph+api'; +import type { ModuleGraphResponse } from '~/app/--/entries/[entry]/modules/graph+api'; import { BundleGraph } from '~/components/BundleGraph'; -import { Page, PageHeader, PageTitle } from '~/components/Page'; -import { StatsModuleFilter } from '~/components/forms/StatsModuleFilter'; -import { useStatsEntry } from '~/providers/stats'; +import { Page, PageContent, PageHeader, PageTitle } from '~/components/Page'; +import { ModuleFiltersForm } from '~/components/forms/ModuleFilter'; +import { useEntry } from '~/providers/entries'; import { Spinner } from '~/ui/Spinner'; import { Tag } from '~/ui/Tag'; import { fetchApi } from '~/utils/api'; @@ -12,7 +12,7 @@ import { type ModuleFilters, moduleFiltersToParams, useModuleFilters } from '~/u import { formatFileSize } from '~/utils/formatString'; export default function StatsPage() { - const { entry } = useStatsEntry(); + const { entry } = useEntry(); const { filters, filtersEnabled } = useModuleFilters(); const modules = useModuleGraphData(entry.id, filters); const treeHasData = !!modules.data?.data?.children?.length; @@ -25,18 +25,26 @@ export default function StatsPage() {

Bundle

{!!modules.data && } - + - {(modules.isPending && !modules.isPlaceholderData) || modules.isError ? ( -
- {modules.isError ? 'Could not load the graph, try reloading this page' : } -
+ {modules.isPending && !modules.isPlaceholderData ? ( + + + + ) : modules.isError ? ( + +

Try restarting Expo Atlas. If this error keeps happening, open a bug report.

+
) : treeHasData ? ( ) : ( -
- {!filtersEnabled ? 'No data available' : 'No data available, try resetting the filters'} -
+ +

+ {filtersEnabled + ? 'Try adjusting or clearing the filters' + : 'Try another bundle entry'} +

+
)} @@ -73,8 +81,8 @@ function useModuleGraphData(entryId: string, filters: ModuleFilters) { queryFn: ({ queryKey }) => { const [_key, entry, filters] = queryKey as [string, string, ModuleFilters | undefined]; const url = filters - ? `/api/stats/${entry}/modules/graph?${moduleFiltersToParams(filters)}` - : `/api/stats/${entry}/modules/graph`; + ? `/entries/${entry}/modules/graph?${moduleFiltersToParams(filters)}` + : `/entries/${entry}/modules/graph`; return fetchApi(url) .then((res) => (res.ok ? res : Promise.reject(res))) diff --git a/webui/src/app/stats/[entry]/modules/[path].tsx b/webui/src/app/(atlas)/[entry]/modules/[path].tsx similarity index 89% rename from webui/src/app/stats/[entry]/modules/[path].tsx rename to webui/src/app/(atlas)/[entry]/modules/[path].tsx index 2755d6c..a468dd5 100644 --- a/webui/src/app/stats/[entry]/modules/[path].tsx +++ b/webui/src/app/(atlas)/[entry]/modules/[path].tsx @@ -2,17 +2,17 @@ import { useQuery } from '@tanstack/react-query'; import { Link, useLocalSearchParams } from 'expo-router'; import { Page, PageHeader, PageTitle } from '~/components/Page'; -import { useStatsEntry } from '~/providers/stats'; +import { useEntry } from '~/providers/entries'; 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 { relativeEntryPath } from '~/utils/stats'; -import { type PartialStatsEntry, type StatsModule } from '~core/data/types'; +import { relativeEntryPath } from '~/utils/entry'; +import { type PartialAtlasEntry, type AtlasModule } from '~core/data/types'; export default function ModulePage() { - const { entry } = useStatsEntry(); + const { entry } = useEntry(); const { path: absolutePath } = useLocalSearchParams<{ path: string }>(); const module = useModuleData(entry.id, absolutePath!); @@ -52,7 +52,7 @@ export default function ModulePage() { @@ -84,8 +84,8 @@ function ModuleSummary({ module, platform, }: { - module: StatsModule; - platform?: PartialStatsEntry['platform']; + module: AtlasModule; + platform?: PartialAtlasEntry['platform']; }) { return (
@@ -108,18 +108,19 @@ function ModuleSummary({ ); } -function getModuleType(module: StatsModule) { +function getModuleType(module: AtlasModule) { const type = module.path.includes('?ctx=') ? 'require.context' : 'file'; return module.package ? `package ${type}` : type; } /** Load the module data from API, by path reference only */ function useModuleData(entryId: string, path: string) { - return useQuery({ + return useQuery({ + refetchOnWindowFocus: false, queryKey: [`module`, entryId, path], queryFn: async ({ queryKey }) => { const [_key, entry, path] = queryKey as [string, string, string]; - return fetchApi(`/api/stats/${entry}/modules`, { + return fetchApi(`/entries/${entry}/modules`, { method: 'POST', body: JSON.stringify({ path }), }) diff --git a/webui/src/app/api/stats/[entry]/index+api.ts b/webui/src/app/--/entries/[entry]/index+api.ts similarity index 100% rename from webui/src/app/api/stats/[entry]/index+api.ts rename to webui/src/app/--/entries/[entry]/index+api.ts diff --git a/webui/src/app/api/stats/[entry]/modules/graph+api.ts b/webui/src/app/--/entries/[entry]/modules/graph+api.ts similarity index 89% rename from webui/src/app/api/stats/[entry]/modules/graph+api.ts rename to webui/src/app/--/entries/[entry]/modules/graph+api.ts index 9feaf39..49cd6cc 100644 --- a/webui/src/app/api/stats/[entry]/modules/graph+api.ts +++ b/webui/src/app/--/entries/[entry]/modules/graph+api.ts @@ -1,7 +1,7 @@ import { getSource } from '~/utils/atlas'; import { filterModules, moduleFiltersFromParams } from '~/utils/filters'; import { type TreemapNode, createModuleTree, finalizeModuleTree } from '~/utils/treemap'; -import type { StatsEntry } from '~core/data/types'; +import type { AtlasEntry } from '~core/data/types'; export type ModuleGraphResponse = { data: TreemapNode; @@ -17,7 +17,7 @@ export type ModuleGraphResponse = { }; export async function GET(request: Request, params: Record<'entry', string>) { - let entry: StatsEntry; + let entry: AtlasEntry; try { entry = await getSource().getEntry(params.entry); @@ -33,10 +33,8 @@ export async function GET(request: Request, params: Record<'entry', string>) { rootPath: query.get('path') || undefined, }); - const tree = createModuleTree(filteredModules); - const response: ModuleGraphResponse = { - data: finalizeModuleTree(tree), + data: finalizeModuleTree(createModuleTree(filteredModules)), entry: { platform: entry.platform as any, moduleSize: allModules.reduce((size, module) => size + module.size, 0), diff --git a/webui/src/app/api/stats/[entry]/modules/index+api.ts b/webui/src/app/--/entries/[entry]/modules/index+api.ts similarity index 92% rename from webui/src/app/api/stats/[entry]/modules/index+api.ts rename to webui/src/app/--/entries/[entry]/modules/index+api.ts index ee72aa5..b1e780f 100644 --- a/webui/src/app/api/stats/[entry]/modules/index+api.ts +++ b/webui/src/app/--/entries/[entry]/modules/index+api.ts @@ -1,9 +1,9 @@ import { getSource } from '~/utils/atlas'; import { filterModules, moduleFiltersFromParams } from '~/utils/filters'; -import { type StatsEntry, type StatsModule } from '~core/data/types'; +import { type AtlasEntry, type AtlasModule } from '~core/data/types'; /** The partial module data, when listing all available modules from a stats entry */ -export type PartialModule = Omit; +export type PartialModule = Omit; export type ModuleListResponse = { data: PartialModule[]; @@ -20,7 +20,7 @@ export type ModuleListResponse = { /** Get all modules as simple list */ export async function GET(request: Request, params: Record<'entry', string>) { - let entry: StatsEntry; + let entry: AtlasEntry; try { entry = await getSource().getEntry(params.entry); @@ -69,7 +69,7 @@ export async function POST(request: Request, params: Record<'entry', string>) { ); } - let entry: StatsEntry; + let entry: AtlasEntry; try { entry = await getSource().getEntry(params.entry); diff --git a/webui/src/app/api/stats/index+api.ts b/webui/src/app/--/entries/index+api.ts similarity index 100% rename from webui/src/app/api/stats/index+api.ts rename to webui/src/app/--/entries/index+api.ts diff --git a/webui/src/app/_layout.tsx b/webui/src/app/_layout.tsx index b4789e7..30a6670 100644 --- a/webui/src/app/_layout.tsx +++ b/webui/src/app/_layout.tsx @@ -1,7 +1,7 @@ import { Slot } from 'expo-router'; +import { EntryProvider } from '~/providers/entries'; import { QueryProvider } from '~/providers/query'; -import { StatsEntryProvider } from '~/providers/stats'; import { ThemeProvider } from '~/providers/theme'; // Import the Expo-required radix styles @@ -32,9 +32,9 @@ export default function RootLayout() { return ( - + - + ); diff --git a/webui/src/app/index.tsx b/webui/src/app/index.tsx index 2050ff8..7ed9f9d 100644 --- a/webui/src/app/index.tsx +++ b/webui/src/app/index.tsx @@ -1,9 +1,9 @@ import { Redirect } from 'expo-router'; -import { useStatsEntry } from '~/providers/stats'; +import { useEntry } from '~/providers/entries'; export default function HomeScreen() { - const { entry } = useStatsEntry(); + const { entry } = useEntry(); - return ; + return ; } diff --git a/webui/src/components/BundleGraph.tsx b/webui/src/components/BundleGraph.tsx index e75050c..d753c3b 100644 --- a/webui/src/components/BundleGraph.tsx +++ b/webui/src/components/BundleGraph.tsx @@ -10,13 +10,13 @@ import { useMemo } from 'react'; import { formatFileSize } from '~/utils/formatString'; import type { TreemapNode } from '~/utils/treemap'; -import type { PartialStatsEntry } from '~core/data/types'; +import type { PartialAtlasEntry } from '~core/data/types'; // Register used echarts components, to avoid loading unused code echarts.use([TooltipComponent, TreemapChart, CanvasRenderer]); type BundleGraphProps = { - entry: PartialStatsEntry; + entry: PartialAtlasEntry; graph: TreemapNode; }; @@ -51,8 +51,8 @@ export function BundleGraph(props: BundleGraphProps) { if (event.event.altKey || event.event.ctrlKey || event.event.metaKey) { router.push({ pathname: data.children?.length - ? '/stats/[entry]/folders/[path]' - : '/stats/[entry]/modules/[path]', + ? '/(atlas)/[entry]/folders/[path]' + : '/(atlas)/[entry]/modules/[path]', params: { entry: props.entry.id, path: data.value === 100 ? data.name : data.modulePath, diff --git a/webui/src/components/Page.tsx b/webui/src/components/Page.tsx index cd990cf..78c2932 100644 --- a/webui/src/components/Page.tsx +++ b/webui/src/components/Page.tsx @@ -1,8 +1,8 @@ import { cva, type VariantProps } from 'class-variance-authority'; import cn from 'classnames'; -import { forwardRef, type HTMLAttributes } from 'react'; +import { forwardRef, type PropsWithChildren, type HTMLAttributes } from 'react'; -import { StatsEntrySelect } from '~/components/forms/StatsEntrySelect'; +import { StatsEntrySelect } from '~/components/forms/EntrySelect'; import { LayoutContent, LayoutNavigation } from '~/ui/Layout'; export { LayoutHeader as PageHeader, LayoutTitle as PageTitle } from '~/ui/Layout'; @@ -45,3 +45,16 @@ export const Page = forwardRef( } ); Page.displayName = 'Page'; + +type PageContentProps = PropsWithChildren & { + title?: string; +}; + +export function PageContent({ title, children }: PageContentProps) { + return ( +
+ {!!title &&

{title}

} + {children} +
+ ); +} diff --git a/webui/src/components/forms/StatsEntrySelect.tsx b/webui/src/components/forms/EntrySelect.tsx similarity index 94% rename from webui/src/components/forms/StatsEntrySelect.tsx rename to webui/src/components/forms/EntrySelect.tsx index 09c3327..bd02563 100644 --- a/webui/src/components/forms/StatsEntrySelect.tsx +++ b/webui/src/components/forms/EntrySelect.tsx @@ -6,14 +6,14 @@ import ChevronDownIcon from 'lucide-react/dist/esm/icons/chevron-down'; // @ts-expect-error import ChevronUpIcon from 'lucide-react/dist/esm/icons/chevron-up'; -import { useStatsEntry } from '~/providers/stats'; +import { useEntry } from '~/providers/entries'; import { Button } from '~/ui/Button'; import { Tag } from '~/ui/Tag'; -import { relativeEntryPath } from '~/utils/stats'; +import { relativeEntryPath } from '~/utils/entry'; export function StatsEntrySelect() { const router = useRouter(); - const { entry, entries } = useStatsEntry(); + const { entry, entries } = useEntry(); return ( router.setParams({ entry })}> diff --git a/webui/src/components/forms/StatsModuleFilter.tsx b/webui/src/components/forms/ModuleFilter.tsx similarity index 97% rename from webui/src/components/forms/StatsModuleFilter.tsx rename to webui/src/components/forms/ModuleFilter.tsx index 60ce905..d2d9334 100644 --- a/webui/src/components/forms/StatsModuleFilter.tsx +++ b/webui/src/components/forms/ModuleFilter.tsx @@ -16,11 +16,11 @@ import { import { debounce } from '~/utils/debounce'; import { useModuleFilters } from '~/utils/filters'; -type StatsModuleFilterProps = { +type ModuleFiltersFormProps = { disableNodeModules?: boolean; }; -export function StatsModuleFilter(props: StatsModuleFilterProps) { +export function ModuleFiltersForm(props: ModuleFiltersFormProps) { const router = useRouter(); const { filters, filtersEnabled } = useModuleFilters(); diff --git a/webui/src/providers/entries.tsx b/webui/src/providers/entries.tsx new file mode 100644 index 0000000..18b6f51 --- /dev/null +++ b/webui/src/providers/entries.tsx @@ -0,0 +1,73 @@ +import { useQuery } from '@tanstack/react-query'; +import { useLocalSearchParams } from 'expo-router'; +import { type PropsWithChildren, createContext, useContext, useMemo } from 'react'; + +import { PageContent } from '~/components/Page'; +import { Spinner } from '~/ui/Spinner'; +import { fetchApi } from '~/utils/api'; +import { type PartialAtlasEntry } from '~core/data/types'; + +type EntryContext = { + entries: NonNullable['data']>; +}; + +export const entryContext = createContext({ + entries: [], +}); + +export const useEntry = () => { + const { entries } = useContext(entryContext); + const { entry: entryId } = useLocalSearchParams<{ entry?: string }>(); + const entry = useMemo( + () => entries.find((entry) => entry.id === entryId) || entries[0], + [entries, entryId] + ); + + return { entry, entries }; +}; + +export function EntryProvider({ children }: PropsWithChildren) { + const entries = useEntryData(); + + if (entries.data?.length) { + return ( + + {children} + + ); + } + + // TODO: add better UX for loading + if (entries.isFetching && !entries.data?.length) { + return ( + + + + ); + } + + // TODO: add better UX for empty state + if (entries.isFetched && !entries.data?.length) { + return ( + +

Open your app in the browser, or device, to collect data.

+
+ ); + } + + // TODO: add better UX for error state + return ( + +

Try restarting Expo Atlas. If this error keeps happening, open a bug report.

+
+ ); +} + +/** Load all available stats entries from API */ +function useEntryData() { + return useQuery({ + refetchOnWindowFocus: false, + queryKey: ['entries'], + queryFn: () => fetchApi('/entries').then((res) => res.json()), + }); +} diff --git a/webui/src/providers/stats.tsx b/webui/src/providers/stats.tsx deleted file mode 100644 index e38cdbd..0000000 --- a/webui/src/providers/stats.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { useLocalSearchParams } from 'expo-router'; -import { type PropsWithChildren, createContext, useContext, useMemo } from 'react'; - -import { Spinner } from '~/ui/Spinner'; -import { fetchApi } from '~/utils/api'; -import { type PartialStatsEntry } from '~core/data/types'; - -type StatsEntryContext = { - entries: NonNullable['data']>; -}; - -export const statsEntryContext = createContext({ - entries: [], -}); - -export const useStatsEntry = () => { - const { entries } = useContext(statsEntryContext); - const { entry: entryId } = useLocalSearchParams<{ entry?: string }>(); - const entry = useMemo( - () => entries.find((entry) => entry.id === entryId) || entries[0], - [entries, entryId] - ); - - return { entry, entries }; -}; - -export function StatsEntryProvider({ children }: PropsWithChildren) { - const entries = useStatsEntriesData(); - - if (entries.data?.length) { - return ( - - {children} - - ); - } - - // TODO: add better UX for loading - if (entries.isFetching && !entries.data?.length) { - return ( -
- -
- ); - } - - // TODO: add better UX for empty state - if (entries.isFetched && !entries.data?.length) { - return ( -
-

No stats found.

-

Open your app in the browser, or device, to collect the stats.

-
- ); - } - - // TODO: add better UX for error state - return ( -
-

No stats source.

-

Try restarting Expo Atlas. If this error keeps happening, open a bug report.

-
- ); -} - -/** Load all available stats entries from API */ -function useStatsEntriesData() { - return useQuery({ - queryKey: ['stats-entries'], - queryFn: () => fetchApi('/api/stats').then((res) => res.json()), - }); -} diff --git a/webui/src/utils/api.ts b/webui/src/utils/api.ts index fb7a6f4..c3bee85 100644 --- a/webui/src/utils/api.ts +++ b/webui/src/utils/api.ts @@ -4,7 +4,7 @@ * * @see https://docs.expo.dev/versions/latest/config/app/#baseurl */ -const baseUrl = __DEV__ ? '' : '/_expo/atlas'; +const baseUrl = __DEV__ ? '/--' : '/_expo/atlas/--'; /** * Fetch data from the API routes, adding the `baseUrl` to all requests. diff --git a/webui/src/utils/stats.ts b/webui/src/utils/entry.ts similarity index 65% rename from webui/src/utils/stats.ts rename to webui/src/utils/entry.ts index d9ef516..d023a75 100644 --- a/webui/src/utils/stats.ts +++ b/webui/src/utils/entry.ts @@ -1,9 +1,9 @@ -import { PartialStatsEntry } from '~core/data/types'; +import { PartialAtlasEntry } from '~core/data/types'; /** * Translate an absolute path to a relative path, based on the entry's project root. * This is a simple replace operation. */ -export function relativeEntryPath(entry: Pick, path: string) { +export function relativeEntryPath(entry: Pick, path: string) { return path.replace(entry.projectRoot + '/', ''); } diff --git a/webui/src/utils/filters.ts b/webui/src/utils/filters.ts index 048580a..5c050e9 100644 --- a/webui/src/utils/filters.ts +++ b/webui/src/utils/filters.ts @@ -2,7 +2,7 @@ import { useGlobalSearchParams } from 'expo-router'; import path from 'path'; import picomatch from 'picomatch'; -import { type StatsModule } from '~core/data/types'; +import { type AtlasModule } from '~core/data/types'; export type ModuleFilters = { /** Only match the project code, or all code including (external) packages */ @@ -63,7 +63,7 @@ export function useModuleFilters() { /** Filter the modules based on the filters, and an optional (root) path. */ export function filterModules( - modules: StatsModule[], + modules: AtlasModule[], options: { projectRoot: string; filters: ModuleFilters; diff --git a/webui/src/utils/treemap.ts b/webui/src/utils/treemap.ts index d4f9115..a053396 100644 --- a/webui/src/utils/treemap.ts +++ b/webui/src/utils/treemap.ts @@ -1,4 +1,4 @@ -import type { StatsModule } from '~core/data/types'; +import type { AtlasModule } from '~core/data/types'; export type TreemapNode = { /** The current path of the node */ @@ -52,7 +52,7 @@ export function finalizeModuleTree(node: TreemapNode): TreemapNode { * Create a nested treemap from the list of modules. * This will group the modules by `module.path` segments, and add metadata to each node. */ -export function createModuleTree(modules: StatsModule[]): TreemapNode { +export function createModuleTree(modules: AtlasModule[]): TreemapNode { const totalSize = modules.reduce((total, module) => total + module.size, 0); const map: TreemapNode = { name: '/', // This is the root, so no prefix