feat(misc): v19 cleanup for Nx plugins (#23104)

This PR removes deprecated code that's been slated for removal in Nx 19
- mentioned as `TODO(v19)` comments.

## Breaking Changes

- **CNW:** `create-nx-workspace` no longer support `--preset=empty` and
`--preset=core`, use `--preset=apps` and `--preset=npm` respectively.
Deprecated in Nx 15.9.
- **Next.js:** `NX_` environment variables are no longer bundled into
Next.js apps, use `NEXT_PUBLIC` instead. Deprecated in Nx 16.8.
- **Webpack, Storybook, Esbuild:** `NX_` environment variables are no
longer bundled into browser bundles, use `NX_PUBLIC` instead. This
removes the possibility of intentional bundling of `NX_` variables.
Deprecated in Nx 18.
- **Cypress:** `cypressComponentConfiguration` generator removed from
`@nx/cypress`, use `configurationGenerator`instead. Deprecated in Nx
16.8.
- **Cypress:** `cypressProjectGenerator` generator removed from
`@nx/cypress`, use `configurationGenerator` instead. Deprecated in Nx
15.9.
- **Expo:** `withNxWebpack` removed from `@nx/expo`, use [metro
bundler](https://docs.expo.dev/guides/customizing-metro/)
(https://docs.expo.dev/guides/customizing-metro/) in app.json instead.
There is a migration to handle this in Nx 19. Deprecated in Nx 15.8.

## Deferred to v20

- **JS:** `classProperties.loose` option removed from `@nx/js/babel`
preset, use `loose` instead. Deprecated in Nx 17.0.
- **ESLint:** Low priority task to "deviations from
@typescript-eslint/recommended" for our lint rules. @JamesHenry will
look at this later before Nx 20, but it is unimportant.
- **React:** component testing does not work with Project Crystal, and
we need the executor + built-in webpack configs to run CT. Will do a
follow-up on this after Nx 19 release. Related issue:
https://github.com/nrwl/nx/issues/21546
- **Next.js:** `withStylus` removal from `@nx/next`, use SASS instead.
It hasn't worked, but we kept the file to throw an error when used.
Deprecated in Nx 17.0.
- **Next.js**: `@nx/next:component` and `@nx/next:page` generators to
not derive the `components` and `app`/`pages` directory. Use `nx g
@nx/next:component apps/myapp/components/button` instead. Deprecated in
Nx 17.0.
- **Webpack:** `isolatedConfig` option removal from
`@nx/webpack:webpack` executor. There is a migration to handle this in
Nx 19. Deprecated in in Nx 17.2.
- **Angular:** `executeWebpackDevServerBuilder` removal from
`@nx/angular/executors`, use `executeDevServerBuilder` instead.
Deprecated in Nx 17.0.
This commit is contained in:
Jack Hsu 2024-05-02 13:37:12 -04:00 committed by GitHub
parent 35f0618347
commit 2e621f324c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 233 additions and 1872 deletions

View File

@ -145,7 +145,7 @@ Prefix to use for Angular component and directive selectors.
Type: `string`
Customizes the initial content of your workspace. Default presets include: ["apps", "empty", "core", "npm", "ts", "web-components", "angular-monorepo", "angular-standalone", "react-monorepo", "react-standalone", "vue-monorepo", "vue-standalone", "nuxt", "nuxt-standalone", "next", "nextjs-standalone", "remix-monorepo", "remix-standalone", "react-native", "expo", "nest", "express", "react", "vue", "angular", "node-standalone", "node-monorepo", "ts-standalone"]. To build your own see https://nx.dev/extending-nx/recipes/create-preset
Customizes the initial content of your workspace. Default presets include: ["apps", "npm", "ts", "web-components", "angular-monorepo", "angular-standalone", "react-monorepo", "react-standalone", "vue-monorepo", "vue-standalone", "nuxt", "nuxt-standalone", "next", "nextjs-standalone", "remix-monorepo", "remix-standalone", "react-native", "expo", "nest", "express", "react", "vue", "angular", "node-standalone", "node-monorepo", "ts-standalone"]. To build your own see https://nx.dev/extending-nx/recipes/create-preset
### routing

View File

@ -7017,14 +7017,6 @@
"isExternal": false,
"disableCollapsible": false
},
{
"id": "cypress-project",
"path": "/nx-api/cypress/generators/cypress-project",
"name": "cypress-project",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "configuration",
"path": "/nx-api/cypress/generators/configuration",

View File

@ -510,15 +510,6 @@
"path": "/nx-api/cypress/generators/init",
"type": "generator"
},
"/nx-api/cypress/generators/cypress-project": {
"description": "Add a Cypress E2E Project.",
"file": "generated/packages/cypress/generators/cypress-project.json",
"hidden": true,
"name": "cypress-project",
"originalFilePath": "/packages/cypress/src/generators/cypress-project/schema.json",
"path": "/nx-api/cypress/generators/cypress-project",
"type": "generator"
},
"/nx-api/cypress/generators/configuration": {
"description": "Add a Cypress E2E Configuration to an existing project.",
"file": "generated/packages/cypress/generators/configuration.json",

View File

@ -502,15 +502,6 @@
"path": "cypress/generators/init",
"type": "generator"
},
{
"description": "Add a Cypress E2E Project.",
"file": "generated/packages/cypress/generators/cypress-project.json",
"hidden": true,
"name": "cypress-project",
"originalFilePath": "/packages/cypress/src/generators/cypress-project/schema.json",
"path": "cypress/generators/cypress-project",
"type": "generator"
},
{
"description": "Add a Cypress E2E Configuration to an existing project.",
"file": "generated/packages/cypress/generators/configuration.json",

View File

@ -1,84 +0,0 @@
{
"name": "cypress-project",
"factory": "./src/generators/cypress-project/cypress-project#cypressProjectGeneratorInternal",
"schema": {
"$schema": "https://json-schema.org/schema",
"$id": "NxCypressProjectGeneratorSchema",
"cli": "nx",
"title": "Create Cypress Configuration for the workspace",
"description": "Create Cypress Configuration for the workspace.",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The name of the frontend project to test.",
"$default": { "$source": "projectName" },
"x-priority": "important"
},
"baseUrl": {
"type": "string",
"description": "The address (with the port) which your application is running on."
},
"name": {
"type": "string",
"description": "Name of the E2E Project.",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What name would you like to use for the e2e project?"
},
"directory": {
"type": "string",
"description": "A directory where the project is placed.",
"x-priority": "important"
},
"projectNameAndRootFormat": {
"description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
},
"js": {
"description": "Generate JavaScript files rather than TypeScript files.",
"type": "boolean",
"default": false
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"setParserOptionsProject": {
"type": "boolean",
"description": "Whether or not to configure the ESLint `parserOptions.project` option. We do not do this by default for lint performance reasons.",
"default": false
},
"skipPackageJson": {
"type": "boolean",
"default": false,
"description": "Do not add dependencies to `package.json`.",
"x-priority": "internal"
},
"bundler": {
"description": "The Cypress bundler to use.",
"type": "string",
"enum": ["vite", "webpack", "none"],
"x-prompt": "Which Cypress bundler do you want to use?",
"default": "webpack"
}
},
"required": ["name"],
"examplesFile": "Adding Cypress to an existing application requires two options. The name of the e2e app to create and what project that e2e app is for.\n\n```bash\nnx g configuration --name=my-app-e2e --project=my-app\n```\n\nWhen providing `--project` option, the generator will look for the `serve` target in that given project. This allows the [cypress executor](/packages/cypress/executors/cypress) to spin up the project and start the cypress runner.\n\nIf you prefer to not have the project served automatically, you can provide a `--base-url` argument in place of `--project`\n\n```bash\nnx g configuration --name=my-app-e2e --base-url=http://localhost:1234\n```\n\n{% callout type=\"note\" title=\"What about API Projects?\" %}\nYou can also run the `configuration` generator against API projects like a [Nest API](/packages/nest/generators/application#@nx/nest:application).\nIf there is a URL to visit then you can test it with Cypress!\n{% /callout %}\n\n## Using Cypress with Vite.js\n\nNow, you can generate your Cypress project with Vite.js as the bundler:\n\n```bash\nnx g configuration --name=my-app-e2e --project=my-app --bundler=vite\n```\n\nThis generator will pass the `bundler` information (`bundler: 'vite'`) to our `nxE2EPreset`, in your project's `cypress.config.ts` file (eg. `my-app-e2e/cypress.config.ts`).\n\n### Customizing the Vite.js configuration\n\nThe `nxE2EPreset` will then use the `bundler` information to generate the correct settings for your Cypress project to use Vite.js. In the background, the way this works is that it's using a custom Vite preprocessor for your files, that's called on the `file:preprocessor` event. If you want to customize this behaviour, you can do so like this in your project's `cypress.config.ts` file:\n\n```ts\nimport { defineConfig } from 'cypress';\nimport { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';\n\nconst config = nxE2EPreset(__filename, { bundler: 'vite' });\nexport default defineConfig({\n e2e: {\n ...config,\n async setupNodeEvents(on, config) {\n // Ensure that `@nx/cypress` events are set up.\n await config.setupNodeEvents(on, config);\n\n // Your settings here\n },\n },\n});\n```\n",
"presets": []
},
"description": "Add a Cypress E2E Project.",
"hidden": true,
"implementation": "/packages/cypress/src/generators/cypress-project/cypress-project#cypressProjectGeneratorInternal.ts",
"aliases": [],
"path": "/packages/cypress/src/generators/cypress-project/schema.json",
"type": "generator"
}

View File

@ -145,7 +145,7 @@ Prefix to use for Angular component and directive selectors.
Type: `string`
Customizes the initial content of your workspace. Default presets include: ["apps", "empty", "core", "npm", "ts", "web-components", "angular-monorepo", "angular-standalone", "react-monorepo", "react-standalone", "vue-monorepo", "vue-standalone", "nuxt", "nuxt-standalone", "next", "nextjs-standalone", "remix-monorepo", "remix-standalone", "react-native", "expo", "nest", "express", "react", "vue", "angular", "node-standalone", "node-monorepo", "ts-standalone"]. To build your own see https://nx.dev/extending-nx/recipes/create-preset
Customizes the initial content of your workspace. Default presets include: ["apps", "npm", "ts", "web-components", "angular-monorepo", "angular-standalone", "react-monorepo", "react-standalone", "vue-monorepo", "vue-standalone", "nuxt", "nuxt-standalone", "next", "nextjs-standalone", "remix-monorepo", "remix-standalone", "react-native", "expo", "nest", "express", "react", "vue", "angular", "node-standalone", "node-monorepo", "ts-standalone"]. To build your own see https://nx.dev/extending-nx/recipes/create-preset
### routing

File diff suppressed because one or more lines are too long

View File

@ -368,7 +368,6 @@
- [cypress](/nx-api/cypress/executors/cypress)
- [generators](/nx-api/cypress/generators)
- [init](/nx-api/cypress/generators/init)
- [cypress-project](/nx-api/cypress/generators/cypress-project)
- [configuration](/nx-api/cypress/generators/configuration)
- [component-configuration](/nx-api/cypress/generators/component-configuration)
- [migrate-to-cypress-11](/nx-api/cypress/generators/migrate-to-cypress-11)

View File

@ -154,10 +154,10 @@ export default defineConfig({
async () => {
const appName = uniq('next-cy-app');
runCLI(
`generate @nx/next:app ${appName} --e2eTestRunner=none --no-interactive`
`generate @nx/next:app ${appName} --directory=apps/${appName} --e2eTestRunner=none --no-interactive --projectNameAndRootFormat=as-provided`
);
runCLI(
`generate @nx/next:component btn --project=${appName} --no-interactive`
`generate @nx/next:component btn --project=${appName} --directory=apps/${appName}/components --nameAndDirectoryFormat=as-provided --no-interactive`
);
runCLI(
`generate @nx/next:cypress-component-configuration --project=${appName} --generate-tests --no-interactive`

View File

@ -118,10 +118,10 @@ function addBabelSupport(path: string) {
function createAppWithCt(appName: string) {
runCLI(
`generate @nx/next:app ${appName} --no-interactive --appDir=false --src=false`
`generate @nx/next:app ${appName} --directory=apps/${appName} --no-interactive --appDir=false --src=false --projectNameAndRootFormat=as-provided`
);
runCLI(
`generate @nx/next:component button --project=${appName} --directory=components --flat --no-interactive`
`generate @nx/next:component button --project=${appName} --directory=apps/${appName}/components --nameAndDirectoryFormat=as-provided --no-interactive`
);
createFile(
`apps/${appName}/public/data.json`,

View File

@ -244,7 +244,7 @@ describe('index.html interpolation (legacy)', () => {
</head>
<body>
<div id='root'></div>
<div>Nx Variable: %NX_VARIABLE%</div>
<div>Nx Variable: %NX_PUBLIC_VARIABLE%</div>
<div>Some other variable: %SOME_OTHER_VARIABLE%</div>
<div>Deploy Url: %DEPLOY_URL%</div>
</body>
@ -252,7 +252,7 @@ describe('index.html interpolation (legacy)', () => {
`;
const envFilePath = `apps/${appName}/.env`;
const envFileContents = `
NX_VARIABLE=foo
NX_PUBLIC_VARIABLE=foo
SOME_OTHER_VARIABLE=bar
}`;

View File

@ -243,39 +243,39 @@ describe('CLI - Environment Variables', () => {
//test if the Nx CLI loads root .env vars
updateFile(
`.env`,
'NX_WS_BASE=ws-base\nNX_SHARED_ENV=shared-in-workspace-base'
'NX_PUBLIC_WS_BASE=ws-base\nNX_PUBLIC_SHARED_ENV=shared-in-workspace-base'
);
updateFile(
`.env.local`,
'NX_WS_ENV_LOCAL=ws-env-local\nNX_SHARED_ENV=shared-in-workspace-env-local'
'NX_PUBLIC_WS_ENV_LOCAL=ws-env-local\nNX_PUBLIC_SHARED_ENV=shared-in-workspace-env-local'
);
updateFile(
`.local.env`,
'NX_WS_LOCAL_ENV=ws-local-env\nNX_SHARED_ENV=shared-in-workspace-local-env'
'NX_PUBLIC_WS_LOCAL_ENV=ws-local-env\nNX_PUBLIC_SHARED_ENV=shared-in-workspace-local-env'
);
updateFile(
`apps/${appName}/.env`,
'NX_APP_BASE=app-base\nNX_SHARED_ENV=shared-in-app-base'
'NX_PUBLIC_APP_BASE=app-base\nNX_PUBLIC_SHARED_ENV=shared-in-app-base'
);
updateFile(
`apps/${appName}/.env.local`,
'NX_APP_ENV_LOCAL=app-env-local\nNX_SHARED_ENV=shared-in-app-env-local'
'NX_PUBLIC_APP_ENV_LOCAL=app-env-local\nNX_PUBLIC_SHARED_ENV=shared-in-app-env-local'
);
updateFile(
`apps/${appName}/.local.env`,
'NX_APP_LOCAL_ENV=app-local-env\nNX_SHARED_ENV=shared-in-app-local-env'
'NX_PUBLIC_APP_LOCAL_ENV=app-local-env\nNX_PUBLIC_SHARED_ENV=shared-in-app-local-env'
);
const main = `apps/${appName}/src/main.ts`;
const newCode = `
const envVars = [process.env.NODE_ENV, process.env.NX_WS_BASE, process.env.NX_WS_ENV_LOCAL, process.env.NX_WS_LOCAL_ENV, process.env.NX_APP_BASE, process.env.NX_APP_ENV_LOCAL, process.env.NX_APP_LOCAL_ENV, process.env.NX_SHARED_ENV];
const envVars = [process.env.NODE_ENV, process.env.NX_PUBLIC_WS_BASE, process.env.NX_PUBLIC_WS_ENV_LOCAL, process.env.NX_PUBLIC_WS_LOCAL_ENV, process.env.NX_PUBLIC_APP_BASE, process.env.NX_PUBLIC_APP_ENV_LOCAL, process.env.NX_PUBLIC_APP_LOCAL_ENV, process.env.NX_PUBLIC_SHARED_ENV];
const nodeEnv = process.env.NODE_ENV;
const nxWsBase = process.env.NX_WS_BASE;
const nxWsEnvLocal = process.env.NX_WS_ENV_LOCAL;
const nxWsLocalEnv = process.env.NX_WS_LOCAL_ENV;
const nxAppBase = process.env.NX_APP_BASE;
const nxAppEnvLocal = process.env.NX_APP_ENV_LOCAL;
const nxAppLocalEnv = process.env.NX_APP_LOCAL_ENV;
const nxSharedEnv = process.env.NX_SHARED_ENV;
const nxWsBase = process.env.NX_PUBLIC_WS_BASE;
const nxWsEnvLocal = process.env.NX_PUBLIC_WS_ENV_LOCAL;
const nxWsLocalEnv = process.env.NX_PUBLIC_WS_LOCAL_ENV;
const nxAppBase = process.env.NX_PUBLIC_APP_BASE;
const nxAppEnvLocal = process.env.NX_PUBLIC_APP_ENV_LOCAL;
const nxAppLocalEnv = process.env.NX_PUBLIC_APP_LOCAL_ENV;
const nxSharedEnv = process.env.NX_PUBLIC_SHARED_ENV;
`;
runCLI(
@ -290,18 +290,18 @@ describe('CLI - Environment Variables', () => {
updateFile(
`apps/${appName2}/.env`,
'NX_APP_BASE=app2-base\nNX_SHARED_ENV=shared2-in-app-base'
'NX_PUBLIC_APP_BASE=app2-base\nNX_PUBLIC_SHARED_ENV=shared2-in-app-base'
);
updateFile(
`apps/${appName2}/.env.local`,
'NX_APP_ENV_LOCAL=app2-env-local\nNX_SHARED_ENV=shared2-in-app-env-local'
'NX_PUBLIC_APP_ENV_LOCAL=app2-env-local\nNX_PUBLIC_SHARED_ENV=shared2-in-app-env-local'
);
updateFile(
`apps/${appName2}/.local.env`,
'NX_APP_LOCAL_ENV=app2-local-env\nNX_SHARED_ENV=shared2-in-app-local-env'
'NX_PUBLIC_APP_LOCAL_ENV=app2-local-env\nNX_PUBLIC_SHARED_ENV=shared2-in-app-local-env'
);
const main2 = `apps/${appName2}/src/main.ts`;
const newCode2 = `const envVars = [process.env.NODE_ENV, process.env.NX_WS_BASE, process.env.NX_WS_ENV_LOCAL, process.env.NX_WS_LOCAL_ENV, process.env.NX_APP_BASE, process.env.NX_APP_ENV_LOCAL, process.env.NX_APP_LOCAL_ENV, process.env.NX_SHARED_ENV];`;
const newCode2 = `const envVars = [process.env.NODE_ENV, process.env.NX_PUBLIC_WS_BASE, process.env.NX_PUBLIC_WS_ENV_LOCAL, process.env.NX_PUBLIC_WS_LOCAL_ENV, process.env.NX_PUBLIC_APP_BASE, process.env.NX_PUBLIC_APP_ENV_LOCAL, process.env.NX_PUBLIC_APP_LOCAL_ENV, process.env.NX_PUBLIC_SHARED_ENV];`;
runCLI(
`generate @nx/web:app ${appName2} --bundler=webpack --no-interactive --compiler=babel`
@ -361,14 +361,14 @@ describe('index.html interpolation', () => {
</head>
<body>
<div id='root'></div>
<div>Nx Variable: %NX_VARIABLE%</div>
<div>Nx Variable: %NX_PUBLIC_VARIABLE%</div>
<div>Some other variable: %SOME_OTHER_VARIABLE%</div>
</body>
</html>
`;
const envFilePath = `apps/${appName}/.env`;
const envFileContents = `
NX_VARIABLE=foo
NX_PUBLIC_VARIABLE=foo
SOME_OTHER_VARIABLE=bar
}`;

View File

@ -164,22 +164,18 @@ describe('Webpack Plugin', () => {
expect(output).toMatch(/Hello/);
}, 500_000);
it('should bundle in non-sensitive NX_ environment variables', () => {
it('should bundle in NX_PUBLIC_ environment variables', () => {
const appName = uniq('app');
runCLI(`generate @nx/web:app ${appName} --bundler webpack`);
updateFile(
`apps/${appName}/src/main.ts`,
`
console.log(process.env['NX_CLOUD_ENCRYPTION_KEY']);
console.log(process.env['NX_CLOUD_ACCESS_TOKEN']);
console.log(process.env['NX_PUBLIC_TEST']);
`
);
runCLI(`build ${appName}`, {
env: {
NX_CLOUD_ENCRYPTION_KEY: 'secret',
NX_CLOUD_ACCESS_TOKEN: 'secret',
NX_PUBLIC_TEST: 'foobar',
},
});
@ -188,7 +184,6 @@ describe('Webpack Plugin', () => {
f.startsWith('main.')
);
const content = readFile(`dist/apps/${appName}/${mainFile}`);
expect(content).not.toMatch(/secret/);
expect(content).toMatch(/foobar/);
});

View File

@ -95,10 +95,6 @@ const pages: Array<{ title: string; path: string }> = [
},
{ title: '@nx/cypress', path: '/packages/cypress' },
{ title: '@nx/cypress:init', path: '/packages/cypress/generators/init' },
{
title: '@nx/cypress:cypress-project',
path: '/packages/cypress/generators/cypress-project',
},
{
title: '@nx/cypress:cypress',
path: '/packages/cypress/executors/cypress',

View File

@ -1 +0,0 @@
export * from '@nx/expo/plugins/with-nx-webpack';

View File

@ -12,9 +12,9 @@ export * from './src/executors/extract-i18n/extract-i18n.impl';
import { executeDevServerBuilder } from './src/builders/dev-server/dev-server.impl';
export {
// TODO(v19): remove this alias
// TODO(v20): remove this alias
/**
* @deprecated Use executeDevServerBuilder instead. It will be removed in Nx v19.
* @deprecated Use executeDevServerBuilder instead. It will be removed in Nx v20.
*/
executeDevServerBuilder as executeWebpackDevServerBuilder,
executeDevServerBuilder,

View File

@ -196,14 +196,7 @@ export const commandsObject: yargs.Argv<Arguments> = yargs
throw error;
});
},
[
normalizeArgsMiddleware,
normalizeAndWarnOnDeprecatedPreset({
// TODO(v19): Remove Empty and Core presets
[Preset.Core]: Preset.NPM,
[Preset.Empty]: Preset.Apps,
}),
] as yargs.MiddlewareFunction<{}>[]
[normalizeArgsMiddleware] as yargs.MiddlewareFunction<{}>[]
)
.help('help', chalk.dim`Show help`)
.updateLocale(yargsDecorator)
@ -248,28 +241,6 @@ async function main(parsedArgs: yargs.Arguments<Arguments>) {
}
}
function normalizeAndWarnOnDeprecatedPreset(
deprecatedPresets: Partial<Record<Preset, Preset>>
): (argv: yargs.Arguments<Arguments>) => Promise<void> {
return async (args: yargs.Arguments<Arguments>): Promise<void> => {
if (!args.preset) return;
if (deprecatedPresets[args.preset]) {
output.addVerticalSeparator();
output.note({
title: `The "${args.preset}" preset is deprecated.`,
bodyLines: [
`The "${
args.preset
}" preset will be removed in a future Nx release. Use the "${
deprecatedPresets[args.preset]
}" preset instead.`,
],
});
args.preset = deprecatedPresets[args.preset] as Preset;
}
};
}
/**
* This function is used to normalize the arguments passed to the command.
* It would:

View File

@ -1,7 +1,5 @@
export enum Preset {
Apps = 'apps',
Empty = 'empty', // same as apps, deprecated
Core = 'core', // same as npm, deprecated
NPM = 'npm',
TS = 'ts',
WebComponents = 'web-components',

View File

@ -9,12 +9,6 @@
"aliases": ["ng-add"],
"hidden": true
},
"cypress-project": {
"factory": "./src/generators/cypress-project/cypress-project#cypressProjectGeneratorInternal",
"schema": "./src/generators/cypress-project/schema.json",
"description": "Add a Cypress E2E Project.",
"hidden": true
},
"configuration": {
"aliases": ["cypress-e2e-configuration", "e2e", "e2e-config"],
"factory": "./src/generators/configuration/configuration#configurationGeneratorInternal",

View File

@ -1,17 +1,4 @@
import { configurationGenerator } from './src/generators/configuration/configuration';
import { componentConfigurationGenerator } from './src/generators/component-configuration/component-configuration';
import { cypressProjectGenerator as _cypressProjectGenerator } from './src/generators/cypress-project/cypress-project';
export { configurationGenerator, componentConfigurationGenerator };
// Maintain backwards compatibility with the old names in case community plugins used them.
// TODO(v19): Remove old name
/** @deprecated Use `configurationGenerator` instead. It will be removed in Nx 19. */
export const cypressComponentConfiguration = componentConfigurationGenerator;
export { configurationGenerator as cypressE2EConfigurationGenerator };
// TODO(v19): Remove project generator
/** @deprecated Add a new project and call `configurationGenerator` instead. It will be removed in Nx 19. */
export const cypressProjectGenerator = _cypressProjectGenerator;
export { configurationGenerator } from './src/generators/configuration/configuration';
export { componentConfigurationGenerator } from './src/generators/component-configuration/component-configuration';
export { cypressInitGenerator } from './src/generators/init/init';
export { migrateCypressProject } from './src/generators/migrate-to-cypress-11/migrate-to-cypress-11';

View File

@ -39,7 +39,6 @@
"@nx/js": "file:../js",
"@phenomnomnominal/tsquery": "~5.0.1",
"detect-port": "^1.5.1",
"semver": "^7.5.3",
"tslib": "^2.3.0"
},
"peerDependencies": {

View File

@ -1,240 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Cypress Project < v7 --linter eslint should add eslint-plugin-cypress 1`] = `
{
"extends": [
"plugin:cypress/recommended",
"../.eslintrc.json",
],
"ignorePatterns": [
"!**/*",
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx",
],
"rules": {},
},
{
"files": [
"src/plugins/index.js",
],
"rules": {
"@typescript-eslint/no-var-requires": "off",
"no-undef": "off",
},
},
],
}
`;
exports[`Cypress Project < v7 nested should update configuration 1`] = `
{
"e2e": {
"configurations": {
"production": {
"devServerTarget": "my-dir-my-app:serve:production",
},
},
"executor": "@nx/cypress:cypress",
"options": {
"cypressConfig": "my-dir/my-app-e2e/cypress.json",
"devServerTarget": "my-dir-my-app:serve",
"testingType": "e2e",
"tsConfig": "my-dir/my-app-e2e/tsconfig.json",
},
},
"lint": {
"executor": "@nx/eslint:lint",
},
}
`;
exports[`Cypress Project < v7 project with directory in its name should set right path names in \`cypress.json\` 1`] = `
"{
"fileServerFolder": ".",
"fixturesFolder": "./src/fixtures",
"integrationFolder": "./src/integration",
"modifyObstructiveCode": false,
"supportFile": "./src/support/index.ts",
"pluginsFile": "./src/plugins/index",
"video": true,
"videosFolder": "../../dist/cypress/my-dir/my-app-e2e/videos",
"screenshotsFolder": "../../dist/cypress/my-dir/my-app-e2e/screenshots",
"chromeWebSecurity": false
}
"
`;
exports[`Cypress Project < v7 project with directory in its name should update configuration 1`] = `
{
"e2e": {
"configurations": {
"production": {
"devServerTarget": "my-dir-my-app:serve:production",
},
},
"executor": "@nx/cypress:cypress",
"options": {
"cypressConfig": "my-dir/my-app-e2e/cypress.json",
"devServerTarget": "my-dir-my-app:serve",
"testingType": "e2e",
"tsConfig": "my-dir/my-app-e2e/tsconfig.json",
},
},
"lint": {
"executor": "@nx/eslint:lint",
},
}
`;
exports[`Cypress Project < v7 should update project configuration (baseUrl) 1`] = `
{
"e2e": {
"executor": "@nx/cypress:cypress",
"options": {
"baseUrl": "http://localhost:3000",
"cypressConfig": "my-app-e2e/cypress.json",
"testingType": "e2e",
"tsConfig": "my-app-e2e/tsconfig.json",
},
},
"lint": {
"executor": "@nx/eslint:lint",
},
}
`;
exports[`Cypress Project < v7 should update project configuration 1`] = `
{
"e2e": {
"configurations": {
"production": {
"devServerTarget": "my-app:serve:production",
},
},
"executor": "@nx/cypress:cypress",
"options": {
"cypressConfig": "my-app-e2e/cypress.json",
"devServerTarget": "my-app:serve",
"testingType": "e2e",
"tsConfig": "my-app-e2e/tsconfig.json",
},
},
"lint": {
"executor": "@nx/eslint:lint",
},
}
`;
exports[`Cypress Project < v7 should update target configurations 1`] = `
{
"e2e": {
"configurations": {
"production": {
"devServerTarget": "my-app:serve:production",
},
},
"executor": "@nx/cypress:cypress",
"options": {
"cypressConfig": "my-app-e2e/cypress.json",
"devServerTarget": "my-app:serve:development",
"testingType": "e2e",
"tsConfig": "my-app-e2e/tsconfig.json",
},
},
"lint": {
"executor": "@nx/eslint:lint",
},
}
`;
exports[`Cypress Project > v10 for bundler:vite should pass the bundler info to nxE2EPreset in \`cypress.config.ts\` 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__dirname, {
bundler: 'vite',
}),
});
"
`;
exports[`Cypress Project > v10 nested should set right path names in \`cypress.config.ts\` 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__dirname),
});
"
`;
exports[`Cypress Project > v10 nested should set right path names in \`tsconfig.e2e.json\` 1`] = `
{
"compilerOptions": {
"allowJs": true,
"outDir": "../../dist/out-tsc",
"sourceMap": false,
"types": [
"cypress",
"node",
],
},
"extends": "../../tsconfig.base.json",
"include": [
"src/**/*.ts",
"src/**/*.js",
"cypress.config.ts",
],
}
`;
exports[`Cypress Project > v10 should set right path names in \`cypress.config.ts\` 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__dirname),
});
"
`;
exports[`Cypress Project > v10 should set right path names in \`tsconfig.e2e.json\` 1`] = `
{
"compilerOptions": {
"allowJs": true,
"outDir": "../dist/out-tsc",
"sourceMap": false,
"types": [
"cypress",
"node",
],
},
"extends": "../tsconfig.base.json",
"include": [
"src/**/*.ts",
"src/**/*.js",
"cypress.config.ts",
],
}
`;
exports[`Cypress Project > v10 should update configuration when eslint is passed 1`] = `
"{
"extends": ["plugin:cypress/recommended", "../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
}
]
}
"
`;

View File

@ -1,602 +0,0 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
addProjectConfiguration,
readJson,
readProjectConfiguration,
Tree,
updateProjectConfiguration,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { cypressProjectGenerator } from './cypress-project';
import { Schema } from './schema';
import { Linter } from '@nx/eslint';
import { installedCypressVersion } from '../../utils/cypress-version';
import { cypressInitGenerator } from '../init/init';
jest.mock('../../utils/cypress-version');
jest.mock('../init/init');
describe('Cypress Project', () => {
let tree: Tree;
const defaultOptions: Omit<Schema, 'name' | 'project'> = {
linter: Linter.EsLint,
};
let mockedInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
let mockInitCypress: jest.Mock<ReturnType<typeof cypressInitGenerator>> =
cypressInitGenerator as never;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
addProjectConfiguration(tree, 'my-app', {
root: 'my-app',
targets: {
serve: {
executor: 'serve-executor',
options: {},
configurations: {
production: {},
},
},
},
});
addProjectConfiguration(tree, 'my-dir-my-app', {
root: 'my-dir/my-app',
targets: {
serve: {
executor: 'serve-executor',
options: {},
configurations: {
production: {},
},
},
},
});
});
afterEach(() => jest.clearAllMocks());
it('should call init if cypress is not installed', async () => {
mockedInstalledCypressVersion.mockReturnValue(null);
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
projectNameAndRootFormat: 'as-provided',
});
expect(mockInitCypress).toHaveBeenCalled();
});
it('should call not init if cypress is installed', async () => {
mockedInstalledCypressVersion.mockReturnValue(10);
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
projectNameAndRootFormat: 'as-provided',
});
expect(mockInitCypress).not.toHaveBeenCalled();
});
describe('> v10', () => {
beforeEach(() => {
mockedInstalledCypressVersion.mockReturnValue(10);
});
it('should generate files for v10 and above', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
projectNameAndRootFormat: 'as-provided',
});
expect(tree.exists('my-app-e2e/cypress.config.ts')).toBeTruthy();
expect(tree.exists('my-app-e2e/src/fixtures/example.json')).toBeTruthy();
expect(tree.exists('my-app-e2e/src/e2e/app.cy.ts')).toBeTruthy();
expect(tree.exists('my-app-e2e/src/support/app.po.ts')).toBeTruthy();
expect(tree.exists('my-app-e2e/src/support/commands.ts')).toBeTruthy();
expect(tree.exists('my-app-e2e/src/support/e2e.ts')).toBeTruthy();
});
it('should update configuration when eslint is passed', async () => {
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
linter: Linter.EsLint,
projectNameAndRootFormat: 'as-provided',
});
expect(tree.read('my-app-e2e/.eslintrc.json', 'utf-8')).toMatchSnapshot();
});
it('should not add lint target when "none" is passed', async () => {
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
linter: Linter.None,
projectNameAndRootFormat: 'as-provided',
});
const project = readProjectConfiguration(tree, 'my-app-e2e');
expect(project.targets.lint).toBeUndefined();
});
it('should update tags and implicit dependencies', async () => {
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
linter: Linter.EsLint,
projectNameAndRootFormat: 'as-provided',
});
const project = readProjectConfiguration(tree, 'my-app-e2e');
expect(project.tags).toEqual([]);
expect(project.implicitDependencies).toEqual(['my-app']);
});
it('should set right path names in `cypress.config.ts`', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
projectNameAndRootFormat: 'as-provided',
});
const cypressConfig = tree.read('my-app-e2e/cypress.config.ts', 'utf-8');
expect(cypressConfig).toMatchSnapshot();
});
it('should set right path names in `tsconfig.e2e.json`', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
projectNameAndRootFormat: 'as-provided',
});
const tsconfigJson = readJson(tree, 'my-app-e2e/tsconfig.json');
expect(tsconfigJson).toMatchSnapshot();
});
it('should extend from tsconfig.base.json', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
projectNameAndRootFormat: 'as-provided',
});
const tsConfig = readJson(tree, 'my-app-e2e/tsconfig.json');
expect(tsConfig.extends).toBe('../tsconfig.base.json');
});
it('should support a root tsconfig.json instead of tsconfig.base.json', async () => {
tree.rename('tsconfig.base.json', 'tsconfig.json');
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
projectNameAndRootFormat: 'as-provided',
});
const tsConfig = readJson(tree, 'my-app-e2e/tsconfig.json');
expect(tsConfig.extends).toBe('../tsconfig.json');
});
describe('for bundler:vite', () => {
it('should pass the bundler info to nxE2EPreset in `cypress.config.ts`', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
bundler: 'vite',
projectNameAndRootFormat: 'as-provided',
});
const cypressConfig = tree.read(
'my-app-e2e/cypress.config.ts',
'utf-8'
);
expect(cypressConfig).toMatchSnapshot();
});
});
describe('nested', () => {
it('should set right path names in `cypress.config.ts`', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-dir-my-app',
directory: 'my-dir/my-app-e2e',
projectNameAndRootFormat: 'as-provided',
});
const cypressConfig = tree.read(
'my-dir/my-app-e2e/cypress.config.ts',
'utf-8'
);
expect(cypressConfig).toMatchSnapshot();
});
it('should set right path names in `tsconfig.e2e.json`', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-dir-my-app',
directory: 'my-dir/my-app-e2e',
projectNameAndRootFormat: 'as-provided',
});
const tsconfigJson = readJson(tree, 'my-dir/my-app-e2e/tsconfig.json');
expect(tsconfigJson).toMatchSnapshot();
});
it('should extend from tsconfig.base.json', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
directory: 'my-dir/my-app-e2e',
projectNameAndRootFormat: 'as-provided',
});
const tsConfig = readJson(tree, 'my-dir/my-app-e2e/tsconfig.json');
expect(tsConfig.extends).toBe('../../tsconfig.base.json');
});
it('should support a root tsconfig.json instead of tsconfig.base.json', async () => {
tree.rename('tsconfig.base.json', 'tsconfig.json');
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
directory: 'my-dir/my-app-e2e',
projectNameAndRootFormat: 'as-provided',
});
const tsConfig = readJson(tree, 'my-dir/my-app-e2e/tsconfig.json');
expect(tsConfig.extends).toBe('../../tsconfig.json');
});
describe('root project', () => {
it('should generate in option.name when root project detected', async () => {
addProjectConfiguration(tree, 'root', { root: '.' });
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'e2e-tests',
baseUrl: 'http://localhost:1234',
project: 'root',
projectNameAndRootFormat: 'as-provided',
});
expect(tree.listChanges().map((c) => c.path)).toEqual(
expect.arrayContaining([
'e2e-tests/cypress.config.ts',
'e2e-tests/src/e2e/app.cy.ts',
'e2e-tests/src/fixtures/example.json',
'e2e-tests/src/support/app.po.ts',
'e2e-tests/src/support/commands.ts',
'e2e-tests/src/support/e2e.ts',
'e2e-tests/tsconfig.json',
])
);
});
it('should not generate a root project when the passed in project is not the root project', async () => {
addProjectConfiguration(tree, 'root', { root: '.' });
addProjectConfiguration(tree, 'my-cool-app', { root: 'my-cool-app' });
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'e2e-tests',
baseUrl: 'http://localhost:1234',
project: 'my-cool-app',
projectNameAndRootFormat: 'as-provided',
});
expect(tree.listChanges().map((c) => c.path)).toEqual(
expect.arrayContaining([
'e2e-tests/cypress.config.ts',
'e2e-tests/src/e2e/app.cy.ts',
'e2e-tests/src/fixtures/example.json',
'e2e-tests/src/support/app.po.ts',
'e2e-tests/src/support/commands.ts',
'e2e-tests/src/support/e2e.ts',
'e2e-tests/tsconfig.json',
])
);
});
});
});
describe('--project', () => {
describe('none', () => {
it('should not add any implicit dependencies', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
baseUrl: 'http://localhost:7788',
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-app-e2e');
expect(projectConfig.implicitDependencies).not.toBeDefined();
expect(projectConfig.tags).toEqual([]);
});
});
it('should not throw an error when --project does not have targets', async () => {
const projectConf = readProjectConfiguration(tree, 'my-app');
delete projectConf.targets;
updateProjectConfiguration(tree, 'my-app', projectConf);
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
linter: Linter.EsLint,
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-app-e2e');
expect(projectConfig.targets['e2e'].options.devServerTarget).toEqual(
'my-app:serve'
);
});
});
it('should generate in the correct folder', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'one-two-other-e2e',
project: 'my-app',
directory: 'one/two/other-e2e',
projectNameAndRootFormat: 'as-provided',
});
const project = readProjectConfiguration(tree, 'one-two-other-e2e');
expect(project).toBeDefined();
[
'one/two/other-e2e/cypress.config.ts',
'one/two/other-e2e/src/e2e/app.cy.ts',
].forEach((path) => expect(tree.exists(path)).toBeTruthy());
});
it('should generate in the correct folder when --project-name-and-root-format=derived', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'other-e2e',
project: 'my-app',
directory: 'one/two',
projectNameAndRootFormat: 'derived',
});
const project = readProjectConfiguration(tree, 'one-two-other-e2e');
expect(project).toBeDefined();
[
'apps/one/two/other-e2e/cypress.config.ts',
'apps/one/two/other-e2e/src/e2e/app.cy.ts',
].forEach((path) => expect(tree.exists(path)).toBeTruthy());
});
describe('serve-static', () => {
it('should configure Cypress with ci configuration if serve-static is found', async () => {
const appConfig = readProjectConfiguration(tree, 'my-app');
appConfig.targets['serve-static'] = {
executor: 'serve-static-executor',
options: {},
configurations: {
production: {},
},
};
updateProjectConfiguration(tree, 'my-app', appConfig);
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
projectNameAndRootFormat: 'as-provided',
});
const e2eConfig = readProjectConfiguration(tree, 'my-app-e2e');
expect(e2eConfig.targets.e2e).toMatchObject({
options: {
devServerTarget: 'my-app:serve',
},
configurations: {
production: { devServerTarget: 'my-app:serve:production' },
ci: { devServerTarget: 'my-app:serve-static' },
},
});
});
it('should not configure Cypress with ci configuration if serve-static is not found', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
projectNameAndRootFormat: 'as-provided',
});
const e2eConfig = readProjectConfiguration(tree, 'my-app-e2e');
expect(e2eConfig.targets.e2e.configurations.ci).toBeUndefined();
});
});
});
describe('v9 - v7', () => {
beforeEach(() => {
mockedInstalledCypressVersion.mockReturnValue(9);
});
it('should generate files', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
projectNameAndRootFormat: 'as-provided',
});
expect(tree.exists('my-app-e2e/cypress.json')).toBeTruthy();
expect(tree.exists('my-app-e2e/src/fixtures/example.json')).toBeTruthy();
expect(
tree.exists('my-app-e2e/src/integration/app.spec.ts')
).toBeTruthy();
expect(tree.exists('my-app-e2e/src/support/app.po.ts')).toBeTruthy();
expect(tree.exists('my-app-e2e/src/support/commands.ts')).toBeTruthy();
expect(tree.exists('my-app-e2e/src/support/index.ts')).toBeTruthy();
});
});
describe('< v7', () => {
beforeEach(() => {
mockedInstalledCypressVersion.mockReturnValue(6);
});
it('should generate a plugin file if cypress is below version 7', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
projectNameAndRootFormat: 'as-provided',
addPlugin: false,
});
expect(tree.exists('my-app-e2e/src/plugins/index.js')).toBeTruthy();
});
it('should update project configuration', async () => {
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
linter: Linter.EsLint,
projectNameAndRootFormat: 'as-provided',
addPlugin: false,
});
const project = readProjectConfiguration(tree, 'my-app-e2e');
expect(project.root).toEqual('my-app-e2e');
expect(project.targets).toMatchSnapshot();
});
it('should update project configuration (baseUrl)', async () => {
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
baseUrl: 'http://localhost:3000',
linter: Linter.EsLint,
projectNameAndRootFormat: 'as-provided',
addPlugin: false,
});
const project = readProjectConfiguration(tree, 'my-app-e2e');
expect(project.root).toEqual('my-app-e2e');
expect(project.targets).toMatchSnapshot();
});
it('should update target configurations', async () => {
const originalProject = readProjectConfiguration(tree, 'my-app');
originalProject.targets.serve.defaultConfiguration = 'development';
originalProject.targets.serve.configurations.development = {};
updateProjectConfiguration(tree, 'my-app', originalProject);
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
linter: Linter.EsLint,
projectNameAndRootFormat: 'as-provided',
addPlugin: false,
});
const project = readProjectConfiguration(tree, 'my-app-e2e');
expect(project.root).toEqual('my-app-e2e');
expect(project.targets).toMatchSnapshot();
});
describe('nested', () => {
it('should update configuration', async () => {
await cypressProjectGenerator(tree, {
name: 'my-dir-my-app-e2e',
project: 'my-dir-my-app',
directory: 'my-dir/my-app-e2e',
linter: Linter.EsLint,
projectNameAndRootFormat: 'as-provided',
addPlugin: false,
});
const projectConfig = readProjectConfiguration(
tree,
'my-dir-my-app-e2e'
);
expect(projectConfig).toBeDefined();
expect(projectConfig.targets).toMatchSnapshot();
});
});
describe('--linter', () => {
describe('eslint', () => {
it('should add eslint-plugin-cypress', async () => {
await cypressProjectGenerator(tree, {
name: 'my-app-e2e',
project: 'my-app',
linter: Linter.EsLint,
projectNameAndRootFormat: 'as-provided',
addPlugin: false,
});
const packageJson = readJson(tree, 'package.json');
expect(
packageJson.devDependencies['eslint-plugin-cypress']
).toBeTruthy();
const eslintrcJson = readJson(tree, 'my-app-e2e/.eslintrc.json');
expect(eslintrcJson).toMatchSnapshot();
});
});
});
describe('project with directory in its name', () => {
beforeEach(async () => {
await cypressProjectGenerator(tree, {
name: 'my-dir-my-app-e2e',
project: 'my-dir-my-app',
directory: 'my-dir/my-app-e2e',
linter: Linter.EsLint,
projectNameAndRootFormat: 'as-provided',
addPlugin: false,
});
});
it('should update configuration', async () => {
const projectConfig = readProjectConfiguration(
tree,
'my-dir-my-app-e2e'
);
expect(projectConfig).toBeDefined();
expect(projectConfig.targets).toMatchSnapshot();
});
it('should update nx.json', async () => {
const project = readProjectConfiguration(tree, 'my-dir-my-app-e2e');
expect(project.tags).toEqual([]);
expect(project.implicitDependencies).toEqual(['my-dir-my-app']);
});
it('should set right path names in `cypress.json`', async () => {
const cypressConfig = tree.read(
'my-dir/my-app-e2e/cypress.json',
'utf-8'
);
expect(cypressConfig).toMatchSnapshot();
});
});
});
});

