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

fix: use sharedRoot for monorepo projects #37

Merged
merged 10 commits into from
Apr 19, 2024
1 change: 1 addition & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export function createExpoAtlasMiddleware(config: MetroConfig) {
graph,
options,
extensions: metroExtensions,
watchFolders: config.watchFolders,
});

return metroCustomSerializer(entryPoint, preModules, graph, options);
Expand Down
19 changes: 11 additions & 8 deletions src/data/AtlasFileSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,20 @@ export class AtlasFileSource implements AtlasSource {
* This only reads the bundle name, and adds a line number as ID.
*/
export async function listAtlasEntries(filePath: string) {
const bundlePattern = /^\["([^"]+)","([^"]+)","([^"]+)/;
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) {
const [_, platform, projectRoot, sharedRoot, entryPoint] = contents.match(bundlePattern) ?? [];
if (platform && projectRoot && sharedRoot && entryPoint) {
entries.push({
id: String(line),
platform: platform as any,
projectRoot,
sharedRoot,
entryPoint,
});
}
Expand All @@ -69,11 +70,12 @@ export async function readAtlasEntry(filePath: string, id: number): Promise<Atla
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],
sharedRoot: atlasEntry[2],
entryPoint: atlasEntry[3],
runtimeModules: atlasEntry[4],
modules: new Map(atlasEntry[5].map((module) => [module.path, module])),
transformOptions: atlasEntry[6],
serializeOptions: atlasEntry[7],
};
}

