Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: drop exessive use of stats naming #30

Merged
merged 2 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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