Skip to content

Commit

Permalink
feat(core): add finder method for barrel-less modules
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rainerhahnekamp authored Oct 8, 2024
1 parent 05eeb71 commit 10cd402
Show file tree
Hide file tree
Showing 18 changed files with 559 additions and 95 deletions.
1 change: 1 addition & 0 deletions packages/core/src/lib/config/default-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const defaultConfig: SheriffConfig = {
depRules: {},
excludeRoot: false,
enableBarrelLess: false,
encapsulatedFolderNameForBarrelLess: 'internal',
log: false,
entryFile: '',
isConfigFileMissing: false,
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/lib/config/sheriff-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ import { UserSheriffConfig } from './user-sheriff-config';
export type SheriffConfig = Required<UserSheriffConfig> & {
// dependency rules will skip if `isConfigFileMissing` is true
isConfigFileMissing: boolean;
barrelFileName: string;
};
2 changes: 2 additions & 0 deletions packages/core/src/lib/config/tests/parse-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('parse Config', () => {
'depRules',
'excludeRoot',
'enableBarrelLess',
"encapsulatedFolderNameForBarrelLess",
'log',
'entryFile',
'isConfigFileMissing',
Expand Down Expand Up @@ -67,6 +68,7 @@ export const config: SheriffConfig = {
tagging: {},
depRules: { noTag: 'noTag' },
enableBarrelLess: false,
encapsulatedFolderNameForBarrelLess: 'internal',
excludeRoot: false,
log: false,
isConfigFileMissing: false,
Expand Down
18 changes: 17 additions & 1 deletion packages/core/src/lib/config/user-sheriff-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
*/
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/lib/fs/default-fs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
);
});
});
14 changes: 14 additions & 0 deletions packages/core/src/lib/fs/default-fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
};
Expand Down
30 changes: 17 additions & 13 deletions packages/core/src/lib/fs/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,42 @@ 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
* parent folder and includes the directory of the referenceFile.
* @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;

Expand Down
11 changes: 9 additions & 2 deletions packages/core/src/lib/fs/getFs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
34 changes: 33 additions & 1 deletion packages/core/src/lib/fs/virtual-fs.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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']);
});
});
});
18 changes: 16 additions & 2 deletions packages/core/src/lib/fs/virtual-fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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';
}
}

Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/lib/main/parse-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ export const parseProject = (

const modulePaths = findModulePaths(
projectDirs,
config.tagging,
config.barrelFileName,
config.enableBarrelLess,
rootDir,
config
);
const modules = createModules(
unassignedFileInfo,
Expand Down
28 changes: 21 additions & 7 deletions packages/core/src/lib/modules/find-module-paths.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
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<FsPath, boolean>
export type ModulePathMap = Record<FsPath, boolean>;

/**
* 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, 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, string[]> = {};

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;
}
Loading

0 comments on commit 10cd402

Please sign in to comment.