View File

@ -1,297 +0,0 @@
import {
addDependenciesToPackageJson,
addProjectConfiguration,
formatFiles,
generateFiles,
GeneratorCallback,
getProjects,
joinPathFragments,
logger,
offsetFromRoot,
ProjectConfiguration,
readProjectConfiguration,
runTasksInSerial,
stripIndents,
toJS,
Tree,
updateJson,
} from '@nx/devkit';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { checkAndCleanWithSemver } from '@nx/devkit/src/utils/semver';
import {
getRelativePathToRootTsConfig,
initGenerator as jsInitGenerator,
} from '@nx/js';
import { Linter } from '@nx/eslint';
import { join } from 'path';
import { major } from 'semver';
import { addLinterToCyProject } from '../../utils/add-linter';
import { installedCypressVersion } from '../../utils/cypress-version';
import {
cypressVersion,
typesNodeVersion,
viteVersion,
} from '../../utils/versions';
import { cypressInitGenerator } from '../init/init';
import { Schema } from './schema';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
export interface CypressProjectSchema extends Schema {
projectName: string;
projectRoot: string;
rootProject: boolean;
}
function createFiles(tree: Tree, options: CypressProjectSchema) {
// if not installed or >v10 use v10 folder
// else use v9 folder
const cypressVersion = installedCypressVersion();
const cypressFiles =
cypressVersion && cypressVersion < 10 ? 'v9-and-under' : 'v10-and-after';
generateFiles(
tree,
join(__dirname, './files', cypressFiles),
options.projectRoot,
{
tmpl: '',
...options,
project: options.project || 'Project',
ext: options.js ? 'js' : 'ts',
offsetFromRoot: offsetFromRoot(options.projectRoot),
rootTsConfigPath: getRelativePathToRootTsConfig(
tree,
options.projectRoot
),
bundler: options.bundler,
}
);
if (cypressVersion && cypressVersion < 7) {
updateJson(tree, join(options.projectRoot, 'cypress.json'), (json) => {
json.pluginsFile = './src/plugins/index';
return json;
});
} else if (cypressVersion < 10) {
const pluginPath = join(options.projectRoot, 'src/plugins/index.js');
if (tree.exists(pluginPath)) {
tree.delete(pluginPath);
}
}
if (options.js) {
toJS(tree);
}
}
function addProject(tree: Tree, options: CypressProjectSchema) {
let e2eProjectConfig: ProjectConfiguration;
const detectedCypressVersion =
installedCypressVersion() ??
major(checkAndCleanWithSemver('cypress', cypressVersion));
const cypressConfig =
detectedCypressVersion < 10 ? 'cypress.json' : 'cypress.config.ts';
if (options.baseUrl) {
e2eProjectConfig = {
root: options.projectRoot,
sourceRoot: joinPathFragments(options.projectRoot, 'src'),
projectType: 'application',
targets: {
e2e: {
executor: '@nx/cypress:cypress',
options: {
cypressConfig: joinPathFragments(
options.projectRoot,
cypressConfig
),
baseUrl: options.baseUrl,
testingType: 'e2e',
},
},
},
tags: [],
implicitDependencies: options.project ? [options.project] : undefined,
};
} else if (options.project) {
const project = readProjectConfiguration(tree, options.project);
if (!project.targets) {
logger.warn(stripIndents`
NOTE: Project, "${options.project}", does not have any targets defined and a baseUrl was not provided. Nx will use
"${options.project}:serve" as the devServerTarget. But you may need to define this target within the project, "${options.project}".
`);
}
const devServerTarget =
project.targets?.serve && project.targets?.serve?.defaultConfiguration
? `${options.project}:serve:${project.targets.serve.defaultConfiguration}`
: `${options.project}:serve`;
e2eProjectConfig = {
root: options.projectRoot,
sourceRoot: joinPathFragments(options.projectRoot, 'src'),
projectType: 'application',
targets: {
e2e: {
executor: '@nx/cypress:cypress',
options: {
cypressConfig: joinPathFragments(
options.projectRoot,
cypressConfig
),
devServerTarget,
testingType: 'e2e',
},
configurations: {
production: {
devServerTarget: `${options.project}:serve:production`,
},
},
},
},
tags: [],
implicitDependencies: options.project ? [options.project] : undefined,
};
if (project.targets?.['serve-static']) {
e2eProjectConfig.targets.e2e.configurations.ci = {
devServerTarget: `${options.project}:serve-static`,
};
}
} else {
throw new Error(`Either project or baseUrl should be specified.`);
}
if (detectedCypressVersion < 7) {
e2eProjectConfig.targets.e2e.options.tsConfig = joinPathFragments(
options.projectRoot,
'tsconfig.json'
);
}
addProjectConfiguration(tree, options.projectName, e2eProjectConfig);
}
/**
* @deprecated use cypressE2EConfigurationGenerator instead
**/
export async function cypressProjectGenerator(host: Tree, schema: Schema) {
return await cypressProjectGeneratorInternal(host, {
projectNameAndRootFormat: 'derived',
addPlugin: false,
...schema,
});
}
/**
* @deprecated use cypressE2EConfigurationGenerator instead
**/
export async function cypressProjectGeneratorInternal(
host: Tree,
schema: Schema
) {
const options = await normalizeOptions(host, schema);
const tasks: GeneratorCallback[] = [];
const cypressVersion = installedCypressVersion();
// if there is an installed cypress version, then we don't call
// init since we want to keep the existing version that is installed
if (!cypressVersion) {
tasks.push(await jsInitGenerator(host, { ...options, skipFormat: true }));
tasks.push(
await cypressInitGenerator(host, {
...options,
skipFormat: true,
addPlugin: options.addPlugin,
})
);
}
createFiles(host, options);
addProject(host, options);
const installTask = await addLinterToCyProject(host, {
...options,
cypressDir: 'src',
linter: schema.linter,
project: options.projectName,
overwriteExisting: true,
});
tasks.push(installTask);
if (!options.skipPackageJson) {
tasks.push(ensureDependencies(host, options));
}
if (!options.skipFormat) {
await formatFiles(host);
}
return runTasksInSerial(...tasks);
}
function ensureDependencies(tree: Tree, options: CypressProjectSchema) {
const devDependencies: Record<string, string> = {
'@types/node': typesNodeVersion,
};
if (options.bundler === 'vite') {
devDependencies['vite'] = viteVersion;
}
return runTasksInSerial(
...[
addDependenciesToPackageJson(tree, {}, devDependencies),
() => {
logShowProjectCommand(options.projectName);
},
]
);
}
async function normalizeOptions(
host: Tree,
options: Schema
): Promise<CypressProjectSchema> {
let maybeRootProject: ProjectConfiguration;
let isRootProject = false;
const projects = getProjects(host);
// nx will set the project option for generators when ran within a project.
// since the root project will always be set for standalone projects we can just check it here.
if (options.project) {
maybeRootProject = projects.get(options.project);
}
if (
maybeRootProject?.root === '.' ||
// should still check to see if we are in a standalone based workspace
(!maybeRootProject &&
Array.from(projects.values()).some((config) => config.root === '.'))
) {
isRootProject = true;
}
let { projectName, projectRoot } = await determineProjectNameAndRootOptions(
host,
{
name: options.name,
projectType: 'application',
directory: isRootProject ? options.name : options.directory,
projectNameAndRootFormat: isRootProject
? 'as-provided'
: options.projectNameAndRootFormat,
callingGenerator: '@nx/cypress:cypress-project',
}
);
options.linter = options.linter || Linter.EsLint;
options.bundler = options.bundler || 'webpack';
options.addPlugin ??= process.env.NX_ADD_PLUGINS !== 'false';
return {
...options,
// other generators depend on the rootProject flag down stream
rootProject: isRootProject,
projectName,
projectRoot,
};
}
export default cypressProjectGenerator;

