Skip to content

Commit

Permalink
refactor: drop exessive use of stats naming
Browse files Browse the repository at this point in the history
  • Loading branch information
byCedric committed Apr 5, 2024
1 parent 2b9bf5a commit 4542fbf
Show file tree
Hide file tree
Showing 32 changed files with 367 additions and 337 deletions.
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.statsFile);
const middleware = createAtlasMiddleware(source);
const baseUrl = '/_expo/atlas'; // Keep in sync with webui `app.json` `baseUrl`

Expand Down
6 changes: 3 additions & 3 deletions src/cli/resolveOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType<typeof resolveOptions>>;

Expand All @@ -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);
}

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 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<AtlasEntry> {
const statsEntry = await parseJsonLine<any[]>(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<any> = 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());
}
32 changes: 16 additions & 16 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 {
export class MetroGraphSource implements AtlasSource {
/** All known stats entries, stored by ID */
protected entries: Map<StatsEntry['id'], StatsEntry> = new Map();
protected entries: Map<AtlasEntry['id'], AtlasEntry> = new Map();

listEntries() {
return Array.from(this.entries.values()).map((entry) => ({
Expand All @@ -45,15 +45,15 @@ 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;
}
}

/** 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 =
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 @@ -94,9 +94,9 @@ export function collectEntryPointModules(

/** Convert a Metro module to a JSON-serializable stats 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
84 changes: 0 additions & 84 deletions src/data/StatsFileSource.ts

This file was deleted.

Loading

0 comments on commit 4542fbf

Please sign in to comment.