diff --git a/packages/detox/src/generators/application/application.ts b/packages/detox/src/generators/application/application.ts index eb392ab29d..018f755b44 100644 --- a/packages/detox/src/generators/application/application.ts +++ b/packages/detox/src/generators/application/application.ts @@ -60,7 +60,7 @@ export async function detoxApplicationGeneratorInternal( ); if (options.isUsingTsSolutionConfig) { - addProjectToTsSolutionWorkspace(host, options.e2eProjectRoot); + await addProjectToTsSolutionWorkspace(host, options.e2eProjectRoot); } sortPackageJsonFields(host, options.e2eProjectRoot); diff --git a/packages/expo/src/generators/application/application.ts b/packages/expo/src/generators/application/application.ts index 231df7b6d7..e4b4a82965 100644 --- a/packages/expo/src/generators/application/application.ts +++ b/packages/expo/src/generators/application/application.ts @@ -65,7 +65,7 @@ export async function expoApplicationGeneratorInternal( // If we are using the new TS solution // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project if (options.isTsSolutionSetup) { - addProjectToTsSolutionWorkspace(host, options.appProjectRoot); + await addProjectToTsSolutionWorkspace(host, options.appProjectRoot); } const lintTask = await addLinting(host, { diff --git a/packages/expo/src/generators/library/library.ts b/packages/expo/src/generators/library/library.ts index a754e84cf9..0e6355a192 100644 --- a/packages/expo/src/generators/library/library.ts +++ b/packages/expo/src/generators/library/library.ts @@ -70,7 +70,7 @@ export async function expoLibraryGeneratorInternal( } if (options.isUsingTsSolutionConfig) { - addProjectToTsSolutionWorkspace(host, options.projectRoot); + await addProjectToTsSolutionWorkspace(host, options.projectRoot); } const initTask = await init(host, { ...options, skipFormat: true }); diff --git a/packages/js/src/generators/library/library.spec.ts b/packages/js/src/generators/library/library.spec.ts index 659c65a257..a5560c1e02 100644 --- a/packages/js/src/generators/library/library.spec.ts +++ b/packages/js/src/generators/library/library.spec.ts @@ -2303,6 +2303,28 @@ describe('lib', () => { ); }); + it('should add the project root to the package manager workspaces config when a more generic pattern would match other projects that were not previously included', async () => { + tree.write( + 'not-included-dir/some-other-project-not-included/package.json', + '{}' + ); + + await libraryGenerator(tree, { + ...defaultOptions, + directory: 'not-included-dir/my-lib', + bundler: 'tsc', + unitTestRunner: 'none', + linter: 'none', + }); + + expect(readJson(tree, 'package.json').workspaces).toContain( + 'not-included-dir/my-lib' + ); + expect(readJson(tree, 'package.json').workspaces).not.toContain( + 'not-included-dir/*' + ); + }); + it('should not add a pattern for a project that already matches an existing pattern', async () => { updateJson(tree, 'package.json', (json) => { json.workspaces = ['packages/**']; diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index cb686d6781..07f1e03432 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -100,12 +100,6 @@ export async function libraryGeneratorInternal( ); const options = await normalizeOptions(tree, schema); - // If we are using the new TS solution - // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project - if (options.isUsingTsSolutionConfig) { - addProjectToTsSolutionWorkspace(tree, options.projectRoot); - } - createFiles(tree, options); await configureProject(tree, options); @@ -114,6 +108,12 @@ export async function libraryGeneratorInternal( tasks.push(addProjectDependencies(tree, options)); } + // If we are using the new TS solution + // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project + if (options.isUsingTsSolutionConfig) { + await addProjectToTsSolutionWorkspace(tree, options.projectRoot); + } + if (options.bundler === 'rollup') { const { configurationGenerator } = ensurePackage('@nx/rollup', nxVersion); await configurationGenerator(tree, { diff --git a/packages/js/src/utils/package-manager-workspaces.ts b/packages/js/src/utils/package-manager-workspaces.ts index 68d4e90d1b..82fe41ceb7 100644 --- a/packages/js/src/utils/package-manager-workspaces.ts +++ b/packages/js/src/utils/package-manager-workspaces.ts @@ -25,7 +25,16 @@ export function getProjectPackageManagerWorkspaceState( return 'no-workspaces'; } - const patterns = getGlobPatternsFromPackageManagerWorkspaces( + const patterns = getPackageManagerWorkspacesPatterns(tree); + const isIncluded = patterns.some((p) => + picomatch(p)(join(projectRoot, 'package.json')) + ); + + return isIncluded ? 'included' : 'excluded'; +} + +export function getPackageManagerWorkspacesPatterns(tree: Tree): string[] { + return getGlobPatternsFromPackageManagerWorkspaces( tree.root, (path) => readJson(tree, path, { expectComments: true }), (path) => { @@ -35,11 +44,6 @@ export function getProjectPackageManagerWorkspaceState( }, (path) => tree.exists(path) ); - const isIncluded = patterns.some((p) => - picomatch(p)(join(projectRoot, 'package.json')) - ); - - return isIncluded ? 'included' : 'excluded'; } export function isUsingPackageManagerWorkspaces(tree: Tree): boolean { diff --git a/packages/js/src/utils/typescript/ts-solution-setup.ts b/packages/js/src/utils/typescript/ts-solution-setup.ts index 0aaee745aa..361d68c657 100644 --- a/packages/js/src/utils/typescript/ts-solution-setup.ts +++ b/packages/js/src/utils/typescript/ts-solution-setup.ts @@ -1,4 +1,5 @@ import { + globAsync, joinPathFragments, offsetFromRoot, output, @@ -11,6 +12,7 @@ import { import { basename, dirname, join } from 'node:path/posix'; import { FsTree } from 'nx/src/generators/tree'; import { + getPackageManagerWorkspacesPatterns, getProjectPackageManagerWorkspaceState, isUsingPackageManagerWorkspaces, } from '../package-manager-workspaces'; @@ -211,7 +213,7 @@ export function updateTsconfigFiles( } } -export function addProjectToTsSolutionWorkspace( +export async function addProjectToTsSolutionWorkspace( tree: Tree, projectDir: string ) { @@ -220,11 +222,26 @@ export function addProjectToTsSolutionWorkspace( return; } - // If dir is "libs/foo" then use "libs/*" so we don't need so many entries in the workspace file. - // If dir is nested like "libs/shared/foo" then we add "libs/shared/*". - // If the dir is just "foo" then we have to add it as is. + // If dir is "libs/foo", we try to use "libs/*" but we only do it if it's + // safe to do so. So, we first check if adding that pattern doesn't result + // in extra projects being matched. If extra projects are matched, or the + // dir is just "foo" then we add it as is. const baseDir = dirname(projectDir); - const pattern = baseDir === '.' ? projectDir : `${baseDir}/*`; + let pattern = projectDir; + if (baseDir !== '.') { + const patterns = getPackageManagerWorkspacesPatterns(tree); + const projectsBefore = await globAsync(tree, patterns); + patterns.push(`${baseDir}/*/package.json`); + const projectsAfter = await globAsync(tree, patterns); + + if (projectsBefore.length + 1 === projectsAfter.length) { + // Adding the pattern to the parent directory only results in one extra + // project being matched, which is the project we're adding. It's safe + // to add the pattern to the parent directory. + pattern = `${baseDir}/*`; + } + } + if (tree.exists('pnpm-workspace.yaml')) { const { load, dump } = require('@zkochan/js-yaml'); const workspaceFile = tree.read('pnpm-workspace.yaml', 'utf-8'); diff --git a/packages/next/src/generators/application/application.ts b/packages/next/src/generators/application/application.ts index 008dc5e4d7..bdc6d13c3d 100644 --- a/packages/next/src/generators/application/application.ts +++ b/packages/next/src/generators/application/application.ts @@ -73,7 +73,7 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) { // If we are using the new TS solution // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project if (options.isTsSolutionSetup) { - addProjectToTsSolutionWorkspace(host, options.appProjectRoot); + await addProjectToTsSolutionWorkspace(host, options.appProjectRoot); } const e2eTask = await addE2e(host, options); diff --git a/packages/next/src/generators/library/library.ts b/packages/next/src/generators/library/library.ts index a5d42afe60..ac50f10d92 100644 --- a/packages/next/src/generators/library/library.ts +++ b/packages/next/src/generators/library/library.ts @@ -168,7 +168,7 @@ export async function libraryGeneratorInternal(host: Tree, rawOptions: Schema) { ); if (options.isUsingTsSolutionConfig) { - addProjectToTsSolutionWorkspace(host, options.projectRoot); + await addProjectToTsSolutionWorkspace(host, options.projectRoot); } sortPackageJsonFields(host, options.projectRoot); diff --git a/packages/node/src/generators/application/application.ts b/packages/node/src/generators/application/application.ts index 9858a78a78..2350a66147 100644 --- a/packages/node/src/generators/application/application.ts +++ b/packages/node/src/generators/application/application.ts @@ -516,7 +516,7 @@ export async function applicationGeneratorInternal(tree: Tree, schema: Schema) { // If we are using the new TS solution // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project if (options.isUsingTsSolutionConfig) { - addProjectToTsSolutionWorkspace(tree, options.appProjectRoot); + await addProjectToTsSolutionWorkspace(tree, options.appProjectRoot); } updateTsConfigOptions(tree, options); diff --git a/packages/node/src/generators/e2e-project/e2e-project.ts b/packages/node/src/generators/e2e-project/e2e-project.ts index 38e37e977e..08efc2b0a7 100644 --- a/packages/node/src/generators/e2e-project/e2e-project.ts +++ b/packages/node/src/generators/e2e-project/e2e-project.ts @@ -259,7 +259,7 @@ export async function e2eProjectGeneratorInternal( // If we are using the new TS solution // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project if (isUsingTsSolutionConfig) { - addProjectToTsSolutionWorkspace(host, options.e2eProjectRoot); + await addProjectToTsSolutionWorkspace(host, options.e2eProjectRoot); } if (!options.skipFormat) { diff --git a/packages/node/src/generators/library/library.ts b/packages/node/src/generators/library/library.ts index 6ba415c49b..89d0863aa2 100644 --- a/packages/node/src/generators/library/library.ts +++ b/packages/node/src/generators/library/library.ts @@ -57,7 +57,7 @@ export async function libraryGeneratorInternal(tree: Tree, schema: Schema) { // If we are using the new TS solution // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project if (options.isUsingTsSolutionConfig) { - addProjectToTsSolutionWorkspace(tree, options.projectRoot); + await addProjectToTsSolutionWorkspace(tree, options.projectRoot); } const tasks: GeneratorCallback[] = []; diff --git a/packages/nuxt/src/generators/application/application.ts b/packages/nuxt/src/generators/application/application.ts index ff07a64af4..5867a3c5c7 100644 --- a/packages/nuxt/src/generators/application/application.ts +++ b/packages/nuxt/src/generators/application/application.ts @@ -151,7 +151,7 @@ export async function applicationGenerator(tree: Tree, schema: Schema) { // If we are using the new TS solution // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project if (options.isUsingTsSolutionConfig) { - addProjectToTsSolutionWorkspace(tree, options.appProjectRoot); + await addProjectToTsSolutionWorkspace(tree, options.appProjectRoot); } tasks.push( diff --git a/packages/plugin/src/generators/e2e-project/e2e.ts b/packages/plugin/src/generators/e2e-project/e2e.ts index 68e1ec81d2..f0edccf4fe 100644 --- a/packages/plugin/src/generators/e2e-project/e2e.ts +++ b/packages/plugin/src/generators/e2e-project/e2e.ts @@ -258,7 +258,7 @@ export async function e2eProjectGeneratorInternal(host: Tree, schema: Schema) { // If we are using the new TS solution // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project if (options.isTsSolutionSetup) { - addProjectToTsSolutionWorkspace(host, options.projectRoot); + await addProjectToTsSolutionWorkspace(host, options.projectRoot); } if (!options.skipFormat) { diff --git a/packages/react-native/src/generators/application/application.ts b/packages/react-native/src/generators/application/application.ts index dc995537a5..353ab052c5 100644 --- a/packages/react-native/src/generators/application/application.ts +++ b/packages/react-native/src/generators/application/application.ts @@ -69,7 +69,7 @@ export async function reactNativeApplicationGeneratorInternal( // If we are using the new TS solution // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project if (options.isTsSolutionSetup) { - addProjectToTsSolutionWorkspace(host, options.appProjectRoot); + await addProjectToTsSolutionWorkspace(host, options.appProjectRoot); } const lintTask = await addLinting(host, { diff --git a/packages/react-native/src/generators/library/library.ts b/packages/react-native/src/generators/library/library.ts index 703ff477d0..c06d4df2b3 100644 --- a/packages/react-native/src/generators/library/library.ts +++ b/packages/react-native/src/generators/library/library.ts @@ -88,7 +88,7 @@ export async function reactNativeLibraryGeneratorInternal( } if (options.isUsingTsSolutionConfig) { - addProjectToTsSolutionWorkspace(host, options.projectRoot); + await addProjectToTsSolutionWorkspace(host, options.projectRoot); } const lintTask = await addLinting(host, { diff --git a/packages/react/src/generators/application/application.ts b/packages/react/src/generators/application/application.ts index 8e86a46429..d96f599b29 100644 --- a/packages/react/src/generators/application/application.ts +++ b/packages/react/src/generators/application/application.ts @@ -72,12 +72,6 @@ export async function applicationGeneratorInternal( const options = await normalizeOptions(tree, schema); - // If we are using the new TS solution - // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project - if (options.isUsingTsSolutionConfig) { - addProjectToTsSolutionWorkspace(tree, options.appProjectRoot); - } - showPossibleWarnings(tree, options); const initTask = await reactInitGenerator(tree, { @@ -115,6 +109,12 @@ export async function applicationGeneratorInternal( await createApplicationFiles(tree, options); addProject(tree, options); + // If we are using the new TS solution + // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project + if (options.isUsingTsSolutionConfig) { + await addProjectToTsSolutionWorkspace(tree, options.appProjectRoot); + } + if (options.style === 'tailwind') { const twTask = await setupTailwindGenerator(tree, { project: options.projectName, diff --git a/packages/react/src/generators/library/library.ts b/packages/react/src/generators/library/library.ts index 35889e456d..6fa241c50f 100644 --- a/packages/react/src/generators/library/library.ts +++ b/packages/react/src/generators/library/library.ts @@ -62,7 +62,7 @@ export async function libraryGeneratorInternal(host: Tree, schema: Schema) { const options = await normalizeOptions(host, schema); if (options.isUsingTsSolutionConfig) { - addProjectToTsSolutionWorkspace(host, options.projectRoot); + await addProjectToTsSolutionWorkspace(host, options.projectRoot); } if (options.publishable === true && !schema.importPath) { diff --git a/packages/remix/src/generators/application/application.impl.ts b/packages/remix/src/generators/application/application.impl.ts index 31cc86c26d..25804153da 100644 --- a/packages/remix/src/generators/application/application.impl.ts +++ b/packages/remix/src/generators/application/application.impl.ts @@ -85,7 +85,7 @@ export async function remixApplicationGeneratorInternal( // If we are using the new TS solution // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project if (options.isUsingTsSolutionConfig) { - addProjectToTsSolutionWorkspace(tree, options.projectRoot); + await addProjectToTsSolutionWorkspace(tree, options.projectRoot); } if (!options.isUsingTsSolutionConfig) { diff --git a/packages/remix/src/generators/library/library.impl.ts b/packages/remix/src/generators/library/library.impl.ts index fbbe9cb224..41812c6dd0 100644 --- a/packages/remix/src/generators/library/library.impl.ts +++ b/packages/remix/src/generators/library/library.impl.ts @@ -30,7 +30,7 @@ export async function remixLibraryGeneratorInternal( const options = await normalizeOptions(tree, schema); if (options.isUsingTsSolutionConfig) { - addProjectToTsSolutionWorkspace(tree, options.projectRoot); + await addProjectToTsSolutionWorkspace(tree, options.projectRoot); } const jsInitTask = await jsInitGenerator(tree, { diff --git a/packages/vue/src/generators/application/application.ts b/packages/vue/src/generators/application/application.ts index 426fefaf3e..11d36bffdd 100644 --- a/packages/vue/src/generators/application/application.ts +++ b/packages/vue/src/generators/application/application.ts @@ -56,7 +56,7 @@ export async function applicationGeneratorInternal( // If we are using the new TS solution // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project if (options.isUsingTsSolutionConfig) { - addProjectToTsSolutionWorkspace(tree, options.appProjectRoot); + await addProjectToTsSolutionWorkspace(tree, options.appProjectRoot); } const nxJson = readNxJson(tree); diff --git a/packages/vue/src/generators/library/library.ts b/packages/vue/src/generators/library/library.ts index a77ef14f18..3121893aa3 100644 --- a/packages/vue/src/generators/library/library.ts +++ b/packages/vue/src/generators/library/library.ts @@ -56,7 +56,7 @@ export async function libraryGeneratorInternal(tree: Tree, schema: Schema) { // If we are using the new TS solution // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project if (options.isUsingTsSolutionConfig) { - addProjectToTsSolutionWorkspace(tree, options.projectRoot); + await addProjectToTsSolutionWorkspace(tree, options.projectRoot); } if (options.isUsingTsSolutionConfig) { diff --git a/packages/web/src/generators/application/application.ts b/packages/web/src/generators/application/application.ts index f6d2cc260f..c472011dfc 100644 --- a/packages/web/src/generators/application/application.ts +++ b/packages/web/src/generators/application/application.ts @@ -301,7 +301,7 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) { const options = await normalizeOptions(host, schema); if (options.isUsingTsSolutionConfig) { - addProjectToTsSolutionWorkspace(host, options.appProjectRoot); + await addProjectToTsSolutionWorkspace(host, options.appProjectRoot); } const tasks: GeneratorCallback[] = []; diff --git a/packages/workspace/package.json b/packages/workspace/package.json index 1745f204b1..82d1f60392 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -39,8 +39,10 @@ }, "dependencies": { "@nx/devkit": "file:../devkit", + "@zkochan/js-yaml": "0.0.7", "chalk": "^4.1.0", "enquirer": "~2.3.6", + "picomatch": "4.0.2", "tslib": "^2.3.0", "yargs-parser": "21.1.1" }, diff --git a/packages/workspace/src/generators/move/move.spec.ts b/packages/workspace/src/generators/move/move.spec.ts index 823d555d9b..906d439cef 100644 --- a/packages/workspace/src/generators/move/move.spec.ts +++ b/packages/workspace/src/generators/move/move.spec.ts @@ -1,14 +1,23 @@ -import 'nx/src/internal-testing-utils/mock-project-graph'; - import { + detectPackageManager, readJson, readProjectConfiguration, Tree, + updateJson, updateProjectConfiguration, } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { moveGenerator } from './move'; +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: jest.fn().mockImplementation(async () => ({ + nodes: {}, + dependencies: {}, + })), + detectPackageManager: jest.fn(), +})); + // nx-ignore-next-line const { libraryGenerator } = require('@nx/js'); @@ -16,6 +25,9 @@ describe('move', () => { let tree: Tree; beforeEach(() => { tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + (detectPackageManager as jest.Mock).mockImplementation((...args) => + jest.requireActual('@nx/devkit').detectPackageManager(...args) + ); }); it('should update jest config when moving down directories', async () => { @@ -195,4 +207,93 @@ describe('move', () => { // check that the project.json file is not present expect(tree.exists('packages/lib1/project.json')).toBeFalsy(); }); + + it('should add new destination to the package manager workspaces config when it does not match any existing pattern and it was previously included', async () => { + updateJson(tree, 'package.json', (json) => { + json.workspaces = ['libs/*']; + return json; + }); + await libraryGenerator(tree, { directory: 'libs/lib1' }); + + await moveGenerator(tree, { + projectName: 'lib1', + destination: 'packages/lib1', + updateImportPath: true, + }); + + const packageJson = readJson(tree, 'package.json'); + expect(packageJson.workspaces).toStrictEqual(['libs/*', 'packages/*']); + }); + + it('should add new destination to the pnpm workspaces config when it does not match any existing pattern and it was previously included', async () => { + (detectPackageManager as jest.Mock).mockReturnValue('pnpm'); + tree.write( + 'pnpm-workspace.yaml', + `packages: +- 'libs/*'` + ); + await libraryGenerator(tree, { directory: 'libs/lib1' }); + + await moveGenerator(tree, { + projectName: 'lib1', + destination: 'packages/lib1', + updateImportPath: true, + }); + + expect(tree.read('pnpm-workspace.yaml', 'utf-8')).toMatchInlineSnapshot(` + "packages: + - 'libs/*' + - 'packages/*' + " + `); + }); + + it('should add the project root to the package manager workspaces config when a more generic pattern would match other projects that were not previously included', async () => { + updateJson(tree, 'package.json', (json) => { + json.workspaces = ['libs/*']; + return json; + }); + await libraryGenerator(tree, { directory: 'libs/lib1' }); + // extra project that's not part of the package manager workspaces + await libraryGenerator(tree, { directory: 'packages/some-package' }); + + await moveGenerator(tree, { + projectName: 'lib1', + destination: 'packages/lib1', + updateImportPath: true, + }); + + const packageJson = readJson(tree, 'package.json'); + expect(packageJson.workspaces).toStrictEqual(['libs/*', 'packages/lib1']); + }); + + it('should not add new destination to the package manager workspaces config when it was not previously included', async () => { + updateJson(tree, 'package.json', (json) => { + json.workspaces = ['apps/*']; + return json; + }); + await libraryGenerator(tree, { directory: 'libs/lib1' }); + + await moveGenerator(tree, { + projectName: 'lib1', + destination: 'packages/lib1', + updateImportPath: true, + }); + + const packageJson = readJson(tree, 'package.json'); + expect(packageJson.workspaces).toStrictEqual(['apps/*']); + }); + + it('should not configure package manager workspaces if it was not previously configured', async () => { + await libraryGenerator(tree, { directory: 'libs/lib1' }); + + await moveGenerator(tree, { + projectName: 'lib1', + destination: 'packages/lib1', + updateImportPath: true, + }); + + const packageJson = readJson(tree, 'package.json'); + expect(packageJson.workspaces).toBeUndefined(); + }); }); diff --git a/packages/workspace/src/generators/move/move.ts b/packages/workspace/src/generators/move/move.ts index 5bbf7788c4..aa0825599c 100644 --- a/packages/workspace/src/generators/move/move.ts +++ b/packages/workspace/src/generators/move/move.ts @@ -1,11 +1,20 @@ import { formatFiles, + installPackagesTask, readProjectConfiguration, removeProjectConfiguration, - Tree, + type GeneratorCallback, + type Tree, } from '@nx/devkit'; +import { isProjectIncludedInPackageManagerWorkspaces } from '../../utilities/package-manager-workspaces'; +import { addProjectToTsSolutionWorkspace } from '../../utilities/typescript/ts-solution-setup'; import { checkDestination } from './lib/check-destination'; import { createProjectConfigurationInNewDestination } from './lib/create-project-configuration-in-new-destination'; +import { + maybeExtractJestConfigBase, + maybeExtractTsConfigBase, + maybeMigrateEslintConfigIfRootProject, +} from './lib/extract-base-configs'; import { moveProjectFiles } from './lib/move-project-files'; import { normalizeSchema } from './lib/normalize-schema'; import { runAngularPlugin } from './lib/run-angular-plugin'; @@ -20,15 +29,14 @@ import { updatePackageJson } from './lib/update-package-json'; import { updateProjectRootFiles } from './lib/update-project-root-files'; import { updateReadme } from './lib/update-readme'; import { updateStorybookConfig } from './lib/update-storybook-config'; -import { - maybeMigrateEslintConfigIfRootProject, - maybeExtractJestConfigBase, - maybeExtractTsConfigBase, -} from './lib/extract-base-configs'; import { Schema } from './schema'; export async function moveGenerator(tree: Tree, rawSchema: Schema) { let projectConfig = readProjectConfiguration(tree, rawSchema.projectName); + const wasIncludedInWorkspaces = isProjectIncludedInPackageManagerWorkspaces( + tree, + projectConfig.root + ); const schema = await normalizeSchema(tree, rawSchema, projectConfig); checkDestination(tree, schema, rawSchema.destination); @@ -61,9 +69,29 @@ export async function moveGenerator(tree: Tree, rawSchema: Schema) { await runAngularPlugin(tree, schema); + let task: GeneratorCallback; + if (wasIncludedInWorkspaces) { + // check if the new destination is included in the package manager workspaces + const isIncludedInWorkspaces = isProjectIncludedInPackageManagerWorkspaces( + tree, + schema.destination + ); + if (!isIncludedInWorkspaces) { + // the new destination is not included in the package manager workspaces + // so we need to add it and run a package install to ensure the symlink + // is created + await addProjectToTsSolutionWorkspace(tree, schema.destination); + task = () => installPackagesTask(tree, true); + } + } + if (!schema.skipFormat) { await formatFiles(tree); } + + if (task) { + return task; + } } export default moveGenerator; diff --git a/packages/workspace/src/utilities/package-manager-workspaces.ts b/packages/workspace/src/utilities/package-manager-workspaces.ts new file mode 100644 index 0000000000..16ae0406a8 --- /dev/null +++ b/packages/workspace/src/utilities/package-manager-workspaces.ts @@ -0,0 +1,49 @@ +import { detectPackageManager, readJson, type Tree } from '@nx/devkit'; +import { join } from 'node:path/posix'; +import { getGlobPatternsFromPackageManagerWorkspaces } from 'nx/src/plugins/package-json'; +import { PackageJson } from 'nx/src/utils/package-json'; +import picomatch = require('picomatch'); + +export function isProjectIncludedInPackageManagerWorkspaces( + tree: Tree, + projectRoot: string +): boolean { + if (!isUsingPackageManagerWorkspaces(tree)) { + return false; + } + + const patterns = getPackageManagerWorkspacesPatterns(tree); + + return patterns.some((p) => picomatch(p)(join(projectRoot, 'package.json'))); +} + +export function getPackageManagerWorkspacesPatterns(tree: Tree): string[] { + return getGlobPatternsFromPackageManagerWorkspaces( + tree.root, + (path) => readJson(tree, path, { expectComments: true }), + (path) => { + const content = tree.read(path, 'utf-8'); + const { load } = require('@zkochan/js-yaml'); + return load(content, { filename: path }); + }, + (path) => tree.exists(path) + ); +} + +export function isUsingPackageManagerWorkspaces(tree: Tree): boolean { + return isWorkspacesEnabled(tree); +} + +export function isWorkspacesEnabled(tree: Tree): boolean { + const packageManager = detectPackageManager(tree.root); + if (packageManager === 'pnpm') { + return tree.exists('pnpm-workspace.yaml'); + } + + // yarn and npm both use the same 'workspaces' property in package.json + if (tree.exists('package.json')) { + const packageJson = readJson(tree, 'package.json'); + return !!packageJson?.workspaces; + } + return false; +} diff --git a/packages/workspace/src/utilities/typescript/ts-solution-setup.ts b/packages/workspace/src/utilities/typescript/ts-solution-setup.ts index eddd5b396c..6deb618a9b 100644 --- a/packages/workspace/src/utilities/typescript/ts-solution-setup.ts +++ b/packages/workspace/src/utilities/typescript/ts-solution-setup.ts @@ -1,11 +1,17 @@ import { detectPackageManager, + globAsync, readJson, type Tree, workspaceRoot, } from '@nx/devkit'; +import { dirname } from 'node:path/posix'; import { FsTree } from 'nx/src/generators/tree'; import { type PackageJson } from 'nx/src/utils/package-json'; +import { + getPackageManagerWorkspacesPatterns, + isProjectIncludedInPackageManagerWorkspaces, +} from '../package-manager-workspaces'; function isUsingPackageManagerWorkspaces(tree: Tree): boolean { return isWorkspacesEnabled(tree); @@ -75,3 +81,62 @@ export function isUsingTsSolutionSetup(tree?: Tree): boolean { isWorkspaceSetupWithTsSolution(tree) ); } + +export async function addProjectToTsSolutionWorkspace( + tree: Tree, + projectDir: string +) { + const isIncluded = isProjectIncludedInPackageManagerWorkspaces( + tree, + projectDir + ); + if (isIncluded) { + return; + } + + // If dir is "libs/foo", we try to use "libs/*" but we only do it if it's + // safe to do so. So, we first check if adding that pattern doesn't result + // in extra projects being matched. If extra projects are matched, or the + // dir is just "foo" then we add it as is. + const baseDir = dirname(projectDir); + let pattern = projectDir; + if (baseDir !== '.') { + const patterns = getPackageManagerWorkspacesPatterns(tree); + const projectsBefore = await globAsync(tree, patterns); + patterns.push(`${baseDir}/*/package.json`); + const projectsAfter = await globAsync(tree, patterns); + + if (projectsBefore.length + 1 === projectsAfter.length) { + // Adding the pattern to the parent directory only results in one extra + // project being matched, which is the project we're adding. It's safe + // to add the pattern to the parent directory. + pattern = `${baseDir}/*`; + } + } + + if (tree.exists('pnpm-workspace.yaml')) { + const { load, dump } = require('@zkochan/js-yaml'); + const workspaceFile = tree.read('pnpm-workspace.yaml', 'utf-8'); + const yamlData = load(workspaceFile) ?? {}; + yamlData.packages ??= []; + + if (!yamlData.packages.includes(pattern)) { + yamlData.packages.push(pattern); + tree.write( + 'pnpm-workspace.yaml', + dump(yamlData, { indent: 2, quotingType: '"', forceQuotes: true }) + ); + } + } else { + // Update package.json + const packageJson = readJson(tree, 'package.json'); + if (!packageJson.workspaces) { + packageJson.workspaces = []; + } + + if (!packageJson.workspaces.includes(pattern)) { + packageJson.workspaces.push(pattern); + tree.write('package.json', JSON.stringify(packageJson, null, 2)); + } + } +}