-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: add data layer with direct metro serialization access
- Loading branch information
Showing
18 changed files
with
488 additions
and
390 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
export * from './build/src/metro/withMetroBundleConfig'; | ||
export * from './build/src/withExpoAtlas'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
module.exports = require('./build/src/metro/withMetroBundleConfig'); | ||
module.exports = require('./build/src/withExpoAtlas'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import type metro from 'metro'; | ||
|
||
import type { StatsEntry, StatsModule, StatsSource } from './types'; | ||
import { getNonBinaryContents } from '../utils/buffer'; | ||
import { getPackageNameFromPath } from '../utils/package'; | ||
|
||
type MetroGraph = metro.Graph | metro.ReadOnlyGraph; | ||
type MetroModule = metro.Module; | ||
|
||
type ConvertGraphToStatsOptions = { | ||
projectRoot: string; | ||
entryPoint: string; | ||
preModules: Readonly<MetroModule[]>; | ||
graph: MetroGraph; | ||
options: Readonly<metro.SerializerOptions>; | ||
}; | ||
|
||
export class MetroGraphSource implements StatsSource { | ||
/** All known stats entries, stored by ID */ | ||
protected entries: Map<StatsEntry['id'], StatsEntry> = new Map(); | ||
|
||
listEntries() { | ||
return Array.from(this.entries.values()).map((entry) => ({ | ||
id: entry.id, | ||
platform: entry.platform, | ||
projectRoot: entry.projectRoot, | ||
entryPoint: entry.entryPoint, | ||
})); | ||
} | ||
|
||
getEntry(id: string) { | ||
const entry = this.entries.get(id); | ||
if (!entry) { | ||
throw new Error(`Stats 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. | ||
*/ | ||
onSerializeGraph(options: ConvertGraphToStatsOptions) { | ||
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 { | ||
const serializeOptions = convertSerializeOptions(options.options); | ||
const transformOptions = convertTransformOptions(options.graph.transformOptions); | ||
const platform = transformOptions?.platform ?? 'unknown'; | ||
|
||
return { | ||
id: `${options.entryPoint}+${platform}`, | ||
platform, | ||
projectRoot: options.projectRoot, | ||
entryPoint: options.entryPoint, | ||
runtimeModules: options.preModules.map((module) => convertModule(options.graph, module)), | ||
modules: collectEntryPointModules(options.graph, options.entryPoint), | ||
serializeOptions, | ||
transformOptions, | ||
}; | ||
} | ||
|
||
/** Find and collect all dependnecies related to the entrypoint within the graph */ | ||
export function collectEntryPointModules(graph: MetroGraph, entryPoint: string) { | ||
const modules = new Map<string, StatsModule>(); | ||
|
||
function discover(modulePath: string) { | ||
const module = graph.dependencies.get(modulePath); | ||
|
||
if (module && !modules.has(modulePath)) { | ||
modules.set(modulePath, convertModule(graph, module)); | ||
module.dependencies.forEach((modulePath) => discover(modulePath.absolutePath)); | ||
} | ||
} | ||
|
||
discover(entryPoint); | ||
return modules; | ||
} | ||
|
||
/** Convert a Metro module to a JSON-serializable stats module */ | ||
export function convertModule(graph: MetroGraph, module: MetroModule): StatsModule { | ||
return { | ||
path: module.path, | ||
package: getPackageNameFromPath(module.path), | ||
size: module.output.reduce((bytes, output) => bytes + Buffer.byteLength(output.data.code), 0), | ||
imports: Array.from(module.dependencies.values()).map((module) => module.absolutePath), | ||
importedBy: Array.from(module.inverseDependencies).filter((dependecy) => | ||
graph.dependencies.has(dependecy) | ||
), | ||
source: getNonBinaryContents(module.getSource()) ?? '[binary file]', | ||
output: module.output.map((output) => ({ | ||
type: output.type, | ||
data: { code: output.data.code }, | ||
})), | ||
}; | ||
} | ||
|
||
/** Convert Metro transform options to a JSON-serializable object */ | ||
export function convertTransformOptions( | ||
transformer: metro.TransformInputOptions | ||
): StatsEntry['transformOptions'] { | ||
return transformer; | ||
} | ||
|
||
/** Convert Metro serialize options to a JSON-serializable object */ | ||
export function convertSerializeOptions( | ||
serializer: metro.SerializerOptions | ||
): StatsEntry['serializeOptions'] { | ||
const options: StatsEntry['serializeOptions'] = { ...serializer }; | ||
|
||
// Delete all filters | ||
delete options['processModuleFilter']; | ||
delete options['createModuleId']; | ||
delete options['getRunModuleStatement']; | ||
delete options['shouldAddToIgnoreList']; | ||
|
||
return options; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import assert from 'assert'; | ||
|
||
import type { PartialStatsEntry, StatsEntry, StatsSource } from './types'; | ||
import { appendNDJsonToFile, mapNDJson, parseNDJsonAtLine } from '../utils/ndjson'; | ||
|
||
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 mapNDJson(statsPath, (index, line) => { | ||
// Skip the stats metadata line | ||
if (index === 1) return; | ||
|
||
const [_, platform, projectRoot, entryPoint] = line.match(bundlePattern) ?? []; | ||
if (platform && projectRoot && entryPoint) { | ||
entries.push({ | ||
id: String(index), | ||
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<StatsEntry> { | ||
const statsEntry = await parseNDJsonAtLine<any[]>(statsPath, id); | ||
return { | ||
id: String(id), | ||
platform: statsEntry[0], | ||
projectRoot: statsEntry[1], | ||
entryPoint: statsEntry[2], | ||
runtimeModules: statsEntry[3], | ||
modules: new Map(statsEntry[4]), | ||
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 writeStatsEntry(statsPath: string, stats: StatsEntry) { | ||
const entry = [ | ||
stats.platform, | ||
stats.projectRoot, | ||
stats.entryPoint, | ||
stats.runtimeModules, | ||
stats.modules, | ||
stats.transformOptions, | ||
stats.serializeOptions, | ||
]; | ||
|
||
return (writeStatsQueue = writeStatsQueue.then(() => appendNDJsonToFile(statsPath, entry))); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import type { MixedOutput } from 'metro'; | ||
|
||
export interface StatsSource { | ||
/** List all available stats entries */ | ||
listEntries(): PartialStatsEntry[] | Promise<PartialStatsEntry[]>; | ||
/** Load the full stats entry, by reference */ | ||
getEntry(ref: string): StatsEntry | Promise<StatsEntry>; | ||
} | ||
|
||
export type PartialStatsEntry = Pick<StatsEntry, 'id' | 'platform' | 'projectRoot' | 'entryPoint'>; | ||
|
||
export type StatsEntry = { | ||
/** The unique reference or ID to this stats entry */ | ||
id: string; | ||
/** The platform for which the bundle was created */ | ||
platform: 'android' | 'ios' | 'web'; | ||
/** The absolute path to the root of the project */ | ||
projectRoot: string; | ||
/** 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[]; | ||
/** All known modules imported within the bundle, stored by absolute path */ | ||
modules: Map<string, StatsModule>; | ||
/** The sarialization options used for this bundle */ | ||
serializeOptions?: Record<string, any>; | ||
/** The transformation options used for this bundle */ | ||
transformOptions?: Record<string, any>; | ||
}; | ||
|
||
export type StatsModule = { | ||
/** The absoluate path of this module */ | ||
path: string; | ||
/** The name of the package this module belongs to, if from an external package */ | ||
package?: string; | ||
/** The original module size, in bytes */ | ||
size: number; | ||
/** Absolute file paths of modules imported inside this module */ | ||
imports: string[]; | ||
/** Absolute file paths of modules importing this module */ | ||
importedBy: string[]; | ||
/** The original source code, as a buffer or string */ | ||
source?: string; | ||
/** The transformed output source code */ | ||
output?: MixedOutput[]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,7 @@ | ||
export let useMetroBundlePlugin: typeof import('./useMetroBundlePlugin').useMetroBundlePlugin; | ||
export type * from './data/types'; | ||
|
||
// @ts-ignore process.env.NODE_ENV is defined by metro transform plugins | ||
if (process.env.NODE_ENV !== 'production') { | ||
useMetroBundlePlugin = require('./useMetroBundlePlugin').useMetroBundlePlugin; | ||
} else { | ||
useMetroBundlePlugin = () => {}; | ||
} | ||
export { MetroGraphSource } from './data/MetroGraphSource'; | ||
export { StatsFileSource } from './data/StatsFileSource'; | ||
|
||
export { AtlasError, AtlasValidationError } from './utils/errors'; | ||
export { createStatsFile, validateStatsFile, getStatsMetdata, getStatsPath } from './utils/stats'; |
Oops, something went wrong.