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

feature: add bundle environments to Atlas #70

Merged
merged 4 commits into from
Aug 28, 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
28 changes: 21 additions & 7 deletions src/data/AtlasFileSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,24 @@ export class AtlasFileSource implements AtlasSource {
/**
* List all entries without parsing the data.
* This only reads the bundle name, and adds a line number as ID.
*
* @note Ensure the (de)serialization is in sync with both {@link readAtlasEntry} and {@link writeAtlasEntry}.
*/
export async function listAtlasEntries(filePath: string) {
const bundlePattern = /^\["([^"]+)","([^"]+)","([^"]+)","([^"]+)"/;
const bundlePattern = /^\["([^"]+)","([^"]+)","([^"]+)","([^"]+)","([^"]+)"/;
const entries: PartialAtlasBundle[] = [];

await forEachJsonLines(filePath, (contents, line) => {
// Skip the metadata line
if (line === 1) return;

const [_, platform, projectRoot, sharedRoot, entryPoint] = contents.match(bundlePattern) ?? [];
if (platform && projectRoot && sharedRoot && entryPoint) {
const [_, platform, projectRoot, sharedRoot, entryPoint, environment] =
contents.match(bundlePattern) ?? [];
if (platform && projectRoot && sharedRoot && entryPoint && environment) {
entries.push({
id: String(line),
platform: platform as any,
environment: environment as any,
projectRoot,
sharedRoot,
entryPoint,
Expand All @@ -63,19 +67,24 @@ export async function listAtlasEntries(filePath: string) {

/**
* Get the entry by id or line number, and parse the data.
*
* @note Ensure the (de)serialization is in sync with both {@link listAtlasEntries} and {@link writeAtlasEntry}.
*/
export async function readAtlasEntry(filePath: string, id: number): Promise<AtlasBundle> {
const atlasEntry = await parseJsonLine<any[]>(filePath, id);
return {
id: String(id),
// These values are all strings
platform: atlasEntry[0],
projectRoot: atlasEntry[1],
sharedRoot: atlasEntry[2],
entryPoint: atlasEntry[3],
runtimeModules: atlasEntry[4],
modules: new Map(atlasEntry[5].map((module: AtlasModule) => [module.absolutePath, module])),
transformOptions: atlasEntry[6],
serializeOptions: atlasEntry[7],
environment: atlasEntry[4],
// These values are more complex
runtimeModules: atlasEntry[5],
modules: new Map(atlasEntry[6].map((module: AtlasModule) => [module.absolutePath, module])),
transformOptions: atlasEntry[7],
serializeOptions: atlasEntry[8],
};
}

Expand All @@ -95,13 +104,18 @@ export function waitUntilAtlasFileReady() {
* Add a new entry to the Atlas file.
* This function also ensures the Atlas file is ready to be written to, due to complications with Expo CLI.
* Eventually, the entry is appended on a new line, so we can load them selectively.
*
* @note Ensure the (de)serialization is in sync with both {@link listAtlasEntries} and {@link readAtlasEntry}.
*/
export function writeAtlasEntry(filePath: string, entry: AtlasBundle) {
const line = [
// These values must all be strings, and are available in PartialAtlasBundle type when listing bundles
entry.platform,
entry.projectRoot,
entry.sharedRoot,
entry.entryPoint,
entry.environment,
// These values can be more complex, but are not available in PartialAtlasBundle type when listing bundles
entry.runtimeModules,
Array.from(entry.modules.values()),
entry.transformOptions,
Expand Down
36 changes: 29 additions & 7 deletions src/data/MetroGraphSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export class MetroGraphSource implements AtlasSource {
return Array.from(this.entries.values()).map((bundle) => ({
id: bundle.id,
platform: bundle.platform,
environment: bundle.environment,
projectRoot: bundle.projectRoot,
sharedRoot: bundle.sharedRoot,
entryPoint: bundle.entryPoint,
Expand Down Expand Up @@ -102,18 +103,17 @@ export function convertMetroConfig(config: MetroConfig): ConvertGraphToAtlasOpti
/** Convert a Metro graph instance to a JSON-serializable entry */
export function convertGraph(options: ConvertGraphToAtlasOptions): AtlasBundle {
const sharedRoot = getSharedRoot(options);
const platform = getPlatform(options) ?? 'unknown';
const environment = getEnvironment(options) ?? 'client';
const serializeOptions = convertSerializeOptions(options);
const transformOptions = convertTransformOptions(options);
const platform =
transformOptions?.customTransformOptions?.environment === 'node'
? 'server'
: transformOptions?.platform ?? 'unknown';

return {
id: Buffer.from(`${path.relative(sharedRoot, options.entryPoint)}+${platform}`).toString(
'base64url'
), // FIX: only use URL allowed characters
id: Buffer.from(
`${path.relative(sharedRoot, options.entryPoint)}+${platform}+${environment}`
).toString('base64url'),
platform,
environment,
projectRoot: options.projectRoot,
sharedRoot,
entryPoint: options.entryPoint,
Expand Down Expand Up @@ -248,3 +248,25 @@ function getSharedRoot(options: Pick<ConvertGraphToAtlasOptions, 'projectRoot' |
function moduleIsVirtual(module: MetroModule) {
return module.path.startsWith('\0');
}

/** Determine the bundle target environment based on the `transformOptions.customTransformOptions` */
function getEnvironment(options: Pick<ConvertGraphToAtlasOptions, 'graph'>) {
const environment = options.graph.transformOptions?.customTransformOptions?.environment;

if (typeof environment === 'string') {
return environment as AtlasBundle['environment'];
}

return null;
}

/** Determine the bundle target platform based on the `transformOptions` */
function getPlatform(options: Pick<ConvertGraphToAtlasOptions, 'graph'>) {
const platform = options.graph.transformOptions?.platform;

if (typeof platform === 'string') {
return platform as AtlasBundle['platform'];
}

return null;
}
6 changes: 4 additions & 2 deletions src/data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ export interface AtlasSource {

export type PartialAtlasBundle = Pick<
AtlasBundle,
'id' | 'platform' | 'projectRoot' | 'sharedRoot' | 'entryPoint'
'id' | 'platform' | 'projectRoot' | 'sharedRoot' | 'entryPoint' | 'environment'
>;

export type AtlasBundle = {
/** The unique reference or ID to this entry */
id: string;
/** The platform for which the bundle was created */
platform: 'android' | 'ios' | 'web' | 'server';
platform: 'android' | 'ios' | 'web' | 'server' | 'unknown';
/** The environment this bundle is compiled for */
environment: 'client' | 'node' | 'react-server';
/** The absolute path to the root of the project */
projectRoot: string;
/** The absolute path to the shared root of all imported modules */
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion webui/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ config.resolver.resolveRequest = (context, moduleName, platform) => {
// Initialize the Expo Atlas global data source in development
if (process.env.NODE_ENV === 'development') {
const { AtlasFileSource } = require('../build/src/data/AtlasFileSource');
const filePath = path.resolve(__dirname, './fixture/atlas-tabs-51.jsonl');
const filePath = path.resolve(__dirname, './fixture/expo-51-default.jsonl');

global.EXPO_ATLAS_SOURCE = new AtlasFileSource(filePath);
}
Expand Down
4 changes: 2 additions & 2 deletions webui/src/app/(atlas)/[bundle].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { keepPreviousData, useQuery } from '@tanstack/react-query';
import type { ModuleGraphResponse } from '~/app/--/bundles/[bundle]/modules/graph+api';
import { BundleGraph } from '~/components/BundleGraph';
import { BundleSelectForm } from '~/components/BundleSelectForm';
import { BundleTag } from '~/components/BundleTag';
import { FileSize } from '~/components/FileSize';
import { ModuleFiltersForm } from '~/components/ModuleFilterForm';
import { PropertySummary } from '~/components/PropertySummary';
Expand All @@ -15,7 +16,6 @@ import {
import { useModuleFilters } from '~/hooks/useModuleFilters';
import { useBundle } from '~/providers/bundle';
import { Layout, LayoutHeader, LayoutNavigation, LayoutTitle } from '~/ui/Layout';
import { Tag } from '~/ui/Tag';
import { fetchApi, handleApiError } from '~/utils/api';
import { type ModuleFilters, moduleFiltersToParams } from '~/utils/filters';

Expand All @@ -38,7 +38,7 @@ export default function BundlePage() {
<LayoutTitle>
<h1 className="text-lg font-bold mr-8">Bundle</h1>
<PropertySummary>
<Tag variant={bundle.platform} />
<BundleTag platform={bundle.platform} environment={bundle.environment} />
{!!modules.data && <span>{modules.data.bundle.moduleFiles} modules</span>}
{!!modules.data && <FileSize byteSize={modules.data.bundle.moduleSize} />}
{!!modules.data && modulesAreFiltered && (
Expand Down
4 changes: 2 additions & 2 deletions webui/src/app/(atlas)/[bundle]/folders/[path].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ModuleGraphResponse } from '~/app/--/bundles/[bundle]/modules/grap
import { BreadcrumbLinks } from '~/components/BreadcrumbLinks';
import { BundleGraph } from '~/components/BundleGraph';
import { BundleSelectForm } from '~/components/BundleSelectForm';
import { BundleTag } from '~/components/BundleTag';
import { FileSize } from '~/components/FileSize';
import { ModuleFiltersForm } from '~/components/ModuleFilterForm';
import { PropertySummary } from '~/components/PropertySummary';
Expand All @@ -17,7 +18,6 @@ import {
import { useModuleFilters } from '~/hooks/useModuleFilters';
import { useBundle } from '~/providers/bundle';
import { Layout, LayoutHeader, LayoutNavigation, LayoutTitle } from '~/ui/Layout';
import { Tag } from '~/ui/Tag';
import { fetchApi, handleApiError } from '~/utils/api';
import { type ModuleFilters, moduleFiltersToParams } from '~/utils/filters';

Expand All @@ -37,7 +37,7 @@ export default function FolderPage() {
<LayoutTitle>
<BreadcrumbLinks bundle={bundle} path={relativePath!} />
<PropertySummary>
<Tag variant={bundle.platform} />
<BundleTag platform={bundle.platform} environment={bundle.environment} />
<span>folder</span>
{!!modules.data?.filtered.moduleFiles && (
<span>
Expand Down
4 changes: 2 additions & 2 deletions webui/src/app/(atlas)/[bundle]/modules/[path].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useLocalSearchParams } from 'expo-router';

import { BreadcrumbLinks } from '~/components/BreadcrumbLinks';
import { BundleSelectForm } from '~/components/BundleSelectForm';
import { BundleTag } from '~/components/BundleTag';
import { FileSize } from '~/components/FileSize';
import { ModuleCode } from '~/components/ModuleCode';
import { ModuleReference } from '~/components/ModuleReference';
Expand All @@ -11,7 +12,6 @@ import { DataErrorState, NoDataState } from '~/components/StateInfo';
import { useBundle } from '~/providers/bundle';
import { Layout, LayoutHeader, LayoutNavigation, LayoutTitle } from '~/ui/Layout';
import { Skeleton } from '~/ui/Skeleton';
import { Tag } from '~/ui/Tag';
import { fetchApi, handleApiError } from '~/utils/api';
import { type AtlasModule } from '~core/data/types';

Expand All @@ -29,7 +29,7 @@ export default function ModulePage() {
<LayoutTitle>
<BreadcrumbLinks bundle={bundle} path={relativePath!} />
<PropertySummary>
<Tag variant={bundle.platform} />
<BundleTag platform={bundle.platform} environment={bundle.environment} />
{!!module.data?.package && <span>{module.data.package}</span>}
{!!module.data && <span>{getModuleType(module.data)}</span>}
{!!module.data && <FileSize byteSize={module.data.size} />}
Expand Down
16 changes: 13 additions & 3 deletions webui/src/components/BundleSelectForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { useRouter } from 'expo-router';
// @ts-expect-error
import ChevronDownIcon from 'lucide-react/dist/esm/icons/chevron-down';

import { BundleTag } from '~/components/BundleTag';
import { useBundle } from '~/providers/bundle';
import { Button } from '~/ui/Button';
import { Tag } from '~/ui/Tag';
import { relativeBundlePath } from '~/utils/bundle';

export function BundleSelectForm() {
Expand All @@ -17,7 +17,12 @@ export function BundleSelectForm() {
<Select.Root value={bundle.id} onValueChange={(bundle) => router.setParams({ bundle })}>
<Select.Trigger asChild>
<Button variant="quaternary" size="sm">
<Tag variant={bundle.platform} size="xs" className="mr-2" />
<BundleTag
className="mr-2"
size="xs"
platform={bundle.platform}
environment={bundle.environment}
/>
<Select.Value placeholder="Select bundle to inspect" />
<Select.Icon className="text-icon-default">
<ChevronDownIcon size={16} className="m-1 mr-0 align-middle" />
Expand All @@ -39,7 +44,12 @@ export function BundleSelectForm() {
<div key={item.id}>
<Select.Item value={item.id} asChild>
<Button variant="quaternary" size="sm" className="w-full !justify-start my-0.5">
<Tag variant={item.platform} size="xs" className="mr-2" />
<BundleTag
className="mr-2"
size="xs"
platform={item.platform}
environment={item.environment}
/>
<Select.ItemText>{relativeBundlePath(item, item.entryPoint)}</Select.ItemText>
</Button>
</Select.Item>
Expand Down
72 changes: 72 additions & 0 deletions webui/src/components/BundleTag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { cva, type VariantProps } from 'class-variance-authority';
import { type ComponentProps } from 'react';

import { Tag } from '~/ui/Tag';
import type { AtlasBundle } from '~core/data/types';

const bundleTagVariants = cva('', {
variants: {
platform: {
android: 'bg-palette-green3 text-palette-green11',
ios: 'bg-palette-blue3 text-palette-blue11',
web: 'bg-palette-orange3 text-palette-orange11',
server: 'bg-palette-orange3 text-palette-orange11',
unknown: '',
},
environment: {
client: '',
node: 'bg-palette-orange3 text-palette-orange11',
'react-server': 'bg-palette-green3 text-palette-green11',
},
},
defaultVariants: {
platform: 'unknown', // Default platform value, see MetroGraphSource
environment: 'client', // Default environment value, see MetroGraphSource
},
});

const platformChildren: Record<AtlasBundle['platform'], string> = {
android: 'Android',
ios: 'iOS',
server: 'Server',
web: 'Web',
unknown: '???',
};

const environmentChildren: Record<AtlasBundle['environment'], string> = {
client: 'Client',
node: 'SSR',
'react-server': 'RSC',
};

type BundelTagProps = Omit<
ComponentProps<typeof Tag> & VariantProps<typeof bundleTagVariants>,
'children'
>;

export function BundleTag({ className, platform, environment, ...props }: BundelTagProps) {
return (
<Tag
className={bundleTagVariants({ platform, environment, className })}
variant="none"
{...props}
>
{getBundelTagChildren({ platform, environment })}
</Tag>
);
}

function getBundelTagChildren(props: BundelTagProps) {
const children: string[] = [];

if (props.platform) {
children.push(platformChildren[props.platform]);
}

// Only add the environment specifier if it's not bundled for the client
if (props.environment && props.environment !== 'client') {
children.push(environmentChildren[props.environment]);
}

return children.join(' × ');
}
30 changes: 7 additions & 23 deletions webui/src/ui/Tag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,11 @@ const tagVariants = cva(
{
variants: {
variant: {
none: '',
neutral: 'bg-element text-secondary',
info: 'bg-info text-info',
warning: 'bg-warning text-warning',
danger: 'bg-danger text-danger',
// Platform-specific variants
web: 'bg-palette-orange3 text-palette-orange11',
server: 'bg-palette-orange3 text-palette-orange11',
ios: 'bg-palette-blue3 text-palette-blue11',
android: 'bg-palette-green3 text-palette-green11',
},
size: {
xs: 'px-3 py-1 text-3xs/4',
Expand All @@ -32,26 +28,14 @@ const tagVariants = cva(
}
);

const platformChildren: Record<'android' | 'ios' | 'server' | 'web', string> = {
android: 'Android',
ios: 'iOS',
server: 'Server',
web: 'Web',
};

type TagProps = ComponentProps<'span'> & VariantProps<typeof tagVariants>;

export const Tag = forwardRef<HTMLSpanElement, TagProps>(
({ className, variant, size, children, ...props }, ref) => {
if (variant && variant in platformChildren) {
children = platformChildren[variant as keyof typeof platformChildren];
}

return (
<span className={tagVariants({ variant, size, className })} ref={ref} {...props}>
{children}
</span>
);
}
({ className, variant, size, children, ...props }, ref) => (
<span className={tagVariants({ variant, size, className })} ref={ref} {...props}>
{children}
</span>
)
);

Tag.displayName = 'Tag';