Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9b5df0e
feat(core): support multiple entry points
michaelbe812 Jul 1, 2025
94b8c9b
chore: add test project for multi-project setup
michaelbe812 Jul 1, 2025
4ddc7a5
docs(core): update CLI docs with information about entryPoints
michaelbe812 Jul 2, 2025
ee42dc3
refactor(core): remove unused import
michaelbe812 Jul 2, 2025
a4fe7b6
refactor(core): move entry type
michaelbe812 Jul 2, 2025
a327ce8
feat(core): validate correct entry settings
michaelbe812 Jul 3, 2025
0c3ee0c
chore(core): fix spacing
michaelbe812 Jul 3, 2025
3a0f337
fix(core): re-add entryPoints to Configuration
michaelbe812 Jul 3, 2025
a43892f
chore(core): surpass eslint issue
michaelbe812 Jul 3, 2025
25e75df
refactor(test-projects): remove empty style sheets
michaelbe812 Jul 3, 2025
fbfbcf5
refactor(core): remove type assertions
michaelbe812 Jul 3, 2025
cad96ec
refactor(core): correct type-narrowing for isEmptyRecord
michaelbe812 Jul 7, 2025
fc61f44
refactor: revert removal of get-entry-from-cli-or-config.ts
rainerhahnekamp Jul 13, 2025
a9c3c21
refactor: add content for entries get-entry-from-cli-or-config.ts
rainerhahnekamp Jul 13, 2025
bb68059
refactor: rename to get-entries-*
rainerhahnekamp Jul 13, 2025
ec19c06
fix(core): add line break before printing next project
michaelbe812 Jul 23, 2025
d04c34d
chore(core): add unit test for testing verify against single entryPoint
michaelbe812 Jul 23, 2025
c8af621
refactor(docs): rework CLI docs after introducing entryPoints
michaelbe812 Jul 24, 2025
058b273
feat(core): export-data can now work against entryPoints
michaelbe812 Jul 24, 2025
49d2c47
fix(core): parseEntryPointsFromCli handles now correctly whitespace b…
michaelbe812 Jul 24, 2025
138c1e0
refactor(core): move projectName to getProjectData options
michaelbe812 Jul 25, 2025
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
79 changes: 70 additions & 9 deletions docs/docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,81 @@ The core package (@softarc/sheriff-core) comes with a CLI to initialize the conf

