fix(bundling): set project type correct for buildable vite projects (#26420)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

This PR:

1. fixes `@nx/vite/plugin` so that it infers the correct `projectType`
2. changes the default `package-json-workspaces` plugin such that
`projectType` is not inferred is `appsDir` and `libsDir` are not set
3. Update PDV to look for the normalized `type` property on projects if
`projectType` is missing (due to no longer being inferred by any
plugin).

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Jack Hsu 2024-06-13 15:48:07 -04:00 committed by GitHub
parent 73c8c3343d
commit fbd7f80bc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 300 additions and 69 deletions

View File

@ -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',
],
},
},
};

View File

@ -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(

View File

@ -59,7 +59,6 @@ describe('Workspaces', () => {
},
},
"name": "my-package",
"projectType": "library",
"root": "packages/my-package",
"sourceRoot": "packages/my-package",
"targets": {

View File

@ -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();
});
});

View File

@ -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;
}
/**

View File

@ -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": {

View File

@ -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: [],
};
});

View File

@ -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: [],
};
});

View File

@ -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();
});
});
});

View File

@ -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