Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
- Dependicus now builds itself from the clone, so `npm install github:descriptinc/dependicus` gives you a working `dependicus` command instead of an empty one.
- pnpm and yarn refuse to run a git dependency's build script until you list the package as trusted. The README has the line of config each one wants.
- Bun and aube can't install Dependicus from git. Bun doesn't install a git dependency's devDependencies, so the build has nothing to run with, and aube doesn't accept git specifiers.
- `GroupingConfig.getValue` may return several values, and the dependency is filed under each of them. A grouping used to be a partition, which doesn't fit a dimension whose membership overlaps: a package used by three teams belongs on all three of their pages. Returning a single string still works.
- `GroupingConfig.ecosystems` limits a grouping to the ecosystems it can actually be computed for, e.g. `['npm']`. Providers for any other ecosystem skip it, and their pages leave it out of the nav, instead of rendering an index with no entries and a nav link to it. Omitting the field keeps today's behavior.

### Changed

Expand Down
17 changes: 15 additions & 2 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,21 @@ export interface GroupingConfig {
key: string;
label: string;
slugPrefix?: string;
/** Extract the grouping value for a dependency. Returns undefined to exclude. */
getValue: (name: string, store: FactStore) => string | undefined;
/**
* Extract the grouping value, or values, for a dependency. Returns undefined
* to exclude it.
*
* Returning several values files the dependency under each of them, for a
* dimension whose membership overlaps: a package used by three teams belongs
* on all three of their pages.
*/
getValue: (name: string, store: FactStore) => string | readonly string[] | undefined;
/**
* Ecosystems this grouping applies to, e.g. `['npm']`. Providers for any
* other ecosystem skip it, and their pages leave it out of the nav.
* Omitted means every ecosystem.
*/
ecosystems?: readonly string[];
/** Return sections to display on this grouping's detail pages. */
getSections?: (context: GroupingDetailContext) => GroupingSection[];
}
58 changes: 58 additions & 0 deletions src/site-builder/services/HtmlWriter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,64 @@ describe('HtmlWriter', () => {
},
};

it('files a dependency under every value getValue returns', () => {
const multi: GroupingConfig = {
key: 'team',
label: 'Teams',
slugPrefix: 'teams',
getValue: () => ['Growth', 'Representation'],
};
const writer = new HtmlWriter({ groupings: [multi] });
const dep = makeMockDependency();
const store = makeMockStore([dep]);
const { details } = writer.toGroupingPages(
[dep],
multi,
store.scoped(dep.ecosystem),
'pnpm/',
);

expect(details.map((d) => d.filename).sort()).toEqual([
'pnpm/teams/Growth.html',
'pnpm/teams/Representation.html',
]);
});

it('skips a grouping whose ecosystems exclude the provider', () => {
const npmOnly: GroupingConfig = {
key: 'team',
label: 'Teams',
slugPrefix: 'teams',
ecosystems: ['npm'],
getValue: () => 'Growth',
};
const writer = new HtmlWriter({ groupings: [npmOnly] });
const goDep = makeMockDependency({ ecosystem: 'gomod' });
const store = makeMockStore([goDep]);
const pages = writer.toAllGroupingPages(
[makeProvider([goDep], { name: 'go', ecosystem: 'gomod' })],
store,
);

expect(pages).toEqual([]);
});

it('keeps a grouping for an ecosystem it lists', () => {
const npmOnly: GroupingConfig = {
key: 'team',
label: 'Teams',
slugPrefix: 'teams',
ecosystems: ['npm'],
getValue: () => 'Growth',
};
const writer = new HtmlWriter({ groupings: [npmOnly] });
const dep = makeMockDependency();
const store = makeMockStore([dep]);
const pages = writer.toAllGroupingPages([makeProvider([dep])], store);

expect(pages.map((pg) => pg.filename)).toContain('pnpm/teams/Growth.html');
});

