Skip to content

Commit

Permalink
Add support for cross-import public APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
illright committed Sep 28, 2024
1 parent 0d7e6c1 commit 5092c2f
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 31 deletions.
57 changes: 31 additions & 26 deletions src/fsd-aware-traverse.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { basename, join } from "node:path";
import { basename, join, parse } from "node:path";

import {
conventionalSegmentNames,
Expand Down Expand Up @@ -71,12 +71,8 @@ export function getSegments(
): Record<string, Folder | File> {
return Object.fromEntries(
sliceOrUnslicedLayer.children
.filter(
(child) =>
child.type !== "file" ||
withoutExtension(basename(child.path)) !== "index",
)
.map((child) => [withoutExtension(basename(child.path)), child]),
.filter((child) => !isIndex(child))
.map((child) => [parse(child.path).name, child]),
);
}

Expand Down Expand Up @@ -163,14 +159,36 @@ export function getIndex(fileOrFolder: File | Folder): File | undefined {
if (fileOrFolder.type === "file") {
return fileOrFolder;
} else {
return fileOrFolder.children.find(
(child) =>
child.type === "file" &&
withoutExtension(basename(child.path)) === "index",
) as File | undefined;
return fileOrFolder.children.find(isIndex) as File | undefined;
}
}

/** Check if a given file or folder is an index file. */
export function isIndex(fileOrFolder: File | Folder): boolean {
return (
fileOrFolder.type === "file" && parse(fileOrFolder.path).name === "index"
);
}

/**
* Check if a given file is a cross-import public API defined in the slice `inSlice` for the slice `forSlice` on a given layer.
*
* @example
* const file = { path: "./src/entities/user/@x/product.ts", type: "file" }
* isCrossImportPublicApi(file, { inSlice: "user", forSlice: "product", layerPath: "./src/entities" }) // true
*/
export function isCrossImportPublicApi(
file: File,
{
inSlice,
forSlice,
layerPath,
}: { inSlice: string; forSlice: string; layerPath: string },
): boolean {
const { dir, name } = parse(file.path);
return name === forSlice && dir === join(layerPath, inSlice, "@x");
}

/**
* Determine if this folder is a slice.
*
Expand All @@ -184,19 +202,6 @@ export function isSlice(
return folder.children.some((child) =>
conventionalSegmentNames
.concat(additionalSegmentNames)
.includes(withoutExtension(basename(child.path))),
.includes(parse(child.path).name),
);
}

/**
* Cut away one layer of extension from a filename.
*
* @example
* withoutExtension("index.tsx") // "index"
* withoutExtension("index.spec.tsx") // "index.spec"
* withoutExtension("index") // "index"
*/
function withoutExtension(filename: string) {
const lastDotIndex = filename.lastIndexOf(".");
return lastDotIndex === -1 ? filename : filename.slice(0, lastDotIndex);
}
116 changes: 113 additions & 3 deletions src/specs/fsd-aware-traverse.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { join } from "node:path";
import { test, expect } from "vitest";

import { getAllSlices, getSlices, type Folder } from "../index.js";
import { parseIntoFolder } from "./prepare-test.js";
import { join } from "node:path";
import {
getAllSlices,
getIndex,
getSlices,
isSlice,
isSliced,
type Folder,
type File,
} from "../index.js";
import { joinFromRoot, parseIntoFolder } from "./prepare-test.js";
import { isCrossImportPublicApi } from "../fsd-aware-traverse.js";

test("getSlices", () => {
const rootFolder = parseIntoFolder(`
Expand Down Expand Up @@ -89,3 +98,104 @@ test("getAllSlices", () => {
layerName: "pages",
});
});

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({
type: "folder",
path: joinFromRoot("project", "src", "shared"),
children: [],
}),
).toBe(false);
expect(
isSliced({
type: "folder",
path: joinFromRoot("project", "src", "entities"),
children: [],
}),
).toBe(true);
});

test("getIndex", () => {
const indexFile: File = {
type: "file",
path: joinFromRoot("project", "src", "shared", "index.ts"),
};
const fileSegment: File = {
type: "file",
path: joinFromRoot("project", "src", "entities", "user", "ui.ts"),
};
const folderSegment = parseIntoFolder(
`
📄 Avatar.tsx
📄 User.tsx
📄 index.ts
`,
joinFromRoot("project", "src", "entities", "user", "ui"),
);
expect(getIndex(indexFile)).toEqual(indexFile);
expect(getIndex(fileSegment)).toEqual(fileSegment);
expect(getIndex(folderSegment)).toEqual({
type: "file",
path: joinFromRoot("project", "src", "entities", "user", "ui", "index.ts"),
});
});

test("isCrossImportPublicApi", () => {
const file: File = {
path: joinFromRoot(
"project",
"src",
"entities",
"user",
"@x",
"product.ts",
),
type: "file",
};

expect(
isCrossImportPublicApi(file, {
inSlice: "user",
forSlice: "product",
layerPath: joinFromRoot("project", "src", "entities"),
}),
).toBe(true);
expect(
isCrossImportPublicApi(file, {
inSlice: "product",
forSlice: "user",
layerPath: joinFromRoot("project", "src", "entities"),
}),
).toBe(false);
});

test("isSlice", () => {
const sliceFolder = parseIntoFolder(
`
📂 ui
📄 Avatar.tsx
📄 User.tsx
📄 index.ts
`,
joinFromRoot("project", "src", "entities", "user"),
);
expect(isSlice(sliceFolder)).toBe(true);

const notSliceFolder = parseIntoFolder(
`
📂 shared
📂 pages
📂 app
`,
joinFromRoot("project", "src"),
);
expect(isSlice(notSliceFolder)).toBe(false);
});
7 changes: 5 additions & 2 deletions src/specs/prepare-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { join } from "node:path";
import type { Folder, File } from "../definitions.js";

/** Parse a multi-line indented string with emojis for files and folders into an FSD root. */
export function parseIntoFolder(fsMarkup: string): Folder {
export function parseIntoFolder(
fsMarkup: string,
basePath = joinFromRoot(),
): Folder {
function parseFolder(lines: Array<string>, path: string): Folder {
const children: Array<Folder | File> = [];

Expand Down Expand Up @@ -38,7 +41,7 @@ export function parseIntoFolder(fsMarkup: string): Folder {
.map((line, _i, lines) => line.slice(lines[0].search(/\S/)))
.filter(Boolean);

return parseFolder(lines, joinFromRoot());
return parseFolder(lines, basePath);
}

export function joinFromRoot(...segments: Array<string>) {
Expand Down

0 comments on commit 5092c2f

Please sign in to comment.