diff --git a/graph/shared/src/lib/use-environment-config.ts b/graph/shared/src/lib/use-environment-config.ts index 1991ed3bb3cef..602f2058d719c 100644 --- a/graph/shared/src/lib/use-environment-config.ts +++ b/graph/shared/src/lib/use-environment-config.ts @@ -10,7 +10,7 @@ export function useEnvironmentConfig(): { watch: boolean; localMode: 'serve' | 'build'; projectGraphResponse?: ProjectGraphClientResponse; - environment: 'dev' | 'watch' | 'release' | 'nx-console'; + environment: 'dev' | 'watch' | 'release' | 'nx-console' | 'docs'; appConfig: AppConfig; useXstateInspect: boolean; } { @@ -25,13 +25,14 @@ export function getEnvironmentConfig() { watch: window.watch, localMode: window.localMode, projectGraphResponse: window.projectGraphResponse, - environment: window.environment, + // If this was not built into JS or HTML, then it is rendered on docs (nx.dev). + environment: window.environment ?? ('docs' as const), appConfig: { ...window.appConfig, showExperimentalFeatures: localStorage.getItem('showExperimentalFeatures') === 'true' ? true - : window.appConfig.showExperimentalFeatures, + : window.appConfig?.showExperimentalFeatures, }, useXstateInspect: window.useXstateInspect, }; diff --git a/graph/ui-project-details/src/lib/show-all-options/show-options-help.tsx b/graph/ui-project-details/src/lib/show-all-options/show-options-help.tsx new file mode 100644 index 0000000000000..4ed41f668c0f1 --- /dev/null +++ b/graph/ui-project-details/src/lib/show-all-options/show-options-help.tsx @@ -0,0 +1,198 @@ +import { Fragment, ReactNode, useMemo, useState } from 'react'; +import { PlayIcon, XMarkIcon } from '@heroicons/react/24/outline'; +import { Transition } from '@headlessui/react'; +import { getExternalApiService, useEnvironmentConfig } from '@nx/graph/shared'; +/* eslint-disable @nx/enforce-module-boundaries */ +// nx-ignore-next-line +import type { TargetConfiguration } from '@nx/devkit'; +import { TerminalOutput } from '@nx/nx-dev/ui-fence'; +import { Tooltip } from '@nx/graph/ui-tooltips'; +import { TooltipTriggerText } from '../target-configuration-details/tooltip-trigger-text'; + +interface ShowOptionsHelpProps { + projectName: string; + targetName: string; + targetConfiguration: TargetConfiguration; +} + +const fallbackHelpExample = { + options: { + silent: true, + }, + args: ['foo'], +}; + +export function ShowOptionsHelp({ + projectName, + targetName, + targetConfiguration, +}: ShowOptionsHelpProps) { + const config = useEnvironmentConfig(); + const environment = config?.environment; + const localMode = config?.localMode; + const [result, setResult] = useState<{ + text: string; + success: boolean; + } | null>(null); + const [isPending, setPending] = useState(false); + const externalApiService = getExternalApiService(); + + const helpData = targetConfiguration.metadata?.help; + const helpCommand = helpData?.command; + const helpExampleOptions = helpData?.example?.options; + const helpExampleArgs = helpData?.example?.args; + + const helpExampleTest = useMemo(() => { + const targetExampleJson = + helpExampleOptions || helpExampleArgs + ? { + options: helpExampleOptions, + args: helpExampleArgs, + } + : fallbackHelpExample; + return JSON.stringify( + { + targets: { + [targetName]: targetExampleJson, + }, + }, + null, + 2 + ); + }, [helpExampleOptions, helpExampleArgs]); + + let runHelpActionElement: null | ReactNode; + if (environment === 'docs') { + // Cannot run help command when rendering in docs (e.g. nx.dev). + runHelpActionElement = null; + } else if (environment === 'release' && localMode === 'build') { + // Cannot run help command when statically built via `nx graph --file=graph.html`. + runHelpActionElement = null; + } else if (isPending || !result) { + runHelpActionElement = ( + + ); + } else { + runHelpActionElement = ( + + ); + } + + return ( + helpCommand && ( + <> +

+ Use --help to see all options for this command, and set + them by{' '} + + passing them + {' '} + to the "options" property in{' '} + +

+ For example, you can use the following configuration for the{' '} + {targetName} target in the{' '} + project.json file for{' '} + {projectName}. +

+
+                    {helpExampleTest}
+                  
+ {helpExampleOptions && ( +

+ The options are CLI options prefixed by{' '} + --, such as ls --color=never, + where you would use {'"color": "never"'} to + set it in the target configuration. +

+ )} + {helpExampleArgs && ( +

+ The args are CLI positional arguments, such + as ls somedir, where you would use{' '} + {'"args": ["somedir"]'} to set it in the + target configuration. +

+ )} + + ) as any + } + > + + project.json + + + . +

