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
14 changes: 14 additions & 0 deletions src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ export type LayerName =
| "pages"
| "app";

export type LayerAliases = Partial<Record<LayerName, string>>;

export interface LayerConvention {
/**
* Project-specific names for canonical FSD layers.
*
* @example
* ```ts
* { pages: "screens" }
* ```
*/
layerAliases?: LayerAliases;
}

export const layerSequence: Array<LayerName> = [
"shared",
"entities",
Expand Down
48 changes: 38 additions & 10 deletions src/fsd-aware-traverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
unslicedLayers,
type File,
type Folder,
type LayerConvention,
type LayerName,
} from "./definitions.js";

Expand All @@ -30,12 +31,30 @@ function removePrefix(path: string): string {
return path.replace(/^([0-9]_|_)/, "");
}

function resolveLayerName(
pathSegment: string,
convention?: LayerConvention,
): LayerName | undefined {
const name = removePrefix(pathSegment);
const layer = layerSequence.find((candidate) => candidate === name);
if (layer) {
return layer;
}

return layerSequence.find((candidate) =>
convention?.layerAliases?.[candidate] === name,
);
}

/**
* Extract layers from an FSD root.
*
* @returns A mapping of layer name to folder object.
*/
export function getLayers(fsdRoot: Folder): Partial<Record<LayerName, Folder>> {
export function getLayers(
fsdRoot: Folder,
convention?: LayerConvention,
): Partial<Record<LayerName, Folder>> {
return Object.fromEntries(
fsdRoot.children.reduce(
(acc, child) => {
Expand All @@ -44,8 +63,7 @@ export function getLayers(fsdRoot: Folder): Partial<Record<LayerName, Folder>> {
}

const name = basename(child.path);
const layer: LayerName | undefined =
layerSequence[layerSequence.indexOf(removePrefix(name))];
const layer = resolveLayerName(name, convention);
if (layer) {
const existingLayer = acc.find((el) => el[0] === layer);
if (!existingLayer) {
Expand Down Expand Up @@ -126,9 +144,10 @@ export function getSegments(
export function getAllSlices(
fsdRoot: Folder,
additionalSegmentNames: Array<string> = [],
convention?: LayerConvention,
): Record<string, Folder & { layerName: string }> {
return Object.values(getLayers(fsdRoot))
.filter(isSliced)
return Object.values(getLayers(fsdRoot, convention))
.filter((layer) => isSliced(layer, convention))
.reduce((slices, layer) => {
return {
...slices,
Expand All @@ -149,14 +168,17 @@ export function getAllSlices(
*
* @returns A flat array of segments along with their name and location in the FSD root (layer, slice).
*/
export function getAllSegments(fsdRoot: Folder): Array<{
export function getAllSegments(
fsdRoot: Folder,
convention?: LayerConvention,
): Array<{
segment: Folder | File;
segmentName: string;
sliceName: string | null;
layerName: LayerName;
}> {
return Object.entries(getLayers(fsdRoot)).flatMap(([layerName, layer]) => {
if (isSliced(layer)) {
return Object.entries(getLayers(fsdRoot, convention)).flatMap(([layerName, layer]) => {
if (isSliced(layer, convention)) {
return Object.entries(getSlices(layer)).flatMap(([sliceName, slice]) =>
Object.entries(getSegments(slice)).map(([segmentName, segment]) => ({
segment,
Expand All @@ -183,10 +205,16 @@ export function getAllSegments(fsdRoot: Folder): Array<{
*
* Only layers Shared and App are not sliced, the rest are.
*/
export function isSliced(layerOrName: Folder | LayerName): boolean {
return !unslicedLayers.includes(
export function isSliced(
layerOrName: Folder | LayerName | string,
convention?: LayerConvention,
): boolean {
const layerName = resolveLayerName(
basename(typeof layerOrName === "string" ? layerOrName : layerOrName.path),
convention,
);

return layerName !== undefined && !unslicedLayers.includes(layerName);
}

/**
Expand Down
80 changes: 80 additions & 0 deletions src/specs/fsd-aware-traverse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,48 @@ describe("getLayers", () => {
pages: root.children[1] as Folder,
});
});

test("accept custom aliases for canonical layers", () => {
const root = parseIntoFolder(`
📂 shared
📂 ui
📄 Button.tsx
📂 screens
📂 editor
📄 index.ts
`);
expect(getLayers(root, { layerAliases: { pages: "screens" } })).toEqual({
shared: root.children[0] as Folder,
pages: root.children[1] as Folder,
});
});

test("accept prefixed custom aliases", () => {
const root = parseIntoFolder(`
📂 screens
📄 home.tsx
📂 _screens
📂 home
📄 index.ts
`);
expect(getLayers(root, { layerAliases: { pages: "screens" } })).toEqual({
pages: root.children[1] as Folder,
});
});

test("prioritizes canonical layer over alias", () => {
const root = parseIntoFolder(`
📂 pages
📂 home
📄 index.ts
📂 screens
📂 dashboard
📄 index.ts
`);
expect(getLayers(root, { layerAliases: { pages: "screens" } })).toEqual({
pages: root.children[0] as Folder,
});
});
});

test("getSlices", () => {
Expand Down Expand Up @@ -128,13 +170,41 @@ test("getAllSlices", () => {
});
});

test("getAllSlices with custom layer aliases", () => {
const rootFolder = parseIntoFolder(`
📂 entities
📂 user
📂 ui
📄 index.ts
📂 screens
📂 home
📂 ui
📄 index.ts
`);

const allSlices = getAllSlices(rootFolder, [], {
layerAliases: { pages: "screens" },
});
expect(Object.keys(allSlices).sort((a, b) => a.localeCompare(b))).toEqual([
"home",
"user",
]);
expect(allSlices.home).toEqual({
...(rootFolder.children[1] as Folder).children[0],
layerName: "screens",
});
});

test("isSliced", () => {
expect(isSliced("shared")).toBe(false);
expect(isSliced("app")).toBe(false);
expect(isSliced("entities")).toBe(true);
expect(isSliced("features")).toBe(true);
expect(isSliced("pages")).toBe(true);
expect(isSliced("widgets")).toBe(true);
expect(isSliced("screens", { layerAliases: { pages: "screens" } })).toBe(
true,
);

expect(
isSliced({
Expand All @@ -150,6 +220,16 @@ test("isSliced", () => {
children: [],
}),
).toBe(true);
expect(
isSliced(
{
type: "folder",
path: joinFromRoot("project", "src", "screens"),
children: [],
},
{ layerAliases: { pages: "screens" } },
),
).toBe(true);
});

describe("getIndexes", () => {
Expand Down