Skip to content

Commit

Permalink
refactor: drop exessive use of stats naming (#30)
Browse files Browse the repository at this point in the history
* refactor: drop exessive use of `stats` naming

* chore: resolve linting issues
  • Loading branch information
byCedric authored Apr 5, 2024
1 parent 2b9bf5a commit 43d68f0
Show file tree
Hide file tree
Showing 37 changed files with 417 additions and 388 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Expo Atlas

Inspect the bundle stats from Metro.
Inspect bundle contents, on module level, from Metro.

> [!Warning]
> This project is highly experimental and will likely not work for your project.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"sideEffects": false,
"name": "expo-atlas",
"version": "0.0.18-preview.2",
"description": "Inspect bundle stats from Metro",
"description": "Inspect bundle contents, on module level, from Metro",
"keywords": [
"expo",
"atlas",
Expand Down
12 changes: 6 additions & 6 deletions src/cli/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ if (args['--version']) {
if (args['--help']) {
printLines([
chalk.bold('Usage'),
` ${chalk.dim('$')} expo-atlas ${chalk.dim('[statsFile]')}`,
` ${chalk.dim('$')} expo-atlas ${chalk.dim('[atlas file]')}`,
'',
chalk.bold('Options'),
` --port${chalk.dim(', -p')} Port to listen on`,
Expand All @@ -52,7 +52,7 @@ async function run() {

printLines([
`Expo Atlas is ready on: ${chalk.underline(href)}`,
` ${chalk.dim(`Using: ${options.statsFile}`)}`,
` ${chalk.dim(`Using: ${options.atlasFile}`)}`,
]);

if (options.browserOpen) {
Expand All @@ -72,10 +72,10 @@ run().catch((error) => {
throw error;
}

if (error.code === 'STATS_FILE_INCOMPATIBLE') {
const statsFile = path.relative(process.cwd(), error.statsFile);
console.error('Stats file is incompatible with this version, use this instead:');
console.error(` npx expo-atlas@${error.incompatibleVersion} ${statsFile}`);
if (error.code === 'ATLAS_FILE_INCOMPATIBLE') {
const atlasFile = path.relative(process.cwd(), error.filePath);
console.error('Atlas file is incompatible with this version, use this instead:');
console.error(` npx expo-atlas@${error.incompatibleVersion} ${atlasFile}`);
} else {
console.error(`${error.message} (${error.code})`);
}
Expand Down
4 changes: 2 additions & 2 deletions src/cli/createServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.atlasFile);
const middleware = createAtlasMiddleware(source);
const baseUrl = '/_expo/atlas'; // Keep in sync with webui `app.json` `baseUrl`

Expand Down
14 changes: 7 additions & 7 deletions src/cli/resolveOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ import freeport from 'freeport-async';
import path from 'path';

import { type Input } from './bin';
import { getStatsPath, validateStatsFile } from '../utils/stats';
import { getAtlasPath, validateAtlasFile } from '../data/AtlasFileSource';

export type Options = Awaited<ReturnType<typeof resolveOptions>>;

export async function resolveOptions(input: Input) {
const statsFile = await resolveStatsFile(input);
const atlasFile = await resolveAtlasFile(input);
const port = await resolvePort(input);
return { statsFile, port, browserOpen: input['--no-open'] !== true };
return { atlasFile, port, browserOpen: input['--no-open'] !== true };
}

async function resolveStatsFile(input: Input) {
const statsFile = input._[0] ?? getStatsPath(process.cwd());
await validateStatsFile(statsFile);
return path.resolve(statsFile);
async function resolveAtlasFile(input: Input) {
const atlasFile = input._[0] ?? getAtlasPath(process.cwd());
await validateAtlasFile(atlasFile);
return path.resolve(atlasFile);
}

async function resolvePort(input: Pick<Input, '--port'>) {
Expand Down
128 changes: 128 additions & 0 deletions src/data/AtlasFileSource.ts
Original file line number Diff line number Diff line change
@@ -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 filePath: string) {
//
}

listEntries() {
return listAtlasEntries(this.filePath);
}

getEntry(id: string) {
const numeric = parseInt(id, 10);
assert(!Number.isNaN(numeric) && numeric > 1, `Invalid entry ID: ${id}`);
return readAtlasEntry(this.filePath, Number(id));
}
}

/**
* List all entries without parsing the data.
* This only reads the bundle name, and adds a line number as ID.
*/
export async function listAtlasEntries(filePath: string) {
const bundlePattern = /^\["([^"]+)","([^"]+)","([^"]+)/;
const entries: PartialAtlasEntry[] = [];

await forEachJsonLines(filePath, (contents, line) => {
// Skip the 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 entry by id or line number, and parse the data.
*/
export async function readAtlasEntry(filePath: string, id: number): Promise<AtlasEntry> {
const atlasEntry = await parseJsonLine<any[]>(filePath, id);
return {
id: String(id),
platform: atlasEntry[0],
projectRoot: atlasEntry[1],
entryPoint: atlasEntry[2],
runtimeModules: atlasEntry[3],
modules: new Map(atlasEntry[4].map((module) => [module.path, module])),
transformOptions: atlasEntry[5],
serializeOptions: atlasEntry[6],
};
}

/** Simple promise to avoid mixing appended data */
let writeQueue: Promise<any> = Promise.resolve();

/**
* Add a new entry to the file.
* This is appended on a new line, so we can load the selectively.
*/
export function writeAtlasEntry(filePath: string, entry: AtlasEntry) {
const line = [
entry.platform,
entry.projectRoot,
entry.entryPoint,
entry.runtimeModules,
Array.from(entry.modules.values()),
entry.transformOptions,
entry.serializeOptions,
];

return (writeQueue = writeQueue.then(() => appendJsonLine(filePath, line)));
}

/** The default location of the metro file */
export function getAtlasPath(projectRoot: string) {
return path.join(projectRoot, '.expo/atlas.jsonl');
}

/** The information to validate if a file is compatible with this library version */
export function getAtlasMetdata(): AtlasMetadata {
return { name, version };
}

/** Validate if the file is compatible with this library version */
export async function validateAtlasFile(filePath: string, metadata = getAtlasMetdata()) {
if (!fs.existsSync(filePath)) {
throw new AtlasValidationError('ATLAS_FILE_NOT_FOUND', filePath);
}

if (env.EXPO_ATLAS_NO_VALIDATION) {
return;
}

const data = await parseJsonLine(filePath, 1);

if (data.name !== metadata.name || data.version !== metadata.version) {
throw new AtlasValidationError('ATLAS_FILE_INCOMPATIBLE', filePath, data.version);
}
}

/**
* Create or overwrite the 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());
}
42 changes: 21 additions & 21 deletions src/data/MetroGraphSource.ts
Original file line number Diff line number Diff line change
@@ -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<MetroModule[]>;
Expand All @@ -20,9 +20,9 @@ type ConvertGraphToStatsOptions = {
};
};

export class MetroGraphSource implements StatsSource {
/** All known stats entries, stored by ID */
protected entries: Map<StatsEntry['id'], StatsEntry> = new Map();
export class MetroGraphSource implements AtlasSource {
/** All known entries, stored by ID */
protected entries: Map<AtlasEntry['id'], AtlasEntry> = new Map();

listEntries() {
return Array.from(this.entries.values()).map((entry) => ({
Expand All @@ -36,24 +36,24 @@ export class MetroGraphSource implements StatsSource {
getEntry(id: string) {
const entry = this.entries.get(id);
if (!entry) {
throw new Error(`Stats entry "${id}" not found.`);
throw new Error(`Entry "${id}" not found.`);
}
return entry;
}

/**
* Event handler when a new graph instance is ready to serialize.
* This converts all relevant data stored in the graph to stats objects.
* This converts all relevant data stored in the graph to objects.
*/
onSerializeGraph(options: ConvertGraphToStatsOptions) {
onSerializeGraph(options: ConvertGraphToAtlasOptions) {
const entry = convertGraph(options);
this.entries.set(entry.id, entry);
return entry;
}
}

/** Convert a Metro graph instance to a JSON-serializable stats entry */
export function convertGraph(options: ConvertGraphToStatsOptions): StatsEntry {
/** Convert a Metro graph instance to a JSON-serializable entry */
export function convertGraph(options: ConvertGraphToAtlasOptions): AtlasEntry {
const serializeOptions = convertSerializeOptions(options);
const transformOptions = convertTransformOptions(options);
const platform =
Expand All @@ -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<ConvertGraphToStatsOptions, 'graph' | 'entryPoint' | 'extensions'>
options: Pick<ConvertGraphToAtlasOptions, 'graph' | 'entryPoint' | 'extensions'>
) {
const modules = new Map<string, StatsModule>();
const modules = new Map<string, AtlasModule>();

function discover(modulePath: string) {
const module = options.graph.dependencies.get(modulePath);
Expand All @@ -92,11 +92,11 @@ export function collectEntryPointModules(
return modules;
}

/** Convert a Metro module to a JSON-serializable stats module */
/** Convert a Metro module to a JSON-serializable Atlas module */
export function convertModule(
options: Pick<ConvertGraphToStatsOptions, 'graph' | 'extensions'>,
options: Pick<ConvertGraphToAtlasOptions, 'graph' | 'extensions'>,
module: MetroModule
): StatsModule {
): AtlasModule {
return {
path: module.path,
package: getPackageNameFromPath(module.path),
Expand All @@ -118,7 +118,7 @@ export function convertModule(
* If a file is an asset, it returns `[binary file]` instead.
*/
function getModuleSourceContent(
options: Pick<ConvertGraphToStatsOptions, 'extensions'>,
options: Pick<ConvertGraphToAtlasOptions, 'extensions'>,
module: MetroModule
) {
const fileExtension = path.extname(module.path).replace('.', '');
Expand All @@ -144,16 +144,16 @@ function getModuleSourceContent(

/** Convert Metro transform options to a JSON-serializable object */
export function convertTransformOptions(
options: Pick<ConvertGraphToStatsOptions, 'graph'>
): StatsEntry['transformOptions'] {
options: Pick<ConvertGraphToAtlasOptions, 'graph'>
): AtlasEntry['transformOptions'] {
return options.graph.transformOptions ?? {};
}

/** Convert Metro serialize options to a JSON-serializable object */
export function convertSerializeOptions(
options: Pick<ConvertGraphToStatsOptions, 'options'>
): StatsEntry['serializeOptions'] {
const serializeOptions: StatsEntry['serializeOptions'] = { ...options.options };
options: Pick<ConvertGraphToAtlasOptions, 'options'>
): AtlasEntry['serializeOptions'] {
const serializeOptions: AtlasEntry['serializeOptions'] = { ...options.options };

// Delete all filters
delete serializeOptions['processModuleFilter'];
Expand Down
Loading

0 comments on commit 43d68f0

Please sign in to comment.