+ + +
+                  {result?.text}
+                
+
+ + } + /> + + ) + ); +} diff --git a/graph/ui-project-details/src/lib/target-configuration-details/target-configuration-details.tsx b/graph/ui-project-details/src/lib/target-configuration-details/target-configuration-details.tsx index 690c6baae3c4a..b6e603f0ea333 100644 --- a/graph/ui-project-details/src/lib/target-configuration-details/target-configuration-details.tsx +++ b/graph/ui-project-details/src/lib/target-configuration-details/target-configuration-details.tsx @@ -17,6 +17,7 @@ import { TargetExecutor } from '../target-executor/target-executor'; import { TargetExecutorTitle } from '../target-executor/target-executor-title'; import { TargetSourceInfo } from '../target-source-info/target-source-info'; import { getTargetExecutorSourceMapKey } from '../target-source-info/get-target-executor-source-map-key'; +import { ShowOptionsHelp } from '../show-all-options/show-options-help'; interface TargetConfigurationDetailsProps { projectName: string; @@ -85,7 +86,7 @@ export default function TargetConfigurationDetails({ : true); return ( -
+
)} + {shouldRenderOptions ? ( + <> +

+ ) as any} + > + + Options + + +

+
+ + ( + + )} + /> + +
+
+ +
+ + ) : ( + '' + )} + {targetConfiguration.inputs && (

@@ -265,37 +304,6 @@ export default function TargetConfigurationDetails({

)} - {shouldRenderOptions ? ( - <> -

- ) as any} - > - - Options - - -

-
- - ( - - )} - /> - -
- - ) : ( - '' - )} - {shouldRenderConfigurations ? ( <>

diff --git a/nx-dev/ui-fence/src/lib/fences/terminal-output.tsx b/nx-dev/ui-fence/src/lib/fences/terminal-output.tsx index 0542ce31c49d0..d001690788d42 100644 --- a/nx-dev/ui-fence/src/lib/fences/terminal-output.tsx +++ b/nx-dev/ui-fence/src/lib/fences/terminal-output.tsx @@ -1,20 +1,22 @@ -import { ReactNode } from 'react'; +import type { JSX, ReactNode } from 'react'; import { TerminalShellWrapper } from './terminal-shell'; export function TerminalOutput({ content, command, path, + actionElement, }: { - content: ReactNode | null; + content: ReactNode; command: string; path: string; + actionElement?: ReactNode; }): JSX.Element { const commandLines = command.split('\n').filter(Boolean); return (
-
+
{commandLines.map((line, index) => { return (
@@ -29,6 +31,9 @@ export function TerminalOutput({

{line}

+ {actionElement ? ( +
{actionElement}
+ ) : null}
); })} diff --git a/packages/cypress/src/plugins/plugin.spec.ts b/packages/cypress/src/plugins/plugin.spec.ts index a5a6ca6c08b52..76249091e4c3e 100644 --- a/packages/cypress/src/plugins/plugin.spec.ts +++ b/packages/cypress/src/plugins/plugin.spec.ts @@ -101,6 +101,15 @@ describe('@nx/cypress/plugin', () => { ], "metadata": { "description": "Runs Cypress Tests", + "help": { + "command": "npx cypress run --help", + "example": { + "args": [ + "--dev", + "--headed", + ], + }, + }, "technologies": [ "cypress", ], @@ -117,6 +126,15 @@ describe('@nx/cypress/plugin', () => { "command": "cypress open", "metadata": { "description": "Opens Cypress", + "help": { + "command": "npx cypress open --help", + "example": { + "args": [ + "--dev", + "--e2e", + ], + }, + }, "technologies": [ "cypress", ], @@ -179,6 +197,15 @@ describe('@nx/cypress/plugin', () => { ], "metadata": { "description": "Runs Cypress Component Tests", + "help": { + "command": "npx cypress run --help", + "example": { + "args": [ + "--dev", + "--headed", + ], + }, + }, "technologies": [ "cypress", ], @@ -195,6 +222,15 @@ describe('@nx/cypress/plugin', () => { "command": "cypress open", "metadata": { "description": "Opens Cypress", + "help": { + "command": "npx cypress open --help", + "example": { + "args": [ + "--dev", + "--e2e", + ], + }, + }, "technologies": [ "cypress", ], @@ -273,6 +309,15 @@ describe('@nx/cypress/plugin', () => { ], "metadata": { "description": "Runs Cypress Tests", + "help": { + "command": "npx cypress run --help", + "example": { + "args": [ + "--dev", + "--headed", + ], + }, + }, "technologies": [ "cypress", ], @@ -306,6 +351,15 @@ describe('@nx/cypress/plugin', () => { ], "metadata": { "description": "Runs Cypress Tests in CI", + "help": { + "command": "npx cypress run --help", + "example": { + "args": [ + "--dev", + "--headed", + ], + }, + }, "nonAtomizedTarget": "e2e", "technologies": [ "cypress", @@ -330,6 +384,15 @@ describe('@nx/cypress/plugin', () => { ], "metadata": { "description": "Runs Cypress Tests in src/test.cy.ts in CI", + "help": { + "command": "npx cypress run --help", + "example": { + "args": [ + "--dev", + "--headed", + ], + }, + }, "nonAtomizedTarget": "e2e", "technologies": [ "cypress", @@ -347,6 +410,15 @@ describe('@nx/cypress/plugin', () => { "command": "cypress open", "metadata": { "description": "Opens Cypress", + "help": { + "command": "npx cypress open --help", + "example": { + "args": [ + "--dev", + "--e2e", + ], + }, + }, "technologies": [ "cypress", ], diff --git a/packages/cypress/src/plugins/plugin.ts b/packages/cypress/src/plugins/plugin.ts index 9467113d6d6bd..1131a816862a5 100644 --- a/packages/cypress/src/plugins/plugin.ts +++ b/packages/cypress/src/plugins/plugin.ts @@ -4,6 +4,7 @@ import { createNodesFromFiles, CreateNodesV2, detectPackageManager, + getPackageManagerCommand, joinPathFragments, logger, normalizePath, @@ -44,6 +45,8 @@ function writeTargetsToCache(cachePath: string, results: CypressTargets) { const cypressConfigGlob = '**/cypress.config.{js,ts,mjs,cjs}'; +const pmc = getPackageManagerCommand(); + export const createNodesV2: CreateNodesV2 = [ cypressConfigGlob, async (configFiles, options, context) => { @@ -211,6 +214,12 @@ async function buildCypressTargets( metadata: { technologies: ['cypress'], description: 'Runs Cypress Tests', + help: { + command: `${pmc.exec} cypress run --help`, + example: { + args: ['--dev', '--headed'], + }, + }, }, }; @@ -271,6 +280,12 @@ async function buildCypressTargets( technologies: ['cypress'], description: `Runs Cypress Tests in ${relativeSpecFilePath} in CI`, nonAtomizedTarget: options.targetName, + help: { + command: `${pmc.exec} cypress run --help`, + example: { + args: ['--dev', '--headed'], + }, + }, }, }; dependsOn.push({ @@ -290,6 +305,12 @@ async function buildCypressTargets( technologies: ['cypress'], description: 'Runs Cypress Tests in CI', nonAtomizedTarget: options.targetName, + help: { + command: `${pmc.exec} cypress run --help`, + example: { + args: ['--dev', '--headed'], + }, + }, }, }; ciTargetGroup.push(options.ciTargetName); @@ -307,6 +328,12 @@ async function buildCypressTargets( metadata: { technologies: ['cypress'], description: 'Runs Cypress Component Tests', + help: { + command: `${pmc.exec} cypress run --help`, + example: { + args: ['--dev', '--headed'], + }, + }, }, }; } @@ -317,6 +344,12 @@ async function buildCypressTargets( metadata: { technologies: ['cypress'], description: 'Opens Cypress', + help: { + command: `${pmc.exec} cypress open --help`, + example: { + args: ['--dev', '--e2e'], + }, + }, }, }; diff --git a/packages/eslint/src/plugins/plugin.spec.ts b/packages/eslint/src/plugins/plugin.spec.ts index fd04f0899eef5..eea28f229cfb6 100644 --- a/packages/eslint/src/plugins/plugin.spec.ts +++ b/packages/eslint/src/plugins/plugin.spec.ts @@ -114,6 +114,20 @@ describe('@nx/eslint/plugin', () => { ], }, ], + "metadata": { + "description": "Runs ESLint on project", + "help": { + "command": "npx eslint --help", + "example": { + "options": { + "max-warnings": 0, + }, + }, + }, + "technologies": [ + "eslint", + ], + }, "options": { "cwd": ".", }, @@ -155,6 +169,20 @@ describe('@nx/eslint/plugin', () => { ], }, ], + "metadata": { + "description": "Runs ESLint on project", + "help": { + "command": "npx eslint --help", + "example": { + "options": { + "max-warnings": 0, + }, + }, + }, + "technologies": [ + "eslint", + ], + }, "options": { "cwd": ".", }, @@ -227,6 +255,20 @@ describe('@nx/eslint/plugin', () => { ], }, ], + "metadata": { + "description": "Runs ESLint on project", + "help": { + "command": "npx eslint --help", + "example": { + "options": { + "max-warnings": 0, + }, + }, + }, + "technologies": [ + "eslint", + ], + }, "options": { "cwd": "apps/my-app", }, @@ -268,6 +310,20 @@ describe('@nx/eslint/plugin', () => { ], }, ], + "metadata": { + "description": "Runs ESLint on project", + "help": { + "command": "npx eslint --help", + "example": { + "options": { + "max-warnings": 0, + }, + }, + }, + "technologies": [ + "eslint", + ], + }, "options": { "cwd": "apps/my-app", }, @@ -381,6 +437,20 @@ describe('@nx/eslint/plugin', () => { ], }, ], + "metadata": { + "description": "Runs ESLint on project", + "help": { + "command": "npx eslint --help", + "example": { + "options": { + "max-warnings": 0, + }, + }, + }, + "technologies": [ + "eslint", + ], + }, "options": { "cwd": "apps/my-app", }, @@ -406,6 +476,20 @@ describe('@nx/eslint/plugin', () => { ], }, ], + "metadata": { + "description": "Runs ESLint on project", + "help": { + "command": "npx eslint --help", + "example": { + "options": { + "max-warnings": 0, + }, + }, + }, + "technologies": [ + "eslint", + ], + }, "options": { "cwd": "libs/my-lib", }, @@ -491,6 +575,20 @@ describe('@nx/eslint/plugin', () => { ], }, ], + "metadata": { + "description": "Runs ESLint on project", + "help": { + "command": "npx eslint --help", + "example": { + "options": { + "max-warnings": 0, + }, + }, + }, + "technologies": [ + "eslint", + ], + }, "options": { "cwd": "apps/my-app", }, @@ -517,6 +615,20 @@ describe('@nx/eslint/plugin', () => { ], }, ], + "metadata": { + "description": "Runs ESLint on project", + "help": { + "command": "npx eslint --help", + "example": { + "options": { + "max-warnings": 0, + }, + }, + }, + "technologies": [ + "eslint", + ], + }, "options": { "cwd": "libs/my-lib", }, @@ -560,6 +672,20 @@ describe('@nx/eslint/plugin', () => { ], }, ], + "metadata": { + "description": "Runs ESLint on project", + "help": { + "command": "npx eslint --help", + "example": { + "options": { + "max-warnings": 0, + }, + }, + }, + "technologies": [ + "eslint", + ], + }, "options": { "cwd": "apps/myapp", }, @@ -608,6 +734,20 @@ describe('@nx/eslint/plugin', () => { ], }, ], + "metadata": { + "description": "Runs ESLint on project", + "help": { + "command": "npx eslint --help", + "example": { + "options": { + "max-warnings": 0, + }, + }, + }, + "technologies": [ + "eslint", + ], + }, "options": { "cwd": "apps/myapp/nested/mylib", }, diff --git a/packages/eslint/src/plugins/plugin.ts b/packages/eslint/src/plugins/plugin.ts index be9338cc9103b..9ed9c9ba3286e 100644 --- a/packages/eslint/src/plugins/plugin.ts +++ b/packages/eslint/src/plugins/plugin.ts @@ -2,12 +2,13 @@ import { CreateNodes, CreateNodesContext, CreateNodesContextV2, + createNodesFromFiles, CreateNodesResult, CreateNodesV2, - TargetConfiguration, - createNodesFromFiles, + getPackageManagerCommand, logger, readJsonFile, + TargetConfiguration, writeJsonFile, } from '@nx/devkit'; import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; @@ -19,13 +20,15 @@ import { combineGlobPatterns } from 'nx/src/utils/globs'; import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context'; import { gte } from 'semver'; import { - ESLINT_CONFIG_FILENAMES, baseEsLintConfigFile, baseEsLintFlatConfigFile, + ESLINT_CONFIG_FILENAMES, isFlatConfig, } from '../utils/config-file'; import { resolveESLintClass } from '../utils/resolve-eslint-class'; +const pmc = getPackageManagerCommand(); + export interface EslintPluginOptions { targetName?: string; extensions?: string[]; @@ -453,7 +456,6 @@ function buildEslintTargets( standaloneSrcPath?: string ) { const isRootProject = projectRoot === '.'; - const targets: Record = {}; const targetConfig: TargetConfiguration = { @@ -481,6 +483,18 @@ function buildEslintTargets( { externalDependencies: ['eslint'] }, ], outputs: ['{options.outputFile}'], + metadata: { + technologies: ['eslint'], + description: 'Runs ESLint on project', + help: { + command: `${pmc.exec} eslint --help`, + example: { + options: { + 'max-warnings': 0, + }, + }, + }, + }, }; // Always set the environment variable to ensure that the ESLint CLI can run on eslint v8 and v9 diff --git a/packages/jest/src/plugins/__snapshots__/plugin.spec.ts.snap b/packages/jest/src/plugins/__snapshots__/plugin.spec.ts.snap new file mode 100644 index 0000000000000..31745e4220588 --- /dev/null +++ b/packages/jest/src/plugins/__snapshots__/plugin.spec.ts.snap @@ -0,0 +1,157 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`@nx/jest/plugin should add package as externalDependencies to the inputs when specified as preset and containing a jest-preset.cjs file 1`] = ` +[ + [ + "proj/jest.config.js", + { + "projects": { + "proj": { + "metadata": undefined, + "root": "proj", + "targets": { + "test": { + "cache": true, + "command": "jest", + "inputs": [ + "default", + "^production", + { + "externalDependencies": [ + "jest", + "some-package", + ], + }, + ], + "metadata": { + "description": "Run Jest Tests", + "help": { + "command": "npx jest --help", + "example": { + "options": { + "coverage": true, + }, + }, + }, + "technologies": [ + "jest", + ], + }, + "options": { + "cwd": "proj", + }, + "outputs": [ + "{workspaceRoot}/coverage", + ], + }, + }, + }, + }, + }, + ], +] +`; + +exports[`@nx/jest/plugin should add package as externalDependencies to the inputs when specified as preset and containing a jest-preset.js file 1`] = ` +[ + [ + "proj/jest.config.js", + { + "projects": { + "proj": { + "metadata": undefined, + "root": "proj", + "targets": { + "test": { + "cache": true, + "command": "jest", + "inputs": [ + "default", + "^production", + { + "externalDependencies": [ + "jest", + "some-package", + ], + }, + ], + "metadata": { + "description": "Run Jest Tests", + "help": { + "command": "npx jest --help", + "example": { + "options": { + "coverage": true, + }, + }, + }, + "technologies": [ + "jest", + ], + }, + "options": { + "cwd": "proj", + }, + "outputs": [ + "{workspaceRoot}/coverage", + ], + }, + }, + }, + }, + }, + ], +] +`; + +exports[`@nx/jest/plugin should add package as externalDependencies to the inputs when specified as preset and containing a jest-preset.json file 1`] = ` +[ + [ + "proj/jest.config.js", + { + "projects": { + "proj": { + "metadata": undefined, + "root": "proj", + "targets": { + "test": { + "cache": true, + "command": "jest", + "inputs": [ + "default", + "^production", + { + "externalDependencies": [ + "jest", + "some-package", + ], + }, + ], + "metadata": { + "description": "Run Jest Tests", + "help": { + "command": "npx jest --help", + "example": { + "options": { + "coverage": true, + }, + }, + }, + "technologies": [ + "jest", + ], + }, + "options": { + "cwd": "proj", + }, + "outputs": [ + "{workspaceRoot}/coverage", + ], + }, + }, + }, + }, + }, + ], +] +`; diff --git a/packages/jest/src/plugins/plugin.spec.ts b/packages/jest/src/plugins/plugin.spec.ts index 27c7ff58d8948..2f7a62bbe84c6 100644 --- a/packages/jest/src/plugins/plugin.spec.ts +++ b/packages/jest/src/plugins/plugin.spec.ts @@ -82,6 +82,14 @@ describe('@nx/jest/plugin', () => { ], "metadata": { "description": "Run Jest Tests", + "help": { + "command": "npx jest --help", + "example": { + "options": { + "coverage": true, + }, + }, + }, "technologies": [ "jest", ], @@ -151,6 +159,14 @@ describe('@nx/jest/plugin', () => { ], "metadata": { "description": "Run Jest Tests", + "help": { + "command": "npx jest --help", + "example": { + "options": { + "coverage": true, + }, + }, + }, "technologies": [ "jest", ], @@ -179,6 +195,14 @@ describe('@nx/jest/plugin', () => { ], "metadata": { "description": "Run Jest Tests in CI", + "help": { + "command": "npx jest --help", + "example": { + "options": { + "coverage": true, + }, + }, + }, "nonAtomizedTarget": "test", "technologies": [ "jest", @@ -202,6 +226,14 @@ describe('@nx/jest/plugin', () => { ], "metadata": { "description": "Run Jest Tests in src/unit.spec.ts", + "help": { + "command": "npx jest --help", + "example": { + "options": { + "coverage": true, + }, + }, + }, "nonAtomizedTarget": "test", "technologies": [ "jest", @@ -261,6 +293,14 @@ describe('@nx/jest/plugin', () => { ], "metadata": { "description": "Run Jest Tests", + "help": { + "command": "npx jest --help", + "example": { + "options": { + "coverage": true, + }, + }, + }, "technologies": [ "jest", ], @@ -305,49 +345,7 @@ describe('@nx/jest/plugin', () => { context ); - expect(results).toMatchInlineSnapshot(` - [ - [ - "proj/jest.config.js", - { - "projects": { - "proj": { - "metadata": undefined, - "root": "proj", - "targets": { - "test": { - "cache": true, - "command": "jest", - "inputs": [ - "default", - "^production", - { - "externalDependencies": [ - "jest", - "some-package", - ], - }, - ], - "metadata": { - "description": "Run Jest Tests", - "technologies": [ - "jest", - ], - }, - "options": { - "cwd": "proj", - }, - "outputs": [ - "{workspaceRoot}/coverage", - ], - }, - }, - }, - }, - }, - ], - ] - `); + expect(results).toMatchSnapshot(); } ); }); diff --git a/packages/jest/src/plugins/plugin.ts b/packages/jest/src/plugins/plugin.ts index 9cc595a5178d3..0d915681b9c9c 100644 --- a/packages/jest/src/plugins/plugin.ts +++ b/packages/jest/src/plugins/plugin.ts @@ -3,6 +3,7 @@ import { CreateNodesContext, createNodesFromFiles, CreateNodesV2, + getPackageManagerCommand, joinPathFragments, logger, normalizePath, @@ -29,6 +30,8 @@ import { combineGlobPatterns } from 'nx/src/utils/globs'; import { minimatch } from 'minimatch'; import { hashObject } from 'nx/src/devkit-internals'; +const pmc = getPackageManagerCommand(); + export interface JestPluginOptions { targetName?: string; ciTargetName?: string; @@ -55,6 +58,7 @@ export const createNodesV2: CreateNodesV2 = [ const optionsHash = hashObject(options); const cachePath = join(workspaceDataDirectory, `jest-${optionsHash}.hash`); const targetsCache = readTargetsCache(cachePath); + try { return await createNodesFromFiles( (configFile, options, context) => @@ -79,6 +83,7 @@ export const createNodes: CreateNodes = [ logger.warn( '`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.' ); + return createNodesInternal(...args, {}); }, ]; @@ -184,6 +189,14 @@ async function buildJestTargets( metadata: { technologies: ['jest'], description: 'Run Jest Tests', + help: { + command: `${pmc.exec} jest --help`, + example: { + options: { + coverage: true, + }, + }, + }, }, }); @@ -238,6 +251,14 @@ async function buildJestTargets( technologies: ['jest'], description: 'Run Jest Tests in CI', nonAtomizedTarget: options.targetName, + help: { + command: `${pmc.exec} jest --help`, + example: { + options: { + coverage: true, + }, + }, + }, }, }; targetGroup.push(options.ciTargetName); @@ -260,6 +281,14 @@ async function buildJestTargets( technologies: ['jest'], description: `Run Jest Tests in ${relativePath}`, nonAtomizedTarget: options.targetName, + help: { + command: `${pmc.exec} jest --help`, + example: { + options: { + coverage: true, + }, + }, + }, }, }; targetGroup.push(targetName); diff --git a/packages/nx/src/command-line/graph/graph.ts b/packages/nx/src/command-line/graph/graph.ts index 57f641bfc5af0..b07b72c372459 100644 --- a/packages/nx/src/command-line/graph/graph.ts +++ b/packages/nx/src/command-line/graph/graph.ts @@ -1,4 +1,5 @@ import { createHash } from 'crypto'; +import { execSync } from 'node:child_process'; import { existsSync, readFileSync, statSync, writeFileSync } from 'fs'; import { copySync, ensureDirSync } from 'fs-extra'; import * as http from 'http'; @@ -17,6 +18,7 @@ import { import { performance } from 'perf_hooks'; import { readNxJson, workspaceLayout } from '../../config/configuration'; import { + FileData, ProjectFileMap, ProjectGraph, ProjectGraphDependency, @@ -27,8 +29,6 @@ import { output } from '../../utils/output'; import { workspaceRoot } from '../../utils/workspace-root'; import { Server } from 'net'; - -import { FileData } from '../../config/project-graph'; import { TaskGraph } from '../../config/task-graph'; import { daemonClient } from '../../daemon/client/client'; import { getRootTsConfigPath } from '../../plugins/js/utils/typescript'; @@ -49,12 +49,10 @@ import { HashPlanner, transferProjectGraph } from '../../native'; import { transformProjectGraphForRust } from '../../native/transform-objects'; import { getAffectedGraphNodes } from '../affected/affected'; import { readFileMapCache } from '../../project-graph/nx-deps-cache'; -import { Hash, getNamedInputs } from '../../hasher/task-hasher'; +import { filterUsingGlobPatterns } from '../../hasher/task-hasher'; import { ConfigurationSourceMaps } from '../../project-graph/utils/project-configuration-utils'; import { createTaskHasher } from '../../hasher/create-task-hasher'; - -import { filterUsingGlobPatterns } from '../../hasher/task-hasher'; import { ProjectGraphError } from '../../project-graph/error-types'; import { isNxCloudUsed } from '../../utils/nx-cloud-utils'; @@ -600,6 +598,7 @@ async function startServer( 'task input generation:start', 'task input generation:end' ); + return; } @@ -621,6 +620,21 @@ async function startServer( return; } + if (sanitizePath === 'help') { + const project = parsedUrl.searchParams.get('project'); + const target = parsedUrl.searchParams.get('target'); + + try { + const text = getHelpTextFromTarget(project, target); + res.writeHead(200, { 'Content-Type': 'application/javascript' }); + res.end(JSON.stringify({ text, success: true })); + } catch (err) { + res.writeHead(200, { 'Content-Type': 'application/javascript' }); + res.end(JSON.stringify({ text: err.message, success: false })); + } + return; + } + let pathname = join(__dirname, '../../core/graph/', sanitizePath); // if the file is not found or is a directory, return index.html if (!existsSync(pathname) || statSync(pathname).isDirectory()) { @@ -1177,3 +1191,27 @@ async function createJsonOutput( return response; } + +function getHelpTextFromTarget( + projectName: string, + targetName: string +): string { + if (!projectName) throw new Error(`Missing project`); + if (!targetName) throw new Error(`Missing target`); + + const project = currentProjectGraphClientResponse.projects?.find( + (p) => p.name === projectName + ); + if (!project) throw new Error(`Cannot find project ${projectName}`); + + const target = project.data.targets[targetName]; + if (!target) throw new Error(`Cannot find target ${targetName}`); + + const command = target.metadata?.help?.command; + if (!command) + throw new Error(`No help command found for ${projectName}:${targetName}`); + + return execSync(command, { + cwd: join(workspaceRoot, project.data.root), + }).toString(); +} diff --git a/packages/nx/src/config/workspace-json-project-json.ts b/packages/nx/src/config/workspace-json-project-json.ts index 71b35304b3afc..4d7b1e4ef4fd0 100644 --- a/packages/nx/src/config/workspace-json-project-json.ts +++ b/packages/nx/src/config/workspace-json-project-json.ts @@ -126,9 +126,17 @@ export interface ProjectMetadata { export interface TargetMetadata { [k: string]: any; + description?: string; technologies?: string[]; nonAtomizedTarget?: string; + help?: { + command: string; + example: { + options?: Record; + args?: string[]; + }; + }; } export interface TargetDependencyConfig { diff --git a/packages/playwright/src/plugins/plugin.spec.ts b/packages/playwright/src/plugins/plugin.spec.ts index c257d7c4bc9e7..c6f4f1b6a9d2a 100644 --- a/packages/playwright/src/plugins/plugin.spec.ts +++ b/packages/playwright/src/plugins/plugin.spec.ts @@ -73,6 +73,14 @@ describe('@nx/playwright/plugin', () => { ], "metadata": { "description": "Runs Playwright Tests", + "help": { + "command": "npx playwright test --help", + "example": { + "options": { + "workers": 1, + }, + }, + }, "technologies": [ "playwright", ], @@ -99,6 +107,14 @@ describe('@nx/playwright/plugin', () => { ], "metadata": { "description": "Runs Playwright Tests in CI", + "help": { + "command": "npx playwright test --help", + "example": { + "options": { + "workers": 1, + }, + }, + }, "nonAtomizedTarget": "e2e", "technologies": [ "playwright", @@ -163,6 +179,14 @@ describe('@nx/playwright/plugin', () => { ], "metadata": { "description": "Runs Playwright Tests", + "help": { + "command": "npx playwright test --help", + "example": { + "options": { + "workers": 1, + }, + }, + }, "technologies": [ "playwright", ], @@ -192,6 +216,14 @@ describe('@nx/playwright/plugin', () => { ], "metadata": { "description": "Runs Playwright Tests in CI", + "help": { + "command": "npx playwright test --help", + "example": { + "options": { + "workers": 1, + }, + }, + }, "nonAtomizedTarget": "e2e", "technologies": [ "playwright", @@ -275,6 +307,14 @@ describe('@nx/playwright/plugin', () => { ], "metadata": { "description": "Runs Playwright Tests in CI", + "help": { + "command": "npx playwright test --help", + "example": { + "options": { + "workers": 1, + }, + }, + }, "nonAtomizedTarget": "e2e", "technologies": [ "playwright", @@ -300,6 +340,14 @@ describe('@nx/playwright/plugin', () => { ], "metadata": { "description": "Runs Playwright Tests in tests/run-me.spec.ts in CI", + "help": { + "command": "npx playwright test --help", + "example": { + "options": { + "workers": 1, + }, + }, + }, "nonAtomizedTarget": "e2e", "technologies": [ "playwright", @@ -328,6 +376,14 @@ describe('@nx/playwright/plugin', () => { ], "metadata": { "description": "Runs Playwright Tests in tests/run-me-2.spec.ts in CI", + "help": { + "command": "npx playwright test --help", + "example": { + "options": { + "workers": 1, + }, + }, + }, "nonAtomizedTarget": "e2e", "technologies": [ "playwright", diff --git a/packages/playwright/src/plugins/plugin.ts b/packages/playwright/src/plugins/plugin.ts index fa4159d9c7814..dc5e84b121db6 100644 --- a/packages/playwright/src/plugins/plugin.ts +++ b/packages/playwright/src/plugins/plugin.ts @@ -7,6 +7,7 @@ import { createNodesFromFiles, CreateNodesV2, detectPackageManager, + getPackageManagerCommand, joinPathFragments, logger, normalizePath, @@ -26,6 +27,8 @@ import { getLockFileName } from '@nx/js'; import { loadConfigFile } from '@nx/devkit/src/utils/config-utils'; import { hashObject } from 'nx/src/hasher/file-hasher'; +const pmc = getPackageManagerCommand(); + export interface PlaywrightPluginOptions { targetName?: string; ciTargetName?: string; @@ -162,6 +165,14 @@ async function buildPlaywrightTargets( metadata: { technologies: ['playwright'], description: 'Runs Playwright Tests', + help: { + command: `${pmc.exec} playwright test --help`, + example: { + options: { + workers: 1, + }, + }, + }, }, }; @@ -216,6 +227,14 @@ async function buildPlaywrightTargets( technologies: ['playwright'], description: `Runs Playwright Tests in ${relativeSpecFilePath} in CI`, nonAtomizedTarget: options.targetName, + help: { + command: `${pmc.exec} playwright test --help`, + example: { + options: { + workers: 1, + }, + }, + }, }, }; dependsOn.push({ @@ -243,6 +262,14 @@ async function buildPlaywrightTargets( technologies: ['playwright'], description: 'Runs Playwright Tests in CI', nonAtomizedTarget: options.targetName, + help: { + command: `${pmc.exec} playwright test --help`, + example: { + options: { + workers: 1, + }, + }, + }, }, }; ciTargetGroup.push(options.ciTargetName); diff --git a/packages/vite/src/plugins/__snapshots__/plugin-vitest.spec.ts.snap b/packages/vite/src/plugins/__snapshots__/plugin-vitest.spec.ts.snap index 44cf90108c62d..5a4b9f8ebe6e6 100644 --- a/packages/vite/src/plugins/__snapshots__/plugin-vitest.spec.ts.snap +++ b/packages/vite/src/plugins/__snapshots__/plugin-vitest.spec.ts.snap @@ -25,6 +25,21 @@ exports[`@nx/vite/plugin root project should create nodes 1`] = ` "env": "CI", }, ], + "metadata": { + "description": "Run Vite tests", + "help": { + "command": "npx vitest --help", + "example": { + "options": { + "bail": 1, + "coverage": true, + }, + }, + }, + "technologies": [ + "vite", + ], + }, "options": { "cwd": ".", }, diff --git a/packages/vite/src/plugins/__snapshots__/plugin-with-test.spec.ts.snap b/packages/vite/src/plugins/__snapshots__/plugin-with-test.spec.ts.snap index f1c1fb7307b6c..a46944aba01a8 100644 --- a/packages/vite/src/plugins/__snapshots__/plugin-with-test.spec.ts.snap +++ b/packages/vite/src/plugins/__snapshots__/plugin-with-test.spec.ts.snap @@ -25,6 +25,21 @@ exports[`@nx/vite/plugin with test node root project should create nodes - with "env": "CI", }, ], + "metadata": { + "description": "Run Vite tests", + "help": { + "command": "npx vitest --help", + "example": { + "options": { + "bail": 1, + "coverage": true, + }, + }, + }, + "technologies": [ + "vite", + ], + }, "options": { "cwd": ".", }, diff --git a/packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap b/packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap index 1c292204a3a28..1b75426da5b23 100644 --- a/packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap +++ b/packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap @@ -26,6 +26,21 @@ exports[`@nx/vite/plugin Library mode should exclude serve and preview targets w ], }, ], + "metadata": { + "description": "Run Vite build", + "help": { + "command": "npx vite build --help", + "example": { + "options": { + "manifest": "manifest.json", + "sourcemap": true, + }, + }, + }, + "technologies": [ + "vite", + ], + }, "options": { "cwd": "my-lib", }, @@ -67,6 +82,21 @@ exports[`@nx/vite/plugin not root project should create nodes 1`] = ` ], }, ], + "metadata": { + "description": "Run Vite build", + "help": { + "command": "npx vite build --help", + "example": { + "options": { + "manifest": "manifest.json", + "sourcemap": true, + }, + }, + }, + "technologies": [ + "vite", + ], + }, "options": { "cwd": "my-app", }, @@ -76,12 +106,40 @@ exports[`@nx/vite/plugin not root project should create nodes 1`] = ` }, "my-serve": { "command": "vite serve", + "metadata": { + "description": "Starts Vite dev server", + "help": { + "command": "npx vite --help", + "example": { + "options": { + "port": 3000, + }, + }, + }, + "technologies": [ + "vite", + ], + }, "options": { "cwd": "my-app", }, }, "preview-site": { "command": "vite preview", + "metadata": { + "description": "Locally preview Vite production build", + "help": { + "command": "npx vite preview --help", + "example": { + "options": { + "port": 3000, + }, + }, + }, + "technologies": [ + "vite", + ], + }, "options": { "cwd": "my-app", }, @@ -127,6 +185,21 @@ exports[`@nx/vite/plugin root project should create nodes 1`] = ` ], }, ], + "metadata": { + "description": "Run Vite build", + "help": { + "command": "npx vite build --help", + "example": { + "options": { + "manifest": "manifest.json", + "sourcemap": true, + }, + }, + }, + "technologies": [ + "vite", + ], + }, "options": { "cwd": ".", }, @@ -136,12 +209,40 @@ exports[`@nx/vite/plugin root project should create nodes 1`] = ` }, "preview": { "command": "vite preview", + "metadata": { + "description": "Locally preview Vite production build", + "help": { + "command": "npx vite preview --help", + "example": { + "options": { + "port": 3000, + }, + }, + }, + "technologies": [ + "vite", + ], + }, "options": { "cwd": ".", }, }, "serve": { "command": "vite serve", + "metadata": { + "description": "Starts Vite dev server", + "help": { + "command": "npx vite --help", + "example": { + "options": { + "port": 3000, + }, + }, + }, + "technologies": [ + "vite", + ], + }, "options": { "cwd": ".", }, diff --git a/packages/vite/src/plugins/plugin.ts b/packages/vite/src/plugins/plugin.ts index 24a61c794a236..e133cb0aaf072 100644 --- a/packages/vite/src/plugins/plugin.ts +++ b/packages/vite/src/plugins/plugin.ts @@ -5,6 +5,7 @@ import { createNodesFromFiles, CreateNodesV2, detectPackageManager, + getPackageManagerCommand, joinPathFragments, logger, ProjectConfiguration, @@ -21,6 +22,8 @@ import { getLockFileName } from '@nx/js'; import { loadViteDynamicImport } from '../utils/executor-utils'; import { hashObject } from 'nx/src/hasher/file-hasher'; +const pmc = getPackageManagerCommand(); + export interface VitePluginOptions { buildTargetName?: string; testTargetName?: string; @@ -28,6 +31,7 @@ export interface VitePluginOptions { previewTargetName?: string; serveStaticTargetName?: string; } + type ViteTargets = Pick; function readTargetsCache(cachePath: string): Record { @@ -229,6 +233,19 @@ async function buildTarget( }, ], outputs, + metadata: { + technologies: ['vite'], + description: `Run Vite build`, + help: { + command: `${pmc.exec} vite build --help`, + example: { + options: { + sourcemap: true, + manifest: 'manifest.json', + }, + }, + }, + }, }; } @@ -238,6 +255,18 @@ function serveTarget(projectRoot: string) { options: { cwd: joinPathFragments(projectRoot), }, + metadata: { + technologies: ['vite'], + description: `Starts Vite dev server`, + help: { + command: `${pmc.exec} vite --help`, + example: { + options: { + port: 3000, + }, + }, + }, + }, }; return targetConfig; @@ -249,6 +278,18 @@ function previewTarget(projectRoot: string) { options: { cwd: joinPathFragments(projectRoot), }, + metadata: { + technologies: ['vite'], + description: `Locally preview Vite production build`, + help: { + command: `${pmc.exec} vite preview --help`, + example: { + options: { + port: 3000, + }, + }, + }, + }, }; return targetConfig; @@ -275,6 +316,19 @@ async function testTarget( { env: 'CI' }, ], outputs, + metadata: { + technologies: ['vite'], + description: `Run Vite tests`, + help: { + command: `${pmc.exec} vitest --help`, + example: { + options: { + bail: 1, + coverage: true, + }, + }, + }, + }, }; } diff --git a/packages/webpack/src/plugins/__snapshots__/plugin.spec.ts.snap b/packages/webpack/src/plugins/__snapshots__/plugin.spec.ts.snap index 0915efc9ae0d5..a61d76d9e5f9c 100644 --- a/packages/webpack/src/plugins/__snapshots__/plugin.spec.ts.snap +++ b/packages/webpack/src/plugins/__snapshots__/plugin.spec.ts.snap @@ -25,6 +25,23 @@ exports[`@nx/webpack/plugin should create nodes 1`] = ` ], }, ], + "metadata": { + "description": "Runs Webpack build", + "help": { + "command": "npx webpack-cli build --help", + "example": { + "args": [ + "--profile", + ], + "options": { + "json": "stats.json", + }, + }, + }, + "technologies": [ + "webpack", + ], + }, "options": { "args": [ "--node-env=production", @@ -37,6 +54,23 @@ exports[`@nx/webpack/plugin should create nodes 1`] = ` }, "my-serve": { "command": "webpack-cli serve", + "metadata": { + "description": "Starts Webpack dev server", + "help": { + "command": "npx webpack-cli serve --help", + "example": { + "options": { + "args": [ + "--client-progress", + "--history-api-fallback ", + ], + }, + }, + }, + "technologies": [ + "webpack", + ], + }, "options": { "args": [ "--node-env=development", @@ -46,6 +80,23 @@ exports[`@nx/webpack/plugin should create nodes 1`] = ` }, "preview-site": { "command": "webpack-cli serve", + "metadata": { + "description": "Starts Webpack dev server in production mode", + "help": { + "command": "npx webpack-cli serve --help", + "example": { + "options": { + "args": [ + "--client-progress", + "--history-api-fallback ", + ], + }, + }, + }, + "technologies": [ + "webpack", + ], + }, "options": { "args": [ "--node-env=production", diff --git a/packages/webpack/src/plugins/plugin.ts b/packages/webpack/src/plugins/plugin.ts index 8924b727d73a9..be92a147cb5ad 100644 --- a/packages/webpack/src/plugins/plugin.ts +++ b/packages/webpack/src/plugins/plugin.ts @@ -6,6 +6,7 @@ import { CreateNodesResult, CreateNodesV2, detectPackageManager, + getPackageManagerCommand, logger, ProjectConfiguration, readJsonFile, @@ -23,6 +24,8 @@ import { dirname, isAbsolute, join, relative, resolve } from 'path'; import { readWebpackOptions } from '../utils/webpack/read-webpack-options'; import { resolveUserDefinedWebpackConfig } from '../utils/webpack/resolve-user-defined-webpack-config'; +const pmc = getPackageManagerCommand(); + export interface WebpackPluginOptions { buildTargetName?: string; serveTargetName?: string; @@ -176,6 +179,19 @@ async function createWebpackTargets( }, ], outputs: [outputPath], + metadata: { + technologies: ['webpack'], + description: 'Runs Webpack build', + help: { + command: `${pmc.exec} webpack-cli build --help`, + example: { + options: { + json: 'stats.json', + }, + args: ['--profile'], + }, + }, + }, }; targets[options.serveTargetName] = { @@ -184,6 +200,18 @@ async function createWebpackTargets( cwd: projectRoot, args: ['--node-env=development'], }, + metadata: { + technologies: ['webpack'], + description: 'Starts Webpack dev server', + help: { + command: `${pmc.exec} webpack-cli serve --help`, + example: { + options: { + args: ['--client-progress', '--history-api-fallback '], + }, + }, + }, + }, }; targets[options.previewTargetName] = { @@ -192,6 +220,18 @@ async function createWebpackTargets( cwd: projectRoot, args: ['--node-env=production'], }, + metadata: { + technologies: ['webpack'], + description: 'Starts Webpack dev server in production mode', + help: { + command: `${pmc.exec} webpack-cli serve --help`, + example: { + options: { + args: ['--client-progress', '--history-api-fallback '], + }, + }, + }, + }, }; targets[options.serveStaticTargetName] = {