Run `npx sheriff init` to create a `sheriff.config.ts`. Its configuration runs with [automatic tagging](./dependency-rules#automatic-tagging), meaning no dependency rules are in place, and it only checks for the module boundaries.

## `verify [main.ts]`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we only had entryFile, it felt acceptable to briefly mention it in a sentence at the end.

Now, with the introduction of entryPoints, the situation has become a bit more complex. I think it would be better to dedicate a separate chapter at the end of the page that explains both entryFile and entryPoints. This would allow us to elaborate on their usage and provide examples.

We could then also keep the verify, list, and export sections more concise by simply referring to this new chapter.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

## `verify`

Run `npx sheriff verify main.ts` to check if your project violates any of your rules. `main.ts` is the entry file where Sheriff should traverse the imports.
Run `npx sheriff verify` to check if your project violates any of your rules. See [Entry Files and Entry Points](#entry-files-and-entry-points) for configuration options.

Depending on your project, you will likely have a different entry file. For example, with an Angular CLI-based project, it would be `npx sheriff verify src/main.ts`.
## `list`

You can omit the entry file if you set a value to the property `entryFile` in the `sheriff.config.ts`.
Run `npx sheriff list` to print out all your modules along their tags. See [Entry Files and Entry Points](#entry-files-and-entry-points) for configuration options.

In that case, you only run `npx sheriff verify`.
## `export`

## `list [main.ts]`
Run `npx sheriff export > export.json` to export the dependency graph in JSON format. The dependency graph includes all reachable files. For every file, it will include the assigned module as well as the tags. See [Entry Files and Entry Points](#entry-files-and-entry-points) for configuration options.

Run `npx sheriff list main.ts` to print out all your modules along their tags. As explained above, you can alternatively use the `entryFile` property in `sheriff.config.ts`.
## Entry Files and Entry Points

## `export [main.ts]`
Sheriff needs to know where to start traversing your project's imports. You can specify this using either an `entryFile` **or** `entryPoints`.

Run `npx sheriff export main.ts > export.json` and the dependency graph will be stored in `export.json` in JSON format. The dependency graph starts from the entry file and includes all reachable files. For every file, it will include the assigned module as well as the tags.
### Entry File

An entry file is a single file that serves as the starting point for Sheriff's analysis. It's typically your application's main entry point.

Depending on your project, you will likely have a different entry file. For example, with an Angular CLI-based project it would be `src/main.ts`.

**Usage with CLI:**
```bash
npx sheriff verify main.ts
npx sheriff list src/main.ts
npx sheriff export src/main.ts > export.json
```

**Usage with configuration:**
You can set the `entryFile` property in `sheriff.config.ts`:
```typescript
export const config: SheriffConfig = {
entryFile: './src/main.ts',
// ... other configuration
};
```

When `entryFile` is set in the configuration, you can omit it from the CLI commands:
```bash
npx sheriff verify
npx sheriff list
npx sheriff export > export.json
```

### Entry Points

Entry points allow you to specify multiple named entry files, useful for workspaces with multiple applications.

**Configuration:**
Define `entryPoints` in `sheriff.config.ts`:
```typescript
export const config: SheriffConfig = {
entryPoints: {
'app-web': './apps/web/src/main.ts',
'app-mobile': './apps/mobile/src/main.ts',
'app-admin': './apps/admin/src/main.ts'
},
// ... other configuration
};
```

**Usage with CLI:**
```bash
# Check specific entry points
npx sheriff verify app-web,app-mobile
npx sheriff list app-admin
npx sheriff export app-web,app-mobile,app-admin > export.json

# If only one entry point is defined, you can omit it
npx sheriff verify
```

### Priority

When both `entryFile` and `entryPoints` are specified in the configuration:
- CLI arguments take precedence over configuration
- If no CLI argument is provided, `entryFile` takes precedence over `entryPoints`
10 changes: 9 additions & 1 deletion packages/core/src/lib/api/get-project-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type ProjectDataEntry = {
imports: string[];
externalLibraries?: string[];
unresolvedImports: string[];
projectName: string;
};

/**
Expand All @@ -22,6 +23,11 @@ export type Options = {
* that contains the external libraries, i.e. node_modules.
*/
includeExternalLibraries?: boolean;
/**
* Adds a property `projectName` to each entry
* that contains the name of the project.
*/
projectName?: string;
};
export type ProjectData = Record<string, ProjectDataEntry>;

Expand Down Expand Up @@ -125,7 +131,7 @@ export function getProjectData(
cwdOrOptions === undefined
? entryFile
: typeof cwdOrOptions === 'string'
? fs.join(cwdOrOptions, entryFile)
? fs.join(entryFile)
: entryFile;

const cwd = typeof cwdOrOptions === 'string' ? cwdOrOptions : undefined;
Expand All @@ -147,6 +153,7 @@ export function getProjectData(
tags: calcOrGetTags(fileInfo.moduleInfo.path, projectInfo, tagsCache),
imports: fileInfo.imports.map((fileInfo) => fileInfo.path),
unresolvedImports: fileInfo.unresolvableImports,
projectName: options.projectName ?? '',
};

if (options.includeExternalLibraries) {
Expand Down Expand Up @@ -183,6 +190,7 @@ function relativizeIfRequired(
relative(toFsPath(importPath)),
),
unresolvedImports: moduleData.unresolvedImports,
projectName: moduleData.projectName,
};

if (options.includeExternalLibraries) {
Expand Down
15 changes: 9 additions & 6 deletions packages/core/src/lib/cli/export-data.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { getEntryFromCliOrConfig } from './internal/get-entries-from-cli-or-config';
import { getEntriesFromCliOrConfig } from './internal/get-entries-from-cli-or-config';
import { cli } from './cli';
import { getProjectData } from '../api/get-project-data';
import getFs from '../fs/getFs';

export function exportData(...args: string[]): void {
const fs = getFs();
const entryFile = getEntryFromCliOrConfig(args[0], false);
const projectEntries = getEntriesFromCliOrConfig(args[0], true);

const data = getProjectData(entryFile, fs.cwd(), {
includeExternalLibraries: true,
});
cli.log(JSON.stringify(data, null, ' '));
for (const entry of projectEntries) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to adopt that one as well.

How do we want to do it? There are some tools which use this function to load data. So we have to be careful.

I see two options:

  1. Every file gets new property called project with the project's name. In that sense, we would just have an additional property which will hopefully not break anything.
  2. We create a nested json, where all the entries get the project's name as their parent property.

What is your opinion on that?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess option 1 will be the more "friendly" option as it will/should not break anything as you already pointed out.
So I would go with this approach.

Option 2 we could keep in mind for a future major version and/or as alternative format for exportData

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rainerhahnekamp
As far as I checked everything correctly we will need to pass either Entry to getProjectData instead of the entryPath or add another argument projectName.

Nevertheless this would introduce a breaking change because getProjectData is exported from the main index.ts.

How should be handle this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if getProjectData should be aware of entryPoints vs entryFile. In the end this is really an API and not a CLI command.

@michaelbe812 michaelbe812 Jul 3, 2025

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I don't know what to do here.

For introducing the new property projectName we must be aware of the entryPoints because the key is the projectName, right?
Also what to do in case of entryFile - there we don't actually have a projectName from my understanding.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right. Sorry for the confusion.

We need to have the possibility to pass on the projectName as well (if there are more projects involved).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

const data = getProjectData(entry.entry.fileInfo.path, fs.cwd(), {
includeExternalLibraries: true,
projectName: entry.projectName,
});
cli.log(JSON.stringify(data, null, ' '));
}
}
4 changes: 4 additions & 0 deletions packages/core/src/lib/cli/internal/entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type Entry<TEntry> = {
projectName: string;
entry: TEntry;
};
101 changes: 92 additions & 9 deletions packages/core/src/lib/cli/internal/get-entries-from-cli-or-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,110 @@ import getFs from '../../fs/getFs';
import { init, ProjectInfo } from '../../main/init';
import { parseConfig } from '../../config/parse-config';
import { toFsPath } from '../../file-info/fs-path';
import { isEmptyRecord } from '../../util/is-empty-record';
import { parseEntryPointsFromCli } from './parse-entry-points-from-cli';
import { Entry } from './entry';

export function getEntryFromCliOrConfig(entryFile?: string): ProjectInfo;
export function getEntryFromCliOrConfig(entryFile?: string, runInit?: boolean): string;
export const DEFAULT_PROJECT_NAME = 'default';

export function getEntryFromCliOrConfig(entryFile = '', runInit = true): ProjectInfo | string {
export function getEntriesFromCliOrConfig(
entryFileOrEntryPoints?: string,
): Array<Entry<ProjectInfo>>;
export function getEntriesFromCliOrConfig(
entryFileOrEntryPoints?: string,
runInit?: true,
Comment thread
rainerhahnekamp marked this conversation as resolved.
): Array<Entry<ProjectInfo>>;
export function getEntriesFromCliOrConfig(
entryFileOrEntryPoints?: string,
runInit?: false,
Comment thread
michaelbe812 marked this conversation as resolved.
): Array<Entry<string>>;
export function getEntriesFromCliOrConfig(
/**
* the CLI forwards either the entry file e.g. "src/main.ts" or
* the entry point(s) e.g. app-i,app-ii
*/
entryFileOrEntryPoints = '',
runInit = true,
): Array<Entry<string>> | Array<Entry<ProjectInfo>> {
const fs = getFs();
if (entryFile) {
return runInit ? init(toFsPath(fs.join(fs.cwd(), entryFile))) : entryFile;
const potentialConfigFile = fs.join(fs.cwd(), 'sheriff.config.ts');

/**
* CLI argument given
*/
if (entryFileOrEntryPoints) {
// CLI argument given and no config file is present -> only entry file can work
if (!fs.exists(potentialConfigFile)) {
return processEntryFile(entryFileOrEntryPoints, runInit, fs);
}

if (fs.exists(potentialConfigFile)) {
// two cases to check: check for entry points otherwise it is an entry file
const sheriffConfig = parseConfig(potentialConfigFile);

const potentialEntryPoints = parseEntryPointsFromCli(
entryFileOrEntryPoints,
sheriffConfig,
);

if (potentialEntryPoints) {
// if entry points are given, return them
return processEntryFile(potentialEntryPoints, runInit, fs);
} else {
// otherwise it is an entry file
return processEntryFile(entryFileOrEntryPoints, runInit, fs);
}
}
}

const potentialConfigFile = fs.join(fs.cwd(), 'sheriff.config.ts');
if (fs.exists(potentialConfigFile)) {
const sheriffConfig = parseConfig(potentialConfigFile);

if (sheriffConfig.entryFile) {
return runInit ? init(toFsPath(fs.join(fs.cwd(), sheriffConfig.entryFile))) : sheriffConfig.entryFile;
return processEntryFile(sheriffConfig.entryFile, runInit, fs);
} else if (
sheriffConfig.entryPoints &&
!isEmptyRecord(sheriffConfig.entryPoints)
) {
return processEntryFile(sheriffConfig.entryPoints, runInit, fs);
} else {
throw new Error(
'No entry file found in sheriff.config.ts. Please provide one via the CLI ',
'No entry file or entry points found in sheriff.config.ts. Please provide the option via the CLI.',
);
}
}

throw new Error('Please provide an entry file, e.g. main.ts');
throw new Error(
'Please provide an entry file (e.g. main.ts) or entry points (e.g. { projectName: "main.ts" })',
);
}

// Helper function to process entry file consistently
function processEntryFile(
entryFileValue: string | Record<string, string>,
runInit: boolean,
fs: ReturnType<typeof getFs>,
): Array<Entry<ProjectInfo>> | Array<Entry<string>> {
if (typeof entryFileValue === 'string') {
return runInit
? [
{
projectName: DEFAULT_PROJECT_NAME,
entry: init(toFsPath(fs.join(fs.cwd(), entryFileValue))),
},
]
: [{ projectName: DEFAULT_PROJECT_NAME, entry: entryFileValue }];
} else {
const entries = Object.entries(entryFileValue);

return runInit
? (entries.map(([projectName, entry]) => ({
projectName,
entry: init(toFsPath(fs.join(fs.cwd(), entry))),
})) as Array<Entry<ProjectInfo>>)
: (entries.map(([projectName, entry]) => ({
projectName,
entry: entry,
})) as Array<Entry<string>>);
}
}
36 changes: 36 additions & 0 deletions packages/core/src/lib/cli/internal/parse-entry-points-from-cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Configuration } from '../../config/configuration';
import { isEmptyRecord } from '../../util/is-empty-record';

export function parseEntryPointsFromCli(
entryFileOrEntryPoints: string,
sheriffConfig: Configuration,
): Record<string, string> | undefined {
const entryPointsFromConfig = sheriffConfig.entryPoints;
if (entryFileOrEntryPoints.includes(',')) {
if (!entryPointsFromConfig || isEmptyRecord(entryPointsFromConfig)) {
return undefined;
}
const splittedEntries = entryFileOrEntryPoints.split(',');
const entryPoints: Record<string, string> = {};

for (const entry of splittedEntries) {
const trimmedEntry = entry.trim();
const entryPoint = entryPointsFromConfig[trimmedEntry];
if (entryPoint) {
entryPoints[trimmedEntry] = entryPoint;
}
}
return entryPoints;
}

// If no comma is found, it could be a single entry point
if (
entryPointsFromConfig &&
entryFileOrEntryPoints in entryPointsFromConfig
) {
const singleEntryPoint = entryPointsFromConfig[entryFileOrEntryPoints];
return { [entryFileOrEntryPoints]: singleEntryPoint };
}

return undefined;
}
50 changes: 32 additions & 18 deletions packages/core/src/lib/cli/list.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
import { FsPath, toFsPath } from '../file-info/fs-path';
import { ProjectInfo } from '../main/init';
import { calcTagsForModule } from '../tags/calc-tags-for-module';
import { getEntryFromCliOrConfig } from './internal/get-entries-from-cli-or-config';
import {
DEFAULT_PROJECT_NAME,
getEntriesFromCliOrConfig,
} from './internal/get-entries-from-cli-or-config';
import getFs from '../fs/getFs';
import { cli } from './cli';
import { logInfoForMissingSheriffConfig } from './internal/log-info-for-missing-sheriff-config';

export function list(args: string[]) {
Comment thread
rainerhahnekamp marked this conversation as resolved.
const projectInfo = getEntryFromCliOrConfig(args[0]);
logInfoForMissingSheriffConfig(projectInfo);

// root doesn't count
const modulesCount = projectInfo.modules.length - 1;
const projectEntries = getEntriesFromCliOrConfig(args[0]);
if (projectEntries.length > 0) {
logInfoForMissingSheriffConfig(projectEntries[0].entry);
}

cli.log(`This project contains ${modulesCount} modules:`);
cli.log('');
for (const [i, projectEntry] of projectEntries.entries()) {
// root doesn't count
const modulesCount = projectEntry.entry.modules.length - 1;
const projectName = projectEntry.projectName;
if (projectName !== DEFAULT_PROJECT_NAME) {
if (i > 0) {
cli.log('');
}
cli.log(cli.bold(`Project: ${projectName}`));
cli.log('');
}
cli.log(`This project contains ${modulesCount} modules:`);
cli.log('');

cli.log('. (root)');
const directory = mapModulesToDirectory(
Array.from(
projectInfo.modules
.filter((module) => !module.isRoot)
.map((module) => toFsPath(module.path)),
),
projectInfo,
);
printDirectory(directory);
cli.log('. (root)');
const directory = mapModulesToDirectory(
Array.from(
projectEntry.entry.modules
.filter((module) => !module.isRoot)
.map((module) => toFsPath(module.path)),
),
projectEntry.entry,
);
printDirectory(directory);
}
}

type Directory = Record<
Expand Down
Loading
Loading