View File

@ -1,10 +0,0 @@
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__dirname<% if (bundler === 'vite'){ %>,
{
bundler: 'vite'
}
<% } %>)
});

View File

@ -1,13 +0,0 @@
import { getGreeting } from '../support/app.po';
describe('<%= project %>', () => {
beforeEach(() => cy.visit('/'));
it('should display welcome message', () => {
// Custom command example, see `../support/commands.ts` file
cy.login('my-email@something.com', 'myPassword');
// Function helper example, see `../support/app.po.ts` file
getGreeting().contains('Welcome <%= project %>');
});
});

View File

@ -1,4 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io"
}

View File

@ -1 +0,0 @@
export const getGreeting = () => cy.get('h1');

View File

@ -1,33 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
<% if (linter === 'eslint') { %>
// eslint-disable-next-line @typescript-eslint/no-namespace<% } %>
declare namespace Cypress {<% if (linter === 'eslint') { %>
// eslint-disable-next-line @typescript-eslint/no-unused-vars<% } %>
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
//
// -- This is a parent command --
Cypress.Commands.add('login', (email, password) => {
console.log('Custom command example: Login', email, password);
});
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

View File

@ -1,17 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';

View File

@ -1,10 +0,0 @@
{
"extends": "<%= rootTsConfigPath %>",
"compilerOptions": {
"sourceMap": false,
"outDir": "<%= offsetFromRoot %>dist/out-tsc",
"allowJs": true,
"types": ["cypress", "node"]
},
"include": ["src/**/*.ts", "src/**/*.js", "cypress.config.ts"]
}

View File

@ -1,12 +0,0 @@
{
"fileServerFolder": ".",
"fixturesFolder": "./src/fixtures",
"integrationFolder": "./src/integration",
"modifyObstructiveCode": false,
"supportFile": "./src/support/index.<%= ext %>",
"pluginsFile": false,
"video": true,
"videosFolder": "<%= offsetFromRoot %>dist/cypress/<%= projectRoot %>/videos",
"screenshotsFolder": "<%= offsetFromRoot %>dist/cypress/<%= projectRoot %>/screenshots",
"chromeWebSecurity": false
}

View File

@ -1,4 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io"
}

View File

@ -1,13 +0,0 @@
import { getGreeting } from '../support/app.po';
describe('<%= project %>', () => {
beforeEach(() => cy.visit('/'));
it('should display welcome message', () => {
// Custom command example, see `../support/commands.ts` file
cy.login('my-email@something.com', 'myPassword');
// Function helper example, see `../support/app.po.ts` file
getGreeting().contains('Welcome <%= project %>');
});
});

View File

@ -1,22 +0,0 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const { preprocessTypescript } = require('@nx/cypress/plugins/preprocessor');
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
// Preprocess Typescript file using Nx helper
on('file:preprocessor', preprocessTypescript(config));
};

View File

@ -1 +0,0 @@
export const getGreeting = () => cy.get('h1');

View File

@ -1,33 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
<% if (linter === 'eslint') { %>
// eslint-disable-next-line @typescript-eslint/no-namespace<% } %>
declare namespace Cypress {<% if (linter === 'eslint') { %>
// eslint-disable-next-line @typescript-eslint/no-unused-vars<% } %>
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
//
// -- This is a parent command --
Cypress.Commands.add('login', (email, password) => {
console.log('Custom command example: Login', email, password);
});
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

View File

@ -1,17 +0,0 @@
// ***********************************************************
// This example support/index.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.ts using ES2015 syntax:
import './commands';

View File

@ -1,10 +0,0 @@
{
"extends": "<%= offsetFromRoot %>tsconfig.base.json",
"compilerOptions": {
"sourceMap": false,
"outDir": "<%= offsetFromRoot %>dist/out-tsc",
"allowJs": true,
"types": ["cypress", "node"]
},
"include": ["src/**/*.ts", "src/**/*.js"]
}

View File

@ -1,17 +0,0 @@
import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
import type { Linter } from '@nx/eslint';
export interface Schema {
project?: string;
baseUrl?: string;
name: string;
directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
linter?: Linter;
js?: boolean;
skipFormat?: boolean;
setParserOptionsProject?: boolean;
skipPackageJson?: boolean;
bundler?: 'webpack' | 'vite' | 'none';
addPlugin?: boolean;
}

View File

@ -1,78 +0,0 @@
{
"$schema": "https://json-schema.org/schema",
"$id": "NxCypressProjectGeneratorSchema",
"cli": "nx",
"title": "Create Cypress Configuration for the workspace",
"description": "Create Cypress Configuration for the workspace.",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The name of the frontend project to test.",
"$default": {
"$source": "projectName"
},
"x-priority": "important"
},
"baseUrl": {
"type": "string",
"description": "The address (with the port) which your application is running on."
},
"name": {
"type": "string",
"description": "Name of the E2E Project.",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use for the e2e project?"
},
"directory": {
"type": "string",
"description": "A directory where the project is placed.",
"x-priority": "important"
},
"projectNameAndRootFormat": {
"description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
},
"js": {
"description": "Generate JavaScript files rather than TypeScript files.",
"type": "boolean",
"default": false
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"setParserOptionsProject": {
"type": "boolean",
"description": "Whether or not to configure the ESLint `parserOptions.project` option. We do not do this by default for lint performance reasons.",
"default": false
},
"skipPackageJson": {
"type": "boolean",
"default": false,
"description": "Do not add dependencies to `package.json`.",
"x-priority": "internal"
},
"bundler": {
"description": "The Cypress bundler to use.",
"type": "string",
"enum": ["vite", "webpack", "none"],
"x-prompt": "Which Cypress bundler do you want to use?",
"default": "webpack"
}
},
"required": ["name"],
"examplesFile": "../../../docs/cypress-project-examples.md"
}

View File

@ -155,11 +155,6 @@ export default defineConfig({
exports[`convertToCypressTen convertCypressProject should infer targets with --all flag 2`] = `
{
"e2e": {
"configurations": {
"production": {
"devServerTarget": "app:serve:production",
},
},
"executor": "@nx/cypress:cypress",
"options": {
"cypressConfig": "app-e2e/cypress.config.ts",
@ -168,11 +163,6 @@ exports[`convertToCypressTen convertCypressProject should infer targets with --a
},
},
"e2e-custom": {
"configurations": {
"production": {
"devServerTarget": "app:serve:production",
},
},
"executor": "@nx/cypress:cypress",
"options": {
"cypressConfig": "app-e2e/cypress.config.ts",
@ -212,11 +202,6 @@ export default defineConfig({
exports[`convertToCypressTen convertCypressProject should not break when an invalid target is passed in 2`] = `
{
"e2e": {
"configurations": {
"production": {
"devServerTarget": "app:serve:production",
},
},
"executor": "@nx/cypress:cypress",
"options": {
"cypressConfig": "app-e2e/cypress.config.ts",
@ -225,11 +210,6 @@ exports[`convertToCypressTen convertCypressProject should not break when an inva
},
},
"e2e-custom": {
"configurations": {
"production": {
"devServerTarget": "app:serve:production",
},
},
"executor": "@nx/cypress:cypress",
"options": {
"cypressConfig": "app-e2e/cypress.config.ts",

View File

@ -8,10 +8,11 @@ import {
Tree,
updateJson,
updateProjectConfiguration,
writeJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { installedCypressVersion } from '../../utils/cypress-version';
import { cypressProjectGenerator } from '../cypress-project/cypress-project';
import { configurationGenerator } from '../configuration/configuration';
import {
createSupportFileImport,
updateImports,
@ -51,11 +52,23 @@ describe('convertToCypressTen', () => {
});
mockedInstalledCypressVersion.mockReturnValue(9);
await cypressProjectGenerator(tree, {
name: 'app-e2e',
addProjectConfiguration(tree, 'app-e2e', {
projectType: 'application',
root: 'app-e2e',
sourceRoot: 'app-e2e/src',
targets: {},
implicitDependencies: ['app'],
tags: [],
});
writeJson(tree, 'app-e2e/tsconfig.json', {
include: ['src/**/*.ts', 'src/**/*.js'],
});
await configurationGenerator(tree, {
skipFormat: true,
project: 'app',
projectNameAndRootFormat: 'as-provided',
project: 'app-e2e',
devServerTarget: 'app:serve',
addPlugin: false,
});
});
@ -296,12 +309,26 @@ describe('convertToCypressTen', () => {
},
});
await cypressProjectGenerator(tree, {
name: 'app-two-e2e',
skipFormat: true,
project: 'app-two',
projectNameAndRootFormat: 'as-provided',
addProjectConfiguration(tree, 'app-two-e2e', {
projectType: 'application',
root: 'app-two-e2e',
sourceRoot: 'app-two-e2e/src',
targets: {},
implicitDependencies: ['app-two'],
tags: [],
});
writeJson(tree, 'app-two-e2e/tsconfig.json', {
include: ['src/**/*.ts', 'src/**/*.js'],
});
await configurationGenerator(tree, {
skipFormat: true,
project: 'app-two-e2e',
devServerTarget: 'app-two:serve',
addPlugin: false,
});
const appOneProjectConfig = readProjectConfiguration(tree, 'app-e2e');
appOneProjectConfig.targets['e2e'].options.cypressConfig = 'cypress.json';
updateProjectConfiguration(tree, 'app-e2e', appOneProjectConfig);
@ -338,11 +365,6 @@ describe('convertToCypressTen', () => {
devServerTarget: 'app:serve',
testingType: 'e2e',
},
configurations: {
production: {
devServerTarget: 'app:serve:production',
},
},
});
expect(readJson(tree, 'app-two-e2e/tsconfig.json').include).toEqual([
'src/**/*.ts',
@ -360,11 +382,6 @@ describe('convertToCypressTen', () => {
devServerTarget: 'app-two:serve',
testingType: 'e2e',
},
configurations: {
production: {
devServerTarget: 'app-two:serve:production',
},
},
});
});
});

View File

@ -1,15 +1,8 @@
// Prevent sensitive keys from being bundled when source code uses entire `process.env` object rather than individual keys (e.g. `process.env.NX_FOO`).
// TODO(v19): Only env vars prefixed with NX_PUBLIC should be bundled. This is a breaking change so we won't do it in v18.
const excludedKeys = ['NX_CLOUD_ACCESS_TOKEN', 'NX_CLOUD_ENCRYPTION_KEY'];
export function getClientEnvironment(): Record<string, string> {
const NX_APP = /^NX_/i;
const nxPublicKeyRegex = /^NX_PUBLIC_/i;
return Object.keys(process.env)
.filter(
(key) =>
!excludedKeys.includes(key) && (NX_APP.test(key) || key === 'NODE_ENV')
)
.filter((key) => nxPublicKeyRegex.test(key) || key === 'NODE_ENV')
.reduce((env, key) => {
env[`process.env.${key}`] = JSON.stringify(process.env[key]);
return env;

View File

@ -55,7 +55,7 @@ export default {
* previously defined v5 of `@typescript-eslint`. v6 of `@typescript-eslint`
* changed how configurations are defined.
*
* TODO(v19): re-evalute these deviations from @typescript-eslint/recommended in v19 of Nx
* TODO(v20): re-evalute these deviations from @typescript-eslint/recommended in v19 of Nx
*/
'no-extra-semi': 'off',
'@typescript-eslint/no-extra-semi': 'error',

View File

@ -38,7 +38,7 @@ export default {
* previously defined v5 of `@typescript-eslint`. v6 of `@typescript-eslint`
* changed how configurations are defined.
*
* TODO(v19): re-evalute these deviations from @typescript-eslint/recommended in v19 of Nx
* TODO(v20): re-evalute these deviations from @typescript-eslint/recommended in v19 of Nx
*/
'no-extra-semi': 'off',
'@typescript-eslint/no-extra-semi': 'error',

View File

@ -65,6 +65,12 @@
"version": "18.0.0-beta.0",
"description": "Remove the offset from the outputDir of the export target",
"implementation": "./src/migrations/update-18-0-0/change-outputDir-export-target"
},
"update-19-0-0-change-webpack-to-metro": {
"version": "19.0.0-beta.9",
"cli": "nx",
"description": "Change webpack to metro in expo projects",
"factory": "./src/migrations/update-19-0-0/change-webpack-to-metro"
}
},
"packageJsonUpdates": {

View File

@ -35,7 +35,6 @@
"node-fetch": "^2.6.7",
"tslib": "^2.3.0",
"tsconfig-paths": "^4.1.2",
"tsconfig-paths-webpack-plugin": "^4.0.0",
"@nx/jest": "file:../jest",
"@nx/js": "file:../js",
"@nx/eslint": "file:../eslint",

View File

@ -1,54 +0,0 @@
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
import { resolve } from 'path';
/**
* @deprecated TODO(v19) use bundler: 'metro' instead, will be removed in v19
* This function add additional rules to expo's webpack config to make expo web working
*/
export async function withNxWebpack(config) {
// add additional rule to load files under libs
const rules = config.module.rules.find((rule) =>
Array.isArray(rule.oneOf)
)?.oneOf;
if (rules) {
rules.push({
test: /\.(mjs|[jt]sx?)$/,
exclude: /node_modules/,
use: {
loader: require.resolve('@nx/webpack/src/utils/web-babel-loader.js'),
options: {
presets: [
[
'@nx/react/babel',
{
runtime: 'automatic',
},
],
],
},
},
});
}
if (!config.resolve) {
config.resolve = {};
}
if (!config.resolve.plugins) {
config.resolve.plugins = [];
}
const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx'];
const tsConfigPath = resolve(__dirname, 'tsconfig.json');
config.resolve.plugins.push(
new TsconfigPathsPlugin({
configFile: tsConfigPath,
extensions,
})
);
config.resolve.fallback = {
...config.resolve.fallback,
crypto: require.resolve('crypto-browserify'),
stream: require.resolve('stream-browserify'),
};
return config;
}

View File

@ -0,0 +1,56 @@
import {
addProjectConfiguration,
getProjects,
readJson,
Tree,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import update from './change-webpack-to-metro';
describe('change-webpack-to-metro', () => {
let tree: Tree;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
addProjectConfiguration(tree, 'product', {
root: 'apps/product',
sourceRoot: 'apps/product/src',
targets: {
start: {
executor: '@nx/expo:start',
},
'export-web': {
executor: '@nx/expo:export',
options: {},
},
},
});
tree.write(
`apps/product/app.json`,
JSON.stringify({
expo: {
web: {
bundler: 'webpack',
},
},
})
);
});
it(`should update project.json with target export-web and change app.json`, async () => {
await update(tree);
getProjects(tree).forEach((project) => {
expect(project.targets['export-web']).toEqual({
executor: '@nx/expo:export',
options: {
bundler: 'metro',
},
});
expect(project.targets['web']).toBeUndefined();
const appJson = readJson(tree, `apps/product/app.json`);
expect(appJson.expo.web.bundler).toEqual('metro');
});
});
});

View File

@ -0,0 +1,38 @@
import {
Tree,
formatFiles,
getProjects,
updateProjectConfiguration,
updateJson,
} from '@nx/devkit';
/**
* Migration:
* - change target 'export-web'
* - change bundler to 'metro'
*/
export default async function update(tree: Tree) {
const projects = getProjects(tree);
for (const [name, config] of projects.entries()) {
if (config.targets?.['start']?.executor === '@nx/expo:start') {
if (config.targets['web']) {
delete config.targets['web'];
}
if (config.targets['export-web']) {
config.targets['export-web'].options.bundler = 'metro';
}
updateJson(tree, `${config.root}/app.json`, (appJson) => {
if (appJson.expo?.web) {
appJson.expo.web.bundler = 'metro';
}
return appJson;
});
}
updateProjectConfiguration(tree, name, config);
}
await formatFiles(tree);
}

View File

@ -37,7 +37,7 @@ module.exports = function (api: any, options: NxWebBabelPresetOptions = {}) {
// Determine settings for `@babel//babel-plugin-transform-class-properties`,
// so that we can sync the `loose` option with `@babel/preset-env`.
// TODO(v19): Remove classProperties since it's no longer needed, now that the class props transform is in preset-env.
// TODO(v20): Remove classProperties since it's no longer needed, now that the class props transform is in preset-env.
const loose = options.classProperties?.loose ?? options.loose ?? true;
if (options.classProperties) {
logger.warn(

View File

@ -327,12 +327,7 @@ export function getNextConfig(
}
/**
* 5. Add env variables prefixed with NX_
*/
addNxEnvVariables(config);
/**
* 6. Add SVGR support if option is on.
* 5. Add SVGR support if option is on.
*/
// Default SVGR support to be on for projects.
@ -393,40 +388,6 @@ export function getNextConfig(
};
}
// Prevent sensitive keys from being bundled when source code uses entire `process.env` object rather than individual keys (e.g. `process.env.NX_FOO`).
// TODO(v19): BREAKING: Only support NEXT_PUBLIC_ env vars and ignore NX_ vars since this is a standard Next.js feature.
const excludedKeys = ['NX_CLOUD_ACCESS_TOKEN', 'NX_CLOUD_ENCRYPTION_KEY'];
function getNxEnvironmentVariables() {
return Object.keys(process.env)
.filter((env) => !excludedKeys.includes(env) && /^NX_/i.test(env))
.reduce((env, key) => {
env[key] = process.env[key];
return env;
}, {});
}
/**
* TODO(v19)
* @deprecated Use Next.js 9.4+ built-in support for environment variables. Reference https://nextjs.org/docs/pages/api-reference/next-config-js/env
*/
function addNxEnvVariables(config: any) {
const maybeDefinePlugin = config.plugins?.find((plugin) => {
return plugin.definitions?.['process.env.NODE_ENV'];
});
if (maybeDefinePlugin) {
const env = getNxEnvironmentVariables();
Object.entries(env)
.map(([name, value]) => [`process.env.${name}`, `"${value}"`])
.filter(([name]) => !maybeDefinePlugin.definitions[name])
.forEach(
([name, value]) => (maybeDefinePlugin.definitions[name] = value)
);
}
}
export function getAliasForProject(
node: ProjectGraphProjectNode,
paths: Record<string, string[]>

View File

@ -1,7 +1,7 @@
import { NextConfigFn } from '../src/utils/config';
import { WithNxOptions } from './with-nx';
// TODO(v19): Remove file, it is here until users migrate over to SASS manually.
// TODO(v20): Remove file, it is here until users migrate over to SASS manually.
export function withStylus(
configOrFn: WithNxOptions | NextConfigFn
): NextConfigFn {

View File

@ -36,7 +36,7 @@ interface Schema {
skipFormat?: boolean;
}
// TODO(v19): Remove this logic once we no longer derive directory.
// TODO(v20): Remove this logic once we no longer derive directory.
function maybeGetDerivedDirectory(host: Tree, options: Schema): string {
if (!options.project) return options.directory;
const workspace = getProjects(host);

View File

@ -56,7 +56,7 @@ async function normalizeOptions(host: Tree, options: Schema) {
if (options.project) {
// Legacy behavior, detect app vs page router from specified project.
// TODO(v19): remove this logic
// TODO(v20): remove this logic
const project = readProjectConfiguration(host, options.project);
// app/ is a reserved folder in nextjs so it is safe to check it's existence
isAppRouter =

View File

@ -283,7 +283,10 @@ function buildTargetWebpack(
return async () => {
customWebpack = await customWebpack;
// TODO(v19): Once webpackConfig is always set in @nx/webpack:webpack and isolatedConfig is removed, we no longer need this default.
// TODO(v20): Component testing need to be agnostic of the underlying executor. With Crystal, we're not using `@nx/webpack:webpack` by default.
// We need to decouple CT from the build target of the app, we just care about bundler config (e.g. webpack.config.js).
// The generated setup should support both Webpack and Vite as documented here: https://docs.cypress.io/guides/component-testing/react/overview
// Related issue: https://github.com/nrwl/nx/issues/21546
const configure = composePluginsSync(withNx(), withWeb());
const defaultWebpack = configure(
{},

View File

@ -18,24 +18,16 @@ import { mergePlugins } from './merge-plugins';
import { withReact } from '../with-react';
import { existsSync } from 'fs';
// Prevent sensitive keys from being bundled when source code uses entire `process.env` object rather than individual keys (e.g. `process.env.NX_FOO`).
// TODO(v19): BREAKING: Only env vars prefixed with NX_PUBLIC should be bundled. This is a breaking change so we won't do it in v18.
const excludedKeys = ['NX_CLOUD_ACCESS_TOKEN', 'NX_CLOUD_ENCRYPTION_KEY'];
// This is shamelessly taken from CRA and modified for NX use
// https://github.com/facebook/create-react-app/blob/4784997f0682e75eb32a897b4ffe34d735912e6c/packages/react-scripts/config/env.js#L71
function getClientEnvironment(mode) {
// Grab NODE_ENV and NX_* and STORYBOOK_* environment variables and prepare them to be
// injected into the application via DefinePlugin in webpack configuration.
const NX_PREFIX = /^NX_/i;
const NX_PREFIX = /^NX_PUBLIC_/i;
const STORYBOOK_PREFIX = /^STORYBOOK_/i;
const raw = Object.keys(process.env)
.filter(
(key) =>
!excludedKeys.includes(key) &&
(NX_PREFIX.test(key) || STORYBOOK_PREFIX.test(key))
)
.filter((key) => NX_PREFIX.test(key) || STORYBOOK_PREFIX.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];

View File

@ -5,22 +5,18 @@ description: Examples and a short guide on how to use the @nx/webpack:webpack bu
`project.json`:
```json
```json5
//...
"my-app": {
"targets": {
//...
"build": {
"executor": "@nx/webpack:webpack",
//...
//...
"options": {
...
},
//...
}
},
}
"targets": {
"build": {
"executor": "@nx/webpack:webpack",
"options": {
"webpackConfig": "apps/my-app/webpack.config.js"
}
},
//...
}
}
```
@ -40,21 +36,19 @@ Copying from the [Babel documentation](https://babeljs.io/docs/config-files#root
Setting `babelUpwardRootMode` to `true` in your `project.json` will set `rootMode` option to `upward` in the Babel config. You may want the `upward` mode in a monorepo when projects must apply their individual `.babelrc` file. We recommend that you don't set it at all, so it will use the default to `false` as the `upward` mode brings additional complexity to the build process.
```json
```json5
//...
"my-app": {
"targets": {
"build": {
"executor": "@nx/webpack:webpack",
"options": {
"babelUpwardRootMode": true,
//...
},
//...
"webpackConfig": "apps/my-app/webpack.config.js",
"babelUpwardRootMode": true
}
},
//...
},
//...
}
}
```
@ -94,23 +88,19 @@ In workspace above, if `demo` imports `a` and `b`, it will apply the config `lib
If you have a custom Babel config file (i.e. not `.babelrc`), you can use the `configFile` option as follows:
```json
```json5
//...
"my-app": {
"targets": {
//...
"build": {
"executor": "@nx/webpack:webpack",
//...
"options": {
//...
"babelConfig": "apps/my-app/.babelrc.custom.json",
},
"configurations": {
...
}
},
}
"targets": {
"build": {
"executor": "@nx/webpack:webpack",
"options": {
"webpackConfig": "apps/my-app/webpack.config.js",
"babelConfig": "apps/my-app/.babelrc.custom.json",
}
},
// ...
}
}
```
@ -133,21 +123,15 @@ Set `isolatedConfig` to `true` in your `project.json` file in the `build` target
```json
//...
"my-app": {
"targets": {
//...
"build": {
"executor": "@nx/webpack:webpack",
//...
"options": {
//...
"webpackConfig": "apps/my-app/webpack.config.js",
"isolatedConfig": true
},
"configurations": {
...
}
},
}
"targets": {
"build": {
"executor": "@nx/webpack:webpack",
"options": {
"webpackConfig": "apps/my-app/webpack.config.js",
"isolatedConfig": true
}
},
}
}
```

View File

@ -47,7 +47,7 @@ export interface WebpackExecutorOptions {
extractLicenses?: boolean;
fileReplacements?: FileReplacement[];
generatePackageJson?: boolean;
// TODO(v19): Remove this option
// TODO(v20): Remove this option
/** @deprecated set webpackConfig and provide an explicit webpack.config.js file (See: https://nx.dev/recipes/webpack/webpack-config-setup) */
isolatedConfig?: boolean;
standardWebpackConfigFunction?: boolean;

View File

@ -1,14 +1,10 @@
// Prevent sensitive keys from being bundled when source code uses entire `process.env` object rather than individual keys (e.g. `process.env.NX_FOO`).
// TODO(v19): Only env vars prefixed with NX_PUBLIC should be bundled. This is a breaking change so we won't do it in v18.
const excludedKeys = ['NX_CLOUD_ACCESS_TOKEN', 'NX_CLOUD_ENCRYPTION_KEY'];
export function getClientEnvironment(mode?: string) {
// Grab NODE_ENV and NX_* environment variables and prepare them to be
// Grab NODE_ENV and NX_PUBLIC_* environment variables and prepare them to be
// injected into the application via DefinePlugin in webpack configuration.
const NX_APP = /^NX_/i;
const nxPublicKeyRegex = /^NX_PUBLIC_/i;
const raw = Object.keys(process.env)
.filter((key) => !excludedKeys.includes(key) && NX_APP.test(key))
.filter((key) => nxPublicKeyRegex.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];

View File

@ -2,7 +2,7 @@ import { interpolateEnvironmentVariablesToIndex } from './interpolate-env-variab
describe('interpolateEnvironmentVariablesToIndex()', () => {
const envDefaults = {
NX_VARIABLE: 'foo',
NX_PUBLIC_VARIABLE: 'foo',
SOME_OTHER_VARIABLE: 'bar',
DEPLOY_URL: 'baz',
};
@ -14,7 +14,7 @@ describe('interpolateEnvironmentVariablesToIndex()', () => {
test('default env variables', () => {
const content = `
<div>Nx Variable: %NX_VARIABLE%</div>
<div>Nx Variable: %NX_PUBLIC_VARIABLE%</div>
<div>Some other variable: %SOME_OTHER_VARIABLE%</div>
<div>Deploy Url: %DEPLOY_URL%</div>
`;
@ -28,7 +28,7 @@ describe('interpolateEnvironmentVariablesToIndex()', () => {
test('Deploy url set as option overrides DEPLOY_URL env variable', () => {
const content = `
<div>Nx Variable: %NX_VARIABLE%</div>
<div>Nx Variable: %NX_PUBLIC_VARIABLE%</div>
<div>Some other variable: %SOME_OTHER_VARIABLE%</div>
<div>Deploy Url: %DEPLOY_URL%</div>
`;
@ -45,7 +45,7 @@ describe('interpolateEnvironmentVariablesToIndex()', () => {
test('No deploy url provided via either option', () => {
delete process.env.DEPLOY_URL;
const content = `
<div>Nx Variable: %NX_VARIABLE%</div>
<div>Nx Variable: %NX_PUBLIC_VARIABLE%</div>
<div>Some other variable: %SOME_OTHER_VARIABLE%</div>
<div>Deploy Url: %DEPLOY_URL%</div>
`;
@ -60,7 +60,7 @@ describe('interpolateEnvironmentVariablesToIndex()', () => {
test('NX_ prefixed option present in index.html, but not present in process.env', () => {
delete process.env.DEPLOY_URL;
const content = `
<div>Nx Variable: %NX_VARIABLE%</div>
<div>Nx Variable: %NX_PUBLIC_VARIABLE%</div>
<div>Some other variable: %SOME_OTHER_VARIABLE%</div>
<div>Some other nx_variable: %NX_SOME_OTHER_VARIABLE%</div>
<div>Deploy Url: %DEPLOY_URL%</div>

View File

@ -6,14 +6,10 @@ export function interpolateEnvironmentVariablesToIndex(
return interpolateEnvironmentVariables(contents, environmentVariables as any);
}
const NX_PREFIX = /^NX_/i;
// Prevent sensitive keys from being bundled when source code uses entire `process.env` object rather than individual keys (e.g. `process.env.NX_FOO`).
// TODO(v19): Only env vars prefixed with NX_PUBLIC should be bundled. This is a breaking change so we won't do it in v18.
const excludedKeys = ['NX_CLOUD_ACCESS_TOKEN', 'NX_CLOUD_ENCRYPTION_KEY'];
const NX_PREFIX = /^NX_PUBLIC_/i;
function isNxEnvironmentKey(x: string): boolean {
return !excludedKeys.includes(x) && NX_PREFIX.test(x);
return NX_PREFIX.test(x);
}
function getClientEnvironment(deployUrl: string) {