diff --git a/.changeset/silver-bees-bake.md b/.changeset/silver-bees-bake.md new file mode 100644 index 00000000..d0b5c4be --- /dev/null +++ b/.changeset/silver-bees-bake.md @@ -0,0 +1,7 @@ +--- +"@commonalityco/data-project": patch +"@commonalityco/utils-core": patch +"commonality": patch +--- + +Adds a `workspaces` property to the project configuration file. This will allow you to override your package manager's workspaces. This will also allow integrated monorepos to filter packages without adding a workspaces property to their package manager. diff --git a/apps/commonality/src/define-config.ts b/apps/commonality/src/define-config.ts index 4ef30498..ccdc3db0 100644 --- a/apps/commonality/src/define-config.ts +++ b/apps/commonality/src/define-config.ts @@ -1,5 +1,5 @@ -import { ProjectConfig } from '@commonalityco/utils-core'; +import { ProjectConfigInput } from '@commonalityco/utils-core'; -export function defineConfig(config: ProjectConfig): ProjectConfig { +export function defineConfig(config: ProjectConfigInput): ProjectConfigInput { return config; } diff --git a/packages/data-constraints/src/get-constraint-results.ts b/packages/data-constraints/src/get-constraint-results.ts index 37588cb4..3a85479c 100644 --- a/packages/data-constraints/src/get-constraint-results.ts +++ b/packages/data-constraints/src/get-constraint-results.ts @@ -1,5 +1,5 @@ import { Dependency, TagsData, ConstraintResult } from '@commonalityco/types'; -import { ProjectConfig } from '@commonalityco/utils-core'; +import { ProjectConfigOutput } from '@commonalityco/utils-core'; const edgeKey = (dep: Dependency) => `${dep.source}|${dep.target}`; @@ -108,7 +108,7 @@ export async function getConstraintResults({ dependencies = [], tagsData = [], }: { - constraints?: ProjectConfig['constraints']; + constraints?: ProjectConfigOutput['constraints']; dependencies: Dependency[]; tagsData: TagsData[]; }): Promise { diff --git a/packages/data-project/package.json b/packages/data-project/package.json index 34112b8b..d6637e1a 100644 --- a/packages/data-project/package.json +++ b/packages/data-project/package.json @@ -30,8 +30,10 @@ "@commonalityco/config-tsconfig": "workspace:*", "@commonalityco/types": "workspace:*", "@types/fs-extra": "^11.0.2", + "@types/mock-fs": "^4.13.4", "@types/node": "^20.10.0", "eslint-config-commonality": "workspace:*", + "mock-fs": "^5.2.0", "typescript": "^5.2.2" }, "dependencies": { @@ -48,4 +50,4 @@ "url": "https://github.com/commonalityco/commonality", "directory": "packages/data-project" } -} \ No newline at end of file +} diff --git a/packages/data-project/src/get-project-config.ts b/packages/data-project/src/get-project-config.ts index c6dd6ee6..9f81806e 100644 --- a/packages/data-project/src/get-project-config.ts +++ b/packages/data-project/src/get-project-config.ts @@ -1,7 +1,10 @@ import jiti from 'jiti'; import { findUp } from 'find-up'; -import { ProjectConfig, projectConfigSchema } from '@commonalityco/utils-core'; -import { ZodError } from 'zod'; +import { + ProjectConfigOutput, + projectConfigSchema, +} from '@commonalityco/utils-core'; +import z, { ZodError } from 'zod'; const normalizeZodMessage = (error: unknown): string => { return (error as ZodError).issues @@ -20,12 +23,14 @@ const normalizeZodMessage = (error: unknown): string => { .join('\n'); }; -export const getValidatedProjectConfig = (config: unknown): ProjectConfig => { +export const getValidatedProjectConfig = ( + config: unknown, +): ProjectConfigOutput => { return projectConfigSchema.parse(config); }; export interface ProjectConfigData { - config: ProjectConfig | Record; + config: ProjectConfigOutput | Record; filepath: string; isEmpty?: boolean; } diff --git a/packages/data-project/src/get-workspace-globs.ts b/packages/data-project/src/get-workspace-globs.ts index 6b42fb89..1dbe8417 100644 --- a/packages/data-project/src/get-workspace-globs.ts +++ b/packages/data-project/src/get-workspace-globs.ts @@ -3,6 +3,7 @@ import fs from 'fs-extra'; import { PackageJson } from '@commonalityco/types'; import yaml from 'yaml'; import { PackageManager } from '@commonalityco/utils-core'; +import { getProjectConfig } from './get-project-config'; const defaultWorkspaceGlobs = ['./**']; @@ -13,6 +14,15 @@ export const getWorkspaceGlobs = async ({ rootDirectory: string; packageManager: PackageManager; }): Promise => { + const projectConfig = await getProjectConfig({ rootDirectory }); + + if ( + projectConfig?.config.workspaces && + projectConfig.config.workspaces.length > 0 + ) { + return projectConfig.config.workspaces; + } + if ( packageManager === PackageManager.NPM || packageManager === PackageManager.BUN || diff --git a/packages/data-project/test/fixtures/empty-configuration/commonality.config.ts b/packages/data-project/test/fixtures/empty-configuration/commonality.config.ts new file mode 100644 index 00000000..154f3a79 --- /dev/null +++ b/packages/data-project/test/fixtures/empty-configuration/commonality.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'commonality'; + +export default defineConfig({}); diff --git a/packages/data-project/test/fixtures/empty-workspaces/commonality.config.ts b/packages/data-project/test/fixtures/empty-workspaces/commonality.config.ts new file mode 100644 index 00000000..921e5549 --- /dev/null +++ b/packages/data-project/test/fixtures/empty-workspaces/commonality.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from 'commonality'; + +export default defineConfig({ + workspaces: [], +}); diff --git a/packages/data-project/test/fixtures/explicit-workspaces/commonality.config.ts b/packages/data-project/test/fixtures/explicit-workspaces/commonality.config.ts new file mode 100644 index 00000000..f5efa96c --- /dev/null +++ b/packages/data-project/test/fixtures/explicit-workspaces/commonality.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from 'commonality'; + +export default defineConfig({ + workspaces: ['apps/**', 'packages/**'], +}); diff --git a/packages/data-project/test/fixtures/npm-workspace/package.json b/packages/data-project/test/fixtures/npm-workspace/package.json index de15564b..c19b60c5 100644 --- a/packages/data-project/test/fixtures/npm-workspace/package.json +++ b/packages/data-project/test/fixtures/npm-workspace/package.json @@ -1,6 +1,6 @@ { "workspaces": [ - "./packages/**", - "./apps/**" + "apps/**", + "packages/**" ] } diff --git a/packages/data-project/test/fixtures/pnpm-workspace/pnpm-workspace.yaml b/packages/data-project/test/fixtures/pnpm-workspace/pnpm-workspace.yaml index 222c943f..44665b75 100644 --- a/packages/data-project/test/fixtures/pnpm-workspace/pnpm-workspace.yaml +++ b/packages/data-project/test/fixtures/pnpm-workspace/pnpm-workspace.yaml @@ -1,3 +1,3 @@ packages: - - './packages/**' - - './apps/**' + - 'apps/**' + - 'packages/**' diff --git a/packages/data-project/test/fixtures/yarn-workspace/package.json b/packages/data-project/test/fixtures/yarn-workspace/package.json index de15564b..c19b60c5 100644 --- a/packages/data-project/test/fixtures/yarn-workspace/package.json +++ b/packages/data-project/test/fixtures/yarn-workspace/package.json @@ -1,6 +1,6 @@ { "workspaces": [ - "./packages/**", - "./apps/**" + "apps/**", + "packages/**" ] } diff --git a/packages/data-project/test/get-project-config.test.ts b/packages/data-project/test/get-project-config.test.ts index 3dbcbb5c..b887f5ee 100644 --- a/packages/data-project/test/get-project-config.test.ts +++ b/packages/data-project/test/get-project-config.test.ts @@ -7,8 +7,19 @@ import { describe, expect, it, test } from 'vitest'; import { fileURLToPath } from 'node:url'; describe('getValidatedProjectConfig', () => { + test('defaults missing properties', () => { + const config = getValidatedProjectConfig({}); + + expect(config).toEqual({ + workspaces: [], + checks: {}, + constraints: {}, + }); + }); + test('strips out invalid properties', () => { const config = getValidatedProjectConfig({ + workspacees: 1, checks: { '*': [ { @@ -41,6 +52,7 @@ describe('getValidatedProjectConfig', () => { }); expect(config).toEqual({ + workspaces: [], checks: { '*': [ { @@ -48,6 +60,7 @@ describe('getValidatedProjectConfig', () => { validate: expect.any(Function), fix: expect.any(Function), message: 'foo', + level: 'warning', }, ], }, @@ -86,6 +99,8 @@ describe('getProjectConfig', () => { }); }); + it('returns defaults when the configuration file is empty', () => {}); + describe('when run in an initialized project', () => { it('should return the parsed project config if the file exists and is valid', async () => { const rootDirectory = path.join( @@ -100,6 +115,7 @@ describe('getProjectConfig', () => { isEmpty: false, filepath: expect.stringContaining('commonality.config.ts'), config: { + workspaces: [], checks: {}, constraints: { '*': { diff --git a/packages/data-project/test/get-workspace-globs.test.ts b/packages/data-project/test/get-workspace-globs.test.ts index e778c160..32deb658 100644 --- a/packages/data-project/test/get-workspace-globs.test.ts +++ b/packages/data-project/test/get-workspace-globs.test.ts @@ -1,88 +1,97 @@ import { PackageManager } from '@commonalityco/utils-core'; import path from 'node:path'; import { getWorkspaceGlobs } from '../src/get-workspace-globs'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, test } from 'vitest'; import { fileURLToPath } from 'node:url'; describe('getWorkspaceGlobs', () => { - describe('when run in an un-initialized project', () => { - it('returns the default globs', async () => { - const rootDirectory = path.join( - path.dirname(fileURLToPath(import.meta.url)), - './fixtures', - 'uninitialized', - ); - - const config = await getWorkspaceGlobs({ - rootDirectory, - packageManager: PackageManager.PNPM, - }); - - expect(config).toEqual(['./**']); + test('returns default globs when there is no package manager workspaces or explicit workspaces configured', async () => { + const rootDirectory = path.join( + path.dirname(fileURLToPath(import.meta.url)), + './fixtures', + 'uninitialized', + ); + + const config = await getWorkspaceGlobs({ + rootDirectory, + packageManager: PackageManager.NPM, + }); + + expect(config).toEqual(['./**']); + }); + + test('returns the workspaces when set via project configuration', async () => { + const rootDirectory = path.join( + path.dirname(fileURLToPath(import.meta.url)), + './fixtures', + 'explicit-workspaces', + ); + + const workspaceGlobs = await getWorkspaceGlobs({ + rootDirectory, + packageManager: PackageManager.PNPM, + }); + + expect(workspaceGlobs).toEqual(['apps/**', 'packages/**']); + }); + + test('returns the default globs when set to an empty array via project configuration', async () => { + const rootDirectory = path.join( + path.dirname(fileURLToPath(import.meta.url)), + './fixtures', + 'empty-workspaces', + ); + + const workspaceGlobs = await getWorkspaceGlobs({ + rootDirectory, + packageManager: PackageManager.PNPM, }); + + expect(workspaceGlobs).toEqual(['./**']); }); - describe('when run in an initialized project', () => { - describe('when the workspace option exists', () => { - const expectedWorkspaceGlobs = ['./packages/**', './apps/**']; - - it(`should return the correct globs for an NPM workspace`, async () => { - const rootDirectory = path.join( - path.dirname(fileURLToPath(import.meta.url)), - './fixtures', - 'npm-workspace', - ); - const workspaceGlobs = await getWorkspaceGlobs({ - rootDirectory, - packageManager: PackageManager.NPM, - }); - - expect(workspaceGlobs).toEqual(expectedWorkspaceGlobs); - }); - - it(`should return the correct globs for a Yarn workspace`, async () => { - const rootDirectory = path.join( - path.dirname(fileURLToPath(import.meta.url)), - './fixtures', - 'yarn-workspace', - ); - const workspaceGlobs = await getWorkspaceGlobs({ - rootDirectory, - packageManager: PackageManager.YARN, - }); - - expect(workspaceGlobs).toEqual(expectedWorkspaceGlobs); - }); - - it(`should return the correct globs for a pnpm workspace`, async () => { - const rootDirectory = path.join( - path.dirname(fileURLToPath(import.meta.url)), - './fixtures', - 'pnpm-workspace', - ); - const workspaceGlobs = await getWorkspaceGlobs({ - rootDirectory, - packageManager: PackageManager.PNPM, - }); - - expect(workspaceGlobs).toEqual(expectedWorkspaceGlobs); - }); + test('returns the workspaces when set with npm', async () => { + const rootDirectory = path.join( + path.dirname(fileURLToPath(import.meta.url)), + './fixtures', + 'npm-workspace', + ); + + const workspaceGlobs = await getWorkspaceGlobs({ + rootDirectory, + packageManager: PackageManager.NPM, }); - describe('when the workspace option does not exist', () => { - it(`should return the default globs`, async () => { - const rootDirectory = path.join( - path.dirname(fileURLToPath(import.meta.url)), - './fixtures', - 'missing-workspace-globs', - ); - const workspaceGlobs = await getWorkspaceGlobs({ - rootDirectory, - packageManager: PackageManager.PNPM, - }); - - expect(workspaceGlobs).toEqual(['./**']); - }); + expect(workspaceGlobs).toEqual(['apps/**', 'packages/**']); + }); + + test('returns the workspaces when set with yarn', async () => { + const rootDirectory = path.join( + path.dirname(fileURLToPath(import.meta.url)), + './fixtures', + 'yarn-workspace', + ); + + const workspaceGlobs = await getWorkspaceGlobs({ + rootDirectory, + packageManager: PackageManager.YARN, }); + + expect(workspaceGlobs).toEqual(['apps/**', 'packages/**']); + }); + + test('returns the workspaces when set with pnpm', async () => { + const rootDirectory = path.join( + path.dirname(fileURLToPath(import.meta.url)), + './fixtures', + 'pnpm-workspace', + ); + + const workspaceGlobs = await getWorkspaceGlobs({ + rootDirectory, + packageManager: PackageManager.PNPM, + }); + + expect(workspaceGlobs).toEqual(['apps/**', 'packages/**']); }); }); diff --git a/packages/ui-constraints/src/feature-graph-chart.tsx b/packages/ui-constraints/src/feature-graph-chart.tsx index a1cb1502..50feaf2c 100644 --- a/packages/ui-constraints/src/feature-graph-chart.tsx +++ b/packages/ui-constraints/src/feature-graph-chart.tsx @@ -8,12 +8,12 @@ import FeatureGraphToolbar from './feature-graph-toolbar'; import { cn } from '@commonalityco/ui-design-system/cn'; import debounce from 'lodash-es/debounce'; import { getElementDefinitions } from '@commonalityco/ui-graph'; -import { ProjectConfig } from '@commonalityco/utils-core'; +import { ProjectConfigOutput } from '@commonalityco/utils-core'; interface GraphProperties { packages: Package[]; results: ConstraintResult[]; - constraints: ProjectConfig['constraints']; + constraints: ProjectConfigOutput['constraints']; dependencies: Dependency[]; theme?: string; onPackageClick: (packageName: string) => void; diff --git a/packages/utils-conformance/src/create-test-check.ts b/packages/utils-conformance/src/create-test-check.ts index 55425525..1febb1be 100644 --- a/packages/utils-conformance/src/create-test-check.ts +++ b/packages/utils-conformance/src/create-test-check.ts @@ -1,6 +1,6 @@ import { Tag, Codeowner, Workspace } from '@commonalityco/types'; import stripAnsi from 'strip-ansi'; -import { Message, Check, CheckContext } from '@commonalityco/utils-core'; +import { Message, CheckInput, CheckContext } from '@commonalityco/utils-core'; type Awaitable = T | PromiseLike; @@ -22,7 +22,7 @@ interface TestCheckContext { tags?: Tag[]; } -export function createTestCheck( +export function createTestCheck( conformer: T, options?: TestCheckContext, ): TestConformer { diff --git a/packages/utils-conformance/src/define-check.ts b/packages/utils-conformance/src/define-check.ts index c85af0ec..5716e0a0 100644 --- a/packages/utils-conformance/src/define-check.ts +++ b/packages/utils-conformance/src/define-check.ts @@ -1,8 +1,10 @@ -import { Check } from '@commonalityco/utils-core'; +import { CheckInput } from '@commonalityco/utils-core'; -type CheckCreator = (...args: O) => C; +type CheckCreator = ( + ...args: O +) => C; -export function defineCheck( +export function defineCheck( checkCreator: CheckCreator, ): CheckCreator { return checkCreator; diff --git a/packages/utils-conformance/src/get-conformance-results.test.ts b/packages/utils-conformance/src/get-conformance-results.test.ts index 31bde2dc..ed63e72f 100644 --- a/packages/utils-conformance/src/get-conformance-results.test.ts +++ b/packages/utils-conformance/src/get-conformance-results.test.ts @@ -2,11 +2,11 @@ import { describe, expect, it } from 'vitest'; import { getConformanceResults } from './get-conformance-results'; import { Package, TagsData } from '@commonalityco/types'; import { PackageType, Status } from '@commonalityco/utils-core'; -import { ProjectConfig } from '@commonalityco/utils-core'; +import { ProjectConfigOutput } from '@commonalityco/utils-core'; describe('getConformanceResults', () => { it('should return errors when workspace is not valid and have a level set to error', async () => { - const conformersByPattern: ProjectConfig['checks'] = { + const conformersByPattern: ProjectConfigOutput['checks'] = { '*': [ { name: 'InvalidWorkspaceConformer', @@ -43,12 +43,13 @@ describe('getConformanceResults', () => { }); it('should return errors when workspace is not valid and do not have a level set', async () => { - const conformersByPattern: ProjectConfig['checks'] = { + const conformersByPattern: ProjectConfigOutput['checks'] = { '*': [ { name: 'InvalidWorkspaceConformer', validate: () => false, message: 'Invalid workspace', + level: 'warning', }, ], }; @@ -79,12 +80,13 @@ describe('getConformanceResults', () => { }); it('should return valid results when checks are valid', async () => { - const conformersByPattern: ProjectConfig['checks'] = { + const conformersByPattern: ProjectConfigOutput['checks'] = { '*': [ { name: 'ValidWorkspaceConformer', validate: () => true, message: 'Valid workspace', + level: 'warning', }, ], }; @@ -115,12 +117,13 @@ describe('getConformanceResults', () => { }); it('should return valid results when checks are valid and there are no tags', async () => { - const conformersByPattern: ProjectConfig['checks'] = { + const conformersByPattern: ProjectConfigOutput['checks'] = { '*': [ { name: 'ValidWorkspaceConformer', validate: () => true, message: 'Valid workspace', + level: 'warning', }, ], }; @@ -151,7 +154,7 @@ describe('getConformanceResults', () => { }); it('should handle exceptions during validation', async () => { - const conformersByPattern: ProjectConfig['checks'] = { + const conformersByPattern: ProjectConfigOutput['checks'] = { '*': [ { name: 'ExceptionConformer', @@ -159,6 +162,7 @@ describe('getConformanceResults', () => { throw new Error('Unexpected error'); }, message: 'Exception during validation', + level: 'warning', }, ], }; @@ -188,12 +192,13 @@ describe('getConformanceResults', () => { }); it('should handle conformers that target patterns other than *', async () => { - const conformersByPattern: ProjectConfig['checks'] = { + const conformersByPattern: ProjectConfigOutput['checks'] = { tag1: [ { name: 'Tag1Conformer', validate: () => true, message: 'Valid workspace for tag1', + level: 'warning', }, ], }; @@ -224,7 +229,7 @@ describe('getConformanceResults', () => { }); it('should return correct result when message property is a function', async () => { - const conformersByPattern: ProjectConfig['checks'] = { + const conformersByPattern: ProjectConfigOutput['checks'] = { '*': [ { name: 'MessageFunctionConformer', @@ -232,6 +237,7 @@ describe('getConformanceResults', () => { message: (context) => ({ title: `Valid package for ${context.package.relativePath}`, }), + level: 'warning', }, ], }; diff --git a/packages/utils-conformance/src/get-conformance-results.ts b/packages/utils-conformance/src/get-conformance-results.ts index e6b7a9a3..30a07698 100644 --- a/packages/utils-conformance/src/get-conformance-results.ts +++ b/packages/utils-conformance/src/get-conformance-results.ts @@ -1,7 +1,7 @@ import { TagsData, CodeownersData, Package } from '@commonalityco/types'; import { Status, - ProjectConfig, + ProjectConfigOutput, Check, Message, } from '@commonalityco/utils-core'; @@ -143,7 +143,7 @@ export const getConformanceResults = async ({ rootDirectory, codeownersData, }: { - conformersByPattern: ProjectConfig['checks']; + conformersByPattern: ProjectConfigOutput['checks']; rootDirectory: string; packages: Package[]; tagsData: TagsData[]; diff --git a/packages/utils-core/src/constants.ts b/packages/utils-core/src/constants.ts index ed1ca83f..4f12e986 100644 --- a/packages/utils-core/src/constants.ts +++ b/packages/utils-core/src/constants.ts @@ -84,7 +84,7 @@ const messageSchema = z const checkSchema = z.object({ name: z.string(), - level: z.union([z.literal('error'), z.literal('warning')]).optional(), + level: z.union([z.literal('error'), z.literal('warning')]).default('warning'), validate: checkFn, fix: checkFn.returns(z.union([z.void(), z.promise(z.void())])).optional(), message: z.union([ @@ -93,14 +93,21 @@ const checkSchema = z.object({ ]), }); -export const projectConfigSchema = z.object({ - checks: z.record(z.array(checkSchema).default([])).optional().default({}), - constraints: z.record(constraintSchema).optional().default({}), +const defaultProjectConfigSchema = z.object({ + workspaces: z.array(z.string()).default([]), + checks: z.record(z.array(checkSchema).default([])).default({}), + constraints: z.record(constraintSchema).default({}), }); -export type ProjectConfig = z.infer; +export const projectConfigSchema = defaultProjectConfigSchema; + +export type ProjectConfigInput = z.input; +export type ProjectConfigOutput = z.output; export type Check = z.infer; +export type CheckInput = z.input; +export type CheckOutput = z.output; + export type CheckContext = z.infer; export type Message = z.infer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4c83807..ea03c610 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -754,12 +754,18 @@ importers: '@types/fs-extra': specifier: ^11.0.2 version: 11.0.4 + '@types/mock-fs': + specifier: ^4.13.4 + version: 4.13.4 '@types/node': specifier: ^20.10.0 version: 20.10.6 eslint-config-commonality: specifier: workspace:* version: link:../../tooling/config-eslint + mock-fs: + specifier: ^5.2.0 + version: 5.2.0 typescript: specifier: ^5.2.2 version: 5.3.3