diff --git a/tools/generators/svp/README.md b/tools/generators/svp/README.md new file mode 100644 index 0000000000000..9a38d547d32af --- /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 0000000000000..d9913111a9dc7 --- /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 0000000000000..b3f4798fb7173 --- /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 0000000000000..9be694e7f7e8c --- /dev/null +++ b/tools/generators/svp/index.ts @@ -0,0 +1,237 @@ +import { + Tree, + formatFiles, + installPackagesTask, + getProjects, + readJson, + joinPathFragments, + writeJson, + readProjectConfiguration, + updateJson, +} from '@nrwl/devkit'; + +import { SvpGeneratorSchema } from './schema'; +import { PackageJson } from '../../types'; +import * as semver from 'semver'; + +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 rootDeps = { dev: rootPackageJson.devDependencies ?? {}, prod: rootPackageJson.dependencies ?? {} }; + + const normalizedDevDepsMap = parsePackages(tree); + const { packagesToRoot, result } = normalizePackages(tree, normalizedDevDepsMap); + + writeJson(tree, 'debug-svp.json', normalizedDevDepsMap); + writeJson(tree, 'debug-svp.norm.json', result); + + const dedupedRootDeps = dedupeRoot(packagesToRoot); + const merged = dedupeRoot(mergeDepMaps(rootDeps, dedupedRootDeps)); + const semverResolved = resolveVersions(merged); + + writeJson(tree, 'debug-root.json', rootDeps); + writeJson(tree, 'debug-root.extracted.json', packagesToRoot); + // 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); + + return () => { + installPackagesTask(tree); + }; +} + +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); + return true; + } catch (err) { + return false; + } +} + +function normalizePackages(tree: Tree, map: DepMap) { + const packagesToRoot: { dev: Record>; prod: Record> } = { dev: {}, prod: {} }; + + const result = Object.entries(map).reduce((acc, [projectName, deps]) => { + const { dev, prod } = deps; + + const normalizedDev = Object.entries(dev).reduce((acc2, [depName, depVersion]) => { + if (depVersion === '*') { + acc2[depName] = '*'; + return acc2; + } + + if (isWorkspaceDep(tree, depName)) { + acc2[depName] = '*'; + return acc2; + } + + 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 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 }; +} + +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 ?? {}, + prod: pkgJson.dependencies ?? {}, + peer: pkgJson.peerDependencies ?? {}, + }; + }); + + return data; +} diff --git a/tools/generators/svp/lib/utils.spec.ts b/tools/generators/svp/lib/utils.spec.ts new file mode 100644 index 0000000000000..91e05a54a86f5 --- /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 0000000000000..c340a27c29c8b --- /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 0000000000000..cb1b82a0f9110 --- /dev/null +++ b/tools/generators/svp/schema.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "id": "svp", + "type": "object", + "properties": {}, + "required": [] +} diff --git a/tools/generators/svp/schema.ts b/tools/generators/svp/schema.ts new file mode 100644 index 0000000000000..0ee04532f697d --- /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 1d9c854543a1f..adfec4aecf465 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 ae4b7b176c2ab..98c790aed8961 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",