diff --git a/tools/generators/star-dev-deps/README.md b/tools/generators/star-dev-deps/README.md new file mode 100644 index 00000000000000..dadc74515f9dec --- /dev/null +++ b/tools/generators/star-dev-deps/README.md @@ -0,0 +1,39 @@ +# star-dev-deps + +For all packages in the monorepo, replace certain dev dependencies on other internal packages in the monorepo with the `*` +version. + +The dependencies to be updated can be configured in `config.ts` + + + +- [NOTES](#notes) +- [Usage](#usage) + - [Examples](#examples) + + + +## NOTES + +- Only affects dev dependencies that are also published from the monorepo +- Does not affect third party dependencies +- Will run for every package in the monorepo +- Only updates dependencies listed in `config.ts` + +## Usage + +```sh +yarn nx workspace-generator star-dev-deps +``` + +Show what will be generated without writing to disk: + +```sh +yarn nx workspace-generator star-dev-deps --dry-run +``` + +### Examples + +```sh +yarn nx workspace-generator star-dev-deps +``` diff --git a/tools/generators/star-dev-deps/config.ts b/tools/generators/star-dev-deps/config.ts new file mode 100644 index 00000000000000..70ffd3e5ad8b70 --- /dev/null +++ b/tools/generators/star-dev-deps/config.ts @@ -0,0 +1 @@ +export const dependenciesToUpdate = ['@fluentui/eslint-plugin', '@fluentui/react-conformance']; diff --git a/tools/generators/star-dev-deps/index.spec.ts b/tools/generators/star-dev-deps/index.spec.ts new file mode 100644 index 00000000000000..098092b210ad49 --- /dev/null +++ b/tools/generators/star-dev-deps/index.spec.ts @@ -0,0 +1,160 @@ +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { + Tree, + readProjectConfiguration, + addProjectConfiguration, + serializeJson, + readWorkspaceConfiguration, + readJson, +} from '@nrwl/devkit'; + +import generator from './index'; +import { StarDevDepsGeneratorSchema } from './schema'; + +describe('star-dev-deps generator', () => { + let tree: Tree; + const options: StarDevDepsGeneratorSchema = { name: 'test' }; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + setupDummyPackage(tree, { + packageName: '@proj/eslint-plugin', + version: '1.0.0', + }); + }); + + it('should change internal package devdeps to * version', async () => { + const devPkgName = '@proj/eslint-plugin'; + + setupDummyPackage(tree, { + packageName: '@proj/react-button', + devDependencies: { [devPkgName]: '1.0.0' }, + }); + + await generator(tree); + const pkgJson = readJson(tree, 'packages/react-button/package.json'); + expect(pkgJson.devDependencies[devPkgName]).toBe('*'); + }); + + it('should run for all packages in the monorepo', async () => { + const devPkgName = '@proj/eslint-plugin'; + + setupDummyPackage(tree, { + packageName: '@proj/react-button', + devDependencies: { [devPkgName]: '1.0.0' }, + }); + + setupDummyPackage(tree, { + packageName: '@proj/react-avatar', + devDependencies: { [devPkgName]: '1.0.0' }, + }); + + setupDummyPackage(tree, { + packageName: '@proj/react-menu', + devDependencies: { [devPkgName]: '1.0.0' }, + }); + + await generator(tree); + + ['react-button', 'react-avatar', 'react-menu'].forEach(name => { + const path = `packages/${name}/package.json`; + const pkgJson = readJson(tree, path); + expect(pkgJson.devDependencies[devPkgName]).toBe('*'); + }); + }); + + it('should also modify non-v9 packages', async () => { + const devPkgName = '@proj/eslint-plugin'; + + setupDummyPackage(tree, { + packageName: '@proj/react-button', + version: '1.0.0', + devDependencies: { [devPkgName]: '1.0.0' }, + }); + + await generator(tree); + const pkgJson = readJson(tree, 'packages/react-button/package.json'); + expect(pkgJson.devDependencies[devPkgName]).toBe('*'); + }); + + it('should only modify dev dependencies', async () => { + const devPkgName = '@proj/eslint-plugin'; + const expectedVersion = '1.0.0'; + + setupDummyPackage(tree, { + packageName: '@proj/react-button', + version: '9.0.0', + dependencies: { [devPkgName]: expectedVersion }, + }); + + await generator(tree); + const pkgJson = readJson(tree, 'packages/react-button/package.json'); + expect(pkgJson.dependencies[devPkgName]).toBe(expectedVersion); + }); + + it('should not modify external dev dependencies', async () => { + const devPkgName = 'jest'; + const expectedVersion = '1.0.0'; + + setupDummyPackage(tree, { + packageName: '@proj/react-button', + version: '9.0.0', + dependencies: { [devPkgName]: expectedVersion }, + }); + + await generator(tree); + const pkgJson = readJson(tree, 'packages/react-button/package.json'); + expect(pkgJson.dependencies[devPkgName]).toBe(expectedVersion); + }); +}); + +function setupDummyPackage( + tree: Tree, + options: Partial<{ + packageName: string; + version: string; + devDependencies: Record; + dependencies: Record; + projectConfiguration: Partial>; + }>, +) { + const workspaceConfig = readWorkspaceConfiguration(tree); + const defaults = { + version: '9.0.0-alpha.40', + dependencies: { + [`@${workspaceConfig.npmScope}/react-make-styles`]: '^9.0.0-alpha.38', + [`@${workspaceConfig.npmScope}/react-theme`]: '^9.0.0-alpha.13', + [`@${workspaceConfig.npmScope}/react-utilities`]: '^9.0.0-alpha.25', + tslib: '^2.1.0', + someThirdPartyDep: '^11.1.2', + }, + }; + + const normalizedOptions = { ...defaults, ...options }; + const pkgName = options.packageName || ''; + const normalizedPkgName = pkgName.replace(`@${workspaceConfig.npmScope}/`, ''); + const paths = { + root: `packages/${normalizedPkgName}`, + }; + + const templates = { + packageJson: { + name: pkgName, + version: normalizedOptions.version, + dependencies: normalizedOptions.dependencies, + devDependencies: normalizedOptions.devDependencies, + }, + }; + + tree.write(`${paths.root}/package.json`, serializeJson(templates.packageJson)); + + addProjectConfiguration(tree, pkgName, { + root: paths.root, + projectType: 'library', + targets: {}, + tags: ['platform:web'], + ...options.projectConfiguration, + }); + + return tree; +} diff --git a/tools/generators/star-dev-deps/index.ts b/tools/generators/star-dev-deps/index.ts new file mode 100644 index 00000000000000..bb106d8528fdbf --- /dev/null +++ b/tools/generators/star-dev-deps/index.ts @@ -0,0 +1,67 @@ +import { Tree, updateJson, getProjects, formatFiles, readWorkspaceConfiguration, readJson } from '@nrwl/devkit'; +import { getProjectConfig, printUserLogs, UserLog } from '../../utils'; +import { PackageJson } from '../../types'; +import { dependenciesToUpdate } from './config'; + +export default async function (host: Tree) { + const userLog: UserLog = []; + + runMigration(host, userLog); + + formatFiles(host); + + return () => { + printUserLogs(userLog); + }; +} + +function runMigrationOnProject(host: Tree, packageName: string, userLog: UserLog) { + const projectConfig = getProjectConfig(host, { packageName }); + const packageJsonPath = projectConfig.paths.packageJson; + + updateJson(host, packageJsonPath, (packageJson: PackageJson) => { + if (packageJson.devDependencies) { + Object.keys(packageJson.devDependencies).forEach(dependency => { + if ( + isPackageInMonorepo(dependency, host) && + packageJson.devDependencies && + dependenciesToUpdate.includes(dependency) + ) { + userLog.push({ + type: 'info', + message: `Updating dev dependency ${dependency} in ${packageName}`, + }); + packageJson.devDependencies[dependency] = '*'; + } + }); + } + + return packageJson; + }); +} + +function runMigration(host: Tree, userLog: UserLog) { + const projects = getProjects(host); + + projects.forEach((project, projectName) => { + runMigrationOnProject(host, projectName, userLog); + }); +} + +/** + * @returns whether the packageName is internally in the monorepo + */ +function isPackageInMonorepo(packageName: string, host: Tree) { + let config: ReturnType; + try { + config = getProjectConfig(host, { packageName }); + } catch (err) { + if (!(err as Error).message.startsWith('Cannot find configuration for')) { + throw err; + } + + return false; + } + + return true; +} diff --git a/tools/generators/star-dev-deps/schema.json b/tools/generators/star-dev-deps/schema.json new file mode 100644 index 00000000000000..1e8340e9594658 --- /dev/null +++ b/tools/generators/star-dev-deps/schema.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "id": "star-dev-deps", + "type": "object", + "properties": {}, + "required": [] +} diff --git a/tools/generators/star-dev-deps/schema.ts b/tools/generators/star-dev-deps/schema.ts new file mode 100644 index 00000000000000..014c504d2054ad --- /dev/null +++ b/tools/generators/star-dev-deps/schema.ts @@ -0,0 +1 @@ +export interface StarDevDepsGeneratorSchema {}