Skip to content

Commit

Permalink
fix: use sharedRoot for monorepo projects (#37)
Browse files Browse the repository at this point in the history
* fix: add `serverRoot` as primary attribute to Atlas files

* fix: use `serverRoot` with fallback to `projectRoot` when making paths relative

* fix: add `serverRoot` to `MetroGraphSource`

* chore(webui): update fixture with `serverRoot`

* refactor: change `serverRoot` to `sharedRoot` instead

* fix(webui): use proper breadcrumb links for new `sharedRoot`

* fix(webui): use proper shader for bundle graph

* chore(webui): drop console log

* chore(webui): tweak layout a bit

* chore(webui): tweak property summary loading state
  • Loading branch information
byCedric authored Apr 19, 2024
1 parent 773b8f8 commit 6a853aa
Show file tree
Hide file tree
Showing 16 changed files with 160 additions and 69 deletions.
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

0 comments on commit 6a853aa

Please sign in to comment.