diff --git a/graph/ui-project-details/src/lib/project-details/project-details.stories.tsx b/graph/ui-project-details/src/lib/project-details/project-details.stories.tsx index cf76d76ca8cfc..dfc131c1e087e 100644 --- a/graph/ui-project-details/src/lib/project-details/project-details.stories.tsx +++ b/graph/ui-project-details/src/lib/project-details/project-details.stories.tsx @@ -835,3 +835,64 @@ export const Cart = { }, }, }; + +// We're normalizing `type` from `projectType`, so if projectType is missing we'll fallback to `type`. +// See: packages/nx/src/project-graph/utils/normalize-project-nodes.ts +export const FallbackType = { + args: { + project: { + name: 'mypkg', + type: 'lib', + data: { + root: '.', + name: 'mypkg', + targets: { + echo: { + executor: 'nx:run-script', + metadata: { + scriptContent: 'echo 1', + runCommand: 'npm run echo', + }, + options: { + script: 'echo', + }, + configurations: {}, + }, + }, + sourceRoot: '.', + implicitDependencies: [], + tags: [], + }, + }, + sourceMap: { + root: ['nx/core/project-json', 'project.json'], + name: ['nx/core/project-json', 'project.json'], + targets: ['nx/core/package-json', 'project.json'], + 'targets.echo': ['nx/core/package-json-workspaces', 'package.json'], + 'targets.echo.executor': [ + 'nx/core/package-json-workspaces', + 'package.json', + ], + 'targets.echo.options': [ + 'nx/core/package-json-workspaces', + 'package.json', + ], + 'targets.echo.metadata': [ + 'nx/core/package-json-workspaces', + 'package.json', + ], + 'targets.echo.options.script': [ + 'nx/core/package-json-workspaces', + 'package.json', + ], + 'targets.echo.metadata.scriptContent': [ + 'nx/core/package-json-workspaces', + 'package.json', + ], + 'targets.echo.metadata.runCommand': [ + 'nx/core/package-json-workspaces', + 'package.json', + ], + }, + }, +}; diff --git a/graph/ui-project-details/src/lib/project-details/project-details.tsx b/graph/ui-project-details/src/lib/project-details/project-details.tsx index 1876cb3e5029e..155ed3be13d4f 100644 --- a/graph/ui-project-details/src/lib/project-details/project-details.tsx +++ b/graph/ui-project-details/src/lib/project-details/project-details.tsx @@ -6,7 +6,6 @@ import type { ProjectGraphProjectNode } from '@nx/devkit'; // nx-ignore-next-line import { GraphError } from 'nx/src/command-line/graph/graph'; /* eslint-enable @nx/enforce-module-boundaries */ - import { EyeIcon } from '@heroicons/react/24/outline'; import { PropertyInfoTooltip, Tooltip } from '@nx/graph/ui-tooltips'; import { TooltipTriggerText } from '../target-configuration-details/tooltip-trigger-text'; @@ -29,6 +28,24 @@ export interface ProjectDetailsProps { viewInProjectGraphPosition?: 'top' | 'bottom'; } +const typeToProjectType = { + app: 'Application', + lib: 'Library', + e2e: 'E2E', +}; + +function getDisplayType(project: ProjectGraphProjectNode) { + if (project.data.projectType) { + return ( + project.data.projectType && + project.data.projectType?.charAt(0)?.toUpperCase() + + project.data.projectType?.slice(1) + ); + } else { + return typeToProjectType[project.type] ?? 'Library'; + } +} + export const ProjectDetails = ({ project, sourceMap, @@ -41,10 +58,7 @@ export const ProjectDetails = ({ const projectData = project.data; const isCompact = variant === 'compact'; - const displayType = - projectData.projectType && - projectData.projectType?.charAt(0)?.toUpperCase() + - projectData.projectType?.slice(1); + const displayType = getDisplayType(project); const technologies = [ ...new Set( diff --git a/packages/nx/src/config/to-project-name.spec.ts b/packages/nx/src/config/to-project-name.spec.ts index d8995c22c076f..bb3eec34801d8 100644 --- a/packages/nx/src/config/to-project-name.spec.ts +++ b/packages/nx/src/config/to-project-name.spec.ts @@ -59,7 +59,6 @@ describe('Workspaces', () => { }, }, "name": "my-package", - "projectType": "library", "root": "packages/my-package", "sourceRoot": "packages/my-package", "targets": { 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 176b8add4de34..ede15e6afed68 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 @@ -59,7 +59,6 @@ describe('nx package.json workspaces plugin', () => { }, }, "name": "root", - "projectType": "library", "root": ".", "sourceRoot": ".", "targets": { @@ -98,7 +97,6 @@ describe('nx package.json workspaces plugin', () => { }, }, "name": "lib-a", - "projectType": "library", "root": "packages/lib-a", "sourceRoot": "packages/lib-a", "targets": { @@ -145,7 +143,6 @@ describe('nx package.json workspaces plugin', () => { }, }, "name": "lib-b", - "projectType": "library", "root": "packages/lib-b", "sourceRoot": "packages/lib-b", "targets": { @@ -235,7 +232,6 @@ describe('nx package.json workspaces plugin', () => { }, }, "name": "vite", - "projectType": "library", "root": "packages/vite", "sourceRoot": "packages/vite", "targets": { @@ -322,7 +318,6 @@ describe('nx package.json workspaces plugin', () => { }, }, "name": "vite", - "projectType": "library", "root": "packages/vite", "sourceRoot": "packages/vite", "targets": { @@ -405,7 +400,6 @@ describe('nx package.json workspaces plugin', () => { }, }, "name": "vite", - "projectType": "library", "root": "packages/vite", "sourceRoot": "packages/vite", "targets": { @@ -478,7 +472,6 @@ describe('nx package.json workspaces plugin', () => { }, }, "name": "root", - "projectType": "library", "root": "packages/a", "sourceRoot": "packages/a", "targets": { @@ -543,7 +536,6 @@ describe('nx package.json workspaces plugin', () => { }, }, "name": "root", - "projectType": "library", "root": "packages/a", "sourceRoot": "packages/a", "targets": { @@ -606,7 +598,6 @@ describe('nx package.json workspaces plugin', () => { }, }, "name": "root", - "projectType": "library", "root": "packages/a", "sourceRoot": "packages/a", "targets": { @@ -624,4 +615,91 @@ describe('nx package.json workspaces plugin', () => { `); }); }); + + it('should infer library and application project types from appsDir and libsDir', () => { + vol.fromJSON( + { + 'nx.json': JSON.stringify({ + workspaceLayout: { + appsDir: 'apps', + libsDir: 'packages', + }, + }), + 'apps/myapp/package.json': JSON.stringify({ + name: 'myapp', + scripts: { test: 'jest' }, + }), + 'packages/mylib/package.json': JSON.stringify({ + name: 'mylib', + scripts: { test: 'jest' }, + }), + }, + '/root' + ); + + expect( + createNodeFromPackageJson('apps/myapp/package.json', '/root').projects[ + 'apps/myapp' + ].projectType + ).toEqual('application'); + + expect( + createNodeFromPackageJson('packages/mylib/package.json', '/root') + .projects['packages/mylib'].projectType + ).toEqual('library'); + }); + + it('should infer library types for root library project if both appsDir and libsDir are set to empty string', () => { + vol.fromJSON( + { + 'nx.json': JSON.stringify({ + workspaceLayout: { + appsDir: '', + libsDir: '', + }, + }), + 'package.json': JSON.stringify({ + name: 'mylib', + scripts: { test: 'jest' }, + }), + }, + '/root' + ); + + expect( + createNodeFromPackageJson('package.json', '/root').projects['.'] + .projectType + ).toEqual('library'); + }); + + it('should infer library project type if only libsDir is set', () => { + vol.fromJSON( + { + 'nx.json': JSON.stringify({ + workspaceLayout: { + libsDir: 'packages', + }, + }), + 'example/package.json': JSON.stringify({ + name: 'example', + scripts: { test: 'jest' }, + }), + 'packages/mylib/package.json': JSON.stringify({ + name: 'mylib', + scripts: { test: 'jest' }, + }), + }, + '/root' + ); + + expect( + createNodeFromPackageJson('packages/mylib/package.json', '/root') + .projects['packages/mylib'].projectType + ).toEqual('library'); + expect( + createNodeFromPackageJson('example/package.json', '/root').projects[ + 'example' + ].projectType + ).toBeUndefined(); + }); }); 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 a9ad27712c8f9..e50d26fe01d43 100644 --- a/packages/nx/src/plugins/package-json-workspaces/create-nodes.ts +++ b/packages/nx/src/plugins/package-json-workspaces/create-nodes.ts @@ -10,8 +10,8 @@ import { combineGlobPatterns } from '../../utils/globs'; import { NX_PREFIX } from '../../utils/logger'; import { output } from '../../utils/output'; import { - PackageJson, getMetadataFromPackageJson, + PackageJson, readTargetsFromPackageJson, } from '../../utils/package-json'; import { joinPathFragments } from '../../utils/path'; @@ -112,22 +112,30 @@ export function buildProjectConfigurationFromPackageJson( } let name = packageJson.name ?? toProjectName(normalizedPath); - const projectType = - nxJson?.workspaceLayout?.appsDir != nxJson?.workspaceLayout?.libsDir && - nxJson?.workspaceLayout?.appsDir && - projectRoot.startsWith(nxJson.workspaceLayout.appsDir) - ? 'application' - : 'library'; - return { + const projectConfiguration: ProjectConfiguration & { name: string } = { root: projectRoot, sourceRoot: projectRoot, name, - projectType, ...packageJson.nx, targets: readTargetsFromPackageJson(packageJson), metadata: getMetadataFromPackageJson(packageJson), }; + + if ( + nxJson?.workspaceLayout?.appsDir != nxJson?.workspaceLayout?.libsDir && + nxJson?.workspaceLayout?.appsDir && + projectRoot.startsWith(nxJson.workspaceLayout.appsDir) + ) { + projectConfiguration.projectType = 'application'; + } else if ( + typeof nxJson?.workspaceLayout?.libsDir !== 'undefined' && + projectRoot.startsWith(nxJson.workspaceLayout.libsDir) + ) { + projectConfiguration.projectType = 'library'; + } + + return projectConfiguration; } /** diff --git a/packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap b/packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap index 4b038c05d973d..1c292204a3a28 100644 --- a/packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap +++ b/packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap @@ -1,5 +1,46 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`@nx/vite/plugin Library mode should exclude serve and preview targets when vite.config.ts is in library mode 1`] = ` +[ + [ + "my-lib/vite.config.ts", + { + "projects": { + "my-lib": { + "metadata": {}, + "projectType": "library", + "root": "my-lib", + "targets": { + "build": { + "cache": true, + "command": "vite build", + "dependsOn": [ + "^build", + ], + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "vite", + ], + }, + ], + "options": { + "cwd": "my-lib", + }, + "outputs": [ + "{workspaceRoot}/dist/{projectRoot}", + ], + }, + }, + }, + }, + }, + ], +] +`; + exports[`@nx/vite/plugin not root project should create nodes 1`] = ` [ [ @@ -8,6 +49,7 @@ exports[`@nx/vite/plugin not root project should create nodes 1`] = ` "projects": { "my-app": { "metadata": {}, + "projectType": "application", "root": "my-app", "targets": { "build-something": { @@ -67,6 +109,7 @@ exports[`@nx/vite/plugin root project should create nodes 1`] = ` "projects": { ".": { "metadata": {}, + "projectType": "application", "root": ".", "targets": { "build": { diff --git a/packages/vite/src/plugins/plugin-vitest.spec.ts b/packages/vite/src/plugins/plugin-vitest.spec.ts index a98fff26d9ad1..e2d677342698d 100644 --- a/packages/vite/src/plugins/plugin-vitest.spec.ts +++ b/packages/vite/src/plugins/plugin-vitest.spec.ts @@ -27,6 +27,7 @@ describe('@nx/vite/plugin', () => { describe('root project', () => { beforeEach(async () => { context = { + configFiles: [], nxJsonConfiguration: { targetDefaults: {}, namedInputs: { @@ -35,7 +36,6 @@ describe('@nx/vite/plugin', () => { }, }, workspaceRoot: '', - configFiles: [], }; }); diff --git a/packages/vite/src/plugins/plugin-with-test.spec.ts b/packages/vite/src/plugins/plugin-with-test.spec.ts index 6b23a14d06760..1dabe2558bd3a 100644 --- a/packages/vite/src/plugins/plugin-with-test.spec.ts +++ b/packages/vite/src/plugins/plugin-with-test.spec.ts @@ -3,26 +3,12 @@ import { createNodes, createNodesV2 } from './plugin'; // This will only create test targets since no build targets are defined in vite.config.ts -jest.mock('vite', () => ({ - resolveConfig: jest.fn().mockImplementation(() => { - return Promise.resolve({ - path: 'vite.config.ts', - test: { - some: 'option', - }, - dependencies: [], - }); - }), -})); - jest.mock('../utils/executor-utils', () => ({ loadViteDynamicImport: jest.fn().mockResolvedValue({ resolveConfig: jest.fn().mockResolvedValue({ - path: 'vite.config.ts', test: { some: 'option', }, - dependencies: [], }), }), })); @@ -33,6 +19,7 @@ describe('@nx/vite/plugin with test node', () => { describe('root project', () => { beforeEach(async () => { context = { + configFiles: [], nxJsonConfiguration: { // These defaults should be overridden by plugin targetDefaults: { @@ -47,7 +34,6 @@ describe('@nx/vite/plugin with test node', () => { }, }, workspaceRoot: '', - configFiles: [], }; }); diff --git a/packages/vite/src/plugins/plugin.spec.ts b/packages/vite/src/plugins/plugin.spec.ts index 62539c5452d23..7d734536fdc4b 100644 --- a/packages/vite/src/plugins/plugin.spec.ts +++ b/packages/vite/src/plugins/plugin.spec.ts @@ -1,36 +1,25 @@ import { CreateNodesContext } from '@nx/devkit'; import { createNodes, createNodesV2 } from './plugin'; import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; - -jest.mock('vite', () => ({ - resolveConfig: jest.fn().mockImplementation(() => { - return Promise.resolve({ - path: 'vite.config.ts', - config: {}, - build: {}, - dependencies: [], - }); - }), -})); +import { loadViteDynamicImport } from '../utils/executor-utils'; jest.mock('../utils/executor-utils', () => ({ loadViteDynamicImport: jest.fn().mockResolvedValue({ - resolveConfig: jest.fn().mockResolvedValue({ - path: 'vite.config.ts', - config: {}, - dependencies: [], - }), + resolveConfig: jest.fn().mockResolvedValue({}), }), })); describe('@nx/vite/plugin', () => { let createNodesFunction = createNodesV2[1]; let context: CreateNodesContext; + describe('root project', () => { let tempFs; + beforeEach(async () => { tempFs = new TempFs('vite-plugin-tests'); context = { + configFiles: [], nxJsonConfiguration: { // These defaults should be overridden by plugin targetDefaults: { @@ -45,7 +34,6 @@ describe('@nx/vite/plugin', () => { }, }, workspaceRoot: tempFs.tempDir, - configFiles: [], }; tempFs.createFileSync('vite.config.ts', ''); tempFs.createFileSync('index.html', ''); @@ -75,9 +63,11 @@ describe('@nx/vite/plugin', () => { describe('not root project', () => { let tempFs; + beforeEach(() => { tempFs = new TempFs('test'); context = { + configFiles: [], nxJsonConfiguration: { namedInputs: { default: ['{projectRoot}/**/*'], @@ -85,7 +75,6 @@ describe('@nx/vite/plugin', () => { }, }, workspaceRoot: tempFs.tempDir, - configFiles: [], }; tempFs.createFileSync( @@ -116,4 +105,48 @@ describe('@nx/vite/plugin', () => { expect(nodes).toMatchSnapshot(); }); }); + + describe('Library mode', () => { + it('should exclude serve and preview targets when vite.config.ts is in library mode', async () => { + const tempFs = new TempFs('test'); + (loadViteDynamicImport as jest.Mock).mockResolvedValue({ + resolveConfig: jest.fn().mockResolvedValue({ + build: { + lib: { + entry: 'index.ts', + name: 'my-lib', + }, + }, + }), + }), + (context = { + configFiles: [], + nxJsonConfiguration: { + namedInputs: { + default: ['{projectRoot}/**/*'], + production: ['!{projectRoot}/**/*.spec.ts'], + }, + }, + workspaceRoot: tempFs.tempDir, + }); + tempFs.createFileSync( + 'my-lib/project.json', + JSON.stringify({ name: 'my-lib' }) + ); + tempFs.createFileSync('my-lib/vite.config.ts', ''); + + const nodes = await createNodesFunction( + ['my-lib/vite.config.ts'], + { + buildTargetName: 'build', + serveTargetName: 'serve', + }, + context + ); + + expect(nodes).toMatchSnapshot(); + + jest.resetModules(); + }); + }); }); diff --git a/packages/vite/src/plugins/plugin.ts b/packages/vite/src/plugins/plugin.ts index 72b3060ec8802..24a61c794a236 100644 --- a/packages/vite/src/plugins/plugin.ts +++ b/packages/vite/src/plugins/plugin.ts @@ -113,14 +113,23 @@ async function createNodesInternal( ); const { targets, metadata } = targetsCache[hash]; + const project: ProjectConfiguration = { + root: projectRoot, + targets, + metadata, + }; + + // If project is buildable, then the project type. + // If it is not buildable, then leave it to other plugins/project.json to set the project type. + if (project.targets[options.buildTargetName]) { + project.projectType = project.targets[options.serveTargetName] + ? 'application' + : 'library'; + } return { projects: { - [projectRoot]: { - root: projectRoot, - targets, - metadata, - }, + [projectRoot]: project, }, }; } @@ -135,7 +144,6 @@ async function buildViteTargets( context.workspaceRoot, configFilePath ); - // Workaround for the `build$3 is not a function` error that we sometimes see in agents. // This should be removed later once we address the issue properly try { @@ -178,11 +186,12 @@ async function buildViteTargets( projectRoot ); - targets[options.serveTargetName] = serveTarget(projectRoot); - - targets[options.previewTargetName] = previewTarget(projectRoot); - - targets[options.serveStaticTargetName] = serveStaticTarget(options) as {}; + // If running in library mode, then there is nothing to serve. + if (!viteConfig.build?.lib) { + targets[options.serveTargetName] = serveTarget(projectRoot); + targets[options.previewTargetName] = previewTarget(projectRoot); + targets[options.serveStaticTargetName] = serveStaticTarget(options) as {}; + } } // if file is vitest.config or vite.config has definition for test, create target for test