Skip to content

Commit

Permalink
feat: DEBUG flag and project.json validation (#31)
Browse files Browse the repository at this point in the history
* feat: support verbose logging

* feat(nx): project.json runtime validation & verbose

* use DEBUG env var instead of verbose mode
  • Loading branch information
EladBezalel authored Jun 11, 2024
1 parent 9a218d0 commit a813e6c
Show file tree
Hide file tree
Showing 7 changed files with 470 additions and 39 deletions.
74 changes: 74 additions & 0 deletions libs/core/src/true-affected.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import { trueAffected } from './true-affected';
import * as git from './git';
import * as lockFiles from './lock-files';

jest.mock('chalk', () => ({
default: {
bold: (str: string) => str,
},
}));

describe('trueAffected', () => {
const cwd = 'libs/core/src/__fixtures__/monorepo';

Expand Down Expand Up @@ -406,4 +412,72 @@ describe('trueAffected', () => {

expect(affected).toEqual(expected);
});

it('should log the progress', async () => {
const changedFiles = [
{
filePath: 'proj1/index.ts',
changedLines: [2],
},
];
jest.spyOn(git, 'getChangedFiles').mockReturnValue(changedFiles);

const debug = jest.fn();
await trueAffected({
cwd,
base: 'main',
rootTsConfig: 'tsconfig.json',
projects: [
{
name: 'proj1',
sourceRoot: 'proj1/',
tsConfig: 'proj1/tsconfig.json',
},
{
name: 'proj2',
sourceRoot: 'proj2/',
tsConfig: 'proj2/tsconfig.json',
},
{
name: 'proj3',
sourceRoot: 'proj3/',
tsConfig: 'proj3/tsconfig.json',
implicitDependencies: ['proj1'],
},
],
logger: {
debug,
} as unknown as Console,
});

expect(debug).toHaveBeenCalledWith('Getting affected projects');
expect(debug).toHaveBeenCalledWith(
expect.stringContaining('Creating project with root tsconfig from')
);
expect(debug).toHaveBeenCalledWith(
expect.stringContaining('Adding source files for project proj1')
);
expect(debug).toHaveBeenCalledWith(
expect.stringContaining('Adding source files for project proj2')
);
expect(debug).toHaveBeenCalledWith(
expect.stringContaining(
'Could not find a tsconfig for project proj3, adding source files paths'
)
);
expect(debug).toHaveBeenCalledWith(
`Found ${changedFiles.length} changed files`
);
expect(debug).toHaveBeenCalledWith(
`Added package proj1 to affected packages for changed line ${changedFiles[0].changedLines[0]} in ${changedFiles[0].filePath}`
);
expect(debug).toHaveBeenCalledWith(
expect.stringMatching(
new RegExp(`^Found identifier .* in .*${changedFiles[0].filePath}$`)
)
);
expect(debug).toHaveBeenCalledWith(
'Added package proj2 to affected packages'
);
});
});
129 changes: 110 additions & 19 deletions libs/core/src/true-affected.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { existsSync } from 'fs';
import { join, resolve } from 'path';
import { Project, Node, ts, SyntaxKind } from 'ts-morph';
import chalk from 'chalk';
import { ChangedFiles, getChangedFiles } from './git';
import { findRootNode, getPackageNameByPath } from './utils';
import { TrueAffected, TrueAffectedProject } from './types';
Expand All @@ -21,14 +22,29 @@ const ignoredRootNodeTypes = [

export const DEFAULT_INCLUDE_TEST_FILES = /\.(spec|test)\.(ts|js)x?/;

const DEFAULT_LOGGER = {
...console,
debug: process.env['DEBUG'] === 'true' ? console.debug : () => {},
};

export const trueAffected = async ({
cwd,
rootTsConfig,
base = 'origin/main',
projects,
include = [DEFAULT_INCLUDE_TEST_FILES],
logger = DEFAULT_LOGGER,
__experimentalLockfileCheck = false,
}: TrueAffected) => {
logger.debug('Getting affected projects');
if (rootTsConfig != null) {
logger.debug(
`Creating project with root tsconfig from ${chalk.bold(
resolve(cwd, rootTsConfig)
)}`
);
}

const project = new Project({
compilerOptions: {
allowJs: true,
Expand All @@ -41,23 +57,27 @@ export const trueAffected = async ({
}),
});

const implicitDeps = (
projects.filter(
({ implicitDependencies = [] }) => implicitDependencies.length > 0
) as Required<TrueAffectedProject>[]
).reduce(
(acc, { name, implicitDependencies }) =>
acc.set(name, implicitDependencies),
new Map<string, string[]>()
);

projects.forEach(
({ sourceRoot, tsConfig = join(sourceRoot, 'tsconfig.json') }) => {
({ name, sourceRoot, tsConfig = join(sourceRoot, 'tsconfig.json') }) => {
const tsConfigPath = resolve(cwd, tsConfig);

if (existsSync(tsConfigPath)) {
logger.debug(
`Adding source files for project ${chalk.bold(
name
)} from tsconfig at ${chalk.bold(tsConfigPath)}`
);

project.addSourceFilesFromTsConfig(tsConfigPath);
} else {
logger.debug(
`Could not find a tsconfig for project ${chalk.bold(
name
)}, adding source files paths in ${chalk.bold(
resolve(cwd, sourceRoot)
)}`
);

project.addSourceFilesAtPaths(
join(resolve(cwd, sourceRoot), '**/*.{ts,js}')
);
Expand All @@ -70,11 +90,13 @@ export const trueAffected = async ({
cwd,
});

logger.debug(`Found ${chalk.bold(changedFiles.length)} changed files`);

const sourceChangedFiles = changedFiles.filter(
({ filePath }) => project.getSourceFile(resolve(cwd, filePath)) != null
);

const ignoredPaths = ['./node_modules', './dist', './.git'];
const ignoredPaths = ['./node_modules', './build', './dist', './.git'];

const nonSourceChangedFiles = changedFiles
.filter(
Expand All @@ -83,12 +105,26 @@ export const trueAffected = async ({
!filePath.endsWith(lockFileName) &&
project.getSourceFile(resolve(cwd, filePath)) == null
)
.flatMap(({ filePath: changedFilePath }) =>
findNonSourceAffectedFiles(cwd, changedFilePath, ignoredPaths)
.flatMap(({ filePath: changedFilePath }) => {
logger.debug(
`Finding non-source affected files for ${chalk.bold(changedFilePath)}`
);

return findNonSourceAffectedFiles(cwd, changedFilePath, ignoredPaths);
});

if (nonSourceChangedFiles.length > 0) {
logger.debug(
`Found ${chalk.bold(
nonSourceChangedFiles.length
)} non-source affected files`
);
}

let changedFilesByLockfile: ChangedFiles[] = [];
if (__experimentalLockfileCheck && hasLockfileChanged(changedFiles)) {
logger.debug('Lockfile has changed, finding affected files');

changedFilesByLockfile = findAffectedFilesByLockfile(
cwd,
base,
Expand All @@ -115,6 +151,14 @@ export const trueAffected = async ({
.map(({ filePath }) => getPackageNameByPath(filePath, projects))
.filter((v): v is string => v != null);

if (changedIncludedFilesPackages.length > 0) {
logger.debug(
`Found ${chalk.bold(
changedIncludedFilesPackages.length
)} affected packages from included files`
);
}

const affectedPackages = new Set<string>(changedIncludedFilesPackages);
const visitedIdentifiers = new Map<string, string[]>();

Expand All @@ -137,17 +181,36 @@ export const trueAffected = async ({
const identifierName = identifier.getText();
const path = rootNode.getSourceFile().getFilePath();

logger.debug(
`Found identifier ${chalk.bold(identifierName)} in ${chalk.bold(path)}`
);

if (identifierName && path) {
const visited = visitedIdentifiers.get(identifierName) ?? [];
if (visited.includes(path)) return;
visitedIdentifiers.set(identifierName, [...visited, path]);
const visited = visitedIdentifiers.get(path) ?? [];
if (visited.includes(identifierName)) {
logger.debug(
`Already visited ${chalk.bold(identifierName)} in ${chalk.bold(path)}`
);

return;
}

visitedIdentifiers.set(path, [...visited, identifierName]);

logger.debug(
`Visiting ${chalk.bold(identifierName)} in ${chalk.bold(path)}`
);
}

refs.forEach((node) => {
const sourceFile = node.getSourceFile();
const pkg = getPackageNameByPath(sourceFile.getFilePath(), projects);

if (pkg) affectedPackages.add(pkg);
if (pkg) {
affectedPackages.add(pkg);

logger.debug(`Added package ${chalk.bold(pkg)} to affected packages`);
}

findReferencesLibs(node);
});
Expand All @@ -170,7 +233,17 @@ export const trueAffected = async ({

const pkg = getPackageNameByPath(sourceFile.getFilePath(), projects);

if (pkg) affectedPackages.add(pkg);
if (pkg) {
affectedPackages.add(pkg);

logger.debug(
`Added package ${chalk.bold(
pkg
)} to affected packages for changed line ${chalk.bold(
line
)} in ${chalk.bold(filePath)}`
);
}

findReferencesLibs(changedNode);
} catch {
Expand All @@ -179,12 +252,30 @@ export const trueAffected = async ({
});
});

const implicitDeps = (
projects.filter(
({ implicitDependencies = [] }) => implicitDependencies.length > 0
) as Required<TrueAffectedProject>[]
).reduce(
(acc, { name, implicitDependencies }) =>
acc.set(name, implicitDependencies),
new Map<string, string[]>()
);

// add implicit deps
affectedPackages.forEach((pkg) => {
const deps = Array.from(implicitDeps.entries())
.filter(([, deps]) => deps.includes(pkg))
.map(([name]) => name);

if (deps.length > 0) {
logger.debug(
`Adding implicit dependencies ${chalk.bold(
deps.join(', ')
)} to ${chalk.bold(pkg)}`
);
}

deps.forEach((dep) => affectedPackages.add(dep));
});

Expand Down
6 changes: 5 additions & 1 deletion libs/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ export interface TrueAffectedProject {
targets?: string[];
}

export interface TrueAffected {
export interface TrueAffectedLogging {
logger?: Console;
}

export interface TrueAffected extends TrueAffectedLogging {
cwd: string;
rootTsConfig?: string;
base?: string;
Expand Down
18 changes: 13 additions & 5 deletions libs/nx/src/cli.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { resolve } from 'path';
import type { TrueAffectedProject } from '@traf/core';

import * as cli from './cli';
import * as nx from './nx';
import { workspaceCwd } from './mocks';
import { TrueAffectedProject } from '@traf/core';

jest.mock('chalk', () => ({
hex: jest.fn().mockReturnValue(jest.fn()),
hex: () => jest.fn(),
bgHex: jest.fn().mockReturnValue({
bold: jest.fn(),
}),
bgGray: {
bold: jest.fn(),
},
chalk: jest.fn(),
}));

Expand Down Expand Up @@ -88,7 +92,7 @@ describe('cli', () => {
]);
expect(affectedActionSpy).toBeCalledWith({
action: 'build',
all: 'true',
all: true,
base: 'master',
cwd: resolve(process.cwd(), workspaceCwd),
includeFiles: ['package.json', 'jest.setup.js'],
Expand All @@ -110,7 +114,8 @@ describe('cli', () => {
beforeEach(() => {
getNxTrueAffectedProjectsSpy = jest
.spyOn(nx, 'getNxTrueAffectedProjects')
.mockImplementation();
.mockImplementation()
.mockResolvedValue([]);

logSpy = jest.spyOn(cli, 'log').mockImplementation();
});
Expand All @@ -132,13 +137,16 @@ describe('cli', () => {
target: [],
});

expect(getNxTrueAffectedProjectsSpy).toBeCalledWith(process.cwd());
expect(getNxTrueAffectedProjectsSpy).toBeCalledWith(process.cwd(), {
logger: expect.any(Object),
});
expect(trafSpy).toHaveBeenCalledWith({
cwd: process.cwd(),
rootTsConfig: 'tsconfig.base.json',
base: 'origin/main',
projects: [],
include: [],
logger: expect.any(Object),
});
});

Expand Down
Loading

0 comments on commit a813e6c

Please sign in to comment.