Compare commits

...

11 Commits

Author SHA1 Message Date
FrozenPandaz b06bc241ea chore(misc): publish 16.0.1 2023-04-28 17:57:49 -04:00
Craigory Coppola 58d089ced0 fix(core): prefer NX_TASKS_RUNNER over NX_RUNNER (#16618)
(cherry picked from commit 22c0047b63)
2023-04-28 17:09:56 -04:00
Caleb Ukle 4554f65c6e fix(testing): fix react CT w/ vite and dependant projects (#16475)
(cherry picked from commit ebcc4363d0)
2023-04-28 17:09:54 -04:00
Caleb Ukle 39ae390692 docs(testing): add example of extending CT preset (#16641)
(cherry picked from commit a3875cc873)
2023-04-28 17:09:53 -04:00
Isaac Mann 81fb58b38a docs(core): remove ci var from gitlab example (#16604)
(cherry picked from commit 0b39480f63)
2023-04-28 17:09:51 -04:00
Emily Xiong d384415d7d fix(core): preserve deps in package.json for nx init react (#16528) 2023-04-28 16:56:20 -04:00
Craigory Coppola 2695e9764d fix(core): empty exclude pattern should not result in error (#16645) 2023-04-28 16:56:19 -04:00
Craigory Coppola e09c05573c fix(core): nx report should not display duplicate packages (#16647) 2023-04-28 16:56:19 -04:00
Miroslav Jonaš 2fc5d33059 fix(testing): ensure e2e cypress can use 4200 port in the test (#16646) 2023-04-28 16:56:18 -04:00
Nicholas Cunningham 99b69b8281 fix(react): Fix .module css being generated when --globalCss is passed (#16648) 2023-04-28 16:56:18 -04:00
Benjamin Cabanes f559e675d1 feat(nx-dev): change code-fences and code-output styles (#16649) 2023-04-28 16:56:17 -04:00
24 changed files with 589 additions and 156 deletions
@@ -34,6 +34,21 @@ The build target option can be changed later via updating the `devServerTarget`
When using component testing make sure to set `skipServe: true` in the component test target options, otherwise `@nx/cypress` will attempt to run the build first which can slow down your component tests. `skipServe: true` is automatically set when using the `cypress-component-configuration` generator.
{% /callout %}
## Configuration
When using the `cypress-component-configuration` generator, a helper function is used in the `cypress.config.ts` to setup the ideal settings for your project.
If you need to add additional configuration properties, you can spread the returned object from the helper function.
```ts {%filename="cypress.config.ts"}
export default defineConfig({
component: {
...nxComponentTestingPreset(__filename),
// add your own config here
},
});
```
## Testing Projects
Run `nx component-test your-lib` to execute the component tests with Cypress.
+1 -3
View File
@@ -3,7 +3,7 @@
Below is an example of a GitLab pipeline setup for an Nx workspace - building and testing only what is affected.
```yaml
image: node:16
image: node:18
stages:
- test
@@ -63,8 +63,6 @@ Read more about [Distributed Task Execution (DTE)](/core-features/distribute-tas
```yaml
image: node:18
variables:
CI: 'true'
# Creating template for DTE agents
.dte-agent:
@@ -34,6 +34,21 @@ The build target option can be changed later via updating the `devServerTarget`
When using component testing make sure to set `skipServe: true` in the component test target options, otherwise `@nx/cypress` will attempt to run the build first which can slow down your component tests. `skipServe: true` is automatically set when using the `cypress-component-configuration` generator.
{% /callout %}
## Configuration
When using the `cypress-component-configuration` generator, a helper function is used in the `cypress.config.ts` to setup the ideal settings for your project.
If you need to add additional configuration properties, you can spread the returned object from the helper function.
```ts {%filename="cypress.config.ts"}
export default defineConfig({
component: {
...nxComponentTestingPreset(__filename),
// add your own config here
},
});
```
## Testing Projects
Run `nx component-test your-lib` to execute the component tests with Cypress.
@@ -14,8 +14,9 @@ The following environment variables are ones that you can set to change the beha
| NX_PROFILE | string | Prepend `NX_PROFILE=profile.json` before running targets with Nx to generate a file that be [loaded in Chrome dev tools](/recipes/other/performance-profiling) to visualize the performance of Nx across multiple processes. |
| NX_PROJECT_GRAPH_CACHE_DIRECTORY | string | The project graph cache is stored in `node_modules/.cache/nx` by default. Set this variable to use a different directory. |
| NX_PROJECT_GRAPH_MAX_WORKERS | number | The number of workers to use when calculating the project graph. |
| NX_RUNNER | string | The name of task runner from the config to use. Can be overridden on the command line with `--runner`. |
| NX_RUNNER | string | The name of task runner from the config to use. Can be overridden on the command line with `--runner`. Not read if NX_TASKS_RUNNER is set. |
| NX_SKIP_NX_CACHE | boolean | Rerun the tasks even when the results are available in the cache |
| NX_TASKS_RUNNER | string | The name of task runner from the config to use. Can be overridden on the command line with `--runner`. Preferred over NX_RUNNER. |
| NX_TASKS_RUNNER_DYNAMIC_OUTPUT | boolean | If set to `false`, will use non-dynamic terminal output strategy (what you see in CI), even when you terminal can support the dynamic version |
| NX_VERBOSE_LOGGING | boolean | If set to `true`, will print debug information useful for troubleshooting |
| NX_DRY_RUN | boolean | If set to `true`, will perform a dry run of the generator. No files will be created and no packages will be installed. |
+8 -5
View File
@@ -4,7 +4,7 @@ import {
cleanupProject,
createFile,
ensureCypressInstallation,
killPorts,
killPort,
newProject,
readJson,
runCLI,
@@ -91,7 +91,7 @@ describe('env vars', () => {
`e2e ${myapp}-e2e --no-watch --env.cliArg="i am from the cli args"`
);
expect(run1).toContain('All specs passed!');
await killPorts(4200);
await killPort(4200);
// tests should not fail because of a config change
updateFile(
`apps/${myapp}-e2e/cypress.config.ts`,
@@ -111,7 +111,7 @@ export default defineConfig({
`e2e ${myapp}-e2e --no-watch --env.cliArg="i am from the cli args"`
);
expect(run2).toContain('All specs passed!');
await killPorts(4200);
await killPort(4200);
// make sure project.json env vars also work
updateFile(
@@ -140,10 +140,13 @@ describe('env vars', () => {
const run3 = runCLI(`e2e ${myapp}-e2e --no-watch`);
expect(run3).toContain('All specs passed!');
expect(await killPorts(4200)).toBeTruthy();
expect(await killPort(4200)).toBeTruthy();
}, 1000000);
it('should run e2e in parallel', () => {
it('should run e2e in parallel', async () => {
// ensure ports are free before running tests
await killPort(4200);
const ngAppName = uniq('ng-app');
runCLI(
`generate @nx/angular:app ${ngAppName} --e2eTestRunner=cypress --linter=eslint --no-interactive`
+5
View File
@@ -40,6 +40,8 @@ describe('nx init (for React)', () => {
expect(packageJson.devDependencies['@nx/jest']).toBeDefined();
expect(packageJson.devDependencies['@nx/vite']).toBeUndefined();
expect(packageJson.devDependencies['@nx/webpack']).toBeDefined();
expect(packageJson.dependencies['redux']).toBeDefined();
expect(packageJson.name).toEqual(appName);
runCLI(`build ${appName}`, {
env: {
@@ -149,6 +151,8 @@ describe('nx init (for React)', () => {
const packageJson = readJson('package.json');
expect(packageJson.devDependencies['@nx/jest']).toBeUndefined();
expect(packageJson.dependencies['redux']).toBeDefined();
expect(packageJson.name).toEqual(appName);
const viteConfig = readFile(`vite.config.js`);
expect(viteConfig).toContain('port: 4200'); // default port
@@ -186,6 +190,7 @@ function createReactApp(appName: string) {
'react-dom': '^18.2.0',
'react-scripts': '5.0.1',
'web-vitals': '2.1.4',
redux: '^3.6.0',
},
scripts: {
start: 'react-scripts start',
@@ -4,6 +4,7 @@ import {
ensureCypressInstallation,
newProject,
runCLI,
runCypressTests,
uniq,
updateFile,
updateJson,
@@ -146,18 +147,22 @@ export default Input;
runCLI(
`generate @nx/react:cypress-component-configuration --project=${appName} --generate-tests`
);
expect(runCLI(`component-test ${appName} --no-watch`)).toContain(
'All specs passed!'
);
if (runCypressTests()) {
expect(runCLI(`component-test ${appName} --no-watch`)).toContain(
'All specs passed!'
);
}
}, 300_000);
it('should successfully component test lib being used in app', () => {
runCLI(
`generate @nx/react:cypress-component-configuration --project=${usedInAppLibName} --generate-tests`
);
expect(runCLI(`component-test ${usedInAppLibName} --no-watch`)).toContain(
'All specs passed!'
);
if (runCypressTests()) {
expect(runCLI(`component-test ${usedInAppLibName} --no-watch`)).toContain(
'All specs passed!'
);
}
}, 300_000);
it('should test buildable lib not being used in app', () => {
@@ -184,9 +189,12 @@ describe(Input.name, () => {
runCLI(
`generate @nx/react:cypress-component-configuration --project=${buildableLibName} --generate-tests --build-target=${appName}:build`
);
expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain(
'All specs passed!'
);
if (runCypressTests()) {
expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain(
'All specs passed!'
);
}
// add tailwind
runCLI(`generate @nx/react:setup-tailwind --project=${buildableLibName}`);
@@ -213,9 +221,11 @@ ${content}`;
}
);
expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain(
'All specs passed!'
);
if (runCypressTests()) {
expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain(
'All specs passed!'
);
}
}, 300_000);
it('should work with async webpack config', () => {
@@ -250,8 +260,41 @@ ${content}`;
return config;
});
const results = runCLI(`component-test ${appName}`);
expect(results).toContain('I am from the custom async Webpack config');
expect(results).toContain('All specs passed!');
if (runCypressTests()) {
const results = runCLI(`component-test ${appName}`);
expect(results).toContain('I am from the custom async Webpack config');
expect(results).toContain('All specs passed!');
}
});
// flaky bc of upstream issue https://github.com/cypress-io/cypress/issues/25913
it.skip('should CT vite projects importing other projects', () => {
const viteLibName = uniq('vite-lib');
runCLI(
`generate @nrwl/react:lib ${viteLibName} --bundler=vite --no-interactive`
);
updateFile(`libs/${viteLibName}/src/lib/${viteLibName}.tsx`, () => {
return `import { Btn } from '@${projectName}/${usedInAppLibName}';
export function MyComponent() {
return (
<>
<Btn text={'I am the app'}/>
<p>hello</p>
</>
);
}
export default MyComponent;`;
});
runCLI(
`generate @nrwl/react:cypress-component-configuration --project=${viteLibName} --generate-tests --bundler=vite --build-target=${appName}:build`
);
if (runCypressTests()) {
expect(runCLI(`component-test ${viteLibName}`)).toContain(
'All specs passed!'
);
}
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"packages": ["build/packages/*", "build/packages/nx/native-packages/*"],
"version": "16.0.0",
"version": "16.0.1",
"granularPathspec": false,
"command": {
"publish": {
@@ -1,14 +1,14 @@
import {
CheckCircleIcon,
ClipboardDocumentCheckIcon,
ClipboardDocumentIcon,
InformationCircleIcon,
} from '@heroicons/react/24/outline';
import React, { ReactNode, useEffect, useState } from 'react';
// @ts-ignore
import { CopyToClipboard } from 'react-copy-to-clipboard';
// @ts-ignore
import SyntaxHighlighter from 'react-syntax-highlighter';
import { CodeOutput } from './fences/codeOutput.component';
import { CodeOutput } from './fences/code-output.component';
import { TerminalOutput } from './fences/terminal-output.component';
function resolveLanguage(lang: string) {
@@ -74,7 +74,7 @@ export function Fence({
children.includes('@nx/') || command.includes('@nx/');
return (
<div className="my-8 w-full">
<div className="code-block group relative inline-flex w-auto min-w-[50%] max-w-full">
<div className="code-block group relative w-full">
<div>
<CopyToClipboard
text={children}
@@ -84,7 +84,7 @@ export function Fence({
>
<button
type="button"
className="not-prose absolute top-0 right-0 z-10 flex rounded-tr-lg border border-slate-200 bg-slate-50/50 p-2 opacity-0 transition-opacity group-hover:opacity-100 dark:border-slate-700 dark:bg-slate-800"
className="not-prose absolute top-0 right-0 z-10 flex rounded-tr-lg border border-slate-200 bg-slate-50/50 p-2 opacity-0 transition-opacity group-hover:opacity-100 dark:border-slate-700 dark:bg-slate-800/60"
>
{copied ? (
<ClipboardDocumentCheckIcon className="h-5 w-5 text-blue-500 dark:text-sky-500" />
@@ -106,12 +106,12 @@ export function Fence({
/>
{showRescopeMessage && (
<a
className="relative block rounded-b-md border border-green-100 bg-green-50 px-4 py-2 text-xs font-medium text-green-600 no-underline hover:underline dark:border-green-900 dark:bg-green-900/30 dark:text-green-400"
className="relative block rounded-b-md border border-slate-200 bg-slate-50 px-4 py-2 text-xs font-medium no-underline hover:underline dark:border-slate-700 dark:bg-slate-800"
href="/recipes/other/rescope"
title="Nx 16 package name changes"
>
<CheckCircleIcon
className="mr-2 inline-block h-5 w-5 text-green-500 dark:text-green-400"
<InformationCircleIcon
className="mr-2 inline-block h-5 w-5"
aria-hidden="true"
/>
Nx 15 and lower use @nrwl/ instead of @nx/
@@ -1,3 +1,4 @@
import { cx } from '@nx/nx-dev/ui-primitives';
import { ReactNode } from 'react';
export function CodeOutput({
@@ -11,10 +12,10 @@ export function CodeOutput({
}): JSX.Element {
return (
<div
className={
'hljs not-prose w-full overflow-x-auto border border-slate-200 bg-slate-50/50 font-mono text-sm dark:border-slate-700 dark:bg-slate-800/60 ' +
(isMessageBelow ? 'rounded-t-lg border-b-0' : 'rounded-lg')
}
className={cx(
'hljs not-prose w-full overflow-x-auto border border-slate-200 bg-slate-50/50 font-mono text-sm dark:border-slate-700 dark:bg-slate-800/60',
isMessageBelow ? 'rounded-t-lg border-b-0' : 'rounded-lg'
)}
>
{!!fileName && (
<div className="flex border-b border-slate-200 bg-slate-50 px-4 py-2 italic text-slate-400 dark:border-slate-700 dark:bg-slate-800/80 dark:text-slate-500">
@@ -1,3 +1,4 @@
import { cx } from '@nx/nx-dev/ui-primitives';
import { ReactNode } from 'react';
export function TerminalOutput({
@@ -13,10 +14,10 @@ export function TerminalOutput({
}): JSX.Element {
return (
<div
className={
'coding not-prose overflow-hidden border border-slate-200 bg-slate-50 font-mono text-sm leading-normal subpixel-antialiased dark:border-slate-700 dark:bg-slate-800 ' +
(isMessageBelow ? 'rounded-t-lg border-b-0' : 'rounded-lg')
}
className={cx(
'hljs not-prose w-full overflow-x-auto border border-slate-200 bg-slate-50/50 font-mono text-sm dark:border-slate-700 dark:bg-slate-800/60',
isMessageBelow ? 'rounded-t-lg border-b-0' : 'rounded-lg'
)}
>
<div className="relative flex justify-center border-b border-slate-200 bg-slate-100/50 p-2 text-slate-400 dark:border-slate-700 dark:bg-slate-700/50 dark:text-slate-500">
<div className="absolute left-2 top-3 flex items-center gap-2">
+3 -3
View File
@@ -122,7 +122,7 @@
"@nrwl/next",
"@nx/node",
"@nrwl/node",
"@nx/nx-plugin",
"@nx/plugin",
"@nrwl/nx-plugin",
"@nx/react",
"@nrwl/react",
@@ -140,11 +140,11 @@
"@nx/webpack",
"@nrwl/webpack",
{
"package": "@nrwl/nx-cloud",
"package": "nx-cloud",
"version": "latest"
},
{
"package": "nx-cloud",
"package": "@nrwl/nx-cloud",
"version": "latest"
}
]
+36 -7
View File
@@ -197,13 +197,15 @@ describe('report', () => {
describe('findInstalledPackagesWeCareAbout', () => {
it('should not list packages that are not installed', () => {
const installed: [string, packageJsonUtils.PackageJson][] =
packagesWeCareAbout.map((x) => [
x,
{
name: x,
version: '1.0.0',
},
]);
packagesWeCareAbout
.filter((x) => !x.startsWith('@nrwl'))
.map((x) => [
x,
{
name: x,
version: '1.0.0',
},
]);
const uninstalled: [string, packageJsonUtils.PackageJson][] = [
installed.pop(),
installed.pop(),
@@ -225,6 +227,33 @@ describe('report', () => {
expect(result).toContain(pkg);
}
});
it('should not list @nrwl packages that are the same version as their equivalent @nx package', () => {
jest.spyOn(packageJsonUtils, 'readModulePackageJson').mockImplementation(
provideMockPackages({
'@nrwl/nx-plugin': { version: '16.0.0' },
'@nx/plugin': { version: '16.0.0' },
'@nrwl/linter': { version: '16.0.0' },
'@nx/linter': { version: '16.0.0' },
'@nrwl/workspace': { version: '16.0.0' },
'@nx/workspace': { version: '16.0.2' },
'@nrwl/tao': { version: '16.0.0' },
'@nrwl/nx-cloud': { version: '16.0.0' },
'nx-cloud': { version: '16.0.0' },
})
);
const result = findInstalledPackagesWeCareAbout().map((x) => x.package);
expect(result).not.toContain('@nrwl/nx-plugin');
expect(result).toContain('@nx/plugin');
expect(result).not.toContain('@nrwl/linter');
expect(result).toContain('@nx/linter');
expect(result).toContain('@nrwl/workspace');
expect(result).toContain('@nx/workspace');
expect(result).toContain('@nrwl/tao');
expect(result).toContain('nx-cloud');
expect(result).not.toContain('@nrwl/nx-cloud');
});
});
describe('findMisalignedPackagesForPackage', () => {
+40 -5
View File
@@ -275,11 +275,46 @@ export function findInstalledCommunityPlugins(): PackageJson[] {
);
}
export function findInstalledPackagesWeCareAbout() {
return packagesWeCareAbout.reduce((acc, next) => {
const v = readPackageVersion(next);
const packagesWeMayCareAbout: Record<string, string> = {};
// TODO (v17): Remove workaround for hiding @nrwl packages when matching @nx package is found.
const packageChangeMap: Record<string, string> = {
'@nrwl/nx-plugin': '@nx/plugin',
'@nx/plugin': '@nrwl/nx-plugin',
'@nrwl/eslint-plugin-nx': '@nx/eslint-plugin',
'@nx/eslint-plugin': '@nrwl/eslint-plugin-nx',
'@nrwl/nx-cloud': 'nx-cloud',
};
for (const pkg of packagesWeCareAbout) {
const v = readPackageVersion(pkg);
if (v) {
acc.push({ package: next, version: v });
// If its a @nrwl scoped package, keep the version if there is no
// corresponding @nx scoped package, or it has a different version.
if (pkg.startsWith('@nrwl/')) {
const otherPackage =
packageChangeMap[pkg] ?? pkg.replace('@nrwl/', '@nx/');
const otherVersion = packagesWeMayCareAbout[otherPackage];
if (!otherVersion || v !== otherVersion) {
packagesWeMayCareAbout[pkg] = v;
}
// If its a @nx scoped package, always keep the version, and
// remove the corresponding @nrwl scoped package if it exists.
} else if (pkg.startsWith('@nx/')) {
const otherPackage =
packageChangeMap[pkg] ?? pkg.replace('@nx/', '@nrwl/');
const otherVersion = packagesWeMayCareAbout[otherPackage];
if (otherVersion && v === otherVersion) {
delete packagesWeMayCareAbout[otherPackage];
}
packagesWeMayCareAbout[pkg] = v;
} else {
packagesWeMayCareAbout[pkg] = v;
}
}
return acc;
}, [] as { package: string; version: string }[]);
}
return Object.entries(packagesWeMayCareAbout).map(([pkg, version]) => ({
package: pkg,
version,
}));
}
+62 -4
View File
@@ -2,13 +2,14 @@ import { execSync } from 'child_process';
import { copySync, moveSync, readdirSync, removeSync } from 'fs-extra';
import { join } from 'path';
import { InitArgs } from '../../command-line/init';
import { fileExists, readJsonFile } from '../../utils/fileutils';
import { fileExists, readJsonFile, writeJsonFile } from '../../utils/fileutils';
import { output } from '../../utils/output';
import {
detectPackageManager,
getPackageManagerCommand,
PackageManagerCommands,
} from '../../utils/package-manager';
import { PackageJson } from '../../utils/package-json';
import { askAboutNxCloud, printFinalMessage } from '../utils';
import { checkForCustomWebpackSetup } from './check-for-custom-webpack-setup';
import { checkForUncommittedChanges } from './check-for-uncommitted-changes';
@@ -76,7 +77,7 @@ async function normalizeOptions(options: Options): Promise<NormalizedOptions> {
const appIsJs = !fileExists(`tsconfig.json`);
const reactAppName = readNameFromPackageJson();
const packageJson = readJsonFile('package.json');
const packageJson = readJsonFile(join(process.cwd(), 'package.json'));
const deps = {
...packageJson.dependencies,
...packageJson.devDependencies,
@@ -105,6 +106,13 @@ async function normalizeOptions(options: Options): Promise<NormalizedOptions> {
};
}
/**
* - Create a temp workspace
* - Move all files to temp workspace
* - Add bundler to temp workspace
* - Move files back to root
* - Clean up unused files
*/
async function reorgnizeWorkspaceStructure(options: NormalizedOptions) {
createTempWorkspace(options);
@@ -162,6 +170,8 @@ async function reorgnizeWorkspaceStructure(options: NormalizedOptions) {
}
function createTempWorkspace(options: NormalizedOptions) {
removeSync('temp-workspace');
execSync(
`npx ${
options.npxYesFlagNeeded ? '-y' : ''
@@ -187,12 +197,60 @@ function createTempWorkspace(options: NormalizedOptions) {
removeSync('node_modules');
}
function copyPackageJsonDepsFromTempWorkspace() {
const repoRoot = process.cwd();
let rootPackageJson = readJsonFile(join(repoRoot, 'package.json'));
const tempWorkspacePackageJson = readJsonFile(
join(repoRoot, 'temp-workspace', 'package.json')
);
rootPackageJson = overridePackageDeps(
'dependencies',
rootPackageJson,
tempWorkspacePackageJson
);
rootPackageJson = overridePackageDeps(
'devDependencies',
rootPackageJson,
tempWorkspacePackageJson
);
rootPackageJson.scripts = {}; // remove existing scripts
writeJsonFile(join(repoRoot, 'package.json'), rootPackageJson);
writeJsonFile(
join(repoRoot, 'temp-workspace', 'package.json'),
rootPackageJson
);
}
function overridePackageDeps(
depConfigName: 'dependencies' | 'devDependencies',
base: PackageJson,
override: PackageJson
): PackageJson {
if (!base[depConfigName]) {
base[depConfigName] = override[depConfigName];
return base;
}
const deps = override[depConfigName];
Object.keys(deps).forEach((dep) => {
if (base.dependencies?.[dep]) {
delete base.dependencies[dep];
}
if (base.devDependencies?.[dep]) {
delete base.devDependencies[dep];
}
base[depConfigName][dep] = deps[dep];
});
return base;
}
function moveFilesToTempWorkspace(options: NormalizedOptions) {
output.log({ title: '🚚 Moving your React app in your new Nx workspace' });
copyPackageJsonDepsFromTempWorkspace();
const requiredCraFiles = [
'project.json',
options.isStandalone ? null : 'package.json',
'package.json',
'src',
'public',
options.appIsJs ? null : 'tsconfig.json',
@@ -292,7 +350,7 @@ function cleanUpUnusedFilesAndAddConfigFiles(options: NormalizedOptions) {
setupE2eProject(options.reactAppName);
} else {
removeSync(join('apps', `${options.reactAppName}-e2e`));
execSync(`${options.pmc.rm} @nx/cypress eslint-plugin-cypress`);
execSync(`${options.pmc.rm} cypress @nx/cypress eslint-plugin-cypress`);
}
if (options.isStandalone) {
+187 -84
View File
@@ -185,100 +185,191 @@ describe('splitArgs', () => {
});
it('should set base and head based on environment variables in affected mode, if they are not provided directly on the command', () => {
const originalNxBase = process.env.NX_BASE;
process.env.NX_BASE = 'envVarSha1';
const originalNxHead = process.env.NX_HEAD;
process.env.NX_HEAD = 'envVarSha2';
withEnvironment(
{
NX_BASE: 'envVarSha1',
NX_HEAD: 'envVarSha2',
},
() => {
expect(
splitArgsIntoNxArgsAndOverrides(
{
__overrides_unparsed__: ['--notNxArg', 'true', '--override'],
$0: '',
},
'affected',
{} as any,
{} as any
).nxArgs
).toEqual({
base: 'envVarSha1',
head: 'envVarSha2',
skipNxCache: false,
parallel: 3,
});
expect(
splitArgsIntoNxArgsAndOverrides(
{
__overrides_unparsed__: ['--notNxArg', 'true', '--override'],
$0: '',
},
'affected',
{} as any,
{} as any
).nxArgs
).toEqual({
base: 'envVarSha1',
head: 'envVarSha2',
skipNxCache: false,
parallel: 3,
});
expect(
splitArgsIntoNxArgsAndOverrides(
{
__overrides_unparsed__: ['--notNxArg', 'true', '--override'],
$0: '',
head: 'directlyOnCommandSha1', // higher priority than $NX_HEAD
},
'affected',
{} as any,
{} as any
).nxArgs
).toEqual({
base: 'envVarSha1',
head: 'directlyOnCommandSha1',
skipNxCache: false,
parallel: 3,
});
expect(
splitArgsIntoNxArgsAndOverrides(
{
__overrides_unparsed__: ['--notNxArg', 'true', '--override'],
$0: '',
head: 'directlyOnCommandSha1', // higher priority than $NX_HEAD
},
'affected',
{} as any,
{} as any
).nxArgs
).toEqual({
base: 'envVarSha1',
head: 'directlyOnCommandSha1',
skipNxCache: false,
parallel: 3,
});
expect(
splitArgsIntoNxArgsAndOverrides(
{
__overrides_unparsed__: ['--notNxArg', 'true', '--override'],
$0: '',
base: 'directlyOnCommandSha2', // higher priority than $NX_BASE
},
'affected',
{} as any,
{} as any
).nxArgs
).toEqual({
base: 'directlyOnCommandSha2',
head: 'envVarSha2',
skipNxCache: false,
parallel: 3,
});
// Reset process data
process.env.NX_BASE = originalNxBase;
process.env.NX_HEAD = originalNxHead;
expect(
splitArgsIntoNxArgsAndOverrides(
{
__overrides_unparsed__: ['--notNxArg', 'true', '--override'],
$0: '',
base: 'directlyOnCommandSha2', // higher priority than $NX_BASE
},
'affected',
{} as any,
{} as any
).nxArgs
).toEqual({
base: 'directlyOnCommandSha2',
head: 'envVarSha2',
skipNxCache: false,
parallel: 3,
});
}
);
});
it('should set runner based on environment variables, if it is not provided directly on the command', () => {
const originalRunner = process.env.NX_RUNNER;
process.env.NX_RUNNER = 'some-env-runner-name';
describe('--runner environment handling', () => {
it('should set runner based on environment NX_RUNNER, if it is not provided directly on the command', () => {
withEnvironment({ NX_RUNNER: 'some-env-runner-name' }, () => {
expect(
splitArgsIntoNxArgsAndOverrides(
{
__overrides_unparsed__: ['--notNxArg', 'true', '--override'],
$0: '',
},
'run-one',
{} as any,
{
tasksRunnerOptions: {
'some-env-runner-name': { runner: '' },
},
}
).nxArgs.runner
).toEqual('some-env-runner-name');
expect(
splitArgsIntoNxArgsAndOverrides(
expect(
splitArgsIntoNxArgsAndOverrides(
{
__overrides_unparsed__: ['--notNxArg', 'true', '--override'],
$0: '',
runner: 'directlyOnCommand', // higher priority than $NX_RUNNER
},
'run-one',
{} as any,
{
tasksRunnerOptions: {
'some-env-runner-name': { runner: '' },
},
}
).nxArgs.runner
).toEqual('directlyOnCommand');
});
});
it('should set runner based on environment NX_TASKS_RUNNER, if it is not provided directly on the command', () => {
withEnvironment({ NX_TASKS_RUNNER: 'some-env-runner-name' }, () => {
expect(
splitArgsIntoNxArgsAndOverrides(
{
__overrides_unparsed__: ['--notNxArg', 'true', '--override'],
$0: '',
},
'run-one',
{} as any,
{
tasksRunnerOptions: {
'some-env-runner-name': { runner: '' },
},
}
).nxArgs.runner
).toEqual('some-env-runner-name');
expect(
splitArgsIntoNxArgsAndOverrides(
{
__overrides_unparsed__: ['--notNxArg', 'true', '--override'],
$0: '',
runner: 'directlyOnCommand', // higher priority than $NX_RUNNER
},
'run-one',
{} as any,
{
tasksRunnerOptions: {
'some-env-runner-name': { runner: '' },
},
}
).nxArgs.runner
).toEqual('directlyOnCommand');
});
});
it('should prefer NX_TASKS_RUNNER', () => {
withEnvironment(
{
__overrides_unparsed__: ['--notNxArg', 'true', '--override'],
$0: '',
NX_TASKS_RUNNER: 'some-env-runner-name',
NX_RUNNER: 'some-other-runner',
},
'run-one',
{} as any,
{} as any
).nxArgs.runner
).toEqual('some-env-runner-name');
() => {
expect(
splitArgsIntoNxArgsAndOverrides(
{
__overrides_unparsed__: ['--notNxArg', 'true', '--override'],
$0: '',
},
'run-one',
{} as any,
{
tasksRunnerOptions: {
'some-env-runner-name': { runner: '' },
'some-other-runner': { runner: '' },
},
}
).nxArgs.runner
).toEqual('some-env-runner-name');
}
);
});
expect(
splitArgsIntoNxArgsAndOverrides(
it('should ignore runners based on environment, if it is valid', () => {
withEnvironment(
{
__overrides_unparsed__: ['--notNxArg', 'true', '--override'],
$0: '',
runner: 'directlyOnCommand', // higher priority than $NX_RUNNER
NX_TASKS_RUNNER: 'some-env-runner-name',
NX_RUNNER: 'some-other-runner',
},
'run-one',
{} as any,
{} as any
).nxArgs.runner
).toEqual('directlyOnCommand');
// Reset process data
process.env.NX_RUNNER = originalRunner;
() => {
expect(
splitArgsIntoNxArgsAndOverrides(
{
__overrides_unparsed__: ['--notNxArg', 'true', '--override'],
$0: '',
},
'run-one',
{} as any,
{} as any
).nxArgs.runner
).not.toBeDefined();
}
);
});
});
describe('--parallel', () => {
@@ -389,3 +480,15 @@ describe('splitArgs', () => {
});
});
});
function withEnvironment(env: Record<string, string>, callback: () => void) {
const originalValues: Record<string, string> = {};
for (const key in env) {
originalValues[key] = process.env[key];
process.env[key] = env[key];
}
callback();
for (const key in env) {
process.env[key] = originalValues[key];
}
}
+53 -10
View File
@@ -181,16 +181,7 @@ export function splitArgsIntoNxArgsAndOverrides(
nxArgs.skipNxCache = process.env.NX_SKIP_NX_CACHE === 'true';
}
if (!nxArgs.runner && process.env.NX_RUNNER) {
nxArgs.runner = process.env.NX_RUNNER;
if (options.printWarnings) {
output.note({
title: `No explicit --runner argument provided, but found environment variable NX_RUNNER so using its value: ${output.bold(
`${nxArgs.runner}`
)}`,
});
}
}
normalizeNxArgsRunner(nxArgs, nxJson, options);
if (args['parallel'] === 'false' || args['parallel'] === false) {
nxArgs['parallel'] = 1;
@@ -210,6 +201,58 @@ export function splitArgsIntoNxArgsAndOverrides(
return { nxArgs, overrides } as any;
}
function normalizeNxArgsRunner(
nxArgs: RawNxArgs,
nxJson: NxJsonConfiguration<string[] | '*'>,
options: { printWarnings: boolean }
) {
if (!nxArgs.runner) {
// TODO: Remove NX_RUNNER environment variable support in Nx v17
for (const envKey of ['NX_TASKS_RUNNER', 'NX_RUNNER']) {
const runner = process.env[envKey];
if (runner) {
const runnerExists = nxJson.tasksRunnerOptions?.[runner];
if (options.printWarnings) {
if (runnerExists) {
output.note({
title: `No explicit --runner argument provided, but found environment variable ${envKey} so using its value: ${output.bold(
`${runner}`
)}`,
});
} else if (
nxArgs.verbose ||
process.env.NX_VERBOSE_LOGGING === 'true'
) {
output.warn({
title: `Could not find ${output.bold(
`${runner}`
)} within \`nx.json\` tasksRunnerOptions.`,
bodyLines: [
`${output.bold(`${runner}`)} was set by ${envKey}`,
``,
`To suppress this message, either:`,
` - provide a valid task runner with --runner`,
` - ensure NX_TASKS_RUNNER matches a task runner defined in nx.json`,
],
});
}
}
if (runnerExists) {
// TODO: Remove in v17
if (envKey === 'NX_RUNNER' && options.printWarnings) {
output.warn({
title:
'NX_RUNNER is deprecated, please use NX_TASKS_RUNNER instead.',
});
}
nxArgs.runner = runner;
}
break;
}
}
}
}
export function parseFiles(options: NxArgs): { files: string[] } {
const { files, uncommitted, untracked, base, head } = options;
@@ -54,6 +54,18 @@ describe('findMatchingProjects', () => {
},
};
it('should return no projects when passed no patterns', () => {
expect(findMatchingProjects([], projectGraph)).toEqual([]);
});
it('should return no projects when passed empty string', () => {
expect(findMatchingProjects([''], projectGraph)).toEqual([]);
});
it('should not throw when a pattern is empty string', () => {
expect(findMatchingProjects(['', 'a'], projectGraph)).toEqual(['a']);
});
it('should expand "*"', () => {
expect(findMatchingProjects(['*'], projectGraph)).toEqual([
'test-project',
@@ -35,12 +35,20 @@ export function findMatchingProjects(
patterns: string[] = [],
projects: ProjectNodeMap
): string[] {
if (!patterns.length || patterns.filter((p) => p.length).length === 0) {
return []; // Short circuit if called with no patterns
}
const projectNames = keys(projects);
const selectedProjects: Set<string> = new Set();
const excludedProjects: Set<string> = new Set();
for (const stringPattern of patterns) {
if (!stringPattern.length) {
continue;
}
const pattern = parseStringPattern(stringPattern, projects);
// Handle wildcard with short-circuit, as its a common case with potentially
@@ -254,16 +262,19 @@ function isValidPatternType(type: string): type is ProjectPatternType {
export const getMatchingStringsWithCache = (() => {
// Map< Pattern, Map< Item, Result >>
const minimatchCache = new Map<string, Map<string, boolean>>();
const regexCache = new Map<string, RegExp>();
return (pattern: string, items: string[]) => {
if (!minimatchCache.has(pattern)) {
minimatchCache.set(pattern, new Map());
}
const patternCache = minimatchCache.get(pattern)!;
let matcher = null;
if (!regexCache.has(pattern)) {
regexCache.set(pattern, minimatch.makeRe(pattern));
}
const matcher = regexCache.get(pattern);
return items.filter((item) => {
let entry = patternCache.get(item);
if (entry === undefined || entry === null) {
matcher ??= minimatch.makeRe(pattern);
entry = item === pattern ? true : matcher.test(item);
patternCache.set(item, entry);
}
@@ -5,6 +5,7 @@ import {
import type { CypressExecutorOptions } from '@nx/cypress/src/executors/cypress/cypress.impl';
import {
ExecutorContext,
joinPathFragments,
logger,
parseTargetString,
ProjectGraph,
@@ -19,7 +20,8 @@ import {
getProjectConfigByPath,
} from '@nx/cypress/src/utils/ct-helpers';
import type { Configuration } from 'webpack';
import { existsSync, lstatSync } from 'fs';
import { dirname, join } from 'path';
type ViteDevServer = {
framework: 'react';
bundler: 'vite';
@@ -66,6 +68,36 @@ export function nxComponentTestingPreset(
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
devServer: {
...({ framework: 'react', bundler: 'vite' } as const),
viteConfig: async () => {
const normalizedPath = ['.ts', '.js'].some((ext) =>
pathToConfig.endsWith(ext)
)
? pathToConfig
: dirname(pathToConfig);
const viteConfigPath = findViteConfig(normalizedPath);
const { mergeConfig, loadConfigFromFile, searchForWorkspaceRoot } =
(await import('vite')) as typeof import('vite');
const resolved = await loadConfigFromFile(
{
mode: 'watch',
command: 'serve',
},
viteConfigPath
);
return mergeConfig(resolved.config, {
server: {
fs: {
allow: [
searchForWorkspaceRoot(normalizedPath),
workspaceRoot,
joinPathFragments(workspaceRoot, 'node_modules/vite'),
],
},
},
});
},
},
};
}
@@ -232,3 +264,12 @@ function buildTargetWebpack(
};
}
}
function findViteConfig(projectRootFullPath: string): string {
const allowsExt = ['js', 'mjs', 'ts', 'cjs', 'mts', 'cts'];
for (const ext of allowsExt) {
if (existsSync(join(projectRootFullPath, `vite.config.${ext}`))) {
return join(projectRootFullPath, `vite.config.${ext}`);
}
}
}
@@ -56,6 +56,13 @@ export async function addFiles(
addDependenciesToPackageJson(tree, {}, { '@nx/webpack': nxVersion });
}
if (
options.bundler === 'vite' ||
(!options.bundler && actualBundler === 'vite')
) {
addDependenciesToPackageJson(tree, {}, { '@nx/vite': nxVersion });
}
if (options.generateTests) {
const filePaths = [];
visitNotIgnoredFiles(tree, projectConfig.sourceRoot, (filePath) => {
@@ -384,6 +384,15 @@ describe('lib', () => {
});
});
describe('--globalCss', () => {
it('should not generate .module styles', async () => {
await libraryGenerator(tree, { ...defaultSchema, globalCss: true });
expect(tree.exists('libs/my-lib/src/lib/my-lib.css'));
expect(tree.exists('libs/my-lib/src/lib/my-lib.module.css')).toBeFalsy();
});
});
describe('--unit-test-runner none', () => {
it('should not generate test configuration', async () => {
await libraryGenerator(tree, {
@@ -141,6 +141,7 @@ export async function libraryGenerator(host: Tree, schema: Schema) {
pascalCaseFiles: options.pascalCaseFiles,
inSourceTests: options.inSourceTests,
skipFormat: true,
globalCss: options.globalCss,
});
tasks.push(componentTask);
}
+2
View File
@@ -114,6 +114,8 @@ const IGNORE_MATCHES_IN_PACKAGE = {
'url-loader',
'webpack',
'webpack-merge',
// used via the CT react plugin installed via vite plugin
'vite',
],
'react-native': ['@nx/storybook'],
rollup: ['@swc/core'],