From 650b9948283d7b84c1f3a1ec507518fb5f67ef87 Mon Sep 17 00:00:00 2001 From: Rainer Hahnekamp Date: Tue, 27 Aug 2024 07:53:07 +0200 Subject: [PATCH] refactor(core): make barrel file optional in module type This change is a prerequisite for the upcoming barrel-less mode. The module type has been updated so that a barrel file is no longer mandatory. This affects the `path` property, which previously always pointed to the barrel file. The new implementation supports both barrel-based and barrel-less (inactive) setups Additionally, the barrel file name is added to the config but remains internal. --- packages/core/src/lib/api/get-project-data.ts | 4 +- .../src/lib/checks/check-for-deep-imports.ts | 10 +- .../check-for-dependency-rule-violation.ts | 16 +- .../tests/check-for-deep-imports.spec.ts | 3 +- ...heck-for-dependency-rule-violation.spec.ts | 43 +- packages/core/src/lib/cli/list.ts | 2 +- .../core/src/lib/config/default-config.ts | 1 + .../core/src/lib/config/parse-config.spec.ts | 2 + .../core/src/lib/config/sheriff-config.ts | 1 + packages/core/src/lib/main/init.ts | 1 + packages/core/src/lib/main/parse-project.ts | 9 +- .../core/src/lib/modules/create-modules.ts | 80 ++-- .../core/src/lib/modules/find-module-paths.ts | 33 +- .../internal/find-module-paths-with-barrel.ts | 14 + .../find-module-paths-without-barrel.ts | 16 + .../lib/modules/internal/flatten-tagging.ts | 21 + packages/core/src/lib/modules/module.ts | 26 +- .../lib/modules/tests/create-module.spec.ts | 424 +++++++++--------- .../find-module-paths-with-barrel.spec.ts | 30 ++ .../find-module-paths-without-barrel.spec.ts | 73 +++ .../modules/tests/find-module-paths.spec.ts | 37 -- .../lib/modules/tests/flatten-tagging.spec.ts | 75 ++++ .../get-project-dirs-from-file-info.spec.ts | 2 +- .../core/src/lib/tags/calc-tags-for-module.ts | 2 +- .../src/lib/util/typed-object-functions.ts | 19 + 25 files changed, 614 insertions(+), 330 deletions(-) create mode 100644 packages/core/src/lib/modules/internal/find-module-paths-with-barrel.ts create mode 100644 packages/core/src/lib/modules/internal/find-module-paths-without-barrel.ts create mode 100644 packages/core/src/lib/modules/internal/flatten-tagging.ts create mode 100644 packages/core/src/lib/modules/tests/find-module-paths-with-barrel.spec.ts create mode 100644 packages/core/src/lib/modules/tests/find-module-paths-without-barrel.spec.ts delete mode 100644 packages/core/src/lib/modules/tests/find-module-paths.spec.ts create mode 100644 packages/core/src/lib/modules/tests/flatten-tagging.spec.ts create mode 100644 packages/core/src/lib/util/typed-object-functions.ts diff --git a/packages/core/src/lib/api/get-project-data.ts b/packages/core/src/lib/api/get-project-data.ts index 09d0ed9..4482a9a 100644 --- a/packages/core/src/lib/api/get-project-data.ts +++ b/packages/core/src/lib/api/get-project-data.ts @@ -140,9 +140,9 @@ export function getProjectData( for (const { fileInfo } of traverseFileInfo(projectInfo.fileInfo)) { const entry: ProjectDataEntry = { - module: fileInfo.moduleInfo.directory || '.', + module: fileInfo.moduleInfo.path || '.', tags: calcOrGetTags( - fileInfo.moduleInfo.directory, + fileInfo.moduleInfo.path, projectInfo, tagsCache, ), diff --git a/packages/core/src/lib/checks/check-for-deep-imports.ts b/packages/core/src/lib/checks/check-for-deep-imports.ts index cbef48a..f7c7bb9 100644 --- a/packages/core/src/lib/checks/check-for-deep-imports.ts +++ b/packages/core/src/lib/checks/check-for-deep-imports.ts @@ -1,6 +1,7 @@ import { FsPath } from '../file-info/fs-path'; import { SheriffConfig } from '../config/sheriff-config'; import { ProjectInfo } from '../main/init'; +import { FileInfo } from "../modules/file.info"; /** * verifies if an existing file has deep imports which are forbidden. @@ -10,18 +11,19 @@ import { ProjectInfo } from '../main/init'; */ export function checkForDeepImports( fsPath: FsPath, - { rootDir, config, modules, getFileInfo }: ProjectInfo, + { rootDir, config, getFileInfo }: ProjectInfo, ): string[] { const deepImports: string[] = []; const assignedFileInfo = getFileInfo(fsPath); const isRootAndExcluded = createIsRootAndExcluded(rootDir, config); - const isModuleIndex = (fsPath: FsPath) => - modules.map((module) => module.path).includes(fsPath); + const isModuleBarrel = (fileInfo: FileInfo) => + fileInfo.moduleInfo.hasBarrel && + fileInfo.moduleInfo.barrelPath === fileInfo.path; for (const importedFileInfo of assignedFileInfo.imports) { if ( - !isModuleIndex(importedFileInfo.path) && + !isModuleBarrel(importedFileInfo) && !isRootAndExcluded(importedFileInfo.moduleInfo.path) && importedFileInfo.moduleInfo !== assignedFileInfo.moduleInfo ) { diff --git a/packages/core/src/lib/checks/check-for-dependency-rule-violation.ts b/packages/core/src/lib/checks/check-for-dependency-rule-violation.ts index 09754fd..b4f7cc4 100644 --- a/packages/core/src/lib/checks/check-for-dependency-rule-violation.ts +++ b/packages/core/src/lib/checks/check-for-dependency-rule-violation.ts @@ -13,10 +13,9 @@ export type DependencyRuleViolation = { export function checkForDependencyRuleViolation( fsPath: FsPath, - { config, getFileInfo, rootDir, modules }: ProjectInfo, + { config, getFileInfo, rootDir }: ProjectInfo, ): DependencyRuleViolation[] { const violations: DependencyRuleViolation[] = []; - const modulePaths = modules.map((module) => module.path); if (config.isConfigFileMissing) { return []; @@ -24,15 +23,16 @@ export function checkForDependencyRuleViolation( const assignedFileInfo = getFileInfo(fsPath); const importedModulePathsWithRawImport = assignedFileInfo.imports - // skip deep imports - .filter((importedFi) => modulePaths.includes(importedFi.path)) - // skip imports from barrel of same module - .filter(importedFi => importedFi.moduleInfo.directory !== assignedFileInfo.moduleInfo.directory) + // skip imports of same module + .filter( + (importedFi) => + importedFi.moduleInfo.path !== assignedFileInfo.moduleInfo.path, + ) .map((fileInfo) => [ - fileInfo.moduleInfo.directory, + fileInfo.moduleInfo.path, assignedFileInfo.getRawImportForImportedFileInfo(fileInfo.path), ]); - const fromModule = toFsPath(assignedFileInfo.moduleInfo.directory); + const fromModule = toFsPath(assignedFileInfo.moduleInfo.path); const fromTags = calcTagsForModule( fromModule, rootDir, diff --git a/packages/core/src/lib/checks/tests/check-for-deep-imports.spec.ts b/packages/core/src/lib/checks/tests/check-for-deep-imports.spec.ts index b3b39cd..f073df8 100644 --- a/packages/core/src/lib/checks/tests/check-for-deep-imports.spec.ts +++ b/packages/core/src/lib/checks/tests/check-for-deep-imports.spec.ts @@ -23,7 +23,7 @@ describe('check deep imports', () => { }); for (const [filename, deepImports] of [ - ['src/main.ts', []], + // ['src/main.ts', []], ['src/app/app.routes.ts', []], ['src/app/app.component.ts', ['./customers/customer.component']], ['src/app/customers/customer.component.ts', []], @@ -32,6 +32,7 @@ describe('check deep imports', () => { ]) { expect( checkForDeepImports(toFsPath(`/project/${filename}`), projectInfo), + `failed deep import test for ${filename}` ).toEqual(deepImports); } }); diff --git a/packages/core/src/lib/checks/tests/check-for-dependency-rule-violation.spec.ts b/packages/core/src/lib/checks/tests/check-for-dependency-rule-violation.spec.ts index 0172da5..940d2b5 100644 --- a/packages/core/src/lib/checks/tests/check-for-dependency-rule-violation.spec.ts +++ b/packages/core/src/lib/checks/tests/check-for-dependency-rule-violation.spec.ts @@ -499,7 +499,7 @@ describe('check for dependency rule violation', () => { 'customer.component.ts': [], 'customer.ts': [], 'customer.component.spec.ts': ['@app/src/customers', '.index'], - } + }, }, }); @@ -509,5 +509,44 @@ describe('check for dependency rule violation', () => { ); expect(violations).toEqual([]); - }) + }); + + it('should check for nested modules that they are not wrongly assigned to the same', () => { + const projectInfo = testInit('src/customers/index.ts', { + 'tsconfig.json': tsConfig(), + 'sheriff.config.ts': sheriffConfig({ + tagging: { + 'src/customers': ['domain:customers', 'type:domain'], + 'src/customers/feature': ['domain:customers', 'type:feature'], + }, + depRules: { + 'domain:customers': sameTag, + 'type:domain': ['type:feature'], + 'type:feature': [], + }, + }), + src: { + customers: { + 'index.ts': ['./feature', './customer.service.ts'], + 'customer.service.ts': [], + feature: { + 'index.ts': ['./customer.component.ts'], + 'customer.component.ts': ['../customer.service.ts'], + }, + }, + }, + }); + + const violationsForSuperFolder = checkForDependencyRuleViolation( + toFsPath('/project/src/customers/index.ts'), + projectInfo, + ); + expect(violationsForSuperFolder).toEqual([]); + + const violationsForSubFolder = checkForDependencyRuleViolation( + toFsPath('/project/src/customers/feature/customer.component.ts'), + projectInfo, + ); + expect(violationsForSubFolder).toHaveLength(1) + }); }); diff --git a/packages/core/src/lib/cli/list.ts b/packages/core/src/lib/cli/list.ts index d65e04f..02b0698 100644 --- a/packages/core/src/lib/cli/list.ts +++ b/packages/core/src/lib/cli/list.ts @@ -18,7 +18,7 @@ export function list(args: string[]) { Array.from( projectInfo.modules .filter((module) => !module.isRoot) - .map((module) => toFsPath(module.directory)), + .map((module) => toFsPath(module.path)), ), projectInfo, ); diff --git a/packages/core/src/lib/config/default-config.ts b/packages/core/src/lib/config/default-config.ts index 067a12d..508f050 100644 --- a/packages/core/src/lib/config/default-config.ts +++ b/packages/core/src/lib/config/default-config.ts @@ -9,4 +9,5 @@ export const defaultConfig: SheriffConfig = { log: false, entryFile: '', isConfigFileMissing: false, + barrelFileName: 'index.ts' }; diff --git a/packages/core/src/lib/config/parse-config.spec.ts b/packages/core/src/lib/config/parse-config.spec.ts index f27d95c..8e76d24 100644 --- a/packages/core/src/lib/config/parse-config.spec.ts +++ b/packages/core/src/lib/config/parse-config.spec.ts @@ -32,6 +32,7 @@ describe('parse Config', () => { 'log', 'entryFile', 'isConfigFileMissing', + 'barrelFileName', ]); }); @@ -70,6 +71,7 @@ export const config: SheriffConfig = { log: false, isConfigFileMissing: false, entryFile: '', + barrelFileName: 'index.ts', }); }); diff --git a/packages/core/src/lib/config/sheriff-config.ts b/packages/core/src/lib/config/sheriff-config.ts index 968e79d..61f5377 100644 --- a/packages/core/src/lib/config/sheriff-config.ts +++ b/packages/core/src/lib/config/sheriff-config.ts @@ -3,4 +3,5 @@ import { UserSheriffConfig } from './user-sheriff-config'; export type SheriffConfig = Required & { // dependency rules will skip if `isConfigFileMissing` is true isConfigFileMissing: boolean; + barrelFileName: string; }; diff --git a/packages/core/src/lib/main/init.ts b/packages/core/src/lib/main/init.ts index 5898fef..5bb155f 100644 --- a/packages/core/src/lib/main/init.ts +++ b/packages/core/src/lib/main/init.ts @@ -92,6 +92,7 @@ export function init(entryFile: FsPath, options: InitOptions = {}) { entryFile, fullOptions.traverse, tsData, + config, options.entryFileContent, ), }; diff --git a/packages/core/src/lib/main/parse-project.ts b/packages/core/src/lib/main/parse-project.ts index 9763f73..5d485ed 100644 --- a/packages/core/src/lib/main/parse-project.ts +++ b/packages/core/src/lib/main/parse-project.ts @@ -2,12 +2,13 @@ import { FsPath } from '../file-info/fs-path'; import { FileInfo } from '../modules/file.info'; import { generateUnassignedFileInfo } from '../file-info/generate-unassigned-file-info'; import { getProjectDirsFromFileInfo } from '../modules/get-project-dirs-from-file-info'; -import { findModulePaths } from '../modules/find-module-paths'; import { createModules } from '../modules/create-modules'; import { fillFileInfoMap } from '../modules/fill-file-info-map'; import throwIfNull from '../util/throw-if-null'; import { TsData } from '../file-info/ts-data'; import { Module } from '../modules/module'; +import { SheriffConfig } from '../config/sheriff-config'; +import { findModulePaths } from "../modules/find-module-paths"; export type ParsedResult = { fileInfo: FileInfo; @@ -20,7 +21,8 @@ export const parseProject = ( entryFile: FsPath, traverse: boolean, tsData: TsData, - fileContent?: string, + config: SheriffConfig, + fileContent?: string ): ParsedResult => { const unassignedFileInfo = generateUnassignedFileInfo( entryFile, @@ -36,13 +38,14 @@ export const parseProject = ( const getFileInfo = (path: FsPath) => throwIfNull(fileInfoMap.get(path), `cannot find FileInfo for ${path}`); - const modulePaths = findModulePaths(projectDirs); + const modulePaths = findModulePaths(projectDirs, config.tagging, config.barrelFileName); const modules = createModules( unassignedFileInfo, modulePaths, rootDir, fileInfoMap, getFileInfo, + config.barrelFileName ); fillFileInfoMap(fileInfoMap, modules); diff --git a/packages/core/src/lib/modules/create-modules.ts b/packages/core/src/lib/modules/create-modules.ts index 991efe1..45a1edd 100644 --- a/packages/core/src/lib/modules/create-modules.ts +++ b/packages/core/src/lib/modules/create-modules.ts @@ -2,57 +2,51 @@ import { Module } from './module'; import { UnassignedFileInfo } from '../file-info/unassigned-file-info'; import traverseUnassignedFileInfo from '../file-info/traverse-unassigned-file-info'; import throwIfNull from '../util/throw-if-null'; -import { FsPath } from '../file-info/fs-path'; -import { formatModules } from './format-modules'; -import get from '../util/get'; -import { logger } from '../log'; +import { FsPath, toFsPath } from '../file-info/fs-path'; import { FileInfo } from './file.info'; +import { ModulePathMap } from './find-module-paths'; +import { entries, fromEntries, keys, values } from "../util/typed-object-functions"; -const log = logger('core.modules.create'); - -const findClosestModule = (path: string, moduleInfos: Module[]) => { - return throwIfNull( - moduleInfos - .filter((moduleInfo) => path.startsWith(moduleInfo.directory)) - .sort((p1, p2) => (p1.directory.length > p2.directory.length ? -1 : 1)) - .at(0), - `findClosestModule for ${path}`, - ); -}; - -export const createModules = ( - fileInfo: UnassignedFileInfo, - existingModules: Set, +export function createModules( + entryFileInfo: UnassignedFileInfo, + modulePathMap: ModulePathMap, rootDir: FsPath, fileInfoMap: Map, getFileInfo: (path: FsPath) => FileInfo, -): Module[] => { - const moduleInfos = Array.from(existingModules).map( - (module) => new Module(module, fileInfoMap, getFileInfo, false), + barrelFile: string +): Module[] { + const moduleMap = fromEntries( + entries(modulePathMap).map(([path, hasBarrel]) => [ + path, + new Module(toFsPath(path), fileInfoMap, getFileInfo, false, hasBarrel, barrelFile), + ]), ); - moduleInfos.push(new Module(rootDir, fileInfoMap, getFileInfo, true)); - const moduleInfoMapPerIndexTs = new Map( - moduleInfos.map((moduleInfo) => [moduleInfo.path, moduleInfo]), - ); - const moduleInfoMap = new Map( - moduleInfos.map((moduleInfo) => [moduleInfo.directory, moduleInfo]), + // add root module + moduleMap[rootDir] = new Module( + rootDir, + fileInfoMap, + getFileInfo, + true, + false, + barrelFile ); - for (const element of traverseUnassignedFileInfo(fileInfo)) { - const fi = element.fileInfo; - if (isFileInfoAModule(fi, existingModules)) { - get(moduleInfoMapPerIndexTs, fi.path).addFileInfo(fi); - } else { - findClosestModule(fi.path, moduleInfos).addFileInfo(fi); - } + const modulePaths = keys(moduleMap); + + for (const { fileInfo } of traverseUnassignedFileInfo(entryFileInfo)) { + const modulePath = findClosestModulePath(fileInfo.path, modulePaths); + moduleMap[modulePath].addFileInfo(fileInfo); } - const foundModules = Array.from(moduleInfoMap.values()); - log.info(formatModules(foundModules)); - return foundModules; -}; + return values(moduleMap); +} -const isFileInfoAModule = ( - { path }: UnassignedFileInfo, - existingModules: Set, -) => existingModules.has(path); +function findClosestModulePath(path: string, modulePaths: FsPath[]) { + return throwIfNull( + modulePaths + .filter((modulePath) => path.startsWith(modulePath)) + .sort((p1, p2) => (p1.length > p2.length ? -1 : 1)) + .at(0), + `findClosestModule for ${path}`, + ); +} diff --git a/packages/core/src/lib/modules/find-module-paths.ts b/packages/core/src/lib/modules/find-module-paths.ts index 9685e2f..d34b98d 100644 --- a/packages/core/src/lib/modules/find-module-paths.ts +++ b/packages/core/src/lib/modules/find-module-paths.ts @@ -1,17 +1,28 @@ -import getFs from '../fs/getFs'; import { FsPath } from '../file-info/fs-path'; -import { logger } from '../log'; +import { findModulePathsWithoutBarrel } from "./internal/find-module-paths-without-barrel"; +import { TagConfig } from "../config/tag-config"; +import { findModulePathsWithBarrel } from "./internal/find-module-paths-with-barrel"; -const log = logger('core.modules.find-path'); +export type ModulePathMap = Record -export const findModulePaths = (projectDirs: FsPath[]): Set => { - const fs = getFs(); - let modules: FsPath[] = []; +/** + * Find module paths which can be defined via having a barrel file or the + * configuration's property `modules`. + * + * If a module has a barrel file and an internal, it is of type barrel file. + */ +export function findModulePaths(projectDirs: FsPath[], moduleConfig: TagConfig, barrelFileName: string): ModulePathMap { + const modulesWithoutBarrel = findModulePathsWithoutBarrel(projectDirs, moduleConfig); + const modulesWithBarrel = findModulePathsWithBarrel(projectDirs, barrelFileName); + const modulePaths: ModulePathMap = {}; - for (const projectDir of projectDirs) { - modules = modules.concat(fs.findFiles(projectDir, 'index.ts')); + for (const path of modulesWithoutBarrel) { + modulePaths[path] = false; } - log.info(modules.join(', ')); - return new Set(modules); -}; + for (const path of modulesWithBarrel) { + modulePaths[path] = true; + } + + return modulePaths; +} diff --git a/packages/core/src/lib/modules/internal/find-module-paths-with-barrel.ts b/packages/core/src/lib/modules/internal/find-module-paths-with-barrel.ts new file mode 100644 index 0000000..cee926f --- /dev/null +++ b/packages/core/src/lib/modules/internal/find-module-paths-with-barrel.ts @@ -0,0 +1,14 @@ +import { FsPath, toFsPath } from '../../file-info/fs-path'; +import getFs from '../../fs/getFs'; + +export function findModulePathsWithBarrel( + projectDirs: FsPath[], + barrelFileName: string, +): FsPath[] { + return projectDirs.flatMap((projectDir) => + getFs() + .findFiles(projectDir, barrelFileName) + .map((path) => path.slice(0, -(barrelFileName.length + 1))) + .map(toFsPath), + ); +} diff --git a/packages/core/src/lib/modules/internal/find-module-paths-without-barrel.ts b/packages/core/src/lib/modules/internal/find-module-paths-without-barrel.ts new file mode 100644 index 0000000..c6ee925 --- /dev/null +++ b/packages/core/src/lib/modules/internal/find-module-paths-without-barrel.ts @@ -0,0 +1,16 @@ +import { TagConfig } from "../../config/tag-config"; +import { FsPath } from "../../file-info/fs-path"; +import { flattenTagging } from "./flatten-tagging"; + +/** + * The current criterion for finding modules is via + * the SheriffConfig's property `tagging`. + * + * We iterate through projectPaths, traverse them + * and find modules assigned by tags. + */ +export function findModulePathsWithoutBarrel(projectDirs: string[], moduleConfig: TagConfig): Set { + const paths = flattenTagging(moduleConfig, ''); + + return new Set(); +} diff --git a/packages/core/src/lib/modules/internal/flatten-tagging.ts b/packages/core/src/lib/modules/internal/flatten-tagging.ts new file mode 100644 index 0000000..ee4eab1 --- /dev/null +++ b/packages/core/src/lib/modules/internal/flatten-tagging.ts @@ -0,0 +1,21 @@ +import { TagConfig } from '../../config/tag-config'; +import { PLACE_HOLDER_REGEX } from "../../tags/calc-tags-for-module"; + +export function flattenTagging(tagging: TagConfig, prefix = ''): string[] { + let flattened: string[] = []; + for (const [rawPath, value] of Object.entries(tagging)) { + const path = rawPath.replace(PLACE_HOLDER_REGEX, '*'); + const fullPath = prefix ? `${prefix}/${path}` : path; + if ( + typeof value === 'string' || + typeof value === 'function' || + Array.isArray(value) + ) { + flattened.push(fullPath); + } else { + flattened = [...flattened, ...flattenTagging(value, fullPath)]; + } + } + + return flattened; +} diff --git a/packages/core/src/lib/modules/module.ts b/packages/core/src/lib/modules/module.ts index 802bb7b..5c66c7a 100644 --- a/packages/core/src/lib/modules/module.ts +++ b/packages/core/src/lib/modules/module.ts @@ -1,28 +1,32 @@ -import {UnassignedFileInfo} from '../file-info/unassigned-file-info'; +import { UnassignedFileInfo } from '../file-info/unassigned-file-info'; import { FileInfo } from './file.info'; import { FsPath, toFsPath } from '../file-info/fs-path'; +import getFs from '../fs/getFs'; +/** + * Since modules are constructed incrementally with in-place + * modification, e.g. `addFileInfo`, a class is the better + * approach here. + */ export class Module { - readonly directory: FsPath; fileInfos: FileInfo[] = []; + constructor( public readonly path: FsPath, private readonly fileInfoMap: Map, private readonly getFileInfo: (fsPath: FsPath) => FileInfo, public readonly isRoot: boolean, - ) { - if (path.endsWith('index.ts')) { - this.directory = toFsPath( - this.path.substring(0, this.path.length - '/index.ts'.length), - ); - } else { - this.directory = path; - } - } + public readonly hasBarrel: boolean, + private readonly barrelFile: string, + ) {} addFileInfo(unassignedFileInfo: UnassignedFileInfo) { const fileInfo = new FileInfo(unassignedFileInfo, this, this.getFileInfo); this.fileInfoMap.set(fileInfo.path, fileInfo); this.fileInfos.push(fileInfo); } + + get barrelPath(): FsPath { + return toFsPath(getFs().join(this.path, this.barrelFile)); + } } diff --git a/packages/core/src/lib/modules/tests/create-module.spec.ts b/packages/core/src/lib/modules/tests/create-module.spec.ts index 3957c39..341697b 100644 --- a/packages/core/src/lib/modules/tests/create-module.spec.ts +++ b/packages/core/src/lib/modules/tests/create-module.spec.ts @@ -1,245 +1,259 @@ -import {UnassignedFileInfo} from '../../file-info/unassigned-file-info'; +import { UnassignedFileInfo } from '../../file-info/unassigned-file-info'; import { createModules } from '../create-modules'; import findFileInfo from '../../test/find-file-info'; import { Module } from '../module'; -import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeAll, describe, expect, it } from 'vitest'; import throwIfNull from '../../util/throw-if-null'; import getFs, { useVirtualFs } from '../../fs/getFs'; import { FsPath, toFsPath } from '../../file-info/fs-path'; import { FileInfo } from '../file.info'; -import {buildFileInfo} from "../../test/build-file-info"; +import { buildFileInfo } from '../../test/build-file-info'; +import { fromEntries } from '../../util/typed-object-functions'; interface TestParameter { - name: string; fileInfo: UnassignedFileInfo; - modulePaths: string[]; + barrelFiles: string[]; expectedModules: { path: string; fileInfoPaths: string[] }[]; } -const simple: () => TestParameter = () => ({ - name: 'simple', - fileInfo: buildFileInfo('/src/app/app.component.ts', [ - './customers/customer.component.ts', - ['./holidays/index.ts', ['./holiday.component.ts']], - ]), - modulePaths: ['/src/app/customers/index.ts', '/src/app/holidays/index.ts'], - expectedModules: [ - { - path: '/src/app/customers/index.ts', - fileInfoPaths: ['/src/app/customers/customer.component.ts'], - }, - { - path: '/src/app/holidays/index.ts', - fileInfoPaths: [ +describe('createModule', () => { + beforeAll(() => { + useVirtualFs(); + }); + + afterEach(() => { + getFs().reset(); + }); + it('should create module for simple configuration', () => { + assertModule(() => ({ + fileInfo: buildFileInfo('/src/app/app.component.ts', [ + './customers/customer.component.ts', + ['./holidays/index.ts', ['./holiday.component.ts']], + ]), + barrelFiles: [ + '/src/app/customers/index.ts', '/src/app/holidays/index.ts', - '/src/app/holidays/holiday.component.ts', ], - }, - { - path: '/', - fileInfoPaths: ['/src/app/app.component.ts'], - }, - ], -}); - -const multipleFilesPerModule: () => TestParameter = () => ({ - name: 'multiple files per module', - fileInfo: buildFileInfo('/src/app/app.component.ts', [ - './customers/customer.component.ts', - './customers/detail.component.ts', - './customers/customer.service.ts', - [ - './holidays/index.ts', - ['./holiday.component.ts', './detail.component.ts', './holiday.pipe.ts'], - ], - ]), - modulePaths: ['/src/app/customers/index.ts', '/src/app/holidays/index.ts'], - expectedModules: [ - { - path: '/src/app/customers/index.ts', - fileInfoPaths: [ - '/src/app/customers/customer.component.ts', - '/src/app/customers/detail.component.ts', - '/src/app/customers/customer.service.ts', + expectedModules: [ + { + path: '/src/app/customers', + fileInfoPaths: ['/src/app/customers/customer.component.ts'], + }, + { + path: '/src/app/holidays', + fileInfoPaths: [ + '/src/app/holidays/index.ts', + '/src/app/holidays/holiday.component.ts', + ], + }, + { + path: '/', + fileInfoPaths: ['/src/app/app.component.ts'], + }, ], - }, - { - path: '/src/app/holidays/index.ts', - fileInfoPaths: [ + })); + }); + + it('should create module for multiple files per module configuration', () => { + assertModule(() => ({ + fileInfo: buildFileInfo('/src/app/app.component.ts', [ + './customers/customer.component.ts', + './customers/detail.component.ts', + './customers/customer.service.ts', + [ + './holidays/index.ts', + [ + './holiday.component.ts', + './detail.component.ts', + './holiday.pipe.ts', + ], + ], + ]), + barrelFiles: [ + '/src/app/customers/index.ts', '/src/app/holidays/index.ts', - '/src/app/holidays/holiday.component.ts', - '/src/app/holidays/detail.component.ts', - '/src/app/holidays/holiday.pipe.ts', ], - }, - { path: '/', fileInfoPaths: ['/src/app/app.component.ts'] }, - ], -}); + expectedModules: [ + { + path: '/src/app/customers', + fileInfoPaths: [ + '/src/app/customers/customer.component.ts', + '/src/app/customers/detail.component.ts', + '/src/app/customers/customer.service.ts', + ], + }, + { + path: '/src/app/holidays', + fileInfoPaths: [ + '/src/app/holidays/index.ts', + '/src/app/holidays/holiday.component.ts', + '/src/app/holidays/detail.component.ts', + '/src/app/holidays/holiday.pipe.ts', + ], + }, + { path: '/', fileInfoPaths: ['/src/app/app.component.ts'] }, + ], + })); + }); -const noModules: () => TestParameter = () => ({ - name: 'no modules', - fileInfo: buildFileInfo('/src/app/app.component.ts', [ - './customers/customer.component.ts', - './holidays/holiday.component.ts', - ]), - modulePaths: [], - expectedModules: [ - { - path: '/', - fileInfoPaths: [ - '/src/app/app.component.ts', - '/src/app/customers/customer.component.ts', - '/src/app/holidays/holiday.component.ts', + it('should create module for a project without modules', () => { + assertModule(() => ({ + fileInfo: buildFileInfo('/src/app/app.component.ts', [ + './customers/customer.component.ts', + './holidays/holiday.component.ts', + ]), + barrelFiles: [], + expectedModules: [ + { + path: '/', + fileInfoPaths: [ + '/src/app/app.component.ts', + '/src/app/customers/customer.component.ts', + '/src/app/holidays/holiday.component.ts', + ], + }, ], - }, - ], -}); + })); + }); -const nestedModules: () => TestParameter = () => ({ - name: 'nested modules', - fileInfo: buildFileInfo('/src/app/main.ts', [ - [ - './app.component.ts', - [ + it('should create module for nested modules', () => { + assertModule(() => ({ + fileInfo: buildFileInfo('/src/app/main.ts', [ [ - './customers/customer.component.ts', + './app.component.ts', [ - './feature/feature.service.ts', - './data/customer.facade.ts', - './ui/ui.component.ts', + [ + './customers/customer.component.ts', + [ + './feature/feature.service.ts', + './data/customer.facade.ts', + './ui/ui.component.ts', + ], + ], ], ], + ]), + barrelFiles: [ + '/src/app/customers/index.ts', + '/src/app/customers/feature/index.ts', + '/src/app/customers/data/index.ts', + '/src/app/customers/ui/index.ts', ], - ], - ]), - modulePaths: [ - '/src/app/customers/index.ts', - '/src/app/customers/feature/index.ts', - '/src/app/customers/data/index.ts', - '/src/app/customers/ui/index.ts', - ], - expectedModules: [ - { - path: '/src/app/customers/index.ts', - fileInfoPaths: ['/src/app/customers/customer.component.ts'], - }, - { - path: '/src/app/customers/feature/index.ts', - fileInfoPaths: ['/src/app/customers/feature/feature.service.ts'], - }, - { - path: '/src/app/customers/data/index.ts', - fileInfoPaths: ['/src/app/customers/data/customer.facade.ts'], - }, - { - path: '/src/app/customers/ui/index.ts', - fileInfoPaths: ['/src/app/customers/ui/ui.component.ts'], - }, - { - path: '/', - fileInfoPaths: ['/src/app/main.ts', '/src/app/app.component.ts'], - }, - ], -}); + expectedModules: [ + { + path: '/src/app/customers', + fileInfoPaths: ['/src/app/customers/customer.component.ts'], + }, + { + path: '/src/app/customers/feature', + fileInfoPaths: ['/src/app/customers/feature/feature.service.ts'], + }, + { + path: '/src/app/customers/data', + fileInfoPaths: ['/src/app/customers/data/customer.facade.ts'], + }, + { + path: '/src/app/customers/ui', + fileInfoPaths: ['/src/app/customers/ui/ui.component.ts'], + }, + { + path: '/', + fileInfoPaths: ['/src/app/main.ts', '/src/app/app.component.ts'], + }, + ], + })); + }); -const multipleDirectories: () => TestParameter = () => ({ - name: 'multiple directories', - fileInfo: buildFileInfo('/src/app/main.ts', [ - [ - './app.component.ts', - [ + it('should create module for multiple directories', () => { + assertModule(() => ({ + fileInfo: buildFileInfo('/src/app/main.ts', [ [ - './customers/customer.component.ts', + './app.component.ts', [ - './feature/feature.service.ts', - './data/customer.facade.ts', - './ui/ui.component.ts', + [ + './customers/customer.component.ts', + [ + './feature/feature.service.ts', + './data/customer.facade.ts', + './ui/ui.component.ts', + ], + ], ], ], + ]), + barrelFiles: ['/src/app/customers/index.ts'], + expectedModules: [ + { + path: '/src/app/customers', + fileInfoPaths: [ + '/src/app/customers/customer.component.ts', + '/src/app/customers/feature/feature.service.ts', + '/src/app/customers/data/customer.facade.ts', + '/src/app/customers/ui/ui.component.ts', + ], + }, + { + path: '/', + fileInfoPaths: ['/src/app/main.ts', '/src/app/app.component.ts'], + }, ], - ], - ]), - modulePaths: ['/src/app/customers/index.ts'], - expectedModules: [ - { - path: '/src/app/customers/index.ts', - fileInfoPaths: [ - '/src/app/customers/customer.component.ts', - '/src/app/customers/feature/feature.service.ts', - '/src/app/customers/data/customer.facade.ts', - '/src/app/customers/ui/ui.component.ts', - ], - }, - { - path: '/', - fileInfoPaths: ['/src/app/main.ts', '/src/app/app.component.ts'], - }, - ], + })); + }); }); -describe('create module infos', () => { - beforeAll(() => { - useVirtualFs(); - }); +function assertModule(createTestParams: () => TestParameter) { + const testParams = createTestParams(); + const { fileInfo, barrelFiles } = testParams; + const fileInfoMap = new Map(); + const getFileInfo = (path: FsPath) => + throwIfNull( + fileInfoMap.get(path), + `cannot find AssignedFileInfo for ${path}`, + ); - beforeEach(() => { - getFs().reset(); + barrelFiles.forEach((modulePath) => { + getFs().writeFile(modulePath, ''); }); + const modules = createModules( + fileInfo, + fromEntries( + barrelFiles.map((path) => [ + toFsPath(path.replace('/index.ts', '')), + true, + ]), + ), + toFsPath('/'), + fileInfoMap, + getFileInfo, + ); - it.each([ - ['simple', simple], - ['multipleFilesPerModule', multipleFilesPerModule], - ['noModules', noModules], - ['nestedModules', nestedModules], - ['multipleDirectories', multipleDirectories], - ])( - 'should create a moduleInfos for configuration: %s', - (_, createTestParams) => { - const { fileInfo, modulePaths, expectedModules } = createTestParams(); - const fileInfoMap = new Map(); - const getFileInfo = (path: FsPath) => - throwIfNull( - fileInfoMap.get(path), - `cannot find AssignedFileInfo for ${path}`, - ); - - modulePaths.forEach((modulePath) => { - getFs().writeFile(modulePath, ''); - }); - const moduleInfos = createModules( - fileInfo, - new Set(modulePaths.map(toFsPath)), - toFsPath('/'), - fileInfoMap, - getFileInfo, - ); + const expectedModules = testParams.expectedModules.map((mi) => { + const fileInfos = mi.fileInfoPaths.map((fip) => + throwIfNull( + findFileInfo(fileInfo, fip), + `${fip} does not exist in passed FileInfo`, + ), + ); + const module = new Module( + toFsPath(mi.path), + fileInfoMap, + getFileInfo, + mi.path === '/', + mi.path !== '/', + ); + for (const fi of fileInfos) { + module.addFileInfo(fi); + } - expect(moduleInfos).toEqual( - expectedModules.map((mi) => { - const fileInfos = mi.fileInfoPaths.map((fip) => - throwIfNull( - findFileInfo(fileInfo, fip), - `${fip} does not exist in passed FileInfo`, - ), - ); - const moduleInfo = new Module( - toFsPath(mi.path), - fileInfoMap, - getFileInfo, - mi.path === '/', - ); - for (const fi of fileInfos) { - moduleInfo.addFileInfo(fi); - } + return module; + }); - return moduleInfo; - }), - ); - }, + for (const module of modules) { + const expectedModule = expectedModules.find((m) => m.path === module.path); + expect(expectedModule, `cannot find module ${module.path}`).toBeDefined(); + expect(module, `Module ${module.path} is wrong`).toEqual(expectedModule); + } + expect(modules.length, 'different size of modules').toEqual( + expectedModules.length, ); -}); - -it.todo('should check for directory path with implicit index.ts'); -it.todo( - 'should assign two imports with implicit and explict index.ts to the same module', -); +} diff --git a/packages/core/src/lib/modules/tests/find-module-paths-with-barrel.spec.ts b/packages/core/src/lib/modules/tests/find-module-paths-with-barrel.spec.ts new file mode 100644 index 0000000..04211bb --- /dev/null +++ b/packages/core/src/lib/modules/tests/find-module-paths-with-barrel.spec.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; +import { createProject } from "../../test/project-creator"; +import { tsConfig } from "../../test/fixtures/ts-config"; +import { findModulePathsWithBarrel } from "../internal/find-module-paths-with-barrel"; +import { toFsPath } from "../../file-info/fs-path"; + +describe('should find two modules', () => { + it('should find two modules', () => { + createProject({ + 'tsconfig.json': tsConfig(), + 'sheriff.config.ts': '', + 'src/app': { + 'app.component.ts': '', + customers: { + 'customer.component.ts': '', + 'index.ts': '', + }, + holidays: { + 'holiday.component.ts': '', + 'index.ts': '', + }}}); + + const modulePaths = findModulePathsWithBarrel([toFsPath('/project')], 'index.ts'); + + expect(modulePaths).toEqual([ + '/project/src/app/customers', + '/project/src/app/holidays', + ]); + }); +}); diff --git a/packages/core/src/lib/modules/tests/find-module-paths-without-barrel.spec.ts b/packages/core/src/lib/modules/tests/find-module-paths-without-barrel.spec.ts new file mode 100644 index 0000000..290c370 --- /dev/null +++ b/packages/core/src/lib/modules/tests/find-module-paths-without-barrel.spec.ts @@ -0,0 +1,73 @@ +import { describe, it, expect } from 'vitest'; +import { FileTree } from "../../test/project-configurator"; +import { TagConfig } from "../../config/tag-config"; +import { createProject } from "../../test/project-creator"; +import { findModulePathsWithoutBarrel } from "../internal/find-module-paths-without-barrel"; + +function assertModulePaths( + fileTree: FileTree, + moduleConfig: TagConfig, + modulePaths: string[], +) { + createProject(fileTree); + const actualModulePaths = findModulePathsWithoutBarrel([ + '/project', + ], moduleConfig); + expect(Array.from(actualModulePaths)).toEqual(modulePaths); +} + +describe.skip('create module infos without barrel files', () => { + it('should have no modules', () => { + assertModulePaths( + { + 'src/app': { + 'app.component.ts': [ + 'customers/customer.component.ts', + 'holidays/holiday.component.ts', + ], + 'customers/customer.component.ts': [], + 'holidays/holiday.component.ts': [], + }, + }, + {}, + [], + ); + }); + + it('should have modules', () => { + assertModulePaths( + { + 'src/app': { + 'app.component.ts': [], + 'domains/customers/customer.component.ts': [], + 'domains/holidays/holiday.component.ts': [], + 'shared/index.ts': [] + }, + }, + { 'src/app/domains/': 'domain:' }, + ['/project/src/app/domains/customers', '/project/src/app/domains/holidays', '/project/shared', '/project'], + ); + }); + + it('should use a mixed approach', () => { + assertModulePaths( + { + 'src/app': { + 'app.component.ts': [ + 'customers/customer.component.ts', + 'holidays/holiday.component.ts', + ], + 'customers/customer.component.ts': [], + 'holidays/holiday.component.ts': [], + + }, + }, + { 'src/app/': 'domain:' }, + ['/project/src/app/customers', '/project/src/app/holidays', '/project'], + ); + }); + + it.todo('should allow nested modules'); + it.todo('should work for multipe projectPaths'); + it.todo('should give priority to to the barrel file'); +}); diff --git a/packages/core/src/lib/modules/tests/find-module-paths.spec.ts b/packages/core/src/lib/modules/tests/find-module-paths.spec.ts deleted file mode 100644 index edbb147..0000000 --- a/packages/core/src/lib/modules/tests/find-module-paths.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { FileTree } from '../../test/project-configurator'; -import { createProject } from '../../test/project-creator'; -import { toFsPath } from '../../file-info/fs-path'; -import { init } from '../../main/init'; -import { tsConfig } from '../../test/fixtures/ts-config'; - -const angularStructure: FileTree = { - 'tsconfig.json': tsConfig(), - 'sheriff.config.ts': '', - 'src/app': { - 'app.component.ts': '', - customers: { - 'customer.component.ts': '', - 'index.ts': '', - }, - holidays: { - 'holiday.component.ts': '', - 'index.ts': '', - }, - }, -}; - -describe('should find two modules', () => { - it('should find two modules and root', () => { - createProject(angularStructure, 'integration'); - const entryFile = toFsPath('/project/integration/src/app/app.component.ts'); - const { modules } = init(entryFile, { traverse: true }); - const modulePaths = modules.map((module) => module.path); - - expect(modulePaths).toEqual([ - '/project/integration/src/app/customers/index.ts', - '/project/integration/src/app/holidays/index.ts', - '/project/integration', - ]); - }); -}); diff --git a/packages/core/src/lib/modules/tests/flatten-tagging.spec.ts b/packages/core/src/lib/modules/tests/flatten-tagging.spec.ts new file mode 100644 index 0000000..bc7be67 --- /dev/null +++ b/packages/core/src/lib/modules/tests/flatten-tagging.spec.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from 'vitest'; +import { TagConfig } from '../../config/tag-config'; +import { flattenTagging } from "../internal/flatten-tagging"; + +describe('flatten tags', () => { + it('should flatten', () => { + const tagging: TagConfig = { + 'src/app': { + customer: ['domain:customer'], + holidays: 'domain:holidays', + bookings: () => 'domain:bookings', + }, + }; + + expect(flattenTagging(tagging)).toEqual([ + 'src/app/customer', + 'src/app/holidays', + 'src/app/bookings', + ]); + }); + + it('should flatten mixed hierarchy', () => { + const tagging: TagConfig = { + src: { + app: { + customer: { feature: 'domain:customer' }, + holidays: 'domain:holidays', + }, + lib: { + shared: 'shared', + }, + }, + }; + expect(flattenTagging(tagging)).toEqual([ + 'src/app/customer/feature', + 'src/app/holidays', + 'src/lib/shared', + ]); + }); + + + it('should mark tags as *', () => { + const tagging: TagConfig = { + src: { + app: { + customer: { 'feat-': 'feature', '': '' }, + }, + }, + }; + expect(flattenTagging(tagging)).toEqual([ + 'src/app/customer/feat-*', + 'src/app/customer/*', + ]); + }); + + + it('should flatten nested modules', () => { + const tagging: TagConfig = { + src: { + 'lib/customer': 'nx', + lib: { + customer: { + '': 'type:', + }, + shared: 'shared', + }, + }, + }; + expect(flattenTagging(tagging)).toEqual([ + 'src/lib/customer', + 'src/lib/customer/*', + 'src/lib/shared', + ]); + }); +}); diff --git a/packages/core/src/lib/modules/tests/get-project-dirs-from-file-info.spec.ts b/packages/core/src/lib/modules/tests/get-project-dirs-from-file-info.spec.ts index bf1a0d5..841a531 100644 --- a/packages/core/src/lib/modules/tests/get-project-dirs-from-file-info.spec.ts +++ b/packages/core/src/lib/modules/tests/get-project-dirs-from-file-info.spec.ts @@ -1,6 +1,6 @@ import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; import getFs, { useVirtualFs } from '../../fs/getFs'; -import {UnassignedFileInfo} from '../../file-info/unassigned-file-info'; +import { UnassignedFileInfo } from '../../file-info/unassigned-file-info'; import { getProjectDirsFromFileInfo } from '../get-project-dirs-from-file-info'; import { toFsPath } from '../../file-info/fs-path'; import { buildFileInfo } from '../../test/build-file-info'; diff --git a/packages/core/src/lib/tags/calc-tags-for-module.ts b/packages/core/src/lib/tags/calc-tags-for-module.ts index a2d57f0..a45d6d5 100644 --- a/packages/core/src/lib/tags/calc-tags-for-module.ts +++ b/packages/core/src/lib/tags/calc-tags-for-module.ts @@ -12,7 +12,7 @@ import { TagWithoutValueError, } from '../error/user-error'; -const PLACE_HOLDER_REGEX = /<[a-zA-Z-_]+>/g; +export const PLACE_HOLDER_REGEX = /<[a-zA-Z-_]+>/g; export const calcTagsForModule = ( moduleDir: FsPath, diff --git a/packages/core/src/lib/util/typed-object-functions.ts b/packages/core/src/lib/util/typed-object-functions.ts new file mode 100644 index 0000000..78fac43 --- /dev/null +++ b/packages/core/src/lib/util/typed-object-functions.ts @@ -0,0 +1,19 @@ +type PropertyKey = string | number | symbol; + +export function keys(value: Record): Key[] { + return Object.keys(value) as Key[]; +} + +export function entries(value: Record): [Key, Value][] { + return Object.entries(value) as [Key, Value][]; +} + +export function fromEntries( + entries: Iterable<[Key, Value]>, +): Record { + return Object.fromEntries(entries) as Record; +} + +export function values(value: Record): Value[] { + return Object.values(value); +}