From 5092c2f3f750652ff6a0318b23336285cd197c55 Mon Sep 17 00:00:00 2001 From: Lev Chelyadinov Date: Sat, 28 Sep 2024 12:30:34 +0200 Subject: [PATCH] Add support for cross-import public APIs --- src/fsd-aware-traverse.ts | 57 +++++++------ src/specs/fsd-aware-traverse.spec.ts | 116 ++++++++++++++++++++++++++- src/specs/prepare-test.ts | 7 +- 3 files changed, 149 insertions(+), 31 deletions(-) diff --git a/src/fsd-aware-traverse.ts b/src/fsd-aware-traverse.ts index a1af43a..e8699c3 100644 --- a/src/fsd-aware-traverse.ts +++ b/src/fsd-aware-traverse.ts @@ -1,4 +1,4 @@ -import { basename, join } from "node:path"; +import { basename, join, parse } from "node:path"; import { conventionalSegmentNames, @@ -71,12 +71,8 @@ export function getSegments( ): Record { 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]), ); } @@ -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. * @@ -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); -} diff --git a/src/specs/fsd-aware-traverse.spec.ts b/src/specs/fsd-aware-traverse.spec.ts index 2a35d89..6c3f54f 100644 --- a/src/specs/fsd-aware-traverse.spec.ts +++ b/src/specs/fsd-aware-traverse.spec.ts @@ -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(` @@ -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); +}); diff --git a/src/specs/prepare-test.ts b/src/specs/prepare-test.ts index 5327982..3fb8ac2 100644 --- a/src/specs/prepare-test.ts +++ b/src/specs/prepare-test.ts @@ -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, path: string): Folder { const children: Array = []; @@ -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) {