Skip to content

Commit

Permalink
feature: add bundle environments to Atlas (#70)
Browse files Browse the repository at this point in the history
* feature: add `environment` to Atlas bundles

This is based on the custom transform options provided by Expo, with a fallback to `client`.

* refactor(webui): extract platforms from `Tag` into `BundleTag` and add `environment`

* refactor(webui): replace `Tag` with `BundleTag` versions

* refactor(webui): update fixture with Expo SDK 51 default template
  • Loading branch information
byCedric authored Aug 28, 2024
1 parent 20b7946 commit f3d4b1f
Show file tree
Hide file tree
Showing 12 changed files with 163 additions and 59 deletions.
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';

0 comments on commit f3d4b1f

Please sign in to comment.