Skip to content
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
89 changes: 62 additions & 27 deletions src/util/project/buildComponentDependencyList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,63 +4,98 @@ import buildComponentDependencyList from './buildComponentDependencyList.js';
describe('buildComponentDependencyList', () => {
const components = [
{
name: 'buttons',
name: 'button',
structure: 'base',
dependency: [],
dependency: ['icon'],
},
{
name: 'images',
name: 'icon',
structure: 'base',
dependency: [],
},
{
name: 'links',
name: 'card',
structure: 'base',
dependency: [],
dependency: ['teaser'],
},
{
name: 'text',
name: 'teaser',
structure: 'base',
dependency: ['links'],
dependency: ['image'],
},
{
name: 'card',
name: 'image',
structure: 'base',
dependency: ['images', 'text', 'links', 'buttons'],
},
{
name: 'menus',
name: 'gallery',
structure: 'molecules',
dependency: ['images', 'text'],
dependency: ['teaser', 'image'],
},
] as Components;

it('Build list of components without dependency', () => {
expect(buildComponentDependencyList(components, 'buttons')).toEqual([
'buttons',
it('returns the root component followed by direct dependencies', () => {
expect(buildComponentDependencyList(components, 'button')).toEqual([
'button',
'icon',
]);
});

it('Build all components dependency for not existing component', () => {
it('returns an empty dependency list for a missing root component', () => {
expect(buildComponentDependencyList(components, 'test')).toEqual([]);
});

it('Build all components dependency tree returning flat list without duplicates', () => {
it('returns nested dependencies in deterministic preorder', () => {
expect(buildComponentDependencyList(components, 'card')).toEqual([
'card',
'images',
'text',
'links',
'buttons',
'teaser',
'image',
]);
});

it('Build all components dependency tree with hierarchical dependency', () => {
expect(buildComponentDependencyList(components, 'menus')).toEqual([
'menus',
'images',
'text',
'links',
it('returns duplicate nested dependencies only once', () => {
expect(buildComponentDependencyList(components, 'gallery')).toEqual([
'gallery',
'teaser',
'image',
]);
});

it('throws a clear error when a dependency is missing', () => {
expect(() =>
buildComponentDependencyList(
[
{
name: 'button',
structure: 'base',
dependency: ['missing'],
},
] as Components,
'button',
),
).toThrow(
'Cannot resolve component dependency "missing" referenced by "button" while resolving "button". Dependency path: button -> missing.',
);
});

it('throws a clear error when dependencies are circular', () => {
expect(() =>
buildComponentDependencyList(
[
{
name: 'a',
structure: 'base',
dependency: ['b'],
},
{
name: 'b',
structure: 'base',
dependency: ['a'],
},
] as Components,
'a',
),
).toThrow(
'Circular component dependency detected while resolving "a": a -> b -> a.',
);
});
});
71 changes: 54 additions & 17 deletions src/util/project/buildComponentDependencyList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,60 @@ import type { Components } from '@emulsify-cli/config';
export default function buildComponentDependencyList(
components: Components,
name: string,
) {
const rootComponent = components.filter(
(component) => component.name == name,
);
if (rootComponent.length == 0) return [];
let finalList = [name];
const list = rootComponent[0].dependency as string[];
if (list && list.length > 0) {
list.forEach((componentName: string) => {
finalList = [
...new Set(
finalList.concat(
buildComponentDependencyList(components, componentName),
),
),
];
});
): string[] {
const componentsByName = new Map<string, Components[number]>();
for (const component of components) {
if (!componentsByName.has(component.name)) {
componentsByName.set(component.name, component);
}
}

if (!componentsByName.has(name)) return [];

const finalList: string[] = [];
const visited = new Set<string>();
const stack: string[] = [];

function visit(componentName: string, referencedBy?: string): void {
const cycleStart = stack.indexOf(componentName);
if (cycleStart !== -1) {
const cyclePath = [...stack.slice(cycleStart), componentName].join(
' -> ',
);

throw new Error(
`Circular component dependency detected while resolving "${name}": ${cyclePath}.`,
);
}

if (visited.has(componentName)) {
return;
}

const component = componentsByName.get(componentName);
if (!component) {
const dependencyPath = [...stack, componentName].join(' -> ');
const referencedByMessage = referencedBy
? ` referenced by "${referencedBy}"`
: '';

throw new Error(
`Cannot resolve component dependency "${componentName}"${referencedByMessage} while resolving "${name}". Dependency path: ${dependencyPath}.`,
);
}

stack.push(componentName);
visited.add(componentName);
finalList.push(componentName);

for (const dependencyName of component.dependency ?? []) {
visit(dependencyName, componentName);
}

stack.pop();
}

visit(name);

return finalList;
}
Loading