From 81f2d1945683c75e6f3920355115dc9de9c5ffb4 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Thu, 19 Jan 2023 17:14:57 +0100 Subject: [PATCH 1/3] feat(tools): add svp generator boilerplate --- tools/generators/svp/README.md | 38 ++++++++++++++ .../generators/svp/files/constants.ts__tmpl__ | 1 + tools/generators/svp/index.spec.ts | 20 ++++++++ tools/generators/svp/index.ts | 50 +++++++++++++++++++ tools/generators/svp/lib/utils.spec.ts | 7 +++ tools/generators/svp/lib/utils.ts | 5 ++ tools/generators/svp/schema.json | 17 +++++++ tools/generators/svp/schema.ts | 6 +++ tsconfig.base.json | 4 +- workspace.json | 14 +++--- 10 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 tools/generators/svp/README.md create mode 100644 tools/generators/svp/files/constants.ts__tmpl__ create mode 100644 tools/generators/svp/index.spec.ts create mode 100644 tools/generators/svp/index.ts create mode 100644 tools/generators/svp/lib/utils.spec.ts create mode 100644 tools/generators/svp/lib/utils.ts create mode 100644 tools/generators/svp/schema.json create mode 100644 tools/generators/svp/schema.ts diff --git a/tools/generators/svp/README.md b/tools/generators/svp/README.md new file mode 100644 index 00000000000000..9a38d547d32af6 --- /dev/null +++ b/tools/generators/svp/README.md @@ -0,0 +1,38 @@ +# svp + +Workspace Generator ...TODO... + + + +- [Usage](#usage) + - [Examples](#examples) +- [Options](#options) + - [`name`](#name) + + + +## Usage + +```sh +yarn nx workspace-generator svp ... +``` + +Show what will be generated without writing to disk: + +```sh +yarn nx workspace-generator svp --dry-run +``` + +### Examples + +```sh +yarn nx workspace-generator svp +``` + +## Options + +#### `name` + +Type: `string` + +TODO... diff --git a/tools/generators/svp/files/constants.ts__tmpl__ b/tools/generators/svp/files/constants.ts__tmpl__ new file mode 100644 index 00000000000000..d9913111a9dc76 --- /dev/null +++ b/tools/generators/svp/files/constants.ts__tmpl__ @@ -0,0 +1 @@ +export const variable = "<%= name %>"; \ No newline at end of file diff --git a/tools/generators/svp/index.spec.ts b/tools/generators/svp/index.spec.ts new file mode 100644 index 00000000000000..b3f4798fb7173c --- /dev/null +++ b/tools/generators/svp/index.spec.ts @@ -0,0 +1,20 @@ +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { Tree, readProjectConfiguration } from '@nrwl/devkit'; + +import generator from './index'; +import { SvpGeneratorSchema } from './schema'; + +describe('svp generator', () => { + let appTree: Tree; + const options: SvpGeneratorSchema = { 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/svp/index.ts b/tools/generators/svp/index.ts new file mode 100644 index 00000000000000..f713be8068a931 --- /dev/null +++ b/tools/generators/svp/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 { SvpGeneratorSchema } from './schema'; + +interface NormalizedSchema extends ReturnType {} + +export default async function (tree: Tree, schema: SvpGeneratorSchema) { + 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: SvpGeneratorSchema) { + 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/svp/lib/utils.spec.ts b/tools/generators/svp/lib/utils.spec.ts new file mode 100644 index 00000000000000..91e05a54a86f53 --- /dev/null +++ b/tools/generators/svp/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/svp/lib/utils.ts b/tools/generators/svp/lib/utils.ts new file mode 100644 index 00000000000000..c340a27c29c8b0 --- /dev/null +++ b/tools/generators/svp/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/svp/schema.json b/tools/generators/svp/schema.json new file mode 100644 index 00000000000000..11c4d27ddd6b48 --- /dev/null +++ b/tools/generators/svp/schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "id": "svp", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Library name", + "$default": { + "$source": "argv", + "index": 0 + } + } + }, + "required": ["name"] +} diff --git a/tools/generators/svp/schema.ts b/tools/generators/svp/schema.ts new file mode 100644 index 00000000000000..ec2e8699df2199 --- /dev/null +++ b/tools/generators/svp/schema.ts @@ -0,0 +1,6 @@ +export interface SvpGeneratorSchema { + /** + * Library name + */ + name: string; +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 1d9c854543a1f3..adfec4aecf465a 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -77,9 +77,9 @@ "@fluentui/react-tooltip": ["packages/react-components/react-tooltip/src/index.ts"], "@fluentui/react-tree": ["packages/react-components/react-tree/src/index.ts"], "@fluentui/react-utilities": ["packages/react-components/react-utilities/src/index.ts"], + "@fluentui/react-virtualizer": ["packages/react-components/react-virtualizer/src/index.ts"], "@fluentui/theme-designer": ["packages/react-components/theme-designer/src/index.ts"], - "@fluentui/tokens": ["packages/tokens/src/index.ts"], - "@fluentui/react-virtualizer": ["packages/react-components/react-virtualizer/src/index.ts"] + "@fluentui/tokens": ["packages/tokens/src/index.ts"] } }, "exclude": ["node_modules"] diff --git a/workspace.json b/workspace.json index ae4b7b176c2abc..98c790aed8961e 100644 --- a/workspace.json +++ b/workspace.json @@ -822,6 +822,13 @@ "tags": ["vNext", "platform:web"], "implicitDependencies": [] }, + "@fluentui/react-virtualizer": { + "root": "packages/react-components/react-virtualizer", + "projectType": "library", + "implicitDependencies": [], + "sourceRoot": "packages/react-components/react-virtualizer/src", + "tags": ["vNext", "platform:web"] + }, "@fluentui/react-window-provider": { "root": "packages/react-window-provider", "projectType": "library", @@ -1077,13 +1084,6 @@ "projectType": "library", "implicitDependencies": [] }, - "@fluentui/react-virtualizer": { - "root": "packages/react-components/react-virtualizer", - "projectType": "library", - "implicitDependencies": [], - "sourceRoot": "packages/react-components/react-virtualizer/src", - "tags": ["vNext", "platform:web"] - }, "@fluentui/vr-tests": { "root": "apps/vr-tests", "projectType": "application", From 198da15e6d6a6dad81e29aa1ec58671fae07adb8 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Thu, 19 Jan 2023 18:30:55 +0100 Subject: [PATCH 2/3] feat(tools): implement svp generator --- tools/generators/svp/index.ts | 126 +++++++++++++++++++++++-------- tools/generators/svp/schema.json | 13 +--- tools/generators/svp/schema.ts | 2 +- 3 files changed, 98 insertions(+), 43 deletions(-) diff --git a/tools/generators/svp/index.ts b/tools/generators/svp/index.ts index f713be8068a931..3e0c9166fb0d62 100644 --- a/tools/generators/svp/index.ts +++ b/tools/generators/svp/index.ts @@ -1,19 +1,55 @@ -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, + installPackagesTask, + getProjects, + readJson, + joinPathFragments, + writeJson, + readProjectConfiguration, + updateJson, +} from '@nrwl/devkit'; import { SvpGeneratorSchema } from './schema'; +import { PackageJson } from '../../types'; + +type DepMap = Record }>; + +export default async function (tree: Tree, _schema: SvpGeneratorSchema) { + const rootPackageJson = readJson(tree, 'package.json'); + const rootDevDeps = rootPackageJson.devDependencies ?? {}; + + const normalizedDevDepsMap = parsePackages(tree); + const { packagesToRoot, result } = normalizePackages(tree, normalizedDevDepsMap); + + writeJson(tree, 'debug-svp.json', normalizedDevDepsMap); + writeJson(tree, 'debug-svp.norm.json', result); -interface NormalizedSchema extends ReturnType {} + const normalizedPackagesToRoot = Object.entries(packagesToRoot).reduce((acc, [packageName, versions]) => { + acc[packageName] = versions[0].replace(/[\^~]/, ''); + return acc; + }, {} as Record); -export default async function (tree: Tree, schema: SvpGeneratorSchema) { - await libraryGenerator(tree, { name: schema.name }); + const newRootDevDeps = { ...rootDevDeps, ...normalizedPackagesToRoot }; - const normalizedOptions = normalizeOptions(tree, schema); + writeJson(tree, 'debug-root.json', rootDevDeps); + writeJson(tree, 'debug-root.norm.json', newRootDevDeps); + writeJson(tree, 'debug-root.extracted.json', packagesToRoot); - addFiles(tree, normalizedOptions); + // update package.json + updateJson(tree, 'package.json', (json: PackageJson) => { + json.devDependencies = newRootDevDeps; + return json; + }); + + // update packages + Object.entries(normalizedDevDepsMap).forEach(([projectName, deps]) => { + const project = readProjectConfiguration(tree, projectName); + updateJson(tree, joinPathFragments(project.root, 'package.json'), (json: PackageJson) => { + json.devDependencies = deps.dev; + return json; + }); + }); await formatFiles(tree); @@ -22,29 +58,57 @@ export default async function (tree: Tree, schema: SvpGeneratorSchema) { }; } -function normalizeOptions(tree: Tree, options: SvpGeneratorSchema) { - const project = getProjectConfig(tree, { packageName: options.name }); +function isWorkspaceDep(tree: Tree, projectName: string) { + try { + readProjectConfiguration(tree, projectName); + return true; + } catch (err) { + return false; + } +} + +function normalizePackages(tree: Tree, map: DepMap) { + const packagesToRoot: Record> = {}; + const result = Object.entries(map).reduce((acc, [projectName, deps]) => { + const { dev } = deps; - return { - ...options, - ...project, - ...names(options.name), - }; + const normalizedDev = Object.entries(dev).reduce((acc2, [depName, depVersion]) => { + if (depVersion === '*') { + acc2[depName] = '*'; + return acc2; + } + + if (isWorkspaceDep(tree, depName)) { + acc2[depName] = '*'; + return acc2; + } + + packagesToRoot[depName] = packagesToRoot[depName] ?? new Set(); + packagesToRoot[depName].add(depVersion); + + return acc2; + }, {} as Record); + + acc[projectName].dev = normalizedDev; + return acc; + }, map); + + const packagesToRootSerialized = Object.entries(packagesToRoot).reduce((acc, [packageName, versions]) => { + acc[packageName] = [...versions]; + return acc; + }, {} as Record); + + return { result, packagesToRoot: packagesToRootSerialized }; } -/** - * NOTE: remove this if your generator doesn't process any static/dynamic templates - */ -function addFiles(tree: Tree, options: NormalizedSchema) { - const templateOptions = { - ...options, - tmpl: '', - }; +function parsePackages(tree: Tree) { + const projects = getProjects(tree); + + const data: DepMap = {}; + projects.forEach((project, projectName) => { + const pkgJson = readJson(tree, joinPathFragments(project.root, 'package.json')); + data[projectName] = { dev: pkgJson.devDependencies ?? {} }; + }); - generateFiles( - tree, - path.join(__dirname, 'files'), - path.join(options.projectConfig.root, options.name), - templateOptions, - ); + return data; } diff --git a/tools/generators/svp/schema.json b/tools/generators/svp/schema.json index 11c4d27ddd6b48..cb1b82a0f91104 100644 --- a/tools/generators/svp/schema.json +++ b/tools/generators/svp/schema.json @@ -3,15 +3,6 @@ "cli": "nx", "id": "svp", "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Library name", - "$default": { - "$source": "argv", - "index": 0 - } - } - }, - "required": ["name"] + "properties": {}, + "required": [] } diff --git a/tools/generators/svp/schema.ts b/tools/generators/svp/schema.ts index ec2e8699df2199..0ee04532f697d5 100644 --- a/tools/generators/svp/schema.ts +++ b/tools/generators/svp/schema.ts @@ -2,5 +2,5 @@ export interface SvpGeneratorSchema { /** * Library name */ - name: string; + // name: string; } From 7c712f52f27027061530111bab710ed2cb5e4104 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Fri, 20 Jan 2023 16:42:22 +0100 Subject: [PATCH 3/3] fixup! feat(tools): implement svp generator --- tools/generators/svp/index.ts | 181 ++++++++++++++++++++++++++++------ 1 file changed, 152 insertions(+), 29 deletions(-) diff --git a/tools/generators/svp/index.ts b/tools/generators/svp/index.ts index 3e0c9166fb0d62..9be694e7f7e8c4 100644 --- a/tools/generators/svp/index.ts +++ b/tools/generators/svp/index.ts @@ -12,12 +12,23 @@ import { import { SvpGeneratorSchema } from './schema'; import { PackageJson } from '../../types'; +import * as semver from 'semver'; -type DepMap = Record }>; +type DepMap = Record< + string, + { prod: Record; dev: Record; peer: Record } +>; + +type RootDepMap = { prod: Record; dev: Record }; + +type RootDepMapNormalized = { + dev: Record; + prod: Record; +}; export default async function (tree: Tree, _schema: SvpGeneratorSchema) { const rootPackageJson = readJson(tree, 'package.json'); - const rootDevDeps = rootPackageJson.devDependencies ?? {}; + const rootDeps = { dev: rootPackageJson.devDependencies ?? {}, prod: rootPackageJson.dependencies ?? {} }; const normalizedDevDepsMap = parsePackages(tree); const { packagesToRoot, result } = normalizePackages(tree, normalizedDevDepsMap); @@ -25,31 +36,52 @@ export default async function (tree: Tree, _schema: SvpGeneratorSchema) { writeJson(tree, 'debug-svp.json', normalizedDevDepsMap); writeJson(tree, 'debug-svp.norm.json', result); - const normalizedPackagesToRoot = Object.entries(packagesToRoot).reduce((acc, [packageName, versions]) => { - acc[packageName] = versions[0].replace(/[\^~]/, ''); - return acc; - }, {} as Record); + const dedupedRootDeps = dedupeRoot(packagesToRoot); + const merged = dedupeRoot(mergeDepMaps(rootDeps, dedupedRootDeps)); + const semverResolved = resolveVersions(merged); - const newRootDevDeps = { ...rootDevDeps, ...normalizedPackagesToRoot }; - - writeJson(tree, 'debug-root.json', rootDevDeps); - writeJson(tree, 'debug-root.norm.json', newRootDevDeps); + writeJson(tree, 'debug-root.json', rootDeps); writeJson(tree, 'debug-root.extracted.json', packagesToRoot); - - // update package.json - updateJson(tree, 'package.json', (json: PackageJson) => { - json.devDependencies = newRootDevDeps; - return json; - }); - - // update packages - Object.entries(normalizedDevDepsMap).forEach(([projectName, deps]) => { - const project = readProjectConfiguration(tree, projectName); - updateJson(tree, joinPathFragments(project.root, 'package.json'), (json: PackageJson) => { - json.devDependencies = deps.dev; + // writeJson(tree, 'debug-root.norm.json', newRootDeps); + writeJson(tree, 'debug-root.deduped.json', dedupedRootDeps); + writeJson(tree, 'debug-root.deduped.merged.json', merged); + writeJson(tree, 'debug-root.deduped.merged.semver-resolved.json', semverResolved); + + // _updatePackages(); + + function _updatePackages() { + const normalizedPackagesToRoot = { + dev: Object.entries(packagesToRoot.dev).reduce((acc, [packageName, versions]) => { + acc[packageName] = versions[0].replace(/[\^~]/, ''); + return acc; + }, {} as Record), + prod: Object.entries(packagesToRoot.prod).reduce((acc, [packageName, versions]) => { + acc[packageName] = versions[0].replace(/[\^~]/, ''); + return acc; + }, {} as Record), + }; + + const newRootDeps: RootDepMap = { + prod: { ...rootDeps.prod, ...normalizedPackagesToRoot.prod }, + dev: { ...rootDeps.dev, ...normalizedPackagesToRoot.dev }, + }; + + // update package.json + updateJson(tree, 'package.json', (json: PackageJson) => { + json.devDependencies = newRootDeps.dev; + json.dependencies = newRootDeps.prod; return json; }); - }); + + // update packages + Object.entries(normalizedDevDepsMap).forEach(([projectName, deps]) => { + const project = readProjectConfiguration(tree, projectName); + updateJson(tree, joinPathFragments(project.root, 'package.json'), (json: PackageJson) => { + json.devDependencies = deps.dev; + return json; + }); + }); + } await formatFiles(tree); @@ -58,6 +90,76 @@ export default async function (tree: Tree, _schema: SvpGeneratorSchema) { }; } +function mergeDepMaps(current: RootDepMap, extracted: RootDepMapNormalized) { + return { + prod: mergeMap(current.prod, extracted.prod), + dev: mergeMap(current.dev, extracted.dev), + }; + + function mergeMap(curr: Record, extr: Record): Record { + const allKeys = [...Object.keys(curr), ...Object.keys(extr)]; + return allKeys.reduce((acc, pkgName) => { + const rootValue = curr[pkgName] ? [curr[pkgName]] : []; + const extractedValue = extr[pkgName] ? extr[pkgName] : []; + acc[pkgName] = [...rootValue, ...extractedValue]; + return acc; + }, {} as Record); + } +} + +function resolveVersions(deps: RootDepMapNormalized) { + return { + prod: resolve(deps.prod), + dev: resolve(deps.dev), + }; + + function resolve(val: Record) { + return Object.entries(val).reduce( + (acc, [pkgName, pkgVersions]) => { + acc[pkgName] = [getLatest(pkgVersions)]; + return acc; + }, + { ...val }, + ); + } + + function getLatest(versions: string[]): string { + const minVersions = versions.map(version => semver.minVersion(version)?.version) as string[]; + const sorted = minVersions.sort(semver.rcompare); + return sorted[0]; + } +} +function dedupeRoot(deps: RootDepMapNormalized) { + // devDeps in root have higher priority as deps might use ^ and lower version in range + const newDevDeps: RootDepMapNormalized['dev'] = Object.entries(deps.dev).reduce( + (acc, [pkgName, pkgVersions]) => { + acc[pkgName] = unique(pkgVersions); + return acc; + }, + { ...deps.dev }, + ); + const newProdDeps = Object.entries(deps.prod).reduce((acc, [pkgName, pkgVersion]) => { + const devDep = newDevDeps[pkgName]; + + if (devDep) { + newDevDeps[pkgName].push(...pkgVersion); + newDevDeps[pkgName] = unique(newDevDeps[pkgName]); + + return acc; + } + + acc[pkgName] = pkgVersion; + + return acc; + }, {} as Record); + + return { prod: newProdDeps, dev: newDevDeps }; +} + +function unique(arr: T) { + return [...new Set(arr)] as T; +} + function isWorkspaceDep(tree: Tree, projectName: string) { try { readProjectConfiguration(tree, projectName); @@ -68,9 +170,10 @@ function isWorkspaceDep(tree: Tree, projectName: string) { } function normalizePackages(tree: Tree, map: DepMap) { - const packagesToRoot: Record> = {}; + const packagesToRoot: { dev: Record>; prod: Record> } = { dev: {}, prod: {} }; + const result = Object.entries(map).reduce((acc, [projectName, deps]) => { - const { dev } = deps; + const { dev, prod } = deps; const normalizedDev = Object.entries(dev).reduce((acc2, [depName, depVersion]) => { if (depVersion === '*') { @@ -83,20 +186,36 @@ function normalizePackages(tree: Tree, map: DepMap) { return acc2; } - packagesToRoot[depName] = packagesToRoot[depName] ?? new Set(); - packagesToRoot[depName].add(depVersion); + packagesToRoot.dev[depName] = packagesToRoot.dev[depName] ?? new Set(); + packagesToRoot.dev[depName].add(depVersion); return acc2; }, {} as Record); + const normalizedProd = Object.entries(prod).reduce((acc2, [depName, depVersion]) => { + if (!isWorkspaceDep(tree, depName)) { + packagesToRoot.prod[depName] = packagesToRoot.prod[depName] ?? new Set(); + packagesToRoot.prod[depName].add(depVersion); + } + + return acc2; + }, prod); + acc[projectName].dev = normalizedDev; + acc[projectName].prod = normalizedProd; return acc; }, map); - const packagesToRootSerialized = Object.entries(packagesToRoot).reduce((acc, [packageName, versions]) => { + const packagesToRootSerializedDev = Object.entries(packagesToRoot.dev).reduce((acc, [packageName, versions]) => { acc[packageName] = [...versions]; return acc; }, {} as Record); + const packagesToRootSerializedProd = Object.entries(packagesToRoot.prod).reduce((acc, [packageName, versions]) => { + acc[packageName] = [...versions]; + return acc; + }, {} as Record); + + const packagesToRootSerialized = { dev: packagesToRootSerializedDev, prod: packagesToRootSerializedProd }; return { result, packagesToRoot: packagesToRootSerialized }; } @@ -107,7 +226,11 @@ function parsePackages(tree: Tree) { const data: DepMap = {}; projects.forEach((project, projectName) => { const pkgJson = readJson(tree, joinPathFragments(project.root, 'package.json')); - data[projectName] = { dev: pkgJson.devDependencies ?? {} }; + data[projectName] = { + dev: pkgJson.devDependencies ?? {}, + prod: pkgJson.dependencies ?? {}, + peer: pkgJson.peerDependencies ?? {}, + }; }); return data;