it('toAllGroupingPages returns empty array when no groupings configured', () => {
const writer = new HtmlWriter();
const dep = makeMockDependency();
Expand Down
58 changes: 35 additions & 23 deletions src/site-builder/services/HtmlWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ export class HtmlWriter {
this.siteName = options?.siteName ?? 'Dependicus';
}

/** Groupings that apply to an ecosystem. Undefined means don't filter. */
private groupingsFor(ecosystem?: string): GroupingConfig[] {
if (ecosystem === undefined) return this.groupings;
return this.groupings.filter((g) => !g.ecosystems || g.ecosystems.includes(ecosystem));
}

/** Nav entries for the groupings an ecosystem's pages should link to. */
private navGroupings(ecosystem?: string): Array<{ label: string; slug: string }> {
return this.groupingsFor(ecosystem).map((g) => ({
label: g.label,
slug: g.slugPrefix ?? g.key,
}));
}

/**
* Group dependencies by a key derived from the FactStore.
*/
Expand Down Expand Up @@ -390,6 +404,7 @@ export class HtmlWriter {
// Render full page with layout.
// Grouping pages are provider-scoped, so default to the first provider for nav links.
const defaultProviderPrefix = providers.length > 0 ? `${providers[0]!.name}/` : '';
const navEcosystem = providers[0]?.ecosystem;

return this.templateService.render('layouts/index', {
title: 'Dependency Report',
Expand All @@ -399,10 +414,7 @@ export class HtmlWriter {
content,
providerPrefix: defaultProviderPrefix,
timestamp: new Date().toLocaleString(),
groupings: this.groupings.map((g) => ({
label: g.label,
slug: g.slugPrefix ?? g.key,
})),
groupings: this.navGroupings(navEcosystem),
});
}

Expand Down Expand Up @@ -458,6 +470,7 @@ export class HtmlWriter {
baseHref = '../',
providerPrefix = '',
): string {
const navEcosystem = dep.ecosystem;
const description =
store.getVersionFact<string>(dep.name, versionInfo.version, FactKeys.DESCRIPTION) ?? '';
const homepage =
Expand Down Expand Up @@ -586,10 +599,7 @@ export class HtmlWriter {
baseHref,
providerPrefix,
timestamp: new Date().toLocaleString(),
groupings: this.groupings.map((g) => ({
label: g.label,
slug: g.slugPrefix ?? g.key,
})),
groupings: this.navGroupings(navEcosystem),
});
}

Expand Down Expand Up @@ -706,20 +716,27 @@ export class HtmlWriter {
grouping: GroupingConfig,
store: FactStore,
providerPrefix = '',
ecosystem?: string,
): { index: DetailPage; details: DetailPage[] } {
const slug = grouping.slugPrefix ?? grouping.key;
const baseHref = providerPrefix ? '../../' : '../';
const navEcosystem = ecosystem;

// Collect all dependencies for each unique grouping value
// Collect all dependencies for each unique grouping value. getValue may
// return several, in which case the dependency belongs under each.
const grouped = new Map<string, DirectDependency[]>();
for (const dep of dependencies) {
const value = grouping.getValue(dep.name, store);
if (!value) continue;
const existing = grouped.get(value);
if (existing) {
existing.push(dep);
} else {
grouped.set(value, [dep]);
const values = typeof value === 'string' ? [value] : value;
for (const single of values) {
if (!single) continue;
const existing = grouped.get(single);
if (existing) {
existing.push(dep);
} else {
grouped.set(single, [dep]);
}
}
}

Expand Down Expand Up @@ -755,10 +772,7 @@ export class HtmlWriter {
baseHref,
providerPrefix,
timestamp: new Date().toLocaleString(),
groupings: this.groupings.map((g) => ({
label: g.label,
slug: g.slugPrefix ?? g.key,
})),
groupings: this.navGroupings(navEcosystem),
});

const index: DetailPage = {
Expand Down Expand Up @@ -810,10 +824,7 @@ export class HtmlWriter {
baseHref,
providerPrefix,
timestamp: new Date().toLocaleString(),
groupings: this.groupings.map((g) => ({
label: g.label,
slug: g.slugPrefix ?? g.key,
})),
groupings: this.navGroupings(navEcosystem),
});

return {
Expand All @@ -838,12 +849,13 @@ export class HtmlWriter {
for (const provider of providers) {
const providerPrefix = `${provider.name}/`;
const scopedStore = store.scoped(provider.ecosystem);
for (const grouping of this.groupings) {
for (const grouping of this.groupingsFor(provider.ecosystem)) {
const { index, details } = this.toGroupingPages(
provider.dependencies,
grouping,
scopedStore,
providerPrefix,
provider.ecosystem,
);
pages.push(index);
pages.push(...details);
Expand Down
Loading