From ff4c08d11aec5f467d4255b0212c8f2e4ce988ed Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Wed, 28 Jun 2023 13:52:34 +0200 Subject: [PATCH 01/10] feat(tools): scaffold normalize-package-dependencies generator --- .../normalize-package-dependencies/README.md | 38 ++++++++++++++ .../files/constants.ts__tmpl__ | 1 + .../index.spec.ts | 20 ++++++++ .../normalize-package-dependencies/index.ts | 50 +++++++++++++++++++ .../lib/utils.spec.ts | 7 +++ .../lib/utils.ts | 5 ++ .../schema.json | 17 +++++++ .../normalize-package-dependencies/schema.ts | 6 +++ 8 files changed, 144 insertions(+) create mode 100644 tools/generators/normalize-package-dependencies/README.md create mode 100644 tools/generators/normalize-package-dependencies/files/constants.ts__tmpl__ create mode 100644 tools/generators/normalize-package-dependencies/index.spec.ts create mode 100644 tools/generators/normalize-package-dependencies/index.ts create mode 100644 tools/generators/normalize-package-dependencies/lib/utils.spec.ts create mode 100644 tools/generators/normalize-package-dependencies/lib/utils.ts create mode 100644 tools/generators/normalize-package-dependencies/schema.json create mode 100644 tools/generators/normalize-package-dependencies/schema.ts diff --git a/tools/generators/normalize-package-dependencies/README.md b/tools/generators/normalize-package-dependencies/README.md new file mode 100644 index 00000000000000..60097e4572a1a3 --- /dev/null +++ b/tools/generators/normalize-package-dependencies/README.md @@ -0,0 +1,38 @@ +# normalize-package-dependencies + +Workspace Generator ...TODO... + + + +- [Usage](#usage) + - [Examples](#examples) +- [Options](#options) + - [`name`](#name) + + + +## Usage + +```sh +yarn nx workspace-generator normalize-package-dependencies ... +``` + +Show what will be generated without writing to disk: + +```sh +yarn nx workspace-generator normalize-package-dependencies --dry-run +``` + +### Examples + +```sh +yarn nx workspace-generator normalize-package-dependencies +``` + +## Options + +#### `name` + +Type: `string` + +TODO... diff --git a/tools/generators/normalize-package-dependencies/files/constants.ts__tmpl__ b/tools/generators/normalize-package-dependencies/files/constants.ts__tmpl__ new file mode 100644 index 00000000000000..d9913111a9dc76 --- /dev/null +++ b/tools/generators/normalize-package-dependencies/files/constants.ts__tmpl__ @@ -0,0 +1 @@ +export const variable = "<%= name %>"; \ No newline at end of file diff --git a/tools/generators/normalize-package-dependencies/index.spec.ts b/tools/generators/normalize-package-dependencies/index.spec.ts new file mode 100644 index 00000000000000..10ae51633d3bf4 --- /dev/null +++ b/tools/generators/normalize-package-dependencies/index.spec.ts @@ -0,0 +1,20 @@ +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { Tree, readProjectConfiguration } from '@nrwl/devkit'; + +import generator from './index'; +import { NormalizePackageDependenciesGeneratorSchema } from './schema'; + +describe('normalize-package-dependencies generator', () => { + let appTree: Tree; + const options: NormalizePackageDependenciesGeneratorSchema = { name: 'test' }; + + beforeEach(() => { + appTree = createTreeWithEmptyWorkspace(); + }); + + it('should run successfully', async () => { + await generator(appTree, options); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toBeDefined(); + }); +}); diff --git a/tools/generators/normalize-package-dependencies/index.ts b/tools/generators/normalize-package-dependencies/index.ts new file mode 100644 index 00000000000000..50918d8043edb7 --- /dev/null +++ b/tools/generators/normalize-package-dependencies/index.ts @@ -0,0 +1,50 @@ +import * as path from 'path'; +import { Tree, formatFiles, installPackagesTask, names, generateFiles } from '@nrwl/devkit'; +import { libraryGenerator } from '@nrwl/workspace/generators'; + +import { getProjectConfig } from '../../utils'; + +import { NormalizePackageDependenciesGeneratorSchema } from './schema'; + +interface NormalizedSchema extends ReturnType {} + +export default async function (tree: Tree, schema: NormalizePackageDependenciesGeneratorSchema) { + await libraryGenerator(tree, { name: schema.name }); + + const normalizedOptions = normalizeOptions(tree, schema); + + addFiles(tree, normalizedOptions); + + await formatFiles(tree); + + return () => { + installPackagesTask(tree); + }; +} + +function normalizeOptions(tree: Tree, options: NormalizePackageDependenciesGeneratorSchema) { + const project = getProjectConfig(tree, { packageName: options.name }); + + return { + ...options, + ...project, + ...names(options.name), + }; +} + +/** + * NOTE: remove this if your generator doesn't process any static/dynamic templates + */ +function addFiles(tree: Tree, options: NormalizedSchema) { + const templateOptions = { + ...options, + tmpl: '', + }; + + generateFiles( + tree, + path.join(__dirname, 'files'), + path.join(options.projectConfig.root, options.name), + templateOptions, + ); +} diff --git a/tools/generators/normalize-package-dependencies/lib/utils.spec.ts b/tools/generators/normalize-package-dependencies/lib/utils.spec.ts new file mode 100644 index 00000000000000..91e05a54a86f53 --- /dev/null +++ b/tools/generators/normalize-package-dependencies/lib/utils.spec.ts @@ -0,0 +1,7 @@ +import { dummyHelper } from './utils'; + +describe(`utils`, () => { + it(`should behave...`, () => { + expect(dummyHelper()).toBe(undefined); + }); +}); diff --git a/tools/generators/normalize-package-dependencies/lib/utils.ts b/tools/generators/normalize-package-dependencies/lib/utils.ts new file mode 100644 index 00000000000000..c340a27c29c8b0 --- /dev/null +++ b/tools/generators/normalize-package-dependencies/lib/utils.ts @@ -0,0 +1,5 @@ +// use this module to define any kind of generic utilities that are used in more than 1 place within the generator implementation + +export function dummyHelper() { + return; +} diff --git a/tools/generators/normalize-package-dependencies/schema.json b/tools/generators/normalize-package-dependencies/schema.json new file mode 100644 index 00000000000000..59e3d531638806 --- /dev/null +++ b/tools/generators/normalize-package-dependencies/schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "id": "normalize-package-dependencies", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Library name", + "$default": { + "$source": "argv", + "index": 0 + } + } + }, + "required": ["name"] +} diff --git a/tools/generators/normalize-package-dependencies/schema.ts b/tools/generators/normalize-package-dependencies/schema.ts new file mode 100644 index 00000000000000..347cafaed0284a --- /dev/null +++ b/tools/generators/normalize-package-dependencies/schema.ts @@ -0,0 +1,6 @@ +export interface NormalizePackageDependenciesGeneratorSchema { + /** + * Library name + */ + name: string; +} From efcc1847e7d9b8e7b98c73807e33575f2b18d1fa Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Wed, 28 Jun 2023 16:12:19 +0200 Subject: [PATCH 02/10] feat(tools): implement normalize-package-dependencies generator --- .../index.spec.ts | 230 +++++++++++++++++- .../normalize-package-dependencies/index.ts | 193 ++++++++++++--- .../schema.json | 20 +- .../normalize-package-dependencies/schema.ts | 12 +- 4 files changed, 406 insertions(+), 49 deletions(-) diff --git a/tools/generators/normalize-package-dependencies/index.spec.ts b/tools/generators/normalize-package-dependencies/index.spec.ts index 10ae51633d3bf4..be5dcc54851974 100644 --- a/tools/generators/normalize-package-dependencies/index.spec.ts +++ b/tools/generators/normalize-package-dependencies/index.spec.ts @@ -1,20 +1,232 @@ import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; -import { Tree, readProjectConfiguration } from '@nrwl/devkit'; +import { Tree, addProjectConfiguration, writeJson, getProjects, readJson, joinPathFragments } from '@nrwl/devkit'; import generator from './index'; -import { NormalizePackageDependenciesGeneratorSchema } from './schema'; +import { PackageJson } from '../../types'; describe('normalize-package-dependencies generator', () => { - let appTree: Tree; - const options: NormalizePackageDependenciesGeneratorSchema = { name: 'test' }; + let tree: Tree; + // eslint-disable-next-line @typescript-eslint/no-empty-function + const noop = () => {}; + const errorLogSpy = jest.spyOn(console, 'error').mockImplementation(noop); + const infoLogSpy = jest.spyOn(console, 'info').mockImplementation(noop); beforeEach(() => { - appTree = createTreeWithEmptyWorkspace(); + tree = createTreeWithEmptyWorkspace(); + tree = createProject(tree, { + projectName: 'react-one', + deps: { dev: {}, prod: { react: '17.x.x' }, peer: {} }, + tags: ['platform:any'], + }); + tree = createProject(tree, { + projectName: 'react-two', + deps: { dev: {}, prod: { react: '17.x.x', '@proj/react-one': '^1.0.0' }, peer: {} }, + tags: ['platform:any', 'scope:two'], + }); + tree = createProject(tree, { + projectName: 'react-app', + projectType: 'application', + deps: { dev: {}, prod: { react: '17.x.x', '@proj/react-one': '^1.0.0', '@proj/react-two': '^1.0.0' }, peer: {} }, + tags: ['platform:web', 'scope:two', 'type:app'], + }); }); - it('should run successfully', async () => { - await generator(appTree, options); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toBeDefined(); + it(`should update workspace dependencies to "*" for version`, async () => { + await generator(tree, {}); + + const { reactApp, reactOne, reactTwo } = getPackageJsonForAllProjects(tree); + + expect(reactOne.dependencies).toEqual({ react: '17.x.x' }); + expect(reactTwo.dependencies).toEqual({ + react: '17.x.x', + '@proj/react-one': '*', + }); + expect(reactApp.dependencies).toEqual({ + react: '17.x.x', + '@proj/react-one': '*', + '@proj/react-two': '*', + }); + }); + + describe(`filters`, () => { + describe(`multiple active`, () => { + it(`should update workspace dependencies that match provided filter options`, async () => { + await generator(tree, { projectType: 'library', tag: 'type:app' }); + + const { reactApp, reactOne, reactTwo } = getPackageJsonForAllProjects(tree); + + expect(reactOne.dependencies).toEqual({ + react: '17.x.x', + }); + expect(reactTwo.dependencies).toEqual({ + react: '17.x.x', + '@proj/react-one': '^1.0.0', + }); + expect(reactApp.dependencies).toEqual({ + react: '17.x.x', + '@proj/react-one': '^1.0.0', + '@proj/react-two': '^1.0.0', + }); + }); + }); + + describe(`--tag`, () => { + it(`should update workspace dependencies that have specified tag`, async () => { + await generator(tree, { tag: 'scope:two' }); + + const { reactApp, reactOne, reactTwo } = getPackageJsonForAllProjects(tree); + + expect(reactOne.dependencies).toEqual({ + react: '17.x.x', + }); + expect(reactTwo.dependencies).toEqual({ + react: '17.x.x', + '@proj/react-one': '*', + }); + expect(reactApp.dependencies).toEqual({ + react: '17.x.x', + '@proj/react-one': '*', + '@proj/react-two': '*', + }); + }); + }); + + describe(`--projectType`, () => { + it(`should update workspace dependencies only for application`, async () => { + await generator(tree, { projectType: 'application' }); + + const { reactApp, reactOne, reactTwo } = getPackageJsonForAllProjects(tree); + + expect(reactOne.dependencies).toEqual({ + react: '17.x.x', + }); + expect(reactTwo.dependencies).toEqual({ + react: '17.x.x', + '@proj/react-one': '^1.0.0', + }); + expect(reactApp.dependencies).toEqual({ + react: '17.x.x', + '@proj/react-one': '*', + '@proj/react-two': '*', + }); + }); + }); + }); + + describe(`--verify`, () => { + it(`should not update any package.json`, async () => { + try { + await generator(tree, { verify: true }); + } catch { + const { reactApp, reactOne, reactTwo } = getPackageJsonForAllProjects(tree); + + expect(reactOne.dependencies).toEqual({ + react: '17.x.x', + }); + expect(reactTwo.dependencies).toEqual({ + react: '17.x.x', + '@proj/react-one': '^1.0.0', + }); + expect(reactApp.dependencies).toEqual({ + react: '17.x.x', + '@proj/react-one': '^1.0.0', + '@proj/react-two': '^1.0.0', + }); + } + }); + + it(`should fail if there are any violations`, async () => { + await expect(generator(tree, { verify: true })).rejects.toThrowErrorMatchingInlineSnapshot( + `"package dependency violations found"`, + ); + + expect(errorLogSpy.mock.calls.flat()).toMatchInlineSnapshot(` + Array [ + "@proj/react-two has following dependency version issues:", + " - @proj/react-one", + "@proj/react-app has following dependency version issues:", + " - @proj/react-one", + " - @proj/react-two", + ] + `); + expect(infoLogSpy.mock.calls.flat()).toMatchInlineSnapshot(` + Array [ + "All these dependencies version should be specified as '*'", + "Fix this by running 'nx workspace-generator normalize-package-dependencies'", + ] + `); + }); + + it(`should fail if there are any violations and filters are used`, async () => { + await expect( + generator(tree, { verify: true, projectType: 'application' }), + ).rejects.toThrowErrorMatchingInlineSnapshot(`"package dependency violations found"`); + + expect(errorLogSpy.mock.calls.flat()).toMatchInlineSnapshot(` + Array [ + "@proj/react-app has following dependency version issues:", + " - @proj/react-one", + " - @proj/react-two", + ] + `); + expect(infoLogSpy.mock.calls.flat()).toMatchInlineSnapshot(` + Array [ + "All these dependencies version should be specified as '*'", + "Fix this by running 'nx workspace-generator normalize-package-dependencies'", + ] + `); + + jest.resetAllMocks(); + + await expect(generator(tree, { verify: true, projectType: 'library', tag: 'type:app' })).resolves.toEqual( + undefined, + ); + + expect(errorLogSpy.mock.calls.flat()).toMatchInlineSnapshot(`Array []`); + expect(infoLogSpy.mock.calls.flat()).toMatchInlineSnapshot(`Array []`); + }); }); }); + +function getPackageJsonForAllProjects(tree: Tree) { + const projects = getProjects(tree); + + const reactOne = projects.get('@proj/react-one'); + const reactTwo = projects.get('@proj/react-two'); + const reactApp = projects.get('@proj/react-app'); + + return { + reactOne: readJson(tree, joinPathFragments(reactOne!.root, 'package.json')), + reactTwo: readJson(tree, joinPathFragments(reactTwo!.root, 'package.json')), + reactApp: readJson(tree, joinPathFragments(reactApp!.root, 'package.json')), + }; +} + +function createProject( + tree: Tree, + options: { + projectName: string; + projectType?: 'application' | 'library'; + tags?: string[]; + deps: { prod: Record; dev: Record; peer: Record }; + }, +) { + const { projectName, deps, tags, projectType = 'library' } = options; + const packageName = `@proj/${projectName}`; + + const rootPath = `packages/${projectName}`; + + writeJson(tree, `packages/${projectName}/package.json`, { + name: packageName, + dependencies: { ...deps.prod }, + devDependencies: { ...deps.dev }, + peerDependencies: { ...deps.peer }, + }); + + addProjectConfiguration(tree, packageName, { + root: rootPath, + projectType, + ...(tags ? { tags } : null), + }); + return tree; +} diff --git a/tools/generators/normalize-package-dependencies/index.ts b/tools/generators/normalize-package-dependencies/index.ts index 50918d8043edb7..04a103d779b128 100644 --- a/tools/generators/normalize-package-dependencies/index.ts +++ b/tools/generators/normalize-package-dependencies/index.ts @@ -1,50 +1,181 @@ -import * as path from 'path'; -import { Tree, formatFiles, installPackagesTask, names, generateFiles } from '@nrwl/devkit'; -import { libraryGenerator } from '@nrwl/workspace/generators'; - -import { getProjectConfig } from '../../utils'; +import { + Tree, + formatFiles, + getProjects, + ProjectConfiguration, + updateJson, + joinPathFragments, + readProjectConfiguration, + readJson, + logger, +} from '@nrwl/devkit'; import { NormalizePackageDependenciesGeneratorSchema } from './schema'; +import { PackageJson } from '../../types'; -interface NormalizedSchema extends ReturnType {} +type ProjectIssues = { [projectName: string]: { [depName: string]: string } }; -export default async function (tree: Tree, schema: NormalizePackageDependenciesGeneratorSchema) { - await libraryGenerator(tree, { name: schema.name }); +type NormalizedSchema = ReturnType; +export default async function (tree: Tree, schema: NormalizePackageDependenciesGeneratorSchema) { const normalizedOptions = normalizeOptions(tree, schema); - addFiles(tree, normalizedOptions); + const filters = getActiveFilters(normalizedOptions); + const projects = getProjects(tree); + const issues: ProjectIssues = {}; - await formatFiles(tree); + projects.forEach(projectConfig => { + if (!shouldBeProjectProcessed(projectConfig, filters)) { + return; + } - return () => { - installPackagesTask(tree); - }; + if (normalizedOptions.verify) { + const foundIssues = getPackageJsonDependenciesIssues(tree, projectConfig); + + if (foundIssues) { + issues[projectConfig.name!] = foundIssues; + } + return; + } + + normalizePackageJsonDependencies(tree, projectConfig); + }); + + reportPackageJsonDependenciesIssues(issues); + + await formatFiles(tree); } -function normalizeOptions(tree: Tree, options: NormalizePackageDependenciesGeneratorSchema) { - const project = getProjectConfig(tree, { packageName: options.name }); +function getActiveFilters(options: NormalizedSchema) { + const active: Partial< + Record, (projectConfig: ProjectConfiguration) => boolean> + > = {}; + + if (options.projectType !== 'any') { + active.projectType = (projectConfig: ProjectConfiguration) => { + return projectConfig.projectType === options.projectType; + }; + } + if (options.tag) { + active.tag = (projectConfig: ProjectConfiguration) => { + return Array.isArray(projectConfig.tags) && projectConfig.tags.includes(options.tag!); + }; + } + + const activeCount = Object.keys(active).length; return { - ...options, - ...project, - ...names(options.name), + hasActive: activeCount > 0, + hasActiveMultiple: activeCount > 1, + filters: active, }; } -/** - * NOTE: remove this if your generator doesn't process any static/dynamic templates - */ -function addFiles(tree: Tree, options: NormalizedSchema) { - const templateOptions = { +function shouldBeProjectProcessed(projectConfig: ProjectConfiguration, filters: ReturnType) { + const filterPredicates = Object.values(filters.filters); + + if (filters.hasActiveMultiple) { + return filterPredicates.every(predicate => predicate(projectConfig)); + } + + if (filters.hasActive) { + return filterPredicates.some(predicate => predicate(projectConfig)); + } + + return true; +} + +function normalizePackageJsonDependencies(tree: Tree, projectConfig: ProjectConfiguration) { + const packageJsonPath = joinPathFragments(projectConfig.root, 'package.json'); + + updateJson(tree, packageJsonPath, json => { + updateDepType(json, 'dependencies'); + updateDepType(json, 'devDependencies'); + + return json; + }); + + return tree; + + function updateDepType(json: PackageJson, depType: 'dependencies' | 'devDependencies' | 'peerDependencies') { + const deps = json[depType]; + if (!deps) { + return; + } + + for (const packageName in deps) { + if (isWorkspaceProject(tree, packageName)) { + deps[packageName] = '*'; + } + } + } +} + +function reportPackageJsonDependenciesIssues(issues: ProjectIssues) { + const issueEntries = Object.entries(issues); + + if (issueEntries.length === 0) { + return; + } + + issueEntries.forEach(([projectName, dependencyIssues]) => { + logger.error(`${projectName} has following dependency version issues:`); + // eslint-disable-next-line guard-for-in + for (const dep in dependencyIssues) { + logger.error(` - ${dep}`); + } + }); + + logger.info(`All these dependencies version should be specified as '*'`); + logger.info(`Fix this by running 'nx workspace-generator normalize-package-dependencies'`); + + throw new Error('package dependency violations found'); +} + +function getPackageJsonDependenciesIssues( + tree: Tree, + projectConfig: ProjectConfiguration, +): Record | null { + const packageJsonPath = joinPathFragments(projectConfig.root, 'package.json'); + const packageJson = readJson(tree, packageJsonPath); + + let issues: Record | null = null; + checkDepType(packageJson, 'dependencies'); + checkDepType(packageJson, 'devDependencies'); + checkDepType(packageJson, 'peerDependencies'); + + return issues; + + function checkDepType(json: PackageJson, depType: 'dependencies' | 'devDependencies' | 'peerDependencies') { + const deps = json[depType]; + if (!deps) { + return null; + } + + for (const packageName in deps) { + if (isWorkspaceProject(tree, packageName) && deps[packageName] !== '*') { + issues = issues ?? {}; + issues[packageName] = deps[packageName]; + } + } + + return issues; + } +} + +function isWorkspaceProject(tree: Tree, projectName: string): boolean { + try { + readProjectConfiguration(tree, projectName); + return true; + } catch { + return false; + } +} + +function normalizeOptions(tree: Tree, schema: NormalizePackageDependenciesGeneratorSchema) { + const options = { projectType: 'any', verify: false, ...schema } as const; + + return { ...options, - tmpl: '', }; - - generateFiles( - tree, - path.join(__dirname, 'files'), - path.join(options.projectConfig.root, options.name), - templateOptions, - ); } diff --git a/tools/generators/normalize-package-dependencies/schema.json b/tools/generators/normalize-package-dependencies/schema.json index 59e3d531638806..d5b71d4dd8f5c6 100644 --- a/tools/generators/normalize-package-dependencies/schema.json +++ b/tools/generators/normalize-package-dependencies/schema.json @@ -4,14 +4,20 @@ "id": "normalize-package-dependencies", "type": "object", "properties": { - "name": { + "projectType": { "type": "string", - "description": "Library name", - "$default": { - "$source": "argv", - "index": 0 - } + "description": "filter generator to library or application. If not specified all projects are being used", + "enum": ["application", "library", "any"], + "default": "any" + }, + "tag": { + "type": "string", + "description": "filter generator to be applied only on projects that contain provided tag" + }, + "verify": { + "type": "boolean", + "description": "Verify package.json dependencies for all projects" } }, - "required": ["name"] + "required": [] } diff --git a/tools/generators/normalize-package-dependencies/schema.ts b/tools/generators/normalize-package-dependencies/schema.ts index 347cafaed0284a..ce0310ef1bfd56 100644 --- a/tools/generators/normalize-package-dependencies/schema.ts +++ b/tools/generators/normalize-package-dependencies/schema.ts @@ -1,6 +1,14 @@ export interface NormalizePackageDependenciesGeneratorSchema { /** - * Library name + * filter generator to library or application. If not specified all projects are being used */ - name: string; + projectType?: 'application' | 'library' | 'any'; + /** + * filter generator to be applied only on projects that contain provided tag + */ + tag?: string; + /** + * Verify package.json dependencies for all projects + */ + verify?: boolean; } From b672c7127127c45cd7f7f80f7468c9ecc03a10fc Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Thu, 29 Jun 2023 19:21:06 +0200 Subject: [PATCH 03/10] fixup! feat(tools): implement normalize-package-dependencies generator --- .../index.spec.ts | 28 +++++++++++-------- .../normalize-package-dependencies/index.ts | 5 ++-- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/tools/generators/normalize-package-dependencies/index.spec.ts b/tools/generators/normalize-package-dependencies/index.spec.ts index be5dcc54851974..a3ff6c87bba5e1 100644 --- a/tools/generators/normalize-package-dependencies/index.spec.ts +++ b/tools/generators/normalize-package-dependencies/index.spec.ts @@ -1,14 +1,18 @@ import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { Tree, addProjectConfiguration, writeJson, getProjects, readJson, joinPathFragments } from '@nrwl/devkit'; +import * as chalk from 'chalk'; import generator from './index'; import { PackageJson } from '../../types'; +import { disableChalk } from '../../utils-testing'; describe('normalize-package-dependencies generator', () => { let tree: Tree; // eslint-disable-next-line @typescript-eslint/no-empty-function const noop = () => {}; - const errorLogSpy = jest.spyOn(console, 'error').mockImplementation(noop); + disableChalk(chalk); + + const logLogSpy = jest.spyOn(console, 'log').mockImplementation(noop); const infoLogSpy = jest.spyOn(console, 'info').mockImplementation(noop); beforeEach(() => { @@ -140,13 +144,13 @@ describe('normalize-package-dependencies generator', () => { `"package dependency violations found"`, ); - expect(errorLogSpy.mock.calls.flat()).toMatchInlineSnapshot(` + expect(logLogSpy.mock.calls.flat()).toMatchInlineSnapshot(` Array [ - "@proj/react-two has following dependency version issues:", - " - @proj/react-one", - "@proj/react-app has following dependency version issues:", - " - @proj/react-one", - " - @proj/react-two", + "@proj/react-two has following dependency version issues:", + " - @proj/react-one", + "@proj/react-app has following dependency version issues:", + " - @proj/react-one", + " - @proj/react-two", ] `); expect(infoLogSpy.mock.calls.flat()).toMatchInlineSnapshot(` @@ -162,11 +166,11 @@ describe('normalize-package-dependencies generator', () => { generator(tree, { verify: true, projectType: 'application' }), ).rejects.toThrowErrorMatchingInlineSnapshot(`"package dependency violations found"`); - expect(errorLogSpy.mock.calls.flat()).toMatchInlineSnapshot(` + expect(logLogSpy.mock.calls.flat()).toMatchInlineSnapshot(` Array [ - "@proj/react-app has following dependency version issues:", - " - @proj/react-one", - " - @proj/react-two", + "@proj/react-app has following dependency version issues:", + " - @proj/react-one", + " - @proj/react-two", ] `); expect(infoLogSpy.mock.calls.flat()).toMatchInlineSnapshot(` @@ -182,7 +186,7 @@ describe('normalize-package-dependencies generator', () => { undefined, ); - expect(errorLogSpy.mock.calls.flat()).toMatchInlineSnapshot(`Array []`); + expect(logLogSpy.mock.calls.flat()).toMatchInlineSnapshot(`Array []`); expect(infoLogSpy.mock.calls.flat()).toMatchInlineSnapshot(`Array []`); }); }); diff --git a/tools/generators/normalize-package-dependencies/index.ts b/tools/generators/normalize-package-dependencies/index.ts index 04a103d779b128..7634bc2b5c977b 100644 --- a/tools/generators/normalize-package-dependencies/index.ts +++ b/tools/generators/normalize-package-dependencies/index.ts @@ -12,6 +12,7 @@ import { import { NormalizePackageDependenciesGeneratorSchema } from './schema'; import { PackageJson } from '../../types'; +import * as chalk from 'chalk'; type ProjectIssues = { [projectName: string]: { [depName: string]: string } }; @@ -119,10 +120,10 @@ function reportPackageJsonDependenciesIssues(issues: ProjectIssues) { } issueEntries.forEach(([projectName, dependencyIssues]) => { - logger.error(`${projectName} has following dependency version issues:`); + logger.log(chalk.bold(chalk.red(`${projectName} has following dependency version issues:`))); // eslint-disable-next-line guard-for-in for (const dep in dependencyIssues) { - logger.error(` - ${dep}`); + logger.log(chalk.red(` - ${dep}`)); } }); From 563dffcd9251b1ff9a37a9723af1c9bf28e133f7 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Thu, 29 Jun 2023 19:21:46 +0200 Subject: [PATCH 04/10] fixup! feat(tools): implement normalize-package-dependencies generator --- .../files/constants.ts__tmpl__ | 1 - .../generators/normalize-package-dependencies/lib/.gitkeep | 0 .../normalize-package-dependencies/lib/utils.spec.ts | 7 ------- .../generators/normalize-package-dependencies/lib/utils.ts | 5 ----- 4 files changed, 13 deletions(-) delete mode 100644 tools/generators/normalize-package-dependencies/files/constants.ts__tmpl__ create mode 100644 tools/generators/normalize-package-dependencies/lib/.gitkeep delete mode 100644 tools/generators/normalize-package-dependencies/lib/utils.spec.ts delete mode 100644 tools/generators/normalize-package-dependencies/lib/utils.ts diff --git a/tools/generators/normalize-package-dependencies/files/constants.ts__tmpl__ b/tools/generators/normalize-package-dependencies/files/constants.ts__tmpl__ deleted file mode 100644 index d9913111a9dc76..00000000000000 --- a/tools/generators/normalize-package-dependencies/files/constants.ts__tmpl__ +++ /dev/null @@ -1 +0,0 @@ -export const variable = "<%= name %>"; \ No newline at end of file diff --git a/tools/generators/normalize-package-dependencies/lib/.gitkeep b/tools/generators/normalize-package-dependencies/lib/.gitkeep new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/tools/generators/normalize-package-dependencies/lib/utils.spec.ts b/tools/generators/normalize-package-dependencies/lib/utils.spec.ts deleted file mode 100644 index 91e05a54a86f53..00000000000000 --- a/tools/generators/normalize-package-dependencies/lib/utils.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { dummyHelper } from './utils'; - -describe(`utils`, () => { - it(`should behave...`, () => { - expect(dummyHelper()).toBe(undefined); - }); -}); diff --git a/tools/generators/normalize-package-dependencies/lib/utils.ts b/tools/generators/normalize-package-dependencies/lib/utils.ts deleted file mode 100644 index c340a27c29c8b0..00000000000000 --- a/tools/generators/normalize-package-dependencies/lib/utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -// use this module to define any kind of generic utilities that are used in more than 1 place within the generator implementation - -export function dummyHelper() { - return; -} From 8d58e85bc07557aa4fffa91196d1bc88adf06479 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Fri, 30 Jun 2023 10:50:58 +0200 Subject: [PATCH 05/10] fixup! fixup! feat(tools): implement normalize-package-dependencies generator --- .../normalize-package-dependencies/README.md | 25 ++++++++++++++++--- .../schema.json | 6 ++--- .../normalize-package-dependencies/schema.ts | 6 ++--- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/tools/generators/normalize-package-dependencies/README.md b/tools/generators/normalize-package-dependencies/README.md index 60097e4572a1a3..2def291f05d98b 100644 --- a/tools/generators/normalize-package-dependencies/README.md +++ b/tools/generators/normalize-package-dependencies/README.md @@ -1,13 +1,20 @@ # normalize-package-dependencies -Workspace Generator ...TODO... +Workspace Generator for package.json dependencies normalization. + +> it applies for all dependencies, devDependencies, peerDependencies + +- update mode: updates projects package.json to use \* as version for inner monorepo dependencies +- verify mode: verifies projects package.json to use \* as version for inner monorepo dependencies - [Usage](#usage) - [Examples](#examples) - [Options](#options) - - [`name`](#name) + - [`verify`](#verify) + - [`projectType`](#projecttype) + - [`tag`](#tag) @@ -31,8 +38,18 @@ yarn nx workspace-generator normalize-package-dependencies ## Options -#### `name` +#### `verify` + +Run generator in check(verification mode). Verify package.json dependencies for all projects or filtered projects (if filters are applied) + +#### `projectType` + +Type: `application | library | any` + +Filter flag. Use to apply generator execution only on projects that contain provided `projectType` within their `project.json#projectType`. + +#### `tag` Type: `string` -TODO... +Filter flag. Use to apply generator execution only on projects that contain provided tag within their `project.json#tags`. diff --git a/tools/generators/normalize-package-dependencies/schema.json b/tools/generators/normalize-package-dependencies/schema.json index d5b71d4dd8f5c6..414d61e4421627 100644 --- a/tools/generators/normalize-package-dependencies/schema.json +++ b/tools/generators/normalize-package-dependencies/schema.json @@ -6,17 +6,17 @@ "properties": { "projectType": { "type": "string", - "description": "filter generator to library or application. If not specified all projects are being used", + "description": "Filter flag. Use to apply generator execution only on projects that contain provided `projectType` within their `project.json#projectType`.", "enum": ["application", "library", "any"], "default": "any" }, "tag": { "type": "string", - "description": "filter generator to be applied only on projects that contain provided tag" + "description": "Filter flag. Use to apply generator execution only on projects that contain provided tag within their `project.json#tags`." }, "verify": { "type": "boolean", - "description": "Verify package.json dependencies for all projects" + "description": "Run generator in check(verification mode). Verify package.json dependencies for all projects or filtered projects (if filters are applied)" } }, "required": [] diff --git a/tools/generators/normalize-package-dependencies/schema.ts b/tools/generators/normalize-package-dependencies/schema.ts index ce0310ef1bfd56..5029e146bca798 100644 --- a/tools/generators/normalize-package-dependencies/schema.ts +++ b/tools/generators/normalize-package-dependencies/schema.ts @@ -1,14 +1,14 @@ export interface NormalizePackageDependenciesGeneratorSchema { /** - * filter generator to library or application. If not specified all projects are being used + * Filter flag. Use to apply generator execution only on projects that contain provided `projectType` within their `project.json#projectType`. */ projectType?: 'application' | 'library' | 'any'; /** - * filter generator to be applied only on projects that contain provided tag + * Filter flag. Use to apply generator execution only on projects that contain provided tag within their `project.json#tags`. */ tag?: string; /** - * Verify package.json dependencies for all projects + * Run generator in check(verification mode). Verify package.json dependencies for all projects or filtered projects (if filters are applied) */ verify?: boolean; } From da37bcbacfe342356eeeedc8227060cfffb3bf45 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 3 Jul 2023 17:21:42 +0200 Subject: [PATCH 06/10] feat(tools): properly resolve same npm scope packages that are installed from npm --- .../index.spec.ts | 82 ++++++++++++++++++- .../normalize-package-dependencies/index.ts | 40 ++++++--- 2 files changed, 109 insertions(+), 13 deletions(-) diff --git a/tools/generators/normalize-package-dependencies/index.spec.ts b/tools/generators/normalize-package-dependencies/index.spec.ts index a3ff6c87bba5e1..b0fd867dbf9b34 100644 --- a/tools/generators/normalize-package-dependencies/index.spec.ts +++ b/tools/generators/normalize-package-dependencies/index.spec.ts @@ -1,11 +1,38 @@ import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; -import { Tree, addProjectConfiguration, writeJson, getProjects, readJson, joinPathFragments } from '@nrwl/devkit'; +import { + Tree, + addProjectConfiguration, + writeJson, + getProjects, + readJson, + joinPathFragments, + ProjectGraph, + readProjectConfiguration, + updateJson, +} from '@nrwl/devkit'; import * as chalk from 'chalk'; import generator from './index'; import { PackageJson } from '../../types'; import { disableChalk } from '../../utils-testing'; +const graphMock: ProjectGraph = { + dependencies: {}, + nodes: {}, + externalNodes: {}, +}; + +jest.mock('@nrwl/devkit', () => { + async function createProjectGraphAsyncMock(): Promise { + return graphMock; + } + + return { + ...jest.requireActual('@nrwl/devkit'), + createProjectGraphAsync: createProjectGraphAsyncMock, + }; +}); + describe('normalize-package-dependencies generator', () => { let tree: Tree; // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -27,6 +54,11 @@ describe('normalize-package-dependencies generator', () => { deps: { dev: {}, prod: { react: '17.x.x', '@proj/react-one': '^1.0.0' }, peer: {} }, tags: ['platform:any', 'scope:two'], }); + tree = createProject(tree, { + projectName: 'react-three', + deps: { dev: {}, prod: { react: '17.x.x', '@proj/react-two': '^1.0.0' }, peer: {} }, + tags: ['platform:any', 'scope:two'], + }); tree = createProject(tree, { projectName: 'react-app', projectType: 'application', @@ -36,15 +68,23 @@ describe('normalize-package-dependencies generator', () => { }); it(`should update workspace dependencies to "*" for version`, async () => { + // this tests that dependency which has different major version (thus is installed from npm) will not be updated to '*' + addNonWorkspaceDependency(tree, '@proj/react-three', { pkgName: '@proj/react-one', pkgVersion: '^0.1.0' }); + await generator(tree, {}); - const { reactApp, reactOne, reactTwo } = getPackageJsonForAllProjects(tree); + const { reactApp, reactOne, reactTwo, reactThree } = getPackageJsonForAllProjects(tree); expect(reactOne.dependencies).toEqual({ react: '17.x.x' }); expect(reactTwo.dependencies).toEqual({ react: '17.x.x', '@proj/react-one': '*', }); + expect(reactThree.dependencies).toEqual({ + react: '17.x.x', + '@proj/react-one': '^0.1.0', + '@proj/react-two': '*', + }); expect(reactApp.dependencies).toEqual({ react: '17.x.x', '@proj/react-one': '*', @@ -148,6 +188,8 @@ describe('normalize-package-dependencies generator', () => { Array [ "@proj/react-two has following dependency version issues:", " - @proj/react-one", + "@proj/react-three has following dependency version issues:", + " - @proj/react-two", "@proj/react-app has following dependency version issues:", " - @proj/react-one", " - @proj/react-two", @@ -197,11 +239,13 @@ function getPackageJsonForAllProjects(tree: Tree) { const reactOne = projects.get('@proj/react-one'); const reactTwo = projects.get('@proj/react-two'); + const reactThree = projects.get('@proj/react-three'); const reactApp = projects.get('@proj/react-app'); return { reactOne: readJson(tree, joinPathFragments(reactOne!.root, 'package.json')), reactTwo: readJson(tree, joinPathFragments(reactTwo!.root, 'package.json')), + reactThree: readJson(tree, joinPathFragments(reactThree!.root, 'package.json')), reactApp: readJson(tree, joinPathFragments(reactApp!.root, 'package.json')), }; } @@ -232,5 +276,39 @@ function createProject( projectType, ...(tags ? { tags } : null), }); + + const depKeys = [...Object.keys(deps.prod), ...Object.keys(deps.dev), ...Object.keys(deps.peer)]; + + graphMock.dependencies[packageName] = depKeys.map(value => { + return { source: packageName, target: value, type: 'static' }; + }); + graphMock.nodes[packageName] = { + name: packageName, + type: projectType === 'library' ? 'lib' : 'app', + data: { name: packageName, root: rootPath, files: [] }, + }; + + return tree; +} + +function addNonWorkspaceDependency( + tree: Tree, + projectName: string, + dependency: { pkgName: string; pkgVersion: string }, +) { + const project = readProjectConfiguration(tree, projectName); + updateJson(tree, joinPathFragments(project.root, 'package.json'), json => { + json.dependencies = json.dependencies ?? {}; + json.dependencies[dependency.pkgName] = dependency.pkgVersion; + + return json; + }); + + graphMock.dependencies[projectName].push({ + source: projectName, + target: `npm:${dependency.pkgName}`, + type: 'static', + }); + return tree; } diff --git a/tools/generators/normalize-package-dependencies/index.ts b/tools/generators/normalize-package-dependencies/index.ts index 7634bc2b5c977b..1b46679be8d582 100644 --- a/tools/generators/normalize-package-dependencies/index.ts +++ b/tools/generators/normalize-package-dependencies/index.ts @@ -5,9 +5,10 @@ import { ProjectConfiguration, updateJson, joinPathFragments, - readProjectConfiguration, readJson, logger, + createProjectGraphAsync, + ProjectGraph, } from '@nrwl/devkit'; import { NormalizePackageDependenciesGeneratorSchema } from './schema'; @@ -21,6 +22,8 @@ type NormalizedSchema = ReturnType; export default async function (tree: Tree, schema: NormalizePackageDependenciesGeneratorSchema) { const normalizedOptions = normalizeOptions(tree, schema); + const graph = await createProjectGraphAsync(); + const filters = getActiveFilters(normalizedOptions); const projects = getProjects(tree); const issues: ProjectIssues = {}; @@ -31,7 +34,7 @@ export default async function (tree: Tree, schema: NormalizePackageDependenciesG } if (normalizedOptions.verify) { - const foundIssues = getPackageJsonDependenciesIssues(tree, projectConfig); + const foundIssues = getPackageJsonDependenciesIssues(tree, projectConfig, graph); if (foundIssues) { issues[projectConfig.name!] = foundIssues; @@ -39,7 +42,7 @@ export default async function (tree: Tree, schema: NormalizePackageDependenciesG return; } - normalizePackageJsonDependencies(tree, projectConfig); + normalizePackageJsonDependencies(tree, projectConfig, graph); }); reportPackageJsonDependenciesIssues(issues); @@ -86,7 +89,9 @@ function shouldBeProjectProcessed(projectConfig: ProjectConfiguration, filters: return true; } -function normalizePackageJsonDependencies(tree: Tree, projectConfig: ProjectConfiguration) { +function normalizePackageJsonDependencies(tree: Tree, projectConfig: ProjectConfiguration, graph: ProjectGraph) { + // const projectDependencies = getDependentPackagesForProject(graph, projectConfig.name!); + const projectDependencies = getProjectDependenciesFromGraph(projectConfig.name!, graph); const packageJsonPath = joinPathFragments(projectConfig.root, 'package.json'); updateJson(tree, packageJsonPath, json => { @@ -105,7 +110,7 @@ function normalizePackageJsonDependencies(tree: Tree, projectConfig: ProjectConf } for (const packageName in deps) { - if (isWorkspaceProject(tree, packageName)) { + if (isProjectDependencyAnWorkspaceProject(graph, packageName, projectDependencies)) { deps[packageName] = '*'; } } @@ -136,7 +141,10 @@ function reportPackageJsonDependenciesIssues(issues: ProjectIssues) { function getPackageJsonDependenciesIssues( tree: Tree, projectConfig: ProjectConfiguration, + graph: ProjectGraph, ): Record | null { + // const projectDependencies = getDependentPackagesForProject(graph, projectConfig.name!); + const projectDependencies = getProjectDependenciesFromGraph(projectConfig.name!, graph); const packageJsonPath = joinPathFragments(projectConfig.root, 'package.json'); const packageJson = readJson(tree, packageJsonPath); @@ -154,7 +162,7 @@ function getPackageJsonDependenciesIssues( } for (const packageName in deps) { - if (isWorkspaceProject(tree, packageName) && deps[packageName] !== '*') { + if (isProjectDependencyAnWorkspaceProject(graph, packageName, projectDependencies) && deps[packageName] !== '*') { issues = issues ?? {}; issues[packageName] = deps[packageName]; } @@ -164,13 +172,23 @@ function getPackageJsonDependenciesIssues( } } -function isWorkspaceProject(tree: Tree, projectName: string): boolean { - try { - readProjectConfiguration(tree, projectName); - return true; - } catch { +function getProjectDependenciesFromGraph(projectName: string, graph: ProjectGraph): string[] { + const projectDeps = graph.dependencies[projectName]; + return projectDeps.map(value => value.target); +} + +function isProjectDependencyAnWorkspaceProject( + graph: ProjectGraph, + dependencyName: string, + projectDependenciesFromGraph: string[], +) { + const projectDeps = graph.dependencies[dependencyName]; + + if (!projectDeps) { return false; } + + return projectDependenciesFromGraph.indexOf(dependencyName) !== -1; } function normalizeOptions(tree: Tree, schema: NormalizePackageDependenciesGeneratorSchema) { From 77281d29414a19ab1e2acb83b4eb0f73ad4d21e2 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 3 Jul 2023 17:26:18 +0200 Subject: [PATCH 07/10] fixup! feat(tools): properly resolve same npm scope packages that are installed from npm --- tools/generators/normalize-package-dependencies/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/generators/normalize-package-dependencies/index.ts b/tools/generators/normalize-package-dependencies/index.ts index 1b46679be8d582..7bf6efa4da55d0 100644 --- a/tools/generators/normalize-package-dependencies/index.ts +++ b/tools/generators/normalize-package-dependencies/index.ts @@ -143,7 +143,6 @@ function getPackageJsonDependenciesIssues( projectConfig: ProjectConfiguration, graph: ProjectGraph, ): Record | null { - // const projectDependencies = getDependentPackagesForProject(graph, projectConfig.name!); const projectDependencies = getProjectDependenciesFromGraph(projectConfig.name!, graph); const packageJsonPath = joinPathFragments(projectConfig.root, 'package.json'); const packageJson = readJson(tree, packageJsonPath); From 05d9adc7b0c06a71591b022b3a5b6ac1c1023efb Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 3 Jul 2023 17:27:16 +0200 Subject: [PATCH 08/10] fixup! feat(tools): properly resolve same npm scope packages that are installed from npm --- tools/generators/normalize-package-dependencies/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/generators/normalize-package-dependencies/index.ts b/tools/generators/normalize-package-dependencies/index.ts index 7bf6efa4da55d0..f84f241144f371 100644 --- a/tools/generators/normalize-package-dependencies/index.ts +++ b/tools/generators/normalize-package-dependencies/index.ts @@ -90,7 +90,6 @@ function shouldBeProjectProcessed(projectConfig: ProjectConfiguration, filters: } function normalizePackageJsonDependencies(tree: Tree, projectConfig: ProjectConfiguration, graph: ProjectGraph) { - // const projectDependencies = getDependentPackagesForProject(graph, projectConfig.name!); const projectDependencies = getProjectDependenciesFromGraph(projectConfig.name!, graph); const packageJsonPath = joinPathFragments(projectConfig.root, 'package.json'); From 87781d040b15e96ae60489761a13804a7779dad2 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Tue, 4 Jul 2023 12:54:25 +0200 Subject: [PATCH 09/10] feat(tools): run normalize-package-deps on peerDeps as well --- tools/generators/normalize-package-dependencies/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/generators/normalize-package-dependencies/index.ts b/tools/generators/normalize-package-dependencies/index.ts index f84f241144f371..b7f50549348af1 100644 --- a/tools/generators/normalize-package-dependencies/index.ts +++ b/tools/generators/normalize-package-dependencies/index.ts @@ -96,6 +96,7 @@ function normalizePackageJsonDependencies(tree: Tree, projectConfig: ProjectConf updateJson(tree, packageJsonPath, json => { updateDepType(json, 'dependencies'); updateDepType(json, 'devDependencies'); + updateDepType(json, 'peerDependencies'); return json; }); From e23ee5e6eb2e8c7d1a11bfea4358a62fe526eb78 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Tue, 4 Jul 2023 12:59:38 +0200 Subject: [PATCH 10/10] refactor(tools): simplify filter processing --- tools/generators/normalize-package-dependencies/index.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tools/generators/normalize-package-dependencies/index.ts b/tools/generators/normalize-package-dependencies/index.ts index b7f50549348af1..bffd5217903420 100644 --- a/tools/generators/normalize-package-dependencies/index.ts +++ b/tools/generators/normalize-package-dependencies/index.ts @@ -70,7 +70,6 @@ function getActiveFilters(options: NormalizedSchema) { return { hasActive: activeCount > 0, - hasActiveMultiple: activeCount > 1, filters: active, }; } @@ -78,12 +77,8 @@ function getActiveFilters(options: NormalizedSchema) { function shouldBeProjectProcessed(projectConfig: ProjectConfiguration, filters: ReturnType) { const filterPredicates = Object.values(filters.filters); - if (filters.hasActiveMultiple) { - return filterPredicates.every(predicate => predicate(projectConfig)); - } - if (filters.hasActive) { - return filterPredicates.some(predicate => predicate(projectConfig)); + return filterPredicates.every(predicate => predicate(projectConfig)); } return true;