From 210889e82192a2aa6b319518f0b2d2fe86bde009 Mon Sep 17 00:00:00 2001 From: Max Kramer Date: Sun, 5 Jun 2022 15:41:30 -0400 Subject: [PATCH] feat(js): fine-grained dep type support for publishable libs Allows lib package.json to define its dependencies appropriately between dependencies and peerDependencies, while providing a way to continue to inherit the workspace's pkg version e.g. "foo": "[inherit]" ISSUES CLOSED: 10550 --- .../utilities/buildable-libs-utils.spec.ts | 178 +++++++++++++++++- .../src/utilities/buildable-libs-utils.ts | 40 ++-- 2 files changed, 199 insertions(+), 19 deletions(-) diff --git a/packages/workspace/src/utilities/buildable-libs-utils.spec.ts b/packages/workspace/src/utilities/buildable-libs-utils.spec.ts index b4e80afc9a22e6..14b7fd066528b8 100644 --- a/packages/workspace/src/utilities/buildable-libs-utils.spec.ts +++ b/packages/workspace/src/utilities/buildable-libs-utils.spec.ts @@ -1,10 +1,20 @@ -import { DependencyType, ProjectGraph } from '@nrwl/devkit'; +import { + DependencyType, + ProjectGraph, + ProjectGraphProjectNode, + readJsonFile, +} from '@nrwl/devkit'; +import { join } from 'path'; +import { vol } from 'memfs'; import { calculateProjectDependencies, DependentBuildableProjectNode, + updateBuildableProjectPackageJsonDependencies, updatePaths, } from './buildable-libs-utils'; +jest.mock('fs', () => require('memfs').fs); + describe('updatePaths', () => { const deps: DependentBuildableProjectNode[] = [ { name: '@proj/lib', node: {} as any, outputs: ['dist/libs/lib'] }, @@ -84,3 +94,169 @@ describe('calculateProjectDependencies', () => { }); }); }); + +describe('updateBuildableProjectPackageJsonDependencies', () => { + beforeEach(() => { + vol.reset(); + }); + + it('should add undeclared npm packages to peerDependency list', () => { + vol.fromJSON({ + './package.json': JSON.stringify({ + name: '@scope/workspace', + version: '0.0.0', + dependencies: { + foo: '^1.0.0', + bar: '~2.0.0', + unused: '<= 9.9', + }, + devDependencies: { + ignored: '3.3.3', + }, + }), + './dist/libs/example/package.json': JSON.stringify({ + name: '@scope/example', + version: '5.0.1', + }), + }); + + const root = '.'; + const node = libNode('example'); + const dependencies: DependentBuildableProjectNode[] = [ + npmDep('foo', '^1.0.0'), + npmDep('bar', '~2.0.0'), + npmDep('ignored', '3.3.3'), + ]; + + updateBuildableProjectPackageJsonDependencies( + root, + 'example', + 'build', + undefined, + node, + dependencies + ); + + const pkg = readJsonFile( + join(root, 'dist', 'libs', 'example', 'package.json') + ); + expect(pkg.dependencies).toMatchObject({ + foo: '^1.0.0', + bar: '~2.0.0', + }); + expect(pkg.peerDependencies).toEqual({}); + expect(pkg.devDependencies).toBeUndefined(); + }); + + it('should inherit versions for declared dependencies', () => { + vol.fromJSON({ + './package.json': JSON.stringify({ + name: '@scope/workspace', + version: '0.0.0', + dependencies: { + foo: '^1.0.0', + bar: '~2.0.0', + zed: '>=4.0.0', + unused: '<= 9.9', + }, + devDependencies: { + ignored: '3.3.3', + }, + }), + './dist/libs/example/package.json': JSON.stringify({ + name: '@scope/example', + version: '5.0.1', + dependencies: { + foo: '[inherit]', + }, + peerDependencies: { + bar: '[inherit]', + }, + devDependencies: { + ignored: '[inherit]', + }, + }), + './dist/libs/other/package.json': JSON.stringify({ + name: '@scope/other', + version: '5.0.2', + }), + }); + + const root = '.'; + const node = libNode('example'); + const dependencies: DependentBuildableProjectNode[] = [ + npmDep('foo', '^1.0.0'), + npmDep('bar', '~2.0.0'), + npmDep('zed', '>=4.0.0'), + npmDep('ignored', '3.3.3'), + libDep('scope', 'other'), + ]; + + updateBuildableProjectPackageJsonDependencies( + root, + 'example', + 'build', + undefined, + node, + dependencies, + 'peerDependencies' + ); + + const pkg = readJsonFile( + join(root, 'dist', 'libs', 'example', 'package.json') + ); + expect(pkg.dependencies).toMatchObject({ + foo: '^1.0.0', + }); + expect(pkg.peerDependencies).toMatchObject({ + '@scope/other': '5.0.2', + bar: '~2.0.0', + }); + expect(pkg.devDependencies).toMatchObject({ + // devDependencies are not supported to inherit + ignored: '[inherit]', + }); + }); +}); + +function libDep(scope: string, name: string): DependentBuildableProjectNode { + return { + name: `@${scope}/${name}`, + node: libNode(name), + outputs: [`dist/libs/${name}`], + }; +} + +function libNode(name: string): ProjectGraphProjectNode { + return { + name, + type: 'lib', + data: { + root: `src/${name}`, + files: [], + targets: { + build: { + options: { + outputPath: `dist/libs/${name}`, + }, + outputs: ['{options.outputPath}'], + }, + }, + }, + }; +} + +function npmDep(name: string, version: string): DependentBuildableProjectNode { + return { + name, + node: { + name: `npm:${name}`, + type: 'npm', + data: { + packageName: name, + version, + }, + }, + outputs: [], + }; +} diff --git a/packages/workspace/src/utilities/buildable-libs-utils.ts b/packages/workspace/src/utilities/buildable-libs-utils.ts index 2cd6d3a844eb8f..5d850749887697 100644 --- a/packages/workspace/src/utilities/buildable-libs-utils.ts +++ b/packages/workspace/src/utilities/buildable-libs-utils.ts @@ -287,7 +287,9 @@ export function updateBuildableProjectPackageJsonDependencies( configurationName: string, node: ProjectGraphProjectNode, dependencies: DependentBuildableProjectNode[], - typeOfDependency: 'dependencies' | 'peerDependencies' = 'dependencies' + typeOfUndeclaredDependency: + | 'dependencies' + | 'peerDependencies' = 'dependencies' ) { const outputs = getOutputsForTargetAndConfiguration( { @@ -309,6 +311,7 @@ export function updateBuildableProjectPackageJsonDependencies( workspacePackageJson = readJsonFile(`${root}/package.json`); } catch (e) { // cannot find or invalid package.json + console.error('failed', e); return; } @@ -321,13 +324,12 @@ export function updateBuildableProjectPackageJsonDependencies( ? entry.node.data.packageName : entry.name; - if ( - !hasDependency(packageJson, 'dependencies', packageName) && - !hasDependency(packageJson, 'devDependencies', packageName) && - !hasDependency(packageJson, 'peerDependencies', packageName) - ) { + const declaredAs = whichDependency(packageJson, packageName); + + // update if not declared or if dep's version should be inherited + if (!declaredAs || packageJson[declaredAs][packageName] === '[inherit]') { + const typeOfDependency = declaredAs || typeOfUndeclaredDependency; try { - let depVersion; if (entry.node.type === 'lib') { const outputs = getOutputsForTargetAndConfiguration( { @@ -342,23 +344,16 @@ export function updateBuildableProjectPackageJsonDependencies( ); const depPackageJsonPath = join(root, outputs[0], 'package.json'); - depVersion = readJsonFile(depPackageJsonPath).version; - + const depVersion = readJsonFile(depPackageJsonPath).version; packageJson[typeOfDependency][packageName] = depVersion; } else if (isNpmProject(entry.node)) { // If an npm dep is part of the workspace devDependencies, do not include it the library - if ( - !!workspacePackageJson.devDependencies?.[ - entry.node.data.packageName - ] - ) { + if (!!workspacePackageJson.devDependencies?.[packageName]) { return; } - depVersion = entry.node.data.version; - - packageJson[typeOfDependency][entry.node.data.packageName] = - depVersion; + const depVersion = entry.node.data.version; + packageJson[typeOfDependency][packageName] = depVersion; } updatePackageJson = true; } catch (e) { @@ -372,6 +367,15 @@ export function updateBuildableProjectPackageJsonDependencies( } } +// verify whether the package.json already specifies the dep in any host +function whichDependency(outputJson, packageName: string) { + return ['dependencies', 'devDependencies', 'peerDependencies'].find( + (depConfigName) => { + return hasDependency(outputJson, depConfigName, packageName); + } + ); +} + // verify whether the package.json already specifies the dep function hasDependency(outputJson, depConfigName: string, packageName: string) { if (outputJson[depConfigName]) {