From 7ad75d7960ac801e584a70ba62c5ce35bc4e5bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CJamesHenry=E2=80=9D?= Date: Fri, 7 Jun 2024 18:35:23 +0400 Subject: [PATCH] fix(core): correctly handle negative patterns in workspaces/packages config --- .../create-nodes.spec.ts | 279 +++++++++++++++++- .../package-json-workspaces/create-nodes.ts | 11 +- 2 files changed, 285 insertions(+), 5 deletions(-) diff --git a/packages/nx/src/plugins/package-json-workspaces/create-nodes.spec.ts b/packages/nx/src/plugins/package-json-workspaces/create-nodes.spec.ts index 511ae154ce982..0402a42fce7c2 100644 --- a/packages/nx/src/plugins/package-json-workspaces/create-nodes.spec.ts +++ b/packages/nx/src/plugins/package-json-workspaces/create-nodes.spec.ts @@ -1,11 +1,15 @@ -import * as memfs from 'memfs'; - import '../../internal-testing-utils/mock-fs'; -import { createNodeFromPackageJson } from './create-nodes'; + +import { vol } from 'memfs'; +import { createNodeFromPackageJson, createNodes } from './create-nodes'; describe('nx package.json workspaces plugin', () => { + afterEach(() => { + vol.reset(); + }); + it('should build projects from package.json files', () => { - memfs.vol.fromJSON( + vol.fromJSON( { 'package.json': JSON.stringify({ name: 'root', @@ -175,4 +179,271 @@ describe('nx package.json workspaces plugin', () => { } `); }); + + describe('negative patterns', () => { + it('should work based on negative patterns defined in package.json workspaces', () => { + vol.fromJSON( + { + 'package.json': JSON.stringify({ + name: 'root', + workspaces: [ + 'packages/*', + // Multiple negative entries + '!packages/fs', + '!packages/orm-browser-example', + '!packages/framework-examples', + ], + }), + 'packages/vite/package.json': JSON.stringify({ name: 'vite' }), + 'packages/fs/package.json': JSON.stringify({ name: 'fs' }), + 'packages/orm-browser-example/package.json': JSON.stringify({ + name: 'orm-browser-example', + }), + 'packages/framework-examples/package.json': JSON.stringify({ + name: 'framework-examples', + }), + }, + '/root' + ); + + const context = { + workspaceRoot: '/root', + configFiles: [], + nxJsonConfiguration: {}, + }; + + // No matching project based on the package.json "workspace" config + expect( + createNodes[1]('package.json', undefined, context) + ).toMatchInlineSnapshot(`{}`); + + // Matching project based on the package.json "workspace" config + expect(createNodes[1]('packages/vite/package.json', undefined, context)) + .toMatchInlineSnapshot(` + { + "projects": { + "packages/vite": { + "metadata": { + "targetGroups": { + "NPM Scripts": [], + }, + }, + "name": "vite", + "projectType": "library", + "root": "packages/vite", + "sourceRoot": "packages/vite", + "targets": { + "nx-release-publish": { + "dependsOn": [ + "^nx-release-publish", + ], + "executor": "@nx/js:release-publish", + "options": {}, + }, + }, + }, + }, + } + `); + + // No matching project based on the package.json "workspace" config + expect( + createNodes[1]('packages/fs/package.json', undefined, context) + ).toMatchInlineSnapshot(`{}`); + + // No matching project based on the package.json "workspace" config + expect( + createNodes[1]( + 'packages/orm-browser-example/package.json', + undefined, + context + ) + ).toMatchInlineSnapshot(`{}`); + + // No matching project based on the package.json "workspace" config + expect( + createNodes[1]( + 'packages/framework-examples/package.json', + undefined, + context + ) + ).toMatchInlineSnapshot(`{}`); + }); + + it('should work based on negative patterns defined in pnpm-workspace.yaml', () => { + vol.fromJSON( + { + 'package.json': JSON.stringify({ name: 'root' }), + // Multiple negative entries + 'pnpm-workspace.yaml': `packages: +- 'packages/*' +- '!packages/fs' +- '!packages/orm-browser-example' +- '!packages/framework-examples' +`, + 'packages/vite/package.json': JSON.stringify({ name: 'vite' }), + 'packages/fs/package.json': JSON.stringify({ name: 'fs' }), + 'packages/orm-browser-example/package.json': JSON.stringify({ + name: 'orm-browser-example', + }), + 'packages/framework-examples/package.json': JSON.stringify({ + name: 'framework-examples', + }), + }, + '/root' + ); + + const context = { + workspaceRoot: '/root', + configFiles: [], + nxJsonConfiguration: {}, + }; + + // No matching project based on the pnpm-workspace.yaml "packages" config + expect( + createNodes[1]('package.json', undefined, context) + ).toMatchInlineSnapshot(`{}`); + + // Matching project based on the pnpm-workspace.yaml "packages" config + expect(createNodes[1]('packages/vite/package.json', undefined, context)) + .toMatchInlineSnapshot(` + { + "projects": { + "packages/vite": { + "metadata": { + "targetGroups": { + "NPM Scripts": [], + }, + }, + "name": "vite", + "projectType": "library", + "root": "packages/vite", + "sourceRoot": "packages/vite", + "targets": { + "nx-release-publish": { + "dependsOn": [ + "^nx-release-publish", + ], + "executor": "@nx/js:release-publish", + "options": {}, + }, + }, + }, + }, + } + `); + + // No matching project based on the pnpm-workspace.yaml "packages" config + expect( + createNodes[1]('packages/fs/package.json', undefined, context) + ).toMatchInlineSnapshot(`{}`); + + // No matching project based on the pnpm-workspace.yaml "packages" config + expect( + createNodes[1]( + 'packages/orm-browser-example/package.json', + undefined, + context + ) + ).toMatchInlineSnapshot(`{}`); + + // No matching project based on the pnpm-workspace.yaml "packages" config + expect( + createNodes[1]( + 'packages/framework-examples/package.json', + undefined, + context + ) + ).toMatchInlineSnapshot(`{}`); + }); + + it('should work based on negative patterns defined in lerna.json', () => { + vol.fromJSON( + { + 'package.json': JSON.stringify({ name: 'root' }), + 'lerna.json': JSON.stringify({ + packages: [ + 'packages/*', + // Multiple negative entries + '!packages/fs', + '!packages/orm-browser-example', + '!packages/framework-examples', + ], + }), + 'packages/vite/package.json': JSON.stringify({ name: 'vite' }), + 'packages/fs/package.json': JSON.stringify({ name: 'fs' }), + 'packages/orm-browser-example/package.json': JSON.stringify({ + name: 'orm-browser-example', + }), + 'packages/framework-examples/package.json': JSON.stringify({ + name: 'framework-examples', + }), + }, + '/root' + ); + + const context = { + workspaceRoot: '/root', + configFiles: [], + nxJsonConfiguration: {}, + }; + + // No matching project based on the lerna.json "packages" config + expect( + createNodes[1]('package.json', undefined, context) + ).toMatchInlineSnapshot(`{}`); + + // Matching project based on the lerna.json "packages" config + expect(createNodes[1]('packages/vite/package.json', undefined, context)) + .toMatchInlineSnapshot(` + { + "projects": { + "packages/vite": { + "metadata": { + "targetGroups": { + "NPM Scripts": [], + }, + }, + "name": "vite", + "projectType": "library", + "root": "packages/vite", + "sourceRoot": "packages/vite", + "targets": { + "nx-release-publish": { + "dependsOn": [ + "^nx-release-publish", + ], + "executor": "@nx/js:release-publish", + "options": {}, + }, + }, + }, + }, + } + `); + + // No matching project based on the lerna.json "packages" config + expect( + createNodes[1]('packages/fs/package.json', undefined, context) + ).toMatchInlineSnapshot(`{}`); + + // No matching project based on the lerna.json "packages" config + expect( + createNodes[1]( + 'packages/orm-browser-example/package.json', + undefined, + context + ) + ).toMatchInlineSnapshot(`{}`); + + // No matching project based on the lerna.json "packages" config + expect( + createNodes[1]( + 'packages/framework-examples/package.json', + undefined, + context + ) + ).toMatchInlineSnapshot(`{}`); + }); + }); }); diff --git a/packages/nx/src/plugins/package-json-workspaces/create-nodes.ts b/packages/nx/src/plugins/package-json-workspaces/create-nodes.ts index 0ee7c34ca7b47..07e79d5c2e101 100644 --- a/packages/nx/src/plugins/package-json-workspaces/create-nodes.ts +++ b/packages/nx/src/plugins/package-json-workspaces/create-nodes.ts @@ -56,7 +56,16 @@ export function buildPackageJsonWorkspacesMatcher( return (p: string) => positivePatterns.some((positive) => minimatch(p, positive)) && - !negativePatterns.some((negative) => minimatch(p, negative)); + /** + * minimatch will return true if the given p is NOT excluded by the negative pattern. + * + * For example if the negative pattern is "!packages/vite", then the given p "packages/vite" will return false, + * the given p "packages/something-else/package.json" will return true. + * + * Therefore, we need to ensure that every negative pattern returns true to validate that the given p is not + * excluded by any of the negative patterns. + */ + negativePatterns.every((negative) => minimatch(p, negative)); } export function createNodeFromPackageJson(pkgJsonPath: string, root: string) {