Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add first prototype #146

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading