feat(core): add --help content to project details view (#26629)

This PR adds help text for each inferred target that provides
`metadata.help.command`. To pass options/args to the target, users are
directed to open `project.json` and copy the values from `--help` output
into `options` property of the target.

To display the options help section, the inferred target must provide
metadata as follows:

```json5
 metadata: {
      help: {
        command: `foo --help`
        example: {
          options: {
            bar: true
          },
        },
      },
    },
```

The `help.command` value will be used to retrieve help text for the
underlying CLI (e.g. `jest --help`). The `help.example` property
contains sample options and args that users can add to their
`project.json` file -- currently rendered in the hover tooltip of
`project.json` hint text.

---

Example with `vite build --help`:

<img width="1257" alt="Screenshot 2024-06-21 at 3 06 21 PM"
src="https://github.com/nrwl/nx/assets/53559/b94cdcde-80da-4fa5-9f93-11af7fbcaf27">


Result of clicking `Run`:
<img width="1257" alt="Screenshot 2024-06-21 at 3 06 24 PM"
src="https://github.com/nrwl/nx/assets/53559/6803a5a8-9bbd-4510-b9ff-fa895a5b3402">

`project.json` tooltip hint:
<img width="1392" alt="Screenshot 2024-06-25 at 12 44 02 PM"
src="https://github.com/nrwl/nx/assets/53559/565002ae-7993-4dda-ac5d-4b685710f65e">
This commit is contained in:
Jack Hsu 2024-06-27 13:33:35 -04:00 committed by GitHub
parent df83dd4c6e
commit d90a735540
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1150 additions and 90 deletions

View File

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

View File

@ -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 = (
<button
className="flex items-center rounded-md border border-slate-500 px-1 disabled:opacity-75"
disabled={isPending}
onClick={
environment === 'nx-console'
? () => {
externalApiService.postEvent({
type: 'run-help',
payload: {
projectName,
targetName,
helpCommand,
},
});
}
: async () => {
setPending(true);
const result = await fetch(
`/help?project=${encodeURIComponent(
projectName
)}&target=${encodeURIComponent(targetName)}`
).then((resp) => resp.json());
setResult(result);
setPending(false);
}
}
>
<PlayIcon className="mr-1 h-4 w-4" />
Run
</button>
);
} else {
runHelpActionElement = (
<button
className="flex items-center rounded-md border border-slate-500 px-1"
onClick={() => setResult(null)}
>
<XMarkIcon className="mr-1 h-4 w-4" />
Clear output
</button>
);
}
return (
helpCommand && (
<>
<p className="mb-4">
Use <code>--help</code> to see all options for this command, and set
them by{' '}
<a
className="text-blue-500 hover:underline"
target="_blank"
href="https://nx.dev/recipes/running-tasks/pass-args-to-commands#pass-args-to-commands"
>
passing them
</a>{' '}
to the <code>"options"</code> property in{' '}
<Tooltip
openAction="hover"
content={
(
<div className="w-fit max-w-md">
<p className="mb-2">
For example, you can use the following configuration for the{' '}
<code>{targetName}</code> target in the{' '}
<code>project.json</code> file for{' '}
<span className="font-semibold">{projectName}</span>.
</p>
<pre className="mb-2 border border-slate-200 bg-slate-100/50 p-2 p-2 text-slate-400 dark:border-slate-700 dark:bg-slate-700/50 dark:text-slate-500">
{helpExampleTest}
</pre>
{helpExampleOptions && (
<p className="mb-2">
The <code>options</code> are CLI options prefixed by{' '}
<code>--</code>, such as <code>ls --color=never</code>,
where you would use <code>{'"color": "never"'}</code> to
set it in the target configuration.
</p>
)}
{helpExampleArgs && (
<p className="mb-2">
The <code>args</code> are CLI positional arguments, such
as <code>ls somedir</code>, where you would use{' '}
<code>{'"args": ["somedir"]'}</code> to set it in the
target configuration.
</p>
)}
</div>
) as any
}
>
<code>
<TooltipTriggerText>project.json</TooltipTriggerText>
</code>
</Tooltip>
.
</p>
<TerminalOutput
command={helpCommand}
path={targetConfiguration.options.cwd ?? ''}
actionElement={runHelpActionElement}
content={
<div className="relative w-full">
<Transition
show={!!result}
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-100"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<pre
className={result && !result.success ? 'text-red-500' : ''}
>
{result?.text}
</pre>
</Transition>
</div>
}
/>
</>
)
);
}

View File

@ -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 (
<div className="relative overflow-hidden rounded-md border border-slate-200 dark:border-slate-700/60">
<div className="relative rounded-md border border-slate-200 dark:border-slate-700/60">
<TargetConfigurationDetailsHeader
isCollasped={collapsed}
toggleCollapse={handleCollapseToggle}
@ -141,6 +142,44 @@ export default function TargetConfigurationDetails({
</div>
)}
{shouldRenderOptions ? (
<>
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="options" />) as any}
>
<span className="font-medium">
<TooltipTriggerText>Options</TooltipTriggerText>
</span>
</Tooltip>
</h4>
<div className="mb-4">
<FadingCollapsible>
<JsonCodeBlock
data={options}
renderSource={(propertyName: string) => (
<TargetSourceInfo
className="flex min-w-0 pl-4"
propertyKey={`targets.${targetName}.options.${propertyName}`}
sourceMap={sourceMap}
/>
)}
/>
</FadingCollapsible>
</div>
<div className="mb-4">
<ShowOptionsHelp
targetConfiguration={targetConfiguration}
projectName={projectName}
targetName={targetName}
/>
</div>
</>
) : (
''
)}
{targetConfiguration.inputs && (
<div className="group">
<h4 className="mb-4">
@ -265,37 +304,6 @@ export default function TargetConfigurationDetails({
</div>
)}
{shouldRenderOptions ? (
<>
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="options" />) as any}
>
<span className="font-medium">
<TooltipTriggerText>Options</TooltipTriggerText>
</span>
</Tooltip>
</h4>
<div className="mb-4">
<FadingCollapsible>
<JsonCodeBlock
data={options}
renderSource={(propertyName: string) => (
<TargetSourceInfo
className="flex min-w-0 pl-4"
propertyKey={`targets.${targetName}.options.${propertyName}`}
sourceMap={sourceMap}
/>
)}
/>
</FadingCollapsible>
</div>
</>
) : (
''
)}
{shouldRenderConfigurations ? (
<>
<h4 className="mb-4 py-2">

View File

@ -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 (
<TerminalShellWrapper>
<div className="overflow-x-auto p-4 pt-2">
<div className="items-left flex flex-col">
<div className="items-left relative flex flex-col">
{commandLines.map((line, index) => {
return (
<div key={index} className="flex">
@ -29,6 +31,9 @@ export function TerminalOutput({
</span>
</p>
<p className="typing mt-0.5 flex-1 pl-2">{line}</p>
{actionElement ? (
<div className="sticky top-0 pl-2">{actionElement}</div>
) : null}
</div>
);
})}

View File

@ -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",
],

View File

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

View File

@ -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",
},

View File

@ -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<string, TargetConfiguration> = {};
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

View File

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

View File

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

View File

@ -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<JestPluginOptions> = [
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<JestPluginOptions> = [
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);

View File

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

View File

@ -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<string, unknown>;
args?: string[];
};
};
}
export interface TargetDependencyConfig {

View File

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

View File

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

View File

@ -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": ".",
},

View File

@ -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": ".",
},

View File

@ -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": ".",
},

View File

@ -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<ProjectConfiguration, 'targets' | 'metadata'>;
function readTargetsCache(cachePath: string): Record<string, ViteTargets> {
@ -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,
},
},
},
},
};
}

View File

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

View File

@ -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] = {