Expand All @@ -88,6 +90,7 @@ export function writeAtlasEntry(filePath: string, entry: AtlasEntry) {
const line = [
entry.platform,
entry.projectRoot,
entry.sharedRoot,
entry.entryPoint,
entry.runtimeModules,
Array.from(entry.modules.values()),
Expand Down
15 changes: 15 additions & 0 deletions src/data/MetroGraphSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import path from 'path';
import type { AtlasEntry, AtlasEntryDelta, AtlasModule, AtlasSource } from './types';
import { bufferIsUtf8 } from '../utils/buffer';
import { getPackageNameFromPath } from '../utils/package';
import { findSharedRoot } from '../utils/paths';

type MetroGraph = metro.Graph | metro.ReadOnlyGraph;
type MetroModule = metro.Module;
Expand All @@ -16,6 +17,7 @@ type ConvertGraphToAtlasOptions = {
preModules: Readonly<MetroModule[]>;
graph: MetroGraph;
options: Readonly<metro.SerializerOptions>;
watchFolders?: Readonly<string[]>;
extensions?: {
source?: Readonly<string[]>;
asset?: Readonly<string[]>;
Expand All @@ -38,6 +40,7 @@ export class MetroGraphSource implements AtlasSource {
id: item.entry.id,
platform: item.entry.platform,
projectRoot: item.entry.projectRoot,
sharedRoot: item.entry.sharedRoot,
entryPoint: item.entry.entryPoint,
}));
}
Expand Down Expand Up @@ -139,6 +142,7 @@ export function convertGraph(options: ConvertGraphToAtlasOptions): AtlasEntry {
id: Buffer.from(`${options.entryPoint}+${platform}`).toString('base64url'), // FIX: only use URL allowed characters
platform,
projectRoot: options.projectRoot,
sharedRoot: convertSharedRoot(options),
entryPoint: options.entryPoint,
runtimeModules: options.preModules.map((module) => convertModule(options, module)),
modules: collectEntryPointModules(options),
Expand Down Expand Up @@ -237,3 +241,14 @@ export function convertSerializeOptions(

return serializeOptions;
}

/** Convert Metro config to a shared root we can use as "relative root" for all file paths */
export function convertSharedRoot(
options: Pick<ConvertGraphToAtlasOptions, 'projectRoot' | 'watchFolders'>
) {
if (!options.watchFolders?.length) {
return options.projectRoot;
}

return findSharedRoot([options.projectRoot, ...options.watchFolders]) ?? options.projectRoot;
}
7 changes: 6 additions & 1 deletion src/data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ export interface AtlasSource {
entryDeltaEnabled(): boolean;
}

export type PartialAtlasEntry = Pick<AtlasEntry, 'id' | 'platform' | 'projectRoot' | 'entryPoint'>;
export type PartialAtlasEntry = Pick<
AtlasEntry,
'id' | 'platform' | 'projectRoot' | 'sharedRoot' | 'entryPoint'
>;

export type AtlasEntry = {
/** The unique reference or ID to this entry */
Expand All @@ -20,6 +23,8 @@ export type AtlasEntry = {
platform: 'android' | 'ios' | 'web' | 'server';
/** The absolute path to the root of the project */
projectRoot: string;
/** The absolute path to the shared root of all imported modules */
sharedRoot: 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 */
Expand Down
11 changes: 10 additions & 1 deletion src/metro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function withExpoAtlas(config: MetroConfig, options: ExpoAtlasOptions = {
}

const atlasFile = options?.atlasFile ?? getAtlasPath(projectRoot);
const watchFolders = config.watchFolders;
const extensions = {
source: config.resolver?.sourceExts,
asset: config.resolver?.assetExts,
Expand All @@ -46,7 +47,15 @@ export function withExpoAtlas(config: MetroConfig, options: ExpoAtlasOptions = {
// Note(cedric): we don't have to await this, it has a built-in write queue
writeAtlasEntry(
atlasFile,
convertGraph({ projectRoot, entryPoint, preModules, graph, options, extensions })
convertGraph({
projectRoot,
entryPoint,
preModules,
graph,
options,
extensions,
watchFolders,
})
);

return originalSerializer(entryPoint, preModules, graph, options);
Expand Down
29 changes: 29 additions & 0 deletions src/utils/paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Find the shared root of all paths.
* This will split all paths by segments and find the longest common prefix.
*/
export function findSharedRoot(paths: string[]) {
if (!paths.length) {
return null;
}

let sharedRoot: string[] = [];

for (const item of paths) {
const segments = item.split('/');

if (!sharedRoot.length) {
sharedRoot = segments;
continue;
}

for (let i = 0; i < sharedRoot.length; i++) {
if (sharedRoot[i] !== segments[i]) {
sharedRoot = sharedRoot.slice(0, i);
break;
}
}
}

return sharedRoot.join('/');
}
10 changes: 5 additions & 5 deletions webui/fixture/atlas-tabs-50.jsonl

Large diffs are not rendered by default.

21 changes: 14 additions & 7 deletions webui/src/app/(atlas)/[entry]/folders/[path].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { PropertySummary } from '~/components/PropertySummary';
import { StateInfo } from '~/components/StateInfo';
import { EntryDeltaToast, useEntry } from '~/providers/entries';
import { Layout, LayoutHeader, LayoutNavigation, LayoutTitle } from '~/ui/Layout';
import { Spinner } from '~/ui/Spinner';
import { Tag } from '~/ui/Tag';
import { fetchApi } from '~/utils/api';
import { type ModuleFilters, useModuleFilters, moduleFiltersToParams } from '~/utils/filters';
Expand All @@ -30,23 +31,29 @@ export default function FolderPage() {
<LayoutHeader>
<LayoutTitle>
<BreadcrumbLinks entry={entry} path={absolutePath!} />
{!!modules.data && (
<PropertySummary>
<Tag variant={entry.platform} />
<span>folder</span>
<PropertySummary>
<Tag variant={entry.platform} />
<span>folder</span>
{!!modules.data?.filtered.moduleFiles && (
<span>
{modules.data.filtered.moduleFiles === 1
? `${modules.data.filtered.moduleFiles} module`
: `${modules.data.filtered.moduleFiles} modules`}
</span>
)}
{!!modules.data?.filtered.moduleSize && (
<span>{formatFileSize(modules.data.filtered.moduleSize)}</span>
</PropertySummary>
)}
)}
</PropertySummary>
</LayoutTitle>
<ModuleFiltersForm disableNodeModules />
</LayoutHeader>
<EntryDeltaToast entryId={entry.id} />
{modules.isError ? (
{modules.isPending && !modules.isPlaceholderData ? (
<StateInfo>
<Spinner />
</StateInfo>
) : modules.isError ? (
<StateInfo title="Failed to generate graph.">
Try restarting Expo Atlas. If this error keeps happening, open a bug report.
</StateInfo>
Expand Down
15 changes: 7 additions & 8 deletions webui/src/app/(atlas)/[entry]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ export default function BundlePage() {
<LayoutHeader>
<LayoutTitle>
<h1 className="text-lg font-bold mr-8">Bundle</h1>
{!!modules.data && (
<PropertySummary>
<Tag variant={entry.platform} />
<span>{modules.data.entry.moduleFiles} modules</span>
<span>{formatFileSize(modules.data.entry.moduleSize)}</span>
{modules.data.filtered.moduleFiles !== modules.data.entry.moduleFiles && (
<PropertySummary>
<Tag variant={entry.platform} />
{!!modules.data && <span>{modules.data.entry.moduleFiles} modules</span>}
{!!modules.data && <span>{formatFileSize(modules.data.entry.moduleSize)}</span>}
{modules.data &&
modules.data.filtered.moduleFiles !== modules.data.entry.moduleFiles && (
<PropertySummary
className="text-tertiary italic"
prefix={<span className="select-none mr-2">visible:</span>}
Expand All @@ -42,8 +42,7 @@ export default function BundlePage() {
<span>{formatFileSize(modules.data.filtered.moduleSize)}</span>
</PropertySummary>
)}
</PropertySummary>
)}
</PropertySummary>
</LayoutTitle>
<ModuleFiltersForm />
</LayoutHeader>
Expand Down
18 changes: 8 additions & 10 deletions webui/src/app/(atlas)/[entry]/modules/[path].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Layout, LayoutHeader, LayoutNavigation, LayoutTitle } from '~/ui/Layout
import { Skeleton } from '~/ui/Skeleton';
import { Tag } from '~/ui/Tag';
import { fetchApi } from '~/utils/api';
import { relativeEntryPath } from '~/utils/entry';
import { relativeBundlePath } from '~/utils/bundle';
import { formatFileSize } from '~/utils/formatString';
import { type AtlasModule } from '~core/data/types';

Expand All @@ -28,14 +28,12 @@ export default function ModulePage() {
<LayoutHeader>
<LayoutTitle>
<BreadcrumbLinks entry={entry} path={absolutePath!} />
{!!module.data && (
<PropertySummary>
<Tag variant={entry.platform} />
{!!module.data.package && <span>{module.data.package}</span>}
<span>{getModuleType(module.data)}</span>
<span>{formatFileSize(module.data.size)}</span>
</PropertySummary>
)}
<PropertySummary>
<Tag variant={entry.platform} />
{!!module.data?.package && <span>{module.data.package}</span>}
{!!module.data && <span>{getModuleType(module.data)}</span>}
{!!module.data && <span>{formatFileSize(module.data.size)}</span>}
</PropertySummary>
</LayoutTitle>
</LayoutHeader>
<EntryDeltaToast entryId={entry.id} modulePath={absolutePath} />
Expand All @@ -62,7 +60,7 @@ export default function ModulePage() {
params: { entry: entry.id, path },
}}
>
{relativeEntryPath(entry, path)}
{relativeBundlePath(entry, path)}
</Link>
</li>
))}
Expand Down
21 changes: 12 additions & 9 deletions webui/src/components/BreadcrumbLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
BreadcrumbPage,
BreadcrumbSeparator,
} from '~/ui/Breadcrumb';
import { relativeEntryPath } from '~/utils/entry';
import { relativeBundlePath, rootBundlePath } from '~/utils/bundle';
import { type PartialAtlasEntry } from '~core/data/types';

type BreadcrumbLinksProps = {
Expand All @@ -20,6 +20,8 @@ type BreadcrumbLinksProps = {
export function BreadcrumbLinks(props: BreadcrumbLinksProps) {
const links = useMemo(() => getBreadcrumbLinks(props), [props.entry.id, props.path]);

console.log({ links, path: props.path });

return (
<Breadcrumb>
<BreadcrumbList className="mr-8">
Expand All @@ -31,8 +33,8 @@ export function BreadcrumbLinks(props: BreadcrumbLinksProps) {
Bundle
</Link>
</BreadcrumbLink>
{links.map((link, index) => (
<Fragment key={`link-${index}`}>
{links.map((link) => (
<Fragment key={link.key}>
<BreadcrumbSeparator className="text-secondary" />
<BreadcrumbItem>
{!link.href ? (
Expand All @@ -56,25 +58,26 @@ export function BreadcrumbLinks(props: BreadcrumbLinksProps) {
}

type BreadcrumbLinkItem = {
key: string;
label: string;
href?: ComponentProps<typeof Link>['href'];
};

function getBreadcrumbLinks(props: BreadcrumbLinksProps): BreadcrumbLinkItem[] {
const relativePath = relativeEntryPath(props.entry, props.path);
const rootPath = rootBundlePath(props.entry).replace(/\/$/, '');
const relativePath = relativeBundlePath(props.entry, props.path).replace(/^\//, '');

return relativePath.split('/').map((label, index, breadcrumbs) => {
const isLastSegment = index === breadcrumbs.length - 1;
const breadcrumb: BreadcrumbLinkItem = { label };
const breadcrumb: BreadcrumbLinkItem = { key: `${index}-${label}`, label };

// NOTE(cedric): a bit of a workaround to avoid linking the module page, might need to change this
if (!isLastSegment || !label.includes('.')) {
const path = `${rootPath}/${breadcrumbs.slice(0, index + 1).join('/')}`;
breadcrumb.key = path;
breadcrumb.href = {
pathname: '/(atlas)/[entry]/folders/[path]',
params: {
entry: props.entry.id,
path: `${props.entry.projectRoot}/${breadcrumbs.slice(0, index + 1).join('/')}`,
},
params: { entry: props.entry.id, path },
};
}

Expand Down
2 changes: 1 addition & 1 deletion webui/src/components/BundleGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ function getBundleGraphSeries(graph: TreemapNode): TreemapSeriesOption {
{
itemStyle: {
color: '#37434A',
borderColorSaturation: 0.18,
borderColorSaturation: 0.15,
colorSaturation: 0.25,
borderWidth: 4,
},
Expand Down
4 changes: 2 additions & 2 deletions webui/src/components/BundleSelectForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import ChevronUpIcon from 'lucide-react/dist/esm/icons/chevron-up';
import { useEntry } from '~/providers/entries';
import { Button } from '~/ui/Button';
import { Tag } from '~/ui/Tag';
import { relativeEntryPath } from '~/utils/entry';
import { relativeBundlePath } from '~/utils/bundle';

export function BundleSelectForm() {
const router = useRouter();
Expand Down Expand Up @@ -45,7 +45,7 @@ export function BundleSelectForm() {
<Select.Item value={item.id} asChild>
<Button variant="quaternary" size="sm" className="w-full">
<Tag variant={item.platform} className="mr-2" />
<Select.ItemText>{relativeEntryPath(entry, item.entryPoint)}</Select.ItemText>
<Select.ItemText>{relativeBundlePath(entry, item.entryPoint)}</Select.ItemText>
</Button>
</Select.Item>
</div>
Expand Down
Loading