From 10cd402510db90cb1690acd7ce82c9da255bffba Mon Sep 17 00:00:00 2001 From: Rainer Hahnekamp Date: Tue, 8 Oct 2024 13:42:43 +0200 Subject: [PATCH] feat(core): add finder method for barrel-less modules Add a function that traverses through the tagged directories and mark them as modules in the barrel-less mode. This is also supports tagging with placeholders which are interpreted as simple wildcards. --- .../core/src/lib/config/default-config.ts | 1 + .../core/src/lib/config/sheriff-config.ts | 1 - .../src/lib/config/tests/parse-config.spec.ts | 2 + .../src/lib/config/user-sheriff-config.ts | 18 +- packages/core/src/lib/fs/default-fs.spec.ts | 12 + packages/core/src/lib/fs/default-fs.ts | 14 + packages/core/src/lib/fs/fs.ts | 30 ++- packages/core/src/lib/fs/getFs.ts | 11 +- packages/core/src/lib/fs/virtual-fs.spec.ts | 34 ++- packages/core/src/lib/fs/virtual-fs.ts | 18 +- packages/core/src/lib/main/parse-project.ts | 5 +- .../core/src/lib/modules/find-module-paths.ts | 28 +- .../create-module-path-patterns-tree.ts | 58 ++++ .../find-module-paths-without-barrel.ts | 101 ++++++- .../create-module-path-patterns-tree.spec.ts | 68 +++++ .../find-module-paths-without-barrel.spec.ts | 249 ++++++++++++++---- .../core/src/lib/tags/calc-tags-for-module.ts | 1 + packages/core/src/lib/test/project-creator.ts | 3 +- 18 files changed, 559 insertions(+), 95 deletions(-) create mode 100644 packages/core/src/lib/modules/internal/create-module-path-patterns-tree.ts create mode 100644 packages/core/src/lib/modules/tests/create-module-path-patterns-tree.spec.ts diff --git a/packages/core/src/lib/config/default-config.ts b/packages/core/src/lib/config/default-config.ts index 9b7f260..db7db13 100644 --- a/packages/core/src/lib/config/default-config.ts +++ b/packages/core/src/lib/config/default-config.ts @@ -7,6 +7,7 @@ export const defaultConfig: SheriffConfig = { depRules: {}, excludeRoot: false, enableBarrelLess: false, + encapsulatedFolderNameForBarrelLess: 'internal', log: false, entryFile: '', isConfigFileMissing: false, diff --git a/packages/core/src/lib/config/sheriff-config.ts b/packages/core/src/lib/config/sheriff-config.ts index 61f5377..968e79d 100644 --- a/packages/core/src/lib/config/sheriff-config.ts +++ b/packages/core/src/lib/config/sheriff-config.ts @@ -3,5 +3,4 @@ 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/config/tests/parse-config.spec.ts b/packages/core/src/lib/config/tests/parse-config.spec.ts index 40dcd84..cc2f5bf 100644 --- a/packages/core/src/lib/config/tests/parse-config.spec.ts +++ b/packages/core/src/lib/config/tests/parse-config.spec.ts @@ -28,6 +28,7 @@ describe('parse Config', () => { 'depRules', 'excludeRoot', 'enableBarrelLess', + "encapsulatedFolderNameForBarrelLess", 'log', 'entryFile', 'isConfigFileMissing', @@ -67,6 +68,7 @@ export const config: SheriffConfig = { tagging: {}, depRules: { noTag: 'noTag' }, enableBarrelLess: false, + encapsulatedFolderNameForBarrelLess: 'internal', excludeRoot: false, log: false, isConfigFileMissing: false, diff --git a/packages/core/src/lib/config/user-sheriff-config.ts b/packages/core/src/lib/config/user-sheriff-config.ts index 5084084..2d1707b 100644 --- a/packages/core/src/lib/config/user-sheriff-config.ts +++ b/packages/core/src/lib/config/user-sheriff-config.ts @@ -112,10 +112,26 @@ export interface UserSheriffConfig { excludeRoot?: boolean; /** - * Enable the barrel-less mode. Set to false by default. + * The barrel file is usually the `index.ts` and exports + * those files which are available outside the module. + */ + barrelFileName?: string; + + /** + * The barrel-less approach means that the module + * does not have an `index.ts` file. Instead, all files + * are directly available, except those which are located + * in a special folder ("internal" be default). */ enableBarrelLess?: boolean; + /** + * The encapsulated folder contains all files + * which are not available outside the module. + * By default, it is set to `internal`. + */ + encapsulatedFolderNameForBarrelLess?: string; + /** * enable internal logging and save it to `sheriff.log` */ diff --git a/packages/core/src/lib/fs/default-fs.spec.ts b/packages/core/src/lib/fs/default-fs.spec.ts index b89c99b..cb5b1ed 100644 --- a/packages/core/src/lib/fs/default-fs.spec.ts +++ b/packages/core/src/lib/fs/default-fs.spec.ts @@ -109,4 +109,16 @@ describe('Default Fs', () => { ).toThrowError('cannot find a file that does not exist'); }); }); + + it('should only find directories', () => { + const subDirectories = fs.readDirectory( + toFsPath(path.join(__dirname, './find-nearest/test1/customers')), + 'directory', + ); + expect(subDirectories).toEqual( + [path.join(__dirname, './find-nearest/test1/customers/admin')].map( + toFsPath, + ), + ); + }); }); diff --git a/packages/core/src/lib/fs/default-fs.ts b/packages/core/src/lib/fs/default-fs.ts index 0f87fbc..bdb3904 100644 --- a/packages/core/src/lib/fs/default-fs.ts +++ b/packages/core/src/lib/fs/default-fs.ts @@ -16,6 +16,20 @@ export class DefaultFs extends Fs { readFile = (path: string): string => fs.readFileSync(path, { encoding: 'utf-8' }).toString(); + override readDirectory( + directory: FsPath, + filter?: 'none' | 'directory', + ): FsPath[] { + return fs + .readdirSync(directory) + .map((child) => toFsPath(path.join(directory, child))) + .filter((path) => + filter === 'none' + ? true + : fs.lstatSync(path).isDirectory(), + ) + } + removeDir = (path: string) => { fs.rmSync(path, { recursive: true }); }; diff --git a/packages/core/src/lib/fs/fs.ts b/packages/core/src/lib/fs/fs.ts index 00d19e3..bc76337 100644 --- a/packages/core/src/lib/fs/fs.ts +++ b/packages/core/src/lib/fs/fs.ts @@ -2,22 +2,23 @@ import * as path from 'path'; import type { FsPath } from '../file-info/fs-path'; export abstract class Fs { - abstract writeFile: (filename: string, contents: string) => void; + abstract writeFile(filename: string, contents: string): void; abstract appendFile(filename: string, contents: string): void; - abstract readFile: (path: FsPath) => string; - abstract removeDir: (path: FsPath) => void; - abstract createDir: (path: string) => void; + abstract readFile(path: FsPath): string; + abstract readDirectory(path: FsPath, filter?: 'none' | 'directory'): FsPath[]; + abstract removeDir(path: FsPath): void; + abstract createDir(path: string): void; abstract exists(path: string): path is FsPath; - abstract tmpdir: () => string; + abstract tmpdir(): string; join = (...paths: string[]) => path.join(...paths); - abstract cwd: () => string; + abstract cwd(): string; - abstract findFiles: (path: FsPath, filename: string) => FsPath[]; + abstract findFiles(path: FsPath, filename: string): FsPath[]; - abstract print: () => void; + abstract print(): void; /** * Used for finding the nearest `tsconfig.json`. It traverses through the @@ -25,15 +26,18 @@ export abstract class Fs { * @param referenceFile * @param filename */ - abstract findNearestParentFile: ( + abstract findNearestParentFile( referenceFile: FsPath, filename: string, - ) => FsPath; + ): FsPath; - relativeTo = (from: string, to: string) => path.relative(from, to); + relativeTo(from: string, to: string) { + return path.relative(from, to); + } - getParent = (fileOrDirectory: FsPath): FsPath => - path.dirname(fileOrDirectory) as FsPath; + getParent(fileOrDirectory: FsPath): FsPath { + return path.dirname(fileOrDirectory) as FsPath; + } pathSeparator = path.sep; diff --git a/packages/core/src/lib/fs/getFs.ts b/packages/core/src/lib/fs/getFs.ts index 8ad575e..b5f285f 100644 --- a/packages/core/src/lib/fs/getFs.ts +++ b/packages/core/src/lib/fs/getFs.ts @@ -4,8 +4,15 @@ import { Fs } from './fs'; let fsImplementation: 'default' | 'virtual' = 'default'; -export const useDefaultFs = () => (fsImplementation = 'default'); -export const useVirtualFs = () => (fsImplementation = 'virtual'); +export function useDefaultFs() { + fsImplementation = 'default'; + return defaultFs +} + +export function useVirtualFs() { + fsImplementation = 'virtual' + return virtualFs; +} const getFs = (): Fs => fsImplementation === 'default' ? defaultFs : virtualFs; diff --git a/packages/core/src/lib/fs/virtual-fs.spec.ts b/packages/core/src/lib/fs/virtual-fs.spec.ts index 682c01c..c568477 100644 --- a/packages/core/src/lib/fs/virtual-fs.spec.ts +++ b/packages/core/src/lib/fs/virtual-fs.spec.ts @@ -1,7 +1,7 @@ import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { VirtualFs } from './virtual-fs'; import { inVfs } from '../test/in-vfs'; -import { toFsPath } from '../file-info/fs-path'; +import { FsPath, toFsPath } from '../file-info/fs-path'; import getFs, { useVirtualFs } from './getFs'; import '../test/expect.extensions'; import { EOL } from 'os'; @@ -262,4 +262,36 @@ describe('Virtual Fs', () => { expect(fs.relativeTo('/project', path)).toBe(solution); }); } + + describe('readDirectory', () => { + beforeEach(() => fs.reset()); + + it('should list files in directory', () => { + fs.writeFile('/project/index.ts', ''); + fs.writeFile('/project/main.ts', ''); + fs.writeFile('/project/sub/foobar.ts', ''); + fs.writeFile('/project/foobar.ts', ''); + const files = fs.readDirectory(toFsPath('/project')); + + expect(files).toEqual( + ['index.ts', 'main.ts', 'sub', 'foobar.ts'].map((f) => `/project/${f}`), + ); + }); + + it('should throw if directory does not exist', () => { + expect(() => fs.readDirectory('/projects' as FsPath)).toThrowError( + 'directory /projects does not exist', + ); + }); + + it('should only return directories', () => { + fs.writeFile('/project/index.ts', ''); + fs.writeFile('/project/main.ts', ''); + fs.writeFile('/project/sub/foobar.ts', ''); + fs.writeFile('/project/foobar.ts', ''); + const files = fs.readDirectory(toFsPath('/project'), 'directory'); + + expect(files).toEqual(['/project/sub']); + }); + }); }); diff --git a/packages/core/src/lib/fs/virtual-fs.ts b/packages/core/src/lib/fs/virtual-fs.ts index cab1bce..3f72729 100644 --- a/packages/core/src/lib/fs/virtual-fs.ts +++ b/packages/core/src/lib/fs/virtual-fs.ts @@ -58,6 +58,20 @@ export class VirtualFs extends Fs { this.root.children.set('project', this.project); } + override readDirectory( + path: FsPath, + filter: 'none' | 'directory' = 'none', + ): FsPath[] { + const result = this.#getNode(path); + if (!result.exists) { + throw new Error(`directory ${path} does not exist`); + } + + return Array.from(result.node.children.values()) + .filter((node) => (filter === 'none' ? true : node.type === 'directory')) + .map((node) => this.#absolutePath(node)); + } + findFiles = (path: FsPath, filename: string): FsPath[] => { const result = this.#getNode(path); if (!result.exists) { @@ -288,8 +302,8 @@ export class VirtualFs extends Fs { } override isFile(path: FsPath): boolean { - const node = this.#getNodeOrThrow(path); - return node.node.type === 'file' + const node = this.#getNodeOrThrow(path); + return node.node.type === 'file'; } } diff --git a/packages/core/src/lib/main/parse-project.ts b/packages/core/src/lib/main/parse-project.ts index e3c77fa..40146d5 100644 --- a/packages/core/src/lib/main/parse-project.ts +++ b/packages/core/src/lib/main/parse-project.ts @@ -40,9 +40,8 @@ export const parseProject = ( const modulePaths = findModulePaths( projectDirs, - config.tagging, - config.barrelFileName, - config.enableBarrelLess, + rootDir, + config ); const modules = createModules( unassignedFileInfo, diff --git a/packages/core/src/lib/modules/find-module-paths.ts b/packages/core/src/lib/modules/find-module-paths.ts index 83cfc1a..7ff2da7 100644 --- a/packages/core/src/lib/modules/find-module-paths.ts +++ b/packages/core/src/lib/modules/find-module-paths.ts @@ -1,9 +1,9 @@ import { FsPath } from '../file-info/fs-path'; -import { findModulePathsWithoutBarrel } from "./internal/find-module-paths-without-barrel"; -import { TagConfig } from "../config/tag-config"; -import { findModulePathsWithBarrel } from "./internal/find-module-paths-with-barrel"; +import { findModulePathsWithBarrel } from './internal/find-module-paths-with-barrel'; +import { findModulePathsWithoutBarrel } from './internal/find-module-paths-without-barrel'; +import { SheriffConfig } from '../config/sheriff-config'; -export type ModulePathMap = Record +export type ModulePathMap = Record; /** * Find module paths which can be defined via having a barrel file or the @@ -11,9 +11,23 @@ export type ModulePathMap = Record * * 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, enableBarrelLess: boolean): ModulePathMap { - const modulesWithoutBarrel = enableBarrelLess ? findModulePathsWithoutBarrel(projectDirs, moduleConfig) : []; - const modulesWithBarrel = findModulePathsWithBarrel(projectDirs, barrelFileName); +export function findModulePaths( + projectDirs: FsPath[], + rootDir: FsPath, + sheriffConfig: SheriffConfig, +): ModulePathMap { + const { + tagging, + enableBarrelLess, + barrelFileName, + } = sheriffConfig; + const modulesWithoutBarrel = enableBarrelLess + ? findModulePathsWithoutBarrel(tagging, rootDir, barrelFileName) + : []; + const modulesWithBarrel = findModulePathsWithBarrel( + projectDirs, + barrelFileName, + ); const modulePaths: ModulePathMap = {}; for (const path of modulesWithoutBarrel) { diff --git a/packages/core/src/lib/modules/internal/create-module-path-patterns-tree.ts b/packages/core/src/lib/modules/internal/create-module-path-patterns-tree.ts new file mode 100644 index 0000000..31df52f --- /dev/null +++ b/packages/core/src/lib/modules/internal/create-module-path-patterns-tree.ts @@ -0,0 +1,58 @@ +export type ModulePathPatternsTree = { + [basePath: string]: ModulePathPatternsTree; +}; + +/** + * Create a tree structure from a list of module path patterns. + * + * Having a tree structure improves the performance because shared + * parent directories only have to be read once. + * + * For example, given the following patterns: + * ```typescript + * ['src/app/feat-*-module/*', 'src/app/services/*', 'src/app/shared/*'] + * ``` + * + * would end up in the following tree: + * ```typescript + * { + * src: { + * app: { + * feat-*-module: {}, + * services: {}, + * shared: {} + * } + * } + * } + * ``` + */ +export function createModulePathPatternsTree( + patterns: string[], +): ModulePathPatternsTree { + const flatTree: Record = {}; + + for (const pattern of patterns) { + const parts = pattern.split('/'); + const basePath = parts[0]; // Get the top-level directory (e.g., "src") + + const restOfPattern = parts.slice(1).join('/'); // Remove the top-level part + + if (!flatTree[basePath]) { + flatTree[basePath] = []; + } + + flatTree[basePath].push(restOfPattern || ''); + } + + // group next subdirectories + const tree: ModulePathPatternsTree = {}; + for (const basePath in flatTree) { + const subPatterns = flatTree[basePath]; + if (subPatterns.length === 1 && subPatterns[0] === '') { + tree[basePath] = {}; + } else { + tree[basePath] = createModulePathPatternsTree(subPatterns); + } + } + return tree; +} 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 index c6ee925..6fa20c8 100644 --- 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 @@ -1,16 +1,101 @@ -import { TagConfig } from "../../config/tag-config"; -import { FsPath } from "../../file-info/fs-path"; -import { flattenTagging } from "./flatten-tagging"; +import { TagConfig } from '../../config/tag-config'; +import { FsPath } from '../../file-info/fs-path'; +import { + createModulePathPatternsTree, + ModulePathPatternsTree, +} from './create-module-path-patterns-tree'; +import getFs from '../../fs/getFs'; +import { FOLDER_CHARACTERS_REGEX_STRING } from '../../tags/calc-tags-for-module'; +import { flattenTagging } from './flatten-tagging'; /** * The current criterion for finding modules is via - * the SheriffConfig's property `tagging`. + * the SheriffConfig's property `modules`. * - * We iterate through projectPaths, traverse them - * and find modules assigned by tags. + * We will traverse the filesystem and match directories + * against the patterns. */ -export function findModulePathsWithoutBarrel(projectDirs: string[], moduleConfig: TagConfig): Set { +export function findModulePathsWithoutBarrel( + moduleConfig: TagConfig, + rootDir: FsPath, + barrelFileName: string +): Set { const paths = flattenTagging(moduleConfig, ''); + const modulePathsPatternTree = createModulePathPatternsTree(paths); + const modules = traverseAndMatch(modulePathsPatternTree, rootDir, barrelFileName); + return new Set(modules); +} + +/** + * Recursively traverse the filesystem and match directories against patterns. + */ +function traverseAndMatch( + groupedPatterns: ModulePathPatternsTree, + basePath: FsPath, + barrelFileName: string +): FsPath[] { + const fs = getFs(); + const matchedDirectories: FsPath[] = []; + + // Check if the current directory should be matched + if ('' in groupedPatterns) { + addAsModuleIfWithoutBarrel(matchedDirectories, basePath, barrelFileName); + } + + const subDirectories = fs.readDirectory(basePath, 'directory'); + for (const subDirectory of subDirectories) { + const currentSegment = fs.relativeTo(basePath, subDirectory); + + const patterns = Object.keys(groupedPatterns); + const matchingPattern = patterns.find((pattern) => + matchPattern(pattern, currentSegment), + ); + + if (matchingPattern) { + if (Object.keys(groupedPatterns[matchingPattern]).length === 0) { + addAsModuleIfWithoutBarrel(matchedDirectories, subDirectory, barrelFileName); + } else { + const newDirectories = traverseAndMatch(groupedPatterns[matchingPattern], subDirectory, barrelFileName); + for (const newDirectory of newDirectories) { + addAsModuleIfWithoutBarrel(matchedDirectories, newDirectory, barrelFileName); + } + } + } + } + + return matchedDirectories; +} + +/** + * Matches a given directory path against a pattern, allowing wildcards. + */ +function matchPattern(pattern: string, pathSegment: string): boolean { + if (pattern === '*' || pattern === pathSegment) { + return true; + } + + if (pattern.includes('*')) { + const regexPattern = pattern.replace( + /\*/g, + `${FOLDER_CHARACTERS_REGEX_STRING}*`, + ); + const regex = new RegExp(`^${regexPattern}$`); + return regex.test(pathSegment); + } + + return false; +} + +function addAsModuleIfWithoutBarrel( + modulePaths: FsPath[], + directory: FsPath, + barrelFileName: string, +) { + const fs = getFs(); + + if (fs.exists(fs.join(directory, barrelFileName))) { + return; + } - return new Set(); + modulePaths.push(directory); } diff --git a/packages/core/src/lib/modules/tests/create-module-path-patterns-tree.spec.ts b/packages/core/src/lib/modules/tests/create-module-path-patterns-tree.spec.ts new file mode 100644 index 0000000..a833def --- /dev/null +++ b/packages/core/src/lib/modules/tests/create-module-path-patterns-tree.spec.ts @@ -0,0 +1,68 @@ +import { createModulePathPatternsTree } from '../internal/create-module-path-patterns-tree'; +import { describe, expect, it } from 'vitest'; + +describe('createModulePathPatternsTree', () => { + it('should group modules with same base path', () => { + const patterns = ['src/app/feat-*-module/*', 'src/app/services/*', 'src/app/customers']; + + const tree = createModulePathPatternsTree(patterns); + + expect(tree).toEqual({ + src: { + app: { + 'feat-*-module': { + '*': {}, + }, + services: { + '*': {}, + }, + customers: {} + }, + }, + }); + }); + + it('should group modules with multiple base paths', () => { + const patterns = [ + 'src/app/feat-*-module/*', + 'src/app/services', + 'src/lib/util', + 'src/assets/images', + ]; + + const tree = createModulePathPatternsTree(patterns); + expect(tree).toEqual({ + src: { + app: { + 'feat-*-module': {'*': {}}, + services: {}, + }, + lib: { + util: { + }, + }, + assets: { + images: {}, + }, + }, + }); + }); + + it('should group patterns for nested modules', () => { + const patterns = ['src/lib', 'src/lib/util', 'src/lib/util/*']; + + const tree = createModulePathPatternsTree(patterns); + + expect(tree).toEqual({ + src: { + lib: { + '': {}, + util: { + '': {}, + '*': {}, + }, + }, + }, + }); + }); +}); 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 index 290c370..558526a 100644 --- 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 @@ -1,73 +1,212 @@ -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"; +import { describe, it, expect, beforeEach } 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'; +import { useVirtualFs } from '../../fs/getFs'; +import { toFsPath } from '../../file-info/fs-path'; -function assertModulePaths( - fileTree: FileTree, - moduleConfig: TagConfig, - modulePaths: string[], -) { - createProject(fileTree); - const actualModulePaths = findModulePathsWithoutBarrel([ - '/project', - ], moduleConfig); - expect(Array.from(actualModulePaths)).toEqual(modulePaths); +function assertProject(fileTree: FileTree) { + return { + withModuleConfig(moduleConfig: TagConfig) { + return { + hasModulePaths(modulePaths: string[]) { + const absoluteModulePaths = modulePaths.map( + (path) => `/project/${path}`, + ); + createProject(fileTree); + const actualModulePaths = findModulePathsWithoutBarrel( + moduleConfig, + toFsPath('/project'), + 'index.ts', + ); + expect(Array.from(actualModulePaths)).toEqual(absoluteModulePaths); + }, + }; + }, + }; } -describe.skip('create module infos without barrel files', () => { +describe('create module infos without barrel files', () => { + beforeEach(() => useVirtualFs().reset()); + 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': [], - }, + assertProject({ + 'src/app': { + 'app.component.ts': [ + 'customers/customer.component.ts', + 'holidays/holiday.component.ts', + ], + 'customers/customer.component.ts': [], + 'holidays/holiday.component.ts': [], }, - {}, - [], - ); + }) + .withModuleConfig({}) + .hasModulePaths([]); }); it('should have modules', () => { - assertModulePaths( - { - 'src/app': { - 'app.component.ts': [], - 'domains/customers/customer.component.ts': [], - 'domains/holidays/holiday.component.ts': [], - 'shared/index.ts': [] - }, + assertProject({ + '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'], - ); + }) + .withModuleConfig({ 'src/app/domains/': 'domain:' }) + .hasModulePaths([ + 'src/app/domains/customers', + 'src/app/domains/holidays', + ]); }); 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': [], + assertProject({ + 'src/app': { + 'app.component.ts': [ + 'customers/customer.component.ts', + 'holidays/holiday.component.ts', + ], + 'customers/customer.component.ts': [], + 'holidays/holiday.component.ts': [], + }, + }) + .withModuleConfig({ 'src/app/': 'domain:' }) + .hasModulePaths(['src/app/customers', 'src/app/holidays']); + }); + + it('should allow nested modules', () => { + assertProject({ + src: { + lib: { + util: { + util1: {}, + util2: {}, + }, + }, + }, + }) + .withModuleConfig({ + 'src/lib': 'lib', + 'src/lib/util': 'util', + 'src/lib/util/': 'util:', + }) + .hasModulePaths([ + 'src/lib', + 'src/lib/util', + 'src/lib/util/util1', + 'src/lib/util/util2', + ]); + }); + + it('should work for multiple projectPaths', () => { + assertProject({ + src: { + app: { + app1: { + feature: {}, + model: {}, + }, + app2: { + ui: {}, + }, + }, + lib: { + shared: {}, + }, + }, + }) + .withModuleConfig({ + 'src/app//': ['app:', 'type:'], + 'src/lib/shared': 'shared', + }) + .hasModulePaths([ + 'src/app/app1/feature', + 'src/app/app1/model', + 'src/app/app2/ui', + 'src/lib/shared', + ]); + }); + + it('should also detect files with multiple placeholders in the same directory', () => + assertProject({ + src: { + app: { + 'feature-shop': {}, + 'ui-grid': {}, + 'data-': {}, + model: {}, + }, + }, + }) + .withModuleConfig({ + 'src/app/-': ['type:', 'name:'], + }) + .hasModulePaths([ + 'src/app/feature-shop', + 'src/app/ui-grid', + 'src/app/data-', + ])); + it('should ignore barrel module because findModulePathsWithBarrel handles it', () => { + assertProject({ + src: { + app: { + customers: { + 'index.ts': [], + feature: {}, + ui: { + 'index.ts': [], + }, + data: {}, + model: {}, + }, + }, + }, + }) + .withModuleConfig({ + 'src/app/': 'lib', + 'src/app//': ['domain:', 'type:'], + }) + .hasModulePaths([ + 'src/app/customers/feature', + 'src/app/customers/data', + 'src/app/customers/model', + ]); + }); + + + it('should not throw if a module does not match any directory (might be barrel module)', () => { + assertProject({ + src: { + app: { + customers: { + 'index.ts': [], + internal: {}, + }, }, }, - { 'src/app/': 'domain:' }, - ['/project/src/app/customers', '/project/src/app/holidays', '/project'], - ); + }) + .withModuleConfig({ + 'src/app/': 'lib', + }) + .hasModulePaths([]); }); - it.todo('should allow nested modules'); - it.todo('should work for multipe projectPaths'); - it.todo('should give priority to to the barrel file'); + it('should stop after a match', () => { + assertProject({ + src: { + app: { + customers: { + }, + }, + }, + }) + .withModuleConfig({ + 'src/app/': 'lib', + 'src/app/customers': 'lib', + }) + .hasModulePaths(['src/app/customers']); + }); }); 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 a45d6d5..8bd2a76 100644 --- a/packages/core/src/lib/tags/calc-tags-for-module.ts +++ b/packages/core/src/lib/tags/calc-tags-for-module.ts @@ -12,6 +12,7 @@ import { TagWithoutValueError, } from '../error/user-error'; +export const FOLDER_CHARACTERS_REGEX_STRING = '[a-zA-Z-_]'; export const PLACE_HOLDER_REGEX = /<[a-zA-Z-_]+>/g; export const calcTagsForModule = ( diff --git a/packages/core/src/lib/test/project-creator.ts b/packages/core/src/lib/test/project-creator.ts index 2a31003..0e25bdb 100644 --- a/packages/core/src/lib/test/project-creator.ts +++ b/packages/core/src/lib/test/project-creator.ts @@ -11,8 +11,7 @@ export function createProject( fileTree: FileTree, testDirName = '/project', ): Fs { - useVirtualFs(); - const fs = getFs(); + const fs = useVirtualFs(); fs.reset(); new ProjectCreator().create(fileTree, testDirName);