feat(core): cleanup for v19 (#22993)

This commit is contained in:
Jason Jean 2024-05-01 12:12:32 -04:00 committed by GitHub
parent 307f8d4624
commit a64a7e2db9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
184 changed files with 236 additions and 16346 deletions

View File

@ -136,7 +136,6 @@ rust-toolchain @nrwl/nx-native-reviewers
/packages/devkit/** @nrwl/nx-devkit-reviewers /packages/devkit/** @nrwl/nx-devkit-reviewers
/packages/devkit/index.ts @FrozenPandaz @vsavkin /packages/devkit/index.ts @FrozenPandaz @vsavkin
/packages/devkit/public-api.ts @FrozenPandaz @vsavkin /packages/devkit/public-api.ts @FrozenPandaz @vsavkin
/packages/devkit/nx.ts @FrozenPandaz @vsavkin
# Gradle # Gradle
/packages/gradle/** @FrozenPandaz @xiongemi /packages/gradle/** @FrozenPandaz @xiongemi

View File

@ -19,7 +19,6 @@ It only uses language primitives and immutable objects
### Classes ### Classes
- [ProjectGraphBuilder](../../devkit/documents/ProjectGraphBuilder) - [ProjectGraphBuilder](../../devkit/documents/ProjectGraphBuilder)
- [Workspaces](../../devkit/documents/Workspaces)
### Interfaces ### Interfaces

View File

@ -25,7 +25,7 @@
**`Deprecated`** **`Deprecated`**
use hashTask(task:Task, taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. This will be removed in v19 use hashTask(task:Task, taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. This will be removed in v20
**hashTask**(`task`, `taskGraph`): `Promise`\<[`Hash`](../../devkit/documents/Hash)\> **hashTask**(`task`, `taskGraph`): `Promise`\<[`Hash`](../../devkit/documents/Hash)\>
@ -42,7 +42,7 @@ use hashTask(task:Task, taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. T
**`Deprecated`** **`Deprecated`**
use hashTask(task:Task, taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. This will be removed in v19 use hashTask(task:Task, taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. This will be removed in v20
**hashTask**(`task`, `taskGraph`, `env`): `Promise`\<[`Hash`](../../devkit/documents/Hash)\> **hashTask**(`task`, `taskGraph`, `env`): `Promise`\<[`Hash`](../../devkit/documents/Hash)\>
@ -76,7 +76,7 @@ use hashTask(task:Task, taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. T
**`Deprecated`** **`Deprecated`**
use hashTasks(tasks:Task[], taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. This will be removed in v19 use hashTasks(tasks:Task[], taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. This will be removed in v20
**hashTasks**(`tasks`, `taskGraph`): `Promise`\<[`Hash`](../../devkit/documents/Hash)[]\> **hashTasks**(`tasks`, `taskGraph`): `Promise`\<[`Hash`](../../devkit/documents/Hash)[]\>
@ -93,7 +93,7 @@ use hashTasks(tasks:Task[], taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instea
**`Deprecated`** **`Deprecated`**
use hashTasks(tasks:Task[], taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. This will be removed in v19 use hashTasks(tasks:Task[], taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. This will be removed in v20
**hashTasks**(`tasks`, `taskGraph`, `env`): `Promise`\<[`Hash`](../../devkit/documents/Hash)[]\> **hashTasks**(`tasks`, `taskGraph`, `env`): `Promise`\<[`Hash`](../../devkit/documents/Hash)[]\>

View File

@ -1,45 +0,0 @@
# Class: Workspaces
**`Deprecated`**
This will be removed in v19. Use [readProjectsConfigurationFromProjectGraph](../../devkit/documents/readProjectsConfigurationFromProjectGraph) instead.
## Table of contents
### Constructors
- [constructor](../../devkit/documents/Workspaces#constructor)
### Methods
- [readWorkspaceConfiguration](../../devkit/documents/Workspaces#readworkspaceconfiguration)
## Constructors
### constructor
**new Workspaces**(`root`): [`Workspaces`](../../devkit/documents/Workspaces)
#### Parameters
| Name | Type |
| :----- | :------- |
| `root` | `string` |
#### Returns
[`Workspaces`](../../devkit/documents/Workspaces)
## Methods
### readWorkspaceConfiguration
**readWorkspaceConfiguration**(): [`ProjectsConfigurations`](../../devkit/documents/ProjectsConfigurations) & [`NxJsonConfiguration`](../../devkit/documents/NxJsonConfiguration)\<`string`[] \| `"*"`\>
#### Returns
[`ProjectsConfigurations`](../../devkit/documents/ProjectsConfigurations) & [`NxJsonConfiguration`](../../devkit/documents/NxJsonConfiguration)\<`string`[] \| `"*"`\>
**`Deprecated`**
Use [readProjectsConfigurationFromProjectGraph](../../devkit/documents/readProjectsConfigurationFromProjectGraph) instead.

View File

@ -8630,23 +8630,6 @@
"isExternal": false, "isExternal": false,
"disableCollapsible": false "disableCollapsible": false
}, },
{
"id": "executors",
"path": "/nx-api/plugin/executors",
"name": "executors",
"children": [
{
"id": "e2e",
"path": "/nx-api/plugin/executors/e2e",
"name": "e2e",
"children": [],
"isExternal": false,
"disableCollapsible": false
}
],
"isExternal": false,
"disableCollapsible": false
},
{ {
"id": "generators", "id": "generators",
"path": "/nx-api/plugin/generators", "path": "/nx-api/plugin/generators",

View File

@ -2072,17 +2072,7 @@
}, },
"root": "/packages/plugin", "root": "/packages/plugin",
"source": "/packages/plugin/src", "source": "/packages/plugin/src",
"executors": { "executors": {},
"/nx-api/plugin/executors/e2e": {
"description": "Creates and runs the E2E tests for an Nx Plugin.",
"file": "generated/packages/plugin/executors/e2e.json",
"hidden": false,
"name": "e2e",
"originalFilePath": "/packages/plugin/src/executors/e2e/schema.json",
"path": "/nx-api/plugin/executors/e2e",
"type": "executor"
}
},
"generators": { "generators": {
"/nx-api/plugin/generators/plugin": { "/nx-api/plugin/generators/plugin": {
"description": "Create a Nx Plugin.", "description": "Create a Nx Plugin.",

View File

@ -2047,17 +2047,7 @@
"originalFilePath": "shared/packages/plugin/plugin" "originalFilePath": "shared/packages/plugin/plugin"
} }
], ],
"executors": [ "executors": [],
{
"description": "Creates and runs the E2E tests for an Nx Plugin.",
"file": "generated/packages/plugin/executors/e2e.json",
"hidden": false,
"name": "e2e",
"originalFilePath": "/packages/plugin/src/executors/e2e/schema.json",
"path": "plugin/executors/e2e",
"type": "executor"
}
],
"generators": [ "generators": [
{ {
"description": "Create a Nx Plugin.", "description": "Create a Nx Plugin.",

View File

@ -19,7 +19,6 @@ It only uses language primitives and immutable objects
### Classes ### Classes
- [ProjectGraphBuilder](../../devkit/documents/ProjectGraphBuilder) - [ProjectGraphBuilder](../../devkit/documents/ProjectGraphBuilder)
- [Workspaces](../../devkit/documents/Workspaces)
### Interfaces ### Interfaces

View File

@ -1,209 +0,0 @@
{
"name": "e2e",
"implementation": "/packages/plugin/src/executors/e2e/e2e.impl.ts",
"schema": {
"version": 2,
"outputCapture": "direct-nodejs",
"title": "Nx Plugin Playground Target",
"description": "Creates a playground for a Nx Plugin.",
"cli": "nx",
"type": "object",
"properties": {
"target": {
"description": "The build target for the Nx Plugin project.",
"type": "string",
"x-priority": "important"
},
"jestConfig": {
"type": "string",
"description": "Jest config file.",
"x-completion-type": "file",
"x-completion-glob": "jest.config@(.js|.ts)",
"x-priority": "important"
},
"tsSpecConfig": {
"type": "string",
"description": "The tsconfig file for specs.",
"x-deprecated": "Use the `tsconfig` property for `ts-jest` in the e2e project `jest.config.js` file. It will be removed in the next major release.",
"x-completion-type": "file",
"x-completion-glob": "tsconfig.*.json",
"x-priority": "important"
},
"codeCoverage": {
"description": "Indicates that test coverage information should be collected and reported in the output. (https://jestjs.io/docs/cli#--coverageboolean)",
"type": "boolean",
"aliases": ["coverage"]
},
"config": {
"description": "The path to a Jest config file specifying how to find and execute tests. If no `rootDir` is set in the config, the directory containing the config file is assumed to be the `rootDir` for the project. This can also be a JSON-encoded value which Jest will use as configuration.",
"type": "string"
},
"clearCache": {
"description": "Deletes the Jest cache directory and then exits without running tests. Will delete Jest's default cache directory. _Note: clearing the cache will reduce performance_.",
"type": "boolean"
},
"detectOpenHandles": {
"description": "Attempt to collect and print open handles preventing Jest from exiting cleanly (https://jestjs.io/docs/cli#--detectopenhandles)",
"type": "boolean"
},
"logHeapUsage": {
"description": "Logs the heap usage after every test. Useful to debug memory leaks. Use together with --runInBand and --expose-gc in node.",
"type": "boolean"
},
"detectLeaks": {
"description": "**EXPERIMENTAL**: Detect memory leaks in tests. After executing a test, it will try to garbage collect the global object used, and fail if it was leaked",
"type": "boolean"
},
"testFile": {
"description": "The name of the file to test.",
"type": "string",
"x-completion-type": "file",
"x-completion-glob": "**/*.+(spec|test).+(ts|js)"
},
"tsConfig": {
"description": "The name of the Typescript configuration file. Set the tsconfig option in the jest config file. ",
"type": "string",
"x-deprecated": "Use the ts-jest configuration options in the jest config file instead."
},
"setupFile": {
"description": "The name of a setup file used by Jest. (use Jest config file https://jestjs.io/docs/en/configuration#setupfilesafterenv-array)",
"type": "string",
"x-deprecated": "Use the setupFilesAfterEnv option in the jest config file. https://jestjs.io/docs/en/configuration#setupfilesafterenv-array"
},
"bail": {
"alias": "b",
"description": "Exit the test suite immediately after `n` number of failing tests. (https://jestjs.io/docs/cli#--bail)",
"oneOf": [{ "type": "number" }, { "type": "boolean" }]
},
"ci": {
"description": "Whether to run Jest in continuous integration (CI) mode. This option is on by default in most popular CI environments. It will prevent snapshots from being written unless explicitly requested. (https://jestjs.io/docs/cli#--ci)",
"type": "boolean"
},
"color": {
"alias": "colors",
"description": "Forces test results output color highlighting (even if `stdout` is not a TTY). Set to false if you would like to have no colors. (https://jestjs.io/docs/cli#--colors)",
"type": "boolean"
},
"findRelatedTests": {
"description": "Find and run the tests that cover a comma separated list of source files that were passed in as arguments. (https://jestjs.io/docs/cli#--findrelatedtests-spaceseparatedlistofsourcefiles)",
"type": "string"
},
"json": {
"description": "Prints the test results in `JSON`. This mode will send all other test output and user messages to `stderr`. (https://jestjs.io/docs/cli#--json)",
"type": "boolean"
},
"maxWorkers": {
"alias": "w",
"description": "Specifies the maximum number of workers the worker-pool will spawn for running tests. This defaults to the number of the cores available on your machine. Useful for CI. (its usually best not to override this default) (https://jestjs.io/docs/cli#--maxworkersnumstring)",
"oneOf": [{ "type": "number" }, { "type": "string" }]
},
"onlyChanged": {
"alias": "o",
"description": "Attempts to identify which tests to run based on which files have changed in the current repository. Only works if you're running tests in a `git` or `hg` repository at the moment. (https://jestjs.io/docs/cli#--onlychanged)",
"type": "boolean"
},
"changedSince": {
"description": "Runs tests related to the changes since the provided branch or commit hash. If the current branch has diverged from the given branch, then only changes made locally will be tested. (https://jestjs.io/docs/cli#--changedsince)",
"type": "string"
},
"outputFile": {
"description": "Write test results to a file when the `--json` option is also specified. (https://jestjs.io/docs/cli#--outputfilefilename)",
"type": "string"
},
"passWithNoTests": {
"description": "Will not fail if no tests are found (for example while using `--testPathPattern`.) (https://jestjs.io/docs/cli#--passwithnotests)",
"type": "boolean"
},
"runInBand": {
"alias": "i",
"description": "Run all tests serially in the current process (rather than creating a worker pool of child processes that run tests). This is sometimes useful for debugging, but such use cases are pretty rare. Useful for CI. (https://jestjs.io/docs/cli#--runinband)",
"type": "boolean",
"default": true
},
"showConfig": {
"description": "Print your Jest config and then exits. (https://jestjs.io/docs/en/cli#--showconfig)",
"type": "boolean"
},
"silent": {
"description": "Prevent tests from printing messages through the console. (https://jestjs.io/docs/cli#--silent)",
"type": "boolean"
},
"testNamePattern": {
"alias": "t",
"description": "Run only tests with a name that matches the regex pattern. (https://jestjs.io/docs/cli#--testnamepatternregex)",
"type": "string"
},
"testPathIgnorePatterns": {
"description": "An array of regexp pattern strings that is matched against all tests paths before executing the test. Only run those tests with a path that does not match with the provided regexp expressions. (https://jestjs.io/docs/cli#--testpathignorepatternsregexarray)",
"type": "array",
"items": { "type": "string" }
},
"testPathPattern": {
"description": "An array of regexp pattern strings that is matched against all tests paths before executing the test. (https://jestjs.io/docs/cli#--testpathpatternregex)",
"type": "array",
"items": { "type": "string" },
"default": []
},
"colors": {
"description": "Forces test results output highlighting even if stdout is not a TTY. (https://jestjs.io/docs/cli#--colors)",
"type": "boolean"
},
"reporters": {
"description": "Run tests with specified reporters. Reporter options are not available via CLI. Example with multiple reporters: `jest --reporters=\"default\" --reporters=\"jest-junit\"`. (https://jestjs.io/docs/cli#--reporters)",
"type": "array",
"items": { "type": "string" }
},
"verbose": {
"description": "Display individual test results with the test suite hierarchy. (https://jestjs.io/docs/cli#--verbose)",
"type": "boolean"
},
"coverageReporters": {
"description": "A list of reporter names that Jest uses when writing coverage reports. Any istanbul reporter.",
"type": "array",
"items": { "type": "string" }
},
"coverageDirectory": {
"description": "The directory where Jest should output its coverage files.",
"type": "string"
},
"testResultsProcessor": {
"description": "Node module that implements a custom results processor. (https://jestjs.io/docs/en/configuration#testresultsprocessor-string)",
"type": "string"
},
"updateSnapshot": {
"alias": "u",
"description": "Use this flag to re-record snapshots. Can be used together with a test suite pattern or with `--testNamePattern` to re-record snapshot for test matching the pattern. (https://jestjs.io/docs/cli#--updatesnapshot)",
"type": "boolean"
},
"useStderr": {
"description": "Divert all output to stderr.",
"type": "boolean"
},
"watch": {
"description": "Watch files for changes and rerun tests related to changed files. If you want to re-run all tests when a file has changed, use the `--watchAll` option. (https://jestjs.io/docs/cli#--watch)",
"type": "boolean"
},
"watchAll": {
"description": "Watch files for changes and rerun all tests when something changes. If you want to re-run only the tests that depend on the changed files, use the `--watch` option. (https://jestjs.io/docs/cli#--watchall)",
"type": "boolean"
},
"testLocationInResults": {
"description": "Adds a location field to test results. Used to report location of a test in a reporter. { \"column\": 4, \"line\": 5 } (https://jestjs.io/docs/cli#--testlocationinresults)",
"type": "boolean"
},
"testTimeout": {
"description": "Default timeout of a test in milliseconds. Default value: `5000`. (https://jestjs.io/docs/cli#--testtimeoutnumber)",
"type": "number"
}
},
"additionalProperties": false,
"required": ["target", "jestConfig"],
"presets": []
},
"description": "Creates and runs the E2E tests for an Nx Plugin.",
"x-deprecated": "@nx/plugin:e2e is deprecated and will be removed in Nx v19. Use @nx/jest:jest instead.",
"aliases": [],
"hidden": false,
"path": "/packages/plugin/src/executors/e2e/schema.json",
"type": "executor"
}

View File

@ -562,8 +562,6 @@
- [plugin](/nx-api/plugin) - [plugin](/nx-api/plugin)
- [documents](/nx-api/plugin/documents) - [documents](/nx-api/plugin/documents)
- [Overview](/nx-api/plugin/documents/overview) - [Overview](/nx-api/plugin/documents/overview)
- [executors](/nx-api/plugin/executors)
- [e2e](/nx-api/plugin/executors/e2e)
- [generators](/nx-api/plugin/generators) - [generators](/nx-api/plugin/generators)
- [plugin](/nx-api/plugin/generators/plugin) - [plugin](/nx-api/plugin/generators/plugin)
- [create-package](/nx-api/plugin/generators/create-package) - [create-package](/nx-api/plugin/generators/create-package)

View File

@ -275,7 +275,6 @@ const pages: Array<{ title: string; path: string }> = [
title: '@nx/plugin:executor', title: '@nx/plugin:executor',
path: '/packages/plugin/generators/executor', path: '/packages/plugin/generators/executor',
}, },
{ title: '@nx/plugin:e2e', path: '/packages/plugin/executors/e2e' },
{ title: '@nx/react', path: '/packages/react' }, { title: '@nx/react', path: '/packages/react' },
{ title: '@nx/react:init', path: '/packages/react/generators/init' }, { title: '@nx/react:init', path: '/packages/react/generators/init' },
{ {

View File

@ -1,29 +1,5 @@
{ {
"generators": { "generators": {
"add-cypress-inputs": {
"cli": "nx",
"version": "15.0.0-beta.0",
"description": "Stop hashing cypress spec files and config files for build targets and dependent tasks",
"factory": "./src/migrations/update-15-0-0/add-cypress-inputs"
},
"update-cy-mount-usage": {
"cli": "nx",
"version": "15.0.0-beta.4",
"description": "Update to using cy.mount in the commands.ts file instead of importing mount for each component test file",
"factory": "./src/migrations/update-15-0-0/update-cy-mount-usage"
},
"update-to-cypress-11": {
"cli": "nx",
"version": "15.1.0-beta.0",
"description": "Update to Cypress v11. This migration will only update if the workspace is already on v10. https://www.cypress.io/blog/2022/11/04/upcoming-changes-to-component-testing/",
"factory": "./src/migrations/update-15-1-0/cypress-11"
},
"update-to-cypress-12": {
"cli": "nx",
"version": "15.5.0-beta.0",
"description": "Update to Cypress v12. Cypress 12 contains a handful of breaking changes that might causes tests to start failing that nx cannot directly fix. Read more Cypress 12 changes: https://docs.cypress.io/guides/references/migration-guide#Migrating-to-Cypress-12-0.This migration will only run if you are already using Cypress v11.",
"factory": "./src/migrations/update-15-5-0/update-to-cypress-12"
},
"update-16-0-0-add-nx-packages": { "update-16-0-0-add-nx-packages": {
"cli": "nx", "cli": "nx",
"version": "16.0.0-beta.1", "version": "16.0.0-beta.1",

View File

@ -1,221 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`update cy.mount usage should add the mount command 1`] = `
"import { mount } from 'cypress/angular'
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts 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
// ***********************************************
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
mount: typeof mount;
}
}
}
//
// -- 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) => { ... })
Cypress.Commands.add('mount', mount);"
`;
exports[`update cy.mount usage should update angular ct test file 1`] = `
"
import { MountConfig } from 'cypress/angular';
describe('MyComponent', () => {
it('should work', () => {
cy.mount(MyComponent);
});
it('should work with config', () => {
cy.mount(MyComponent, {...config, componentProperties: {foo: 'bar'}});
});
});
"
`;
exports[`update cy.mount usage should update angular react test file 1`] = `
"
describe('MyComponent', () => {
it('should work', () => {
cy.mount(<MyComponent />);
});
it('should work with config', () => {
cy.mount(<MyComponent title={"blah"}/>,);
});
});
"
`;
exports[`update cy.mount usage should update angular react18 test file 1`] = `
"
describe('MyComponent', () => {
it('should work', () => {
cy.mount(<MyComponent />);
});
it('should work with config', () => {
cy.mount(<MyComponent title={"blah"}/>);
});
});
"
`;
exports[`update cy.mount usage should work 1`] = `
"import { mount } from 'cypress/angular';
// eslint-disable-next-line @typescript-eslint/no-namespace
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
mount: typeof mount;
}
}
}
// -- This is a parent command --
Cypress.Commands.add('login', (email, password) => {
console.log('Custom command example: Login', email, password);
});
Cypress.Commands.add('mount', mount);
"
`;
exports[`update cy.mount usage should work 2`] = `
"import { MountConfig } from 'cypress/angular';
describe('MyComponent', () => {
it('should work', () => {
cy.mount(<MyComponent />);
});
it('should work with config', () => {
cy.mount(<MyComponent title={'blah'} />);
});
});
"
`;
exports[`update cy.mount usage should work 3`] = `
"import { MountConfig } from 'cypress/angular';
describe('MyComponent', () => {
it('should work', () => {
cy.mount(<MyComponent />);
});
it('should work with config', () => {
cy.mount(<MyComponent title={'blah'} />);
});
});
"
`;
exports[`update cy.mount usage should work 4`] = `
"import { MountConfig } from 'cypress/angular';
describe('MyComponent', () => {
it('should work', () => {
cy.mount(MyComponent);
});
it('should work with config', () => {
cy.mount(MyComponent, { ...config, componentProperties: { foo: 'bar' } });
});
});
"
`;
exports[`update cy.mount usage should work 5`] = `
"// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
mount(c: any): void;
}
}
// -- This is a parent command --
Cypress.Commands.add('login', (email, password) => {
console.log('Custom command example: Login', email, password);
});
Cypress.Commands.add('mount', (any) => {
console.log(mount);
});
"
`;
exports[`update cy.mount usage should work 6`] = `
"describe('MyComponent', () => {
it('should work', () => {
cy.mount(<MyComponent />);
});
it('should work with config', () => {
cy.mount(<MyComponent title={'blah'} />);
});
});
"
`;
exports[`update cy.mount usage should work 7`] = `
"describe('MyComponent', () => {
it('should work', () => {
cy.mount(<MyComponent />);
});
it('should work with config', () => {
cy.mount(<MyComponent title={'blah'} />);
});
});
"
`;
exports[`update cy.mount usage should work 8`] = `
"describe('MyComponent', () => {
it('should work', () => {
cy.mount(MyComponent);
});
it('should work with config', () => {
cy.mount(MyComponent, { ...config, componentProperties: { foo: 'bar' } });
});
});
"
`;

View File

@ -1,141 +0,0 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
addProjectConfiguration,
readNxJson,
Tree,
updateNxJson,
} from '@nx/devkit';
import addCypressInputs from './add-cypress-inputs';
describe('15.0.0 migration (add-cypress-inputs)', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
it('should add inputs configuration for cypress targets', async () => {
updateNxJson(tree, {
namedInputs: {
default: ['{projectRoot}/**/*', 'sharedGlobals'],
sharedGlobals: [],
production: ['default'],
},
});
addProjectConfiguration(tree, 'proj', {
root: 'proj',
targets: {
e2e: {
executor: '@nrwl/cypress:cypress',
options: {},
},
e2e2: {
executor: '@nrwl/cypress:cypress',
options: {},
},
notTest: {
executor: 'nx:run-commands',
},
},
});
tree.write('jest.preset.js', '');
await addCypressInputs(tree);
const updated = readNxJson(tree);
expect(updated).toMatchInlineSnapshot(`
{
"namedInputs": {
"default": [
"{projectRoot}/**/*",
"sharedGlobals",
],
"production": [
"default",
],
"sharedGlobals": [],
},
"targetDefaults": {
"e2e": {
"inputs": [
"default",
"^production",
],
},
"e2e2": {
"inputs": [
"default",
"^production",
],
},
},
}
`);
});
it('should inputs configuration for cypress component testing targets', async () => {
updateNxJson(tree, {
namedInputs: {
default: ['{projectRoot}/**/*', 'sharedGlobals'],
sharedGlobals: [],
production: ['default'],
},
});
addProjectConfiguration(tree, 'proj', {
root: 'proj',
targets: {
e2e: {
executor: '@nrwl/cypress:cypress',
options: {
testingType: 'component',
},
},
e2e2: {
executor: '@nrwl/cypress:cypress',
options: {
testingType: 'component',
},
},
notTest: {
executor: 'nx:run-commands',
},
},
});
tree.write('jest.preset.js', '');
await addCypressInputs(tree);
const updated = readNxJson(tree);
expect(updated).toMatchInlineSnapshot(`
{
"namedInputs": {
"default": [
"{projectRoot}/**/*",
"sharedGlobals",
],
"production": [
"default",
"!{projectRoot}/cypress/**/*",
"!{projectRoot}/**/*.cy.[jt]s?(x)",
"!{projectRoot}/cypress.config.[jt]s",
],
"sharedGlobals": [],
},
"targetDefaults": {
"e2e": {
"inputs": [
"default",
"^production",
],
},
"e2e2": {
"inputs": [
"default",
"^production",
],
},
},
}
`);
});
});

View File

@ -1,52 +0,0 @@
import { formatFiles, readNxJson, Tree, updateNxJson } from '@nx/devkit';
import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
import { CypressExecutorOptions } from '../../executors/cypress/cypress.impl';
export default async function (tree: Tree) {
const nxJson = readNxJson(tree);
const { cypressTargets, hasComponentTesting } = getCypressTargetNames(tree);
const hasProductionFileset = !!nxJson.namedInputs?.production;
if (hasComponentTesting && hasProductionFileset && cypressTargets.size > 0) {
const productionFileset = new Set(nxJson.namedInputs.production);
for (const exclusion of [
'!{projectRoot}/cypress/**/*',
'!{projectRoot}/**/*.cy.[jt]s?(x)',
'!{projectRoot}/cypress.config.[jt]s',
]) {
productionFileset.add(exclusion);
}
nxJson.namedInputs.production = Array.from(productionFileset);
}
for (const targetName of cypressTargets) {
nxJson.targetDefaults ??= {};
const cypressTargetDefaults = (nxJson.targetDefaults[targetName] ??= {});
cypressTargetDefaults.inputs ??= [
'default',
hasProductionFileset ? '^production' : '^default',
];
}
updateNxJson(tree, nxJson);
await formatFiles(tree);
}
function getCypressTargetNames(tree: Tree) {
const cypressTargets = new Set<string>();
let hasComponentTesting = false;
forEachExecutorOptions<CypressExecutorOptions>(
tree,
'@nrwl/cypress:cypress',
(options, __, target) => {
cypressTargets.add(target);
if (options.testingType === 'component') {
hasComponentTesting = true;
}
}
);
return { cypressTargets, hasComponentTesting };
}

View File

@ -1,338 +0,0 @@
import { installedCypressVersion } from '../../utils/cypress-version';
import {
readProjectConfiguration,
Tree,
updateProjectConfiguration,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
addMountCommand,
updateCyFile,
updateCyMountUsage,
} from './update-cy-mount-usage';
import { libraryGenerator } from '@nx/js';
import cypressComponentConfiguration from '../../generators/component-configuration/component-configuration';
jest.mock('../../utils/cypress-version');
// nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
createProjectGraphAsync: jest
.fn()
.mockImplementation(async () => ({ nodes: {}, dependencies: {} })),
}));
describe('update cy.mount usage', () => {
let tree: Tree;
let mockedInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
// TODO(@leosvelperez): Turn these tests back to adding the plugin
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
mockedInstalledCypressVersion.mockReturnValue(10);
});
it('should work', async () => {
await setup(tree);
await updateCyMountUsage(tree);
expect(
tree.read('libs/my-lib/cypress/support/commands.ts', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('libs/my-lib/src/lib/my-cmp-one.cy.js', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('libs/my-lib/src/lib/my-cmp-two.cy.tsx', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('libs/my-lib/src/lib/my-cmp-three.cy.ts', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('libs/another-lib/cypress/support/commands.ts', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('libs/another-lib/src/lib/my-cmp-one.cy.js', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('libs/another-lib/src/lib/my-cmp-two.cy.tsx', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('libs/another-lib/src/lib/my-cmp-three.cy.ts', 'utf-8')
).toMatchSnapshot();
});
it('should add the mount command', async () => {
tree.write(
'apps/my-app/cypress/support/commands.ts',
`/// <reference types="cypress" />
// ***********************************************
// This example commands.ts 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
// ***********************************************
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
// 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) => { ... })
`
);
addMountCommand(tree, 'apps/my-app', 'angular');
expect(
tree.read('apps/my-app/cypress/support/commands.ts', 'utf-8')
).toMatchSnapshot();
});
it('should update angular ct test file', () => {
tree.write(
'my-file.cy.ts',
`
import { MountConfig, mount } from 'cypress/angular';
describe('MyComponent', () => {
it('should work', () => {
mount(MyComponent);
});
it('should work with config', () => {
mount(MyComponent, {...config, componentProperties: {foo: 'bar'}});
});
});
`
);
updateCyFile(tree, 'my-file.cy.ts', 'angular');
expect(tree.read('my-file.cy.ts', 'utf-8')).toMatchSnapshot();
});
it('should update angular react18 test file', () => {
tree.write(
'my-file.cy.ts',
`
import { mount } from 'cypress/react18';
describe('MyComponent', () => {
it('should work', () => {
mount(<MyComponent />);
});
it('should work with config', () => {
mount(<MyComponent title={"blah"}/>);
});
});
`
);
updateCyFile(tree, 'my-file.cy.ts', 'react18');
expect(tree.read('my-file.cy.ts', 'utf-8')).toMatchSnapshot();
});
it('should update angular react test file', () => {
tree.write(
'my-file.cy.ts',
`
import { mount } from 'cypress/react';
describe('MyComponent', () => {
it('should work', () => {
mount(<MyComponent />);
});
it('should work with config', () => {
mount(<MyComponent title={"blah"}/>,);
});
});
`
);
updateCyFile(tree, 'my-file.cy.ts', 'react');
expect(tree.read('my-file.cy.ts', 'utf-8')).toMatchSnapshot();
});
});
async function setup(tree: Tree) {
await libraryGenerator(tree, { name: 'my-lib' });
await libraryGenerator(tree, { name: 'another-lib' });
await cypressComponentConfiguration(tree, {
project: 'my-lib',
skipFormat: false,
});
await cypressComponentConfiguration(tree, {
project: 'another-lib',
skipFormat: false,
});
const myLib = readProjectConfiguration(tree, 'my-lib');
myLib.targets['build'] = {
executor: '@nrwl/angular:webpack-browser',
options: {},
};
myLib.targets['component-test'].executor = '@nrwl/cypress:cypress';
myLib.targets['component-test'].options.devServerTarget = 'my-lib:build';
updateProjectConfiguration(tree, 'my-lib', myLib);
const anotherLib = readProjectConfiguration(tree, 'another-lib');
anotherLib.targets['build'] = {
executor: '@nrwl/webpack:webpack',
options: {},
};
anotherLib.targets['component-test'].executor = '@nrwl/cypress:cypress';
anotherLib.targets['component-test'].options.devServerTarget =
'another-lib:build';
updateProjectConfiguration(tree, 'another-lib', anotherLib);
tree.write(
'libs/my-lib/cypress/support/commands.ts',
`
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
// 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);
});`
);
tree.write(
'libs/my-lib/src/lib/my-cmp-one.cy.js',
`const { mount } =require('cypress/react');
describe('MyComponent', () => {
it('should work', () => {
mount(<MyComponent />);
});
it('should work with config', () => {
mount(<MyComponent title={"blah"}/>,);
});
});
`
);
tree.write(
'libs/my-lib/src/lib/my-cmp-two.cy.tsx',
`import { mount } from 'cypress/react18';
describe('MyComponent', () => {
it('should work', () => {
mount(<MyComponent />);
});
it('should work with config', () => {
mount(<MyComponent title={"blah"}/>,);
});
});
`
);
tree.write(
'libs/my-lib/src/lib/my-cmp-three.cy.ts',
`import { mount, MountConfig } from 'cypress/angular';
describe('MyComponent', () => {
it('should work', () => {
mount(MyComponent);
});
it('should work with config', () => {
mount(MyComponent, {...config, componentProperties: {foo: 'bar'}});
});
});
`
);
tree.write(
'libs/another-lib/cypress/support/commands.ts',
`
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
mount(c: any): void;
}
}
// -- This is a parent command --
Cypress.Commands.add('login', (email, password) => {
console.log('Custom command example: Login', email, password);
});
Cypress.Commands.add('mount', (any) => {
console.log(mount);
});
`
);
tree.write(
'libs/another-lib/src/lib/my-cmp-one.cy.js',
`const { mount } = require('cypress/react');
describe('MyComponent', () => {
it('should work', () => {
mount(<MyComponent />);
});
it('should work with config', () => {
mount(<MyComponent title={"blah"}/>,);
});
});
`
);
tree.write(
'libs/another-lib/src/lib/my-cmp-two.cy.tsx',
`import { mount } from 'cypress/react18';
describe('MyComponent', () => {
it('should work', () => {
mount(<MyComponent />);
});
it('should work with config', () => {
mount(<MyComponent title={"blah"}/>,);
});
});
`
);
tree.write(
'libs/another-lib/src/lib/my-cmp-three.cy.ts',
`import { mount, MountConfig } from 'cypress/angular';
describe('MyComponent', () => {
it('should work', () => {
mount(MyComponent);
});
it('should work with config', () => {
mount(MyComponent, {...config, componentProperties: {foo: 'bar'}});
});
});
`
);
}

View File

@ -1,197 +0,0 @@
import { CypressExecutorOptions } from '../../executors/cypress/cypress.impl';
import { CY_FILE_MATCHER } from '../../utils/ct-helpers';
import { installedCypressVersion } from '../../utils/cypress-version';
import {
createProjectGraphAsync,
formatFiles,
getProjects,
joinPathFragments,
parseTargetString,
readJson,
TargetConfiguration,
Tree,
visitNotIgnoredFiles,
} from '@nx/devkit';
import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
import { checkAndCleanWithSemver } from '@nx/devkit/src/utils/semver';
import { tsquery } from '@phenomnomnominal/tsquery';
import { gte } from 'semver';
import * as ts from 'typescript';
export async function updateCyMountUsage(tree: Tree) {
if (installedCypressVersion() < 10) {
return;
}
const projects = getProjects(tree);
const graph = await createProjectGraphAsync();
forEachExecutorOptions<CypressExecutorOptions>(
tree,
'@nrwl/cypress:cypress',
(options, projectName) => {
if (options.testingType !== 'component' || !options.devServerTarget) {
return;
}
const parsed = parseTargetString(options.devServerTarget, graph);
if (!parsed?.project || !parsed?.target) {
return;
}
const buildProjectConfig = projects.get(parsed.project);
const framework = getFramework(
tree,
parsed.configuration
? buildProjectConfig.targets[parsed.target].configurations[
parsed.configuration
]
: buildProjectConfig.targets[parsed.target]
);
const ctProjectConfig = projects.get(projectName);
addMountCommand(tree, ctProjectConfig.root, framework);
visitNotIgnoredFiles(tree, ctProjectConfig.sourceRoot, (filePath) => {
if (CY_FILE_MATCHER.test(filePath)) {
updateCyFile(tree, filePath, framework);
}
});
}
);
await formatFiles(tree);
}
export function addMountCommand(
tree: Tree,
projectRoot: string,
framework: string
) {
const commandFilePath = joinPathFragments(
projectRoot,
'cypress',
'support',
'commands.ts'
);
if (!tree.exists(commandFilePath)) {
return;
}
const commandFile = tree.read(commandFilePath, 'utf-8');
const mountCommand = tsquery.query<ts.PropertyAccessExpression>(
commandFile,
'CallExpression:has(StringLiteral[value="mount"]) PropertyAccessExpression:has(Identifier[name="add"])'
);
if (mountCommand?.length > 0) {
return;
}
const existingCommands = tsquery.query<
ts.MethodSignature | ts.PropertySignature
>(
commandFile,
'InterfaceDeclaration:has(Identifier[name="Chainable"]) > MethodSignature, InterfaceDeclaration:has(Identifier[name="Chainable"]) > PropertySignature'
);
const isGlobalDeclaration = tsquery.query<ts.ModuleDeclaration>(
commandFile,
'ModuleDeclaration > Identifier[name="global"]'
);
const updatedInterface = tsquery.replace(
commandFile,
'ModuleDeclaration:has(Identifier[name="Cypress"])',
(node: ts.ModuleDeclaration) => {
const newModuleDelcaration = `declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
${existingCommands.map((c) => c.getText()).join('\n')}
mount: typeof mount;
}
}
}`;
/*
* this is to prevent the change being applied twice since
* declare global { 1
* interface Cypress { 2
* }
* }
* matches twice.
* i.e. if there is no global declaration, then add it
* or if the node is the global declaration, then add it,
* but not to the cypress module declaration inside the global declaration
*/
if (
isGlobalDeclaration?.length === 0 ||
node.name.getText() === 'global'
) {
return newModuleDelcaration;
}
}
);
const updatedCommandFile = `import { mount } from 'cypress/${framework}'\n${updatedInterface}\nCypress.Commands.add('mount', mount);`;
tree.write(commandFilePath, updatedCommandFile);
}
function getFramework(
tree: Tree,
target: TargetConfiguration
): 'angular' | 'react' | 'react18' {
if (
target.executor === '@nrwl/angular:webpack-browser' ||
target.executor === '@angular-devkit/build-angular:browser'
) {
return 'angular';
}
const pkgJson = readJson(tree, 'package.json');
const reactDomVersion = pkgJson?.dependencies?.['react-dom'];
const hasReact18 =
reactDomVersion &&
gte(checkAndCleanWithSemver('react-dom', reactDomVersion), '18.0.0');
if (hasReact18) {
return 'react18';
}
return 'react';
}
export function updateCyFile(
tree: Tree,
filePath: string,
framework: 'angular' | 'react' | 'react18'
) {
if (!tree.exists(filePath)) {
return;
}
const contents = tree.read(filePath, 'utf-8');
const withCyMount = tsquery.replace(
contents,
':matches(CallExpression>Identifier[name="mount"])',
(node: ts.CallExpression) => {
return `cy.mount`;
}
);
const withUpdatedImports = tsquery.replace(
withCyMount,
':matches(ImportDeclaration, VariableStatement):has(Identifier[name="mount"]):has(StringLiteral[value="cypress/react"], StringLiteral[value="cypress/angular"], StringLiteral[value="cypress/react18"])',
(node: ts.ImportDeclaration) => {
switch (framework) {
case 'angular':
return `import { MountConfig } from 'cypress/angular';`;
case 'react18':
case 'react':
return ' '; // have to return non falsy string to remove the node
default:
return node.getText().replace('mount', '');
}
}
);
tree.write(filePath, withUpdatedImports);
}
export default updateCyMountUsage;

View File

@ -1,198 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Cypress 11 Migration should migrate to v11 1`] = `
"it('calls the prop', () => {
cy.mount(<Comp onUnmount={cy.stub().as('onUnmount')} />);
cy.contains('My component');
});
describe('again', () => {
it('calls the prop', () => {
cy.mount(<Comp onUnmount={cy.stub().as('onUnmount')} />);
cy.contains('My component');
});
});
"
`;
exports[`Cypress 11 Migration should migrate to v11 2`] = `
"/** TODO: mountHook is deprecate.
* Use a wrapper component instead.
* See post for details: https://www.cypress.io/blog/2022/11/04/upcoming-changes-to-component-testing/#reactmounthook-removed
* */
import { mountHook, getContainerEl } from 'cypress/react18';
import ReactDom from 'react-dom';
import { useCounter } from './useCounter';
it('increments the count', () => {
mountHook(() => useCounter()).then((result) => {
expect(result.current.count).to.equal(0);
result.current.increment();
expect(result.current.count).to.equal(1);
result.current.increment();
expect(result.current.count).to.equal(2);
});
});
describe('blah', () => {
it('increments the count', () => {
mountHook(() => useCounter()).then((result) => {
expect(result.current.count).to.equal(0);
result.current.increment();
expect(result.current.count).to.equal(1);
result.current.increment();
expect(result.current.count).to.equal(2);
});
});
});
it('calls the prop', () => {
cy.mount(<Comp onUnmount={cy.stub().as('onUnmount')} />);
cy.contains('My component');
cy.then(() => ReactDom.unmountComponentAtNode(getContainerEl()));
cy.contains('My component').should('not.exist');
cy.get('@onUnmount').should('have.been.calledOnce');
});
describe('again', () => {
it('calls the prop', () => {
cy.mount(<Comp onUnmount={cy.stub().as('onUnmount')} />);
cy.contains('My component');
cy.then(() => ReactDom.unmountComponentAtNode(getContainerEl()));
cy.contains('My component').should('not.exist');
cy.get('@onUnmount').should('have.been.calledOnce');
});
});
"
`;
exports[`Cypress 11 Migration should migrate to v11 3`] = `
"/** TODO: mountHook is deprecate.
* Use a wrapper component instead.
* See post for details: https://www.cypress.io/blog/2022/11/04/upcoming-changes-to-component-testing/#reactmounthook-removed
* */
import { mountHook, getContainerEl } from 'cypress/react';
import ReactDom from 'react-dom';
import { useCounter } from './useCounter';
it('increments the count', () => {
mountHook(() => useCounter()).then((result) => {
expect(result.current.count).to.equal(0);
result.current.increment();
expect(result.current.count).to.equal(1);
result.current.increment();
expect(result.current.count).to.equal(2);
});
});
describe('blah', () => {
it('increments the count', () => {
mountHook(() => useCounter()).then((result) => {
expect(result.current.count).to.equal(0);
result.current.increment();
expect(result.current.count).to.equal(1);
result.current.increment();
expect(result.current.count).to.equal(2);
});
});
});
it('calls the prop', () => {
cy.mount(<Comp onUnmount={cy.stub().as('onUnmount')} />);
cy.contains('My component');
cy.then(() => ReactDom.unmountComponentAtNode(getContainerEl()));
cy.contains('My component').should('not.exist');
cy.get('@onUnmount').should('have.been.calledOnce');
});
describe('again', () => {
it('calls the prop', () => {
cy.mount(<Comp onUnmount={cy.stub().as('onUnmount')} />);
cy.contains('My component');
cy.then(() => ReactDom.unmountComponentAtNode(getContainerEl()));
cy.contains('My component').should('not.exist');
cy.get('@onUnmount').should('have.been.calledOnce');
});
});
"
`;
exports[`Cypress 11 Migration should migrate to v11 4`] = `
"import { TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
describe('MyComponent', () => {
const config = {
imports: [],
declarations: [],
providers: [{ provide: 'foo', useValue: 'bar' }],
};
it('direct usage', () => {
TestBed.overrideComponent(MyComponent, {
add: { providers: config.providers },
});
cy.mount(MyComponent, config);
});
it('spread usage', () => {
TestBed.overrideComponent(MyComponent, {
add: { providers: [{ provide: 'foo', useValue: 'bar' }] },
});
cy.mount(MyComponent, { ...config, providers: undefined });
});
it('inlined usage', () => {
TestBed.overrideComponent(MyComponent, {
add: { providers: [{ provide: 'foo', useValue: 'bar' }] },
});
cy.mount(MyComponent, {
imports: [],
declarations: [],
providers: undefined,
});
});
});
"
`;
exports[`Cypress 11 Migration should migrate to v11 5`] = `
"import { MountConfig } from 'cypress/angular';
import { MyComponent } from './my.component';
import { TestBed } from '@angular/core/testing';
describe('MyComponent', () => {
const config: MountConfig = {
imports: [],
declarations: [],
providers: [{ provide: 'foo', useValue: 'bar' }],
};
it('direct usage', () => {
TestBed.overrideComponent(MyComponent, {
add: { providers: config.providers },
});
cy.mount(MyComponent, config);
});
it('spread usage', () => {
TestBed.overrideComponent(MyComponent, {
add: { providers: [{ provide: 'foo', useValue: 'bar' }] },
});
cy.mount(MyComponent, { ...config, providers: undefined });
});
it('inlined usage', () => {
TestBed.overrideComponent(MyComponent, {
add: { providers: [{ provide: 'foo', useValue: 'bar' }] },
});
cy.mount(MyComponent, {
imports: [],
declarations: [],
providers: undefined,
});
});
});
"
`;

View File

@ -1,344 +0,0 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
addProjectConfiguration,
readProjectConfiguration,
Tree,
updateProjectConfiguration,
} from '@nx/devkit';
import { libraryGenerator } from '@nx/js';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import updateToCypress11 from './cypress-11';
import { installedCypressVersion } from '../../utils/cypress-version';
jest.mock('../../utils/cypress-version');
import cypressComponentConfiguration from '../../generators/component-configuration/component-configuration';
describe('Cypress 11 Migration', () => {
let tree: Tree;
let mockInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
// TODO(@leosvelperez): Turn this back to adding the plugin
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
it('should not update if cypress <v10 is used', async () => {
// setup called the component setup. mock to v10 so it doesn't throw.
mockInstalledCypressVersion.mockReturnValue(10);
await setup(tree);
mockInstalledCypressVersion.mockReturnValue(9);
const beforeReact = tree.read(
'libs/my-react-lib/src/lib/no-import.cy.ts',
'utf-8'
);
const beforeNg = tree.read(
'libs/my-ng-lib/src/lib/no-import.component.cy.ts',
'utf-8'
);
await updateToCypress11(tree);
const actualReact = tree.read(
'libs/my-react-lib/src/lib/no-import.cy.ts',
'utf-8'
);
const actualNg = tree.read(
'libs/my-ng-lib/src/lib/no-import.component.cy.ts',
'utf-8'
);
expect(actualReact).toEqual(beforeReact);
expect(actualNg).toEqual(beforeNg);
});
it('should migrate to v11', async () => {
mockInstalledCypressVersion.mockReturnValue(10);
await setup(tree);
await updateToCypress11(tree);
expect(
tree.read('libs/my-react-lib/src/lib/no-import.cy.ts', 'utf-8')
).toMatchSnapshot();
expect(
tree.read(
'libs/my-react-lib/src/lib/with-import-18.component.cy.ts',
'utf-8'
)
).toMatchSnapshot();
expect(
tree.read(
'libs/my-react-lib/src/lib/with-import.component.cy.ts',
'utf-8'
)
).toMatchSnapshot();
expect(
tree.read('libs/my-ng-lib/src/lib/no-import.component.cy.ts', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('libs/my-ng-lib/src/lib/with-import.component.cy.ts', 'utf-8')
).toMatchSnapshot();
});
it('should only update component projects', async () => {
addProjectConfiguration(tree, 'my-e2e-app', {
projectType: 'application',
root: 'apps/my-e2e-app',
sourceRoot: 'apps/my-e2e-app/src',
targets: {
e2e: {
executor: '@nrwl/cypress:cypress',
options: {
cypressConfig: 'apps/my-e2e-app/cypress.config.ts',
},
},
},
});
const content = `import { MountConfig } from 'cypress/angular';
import { MyComponent } from './my.component';
describe('MyComponent', () => {
const config: MountConfig = {
imports: [],
declarations: [],
providers: [{ provide: 'foo', useValue: 'bar' }],
};
it('direct usage', () => {
cy.mount(MyComponent, config);
});
});
`;
tree.write('apps/my-e2e-app/src/somthing.component.cy.ts', content);
await updateToCypress11(tree);
expect(
tree.read('apps/my-e2e-app/src/somthing.component.cy.ts', 'utf-8')
).toEqual(content);
});
});
async function setup(tree: Tree) {
await libraryGenerator(tree, {
name: 'my-react-lib',
});
await cypressComponentConfiguration(tree, {
project: 'my-react-lib',
skipFormat: true,
});
const projectConfig = readProjectConfiguration(tree, 'my-react-lib');
projectConfig.targets['component-test'].executor = '@nrwl/cypress:cypress';
updateProjectConfiguration(tree, 'my-react-lib', projectConfig);
tree.write(
'libs/my-react-lib/cypress/support/commands.ts',
`/// <reference types="cypress" />
import { mount } from 'cypress/react18'
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
mount: typeof mount;
}
}
}
Cypress.Commands.add('mount', mount)
`
);
tree.write(
'libs/my-react-lib/src/lib/no-import.cy.ts',
`
it('calls the prop', () => {
cy.mount(<Comp onUnmount={cy.stub().as('onUnmount')} />)
cy.contains('My component')
})
describe('again', () => {
it('calls the prop', () => {
cy.mount(<Comp onUnmount={cy.stub().as('onUnmount')} />)
cy.contains('My component')
})
})`
);
tree.write(
'libs/my-react-lib/src/lib/with-import.component.cy.ts',
`import { mountHook, unmount } from 'cypress/react'
import { useCounter } from './useCounter'
it('increments the count', () => {
mountHook(() => useCounter()).then((result) => {
expect(result.current.count).to.equal(0)
result.current.increment()
expect(result.current.count).to.equal(1)
result.current.increment()
expect(result.current.count).to.equal(2)
})
})
describe('blah', () => {
it('increments the count', () => {
mountHook(() => useCounter()).then((result) => {
expect(result.current.count).to.equal(0)
result.current.increment()
expect(result.current.count).to.equal(1)
result.current.increment()
expect(result.current.count).to.equal(2)
})
})
})
it('calls the prop', () => {
cy.mount(<Comp onUnmount={cy.stub().as('onUnmount')} />)
cy.contains('My component')
unmount()
cy.contains('My component').should('not.exist')
cy.get('@onUnmount').should('have.been.calledOnce')
})
describe('again', () => {
it('calls the prop', () => {
cy.mount(<Comp onUnmount={cy.stub().as('onUnmount')} />)
cy.contains('My component')
unmount()
cy.contains('My component').should('not.exist')
cy.get('@onUnmount').should('have.been.calledOnce')
})
})`
);
tree.write(
'libs/my-react-lib/src/lib/with-import-18.component.cy.ts',
`import { mountHook, unmount } from 'cypress/react18';
import { useCounter } from './useCounter';
it('increments the count', () => {
mountHook(() => useCounter()).then((result) => {
expect(result.current.count).to.equal(0)
result.current.increment()
expect(result.current.count).to.equal(1)
result.current.increment()
expect(result.current.count).to.equal(2)
})
})
describe('blah', () => {
it('increments the count', () => {
mountHook(() => useCounter()).then((result) => {
expect(result.current.count).to.equal(0)
result.current.increment()
expect(result.current.count).to.equal(1)
result.current.increment()
expect(result.current.count).to.equal(2)
})
})
})
it('calls the prop', () => {
cy.mount(<Comp onUnmount={cy.stub().as('onUnmount')} />)
cy.contains('My component')
unmount()
cy.contains('My component').should('not.exist')
cy.get('@onUnmount').should('have.been.calledOnce')
})
describe('again', () => {
it('calls the prop', () => {
cy.mount(<Comp onUnmount={cy.stub().as('onUnmount')} />)
cy.contains('My component')
unmount()
cy.contains('My component').should('not.exist')
cy.get('@onUnmount').should('have.been.calledOnce')
})
})`
);
await libraryGenerator(tree, {
name: 'my-ng-lib',
});
await cypressComponentConfiguration(tree, {
project: 'my-ng-lib',
skipFormat: true,
});
const projectConfig2 = readProjectConfiguration(tree, 'my-ng-lib');
projectConfig2.targets['component-test'].executor = '@nrwl/cypress:cypress';
updateProjectConfiguration(tree, 'my-ng-lib', projectConfig2);
tree.write(
'libs/my-ng-lib/cypress/support/commands.ts',
`/// <reference types="cypress" />
import { mount } from 'cypress/angular'
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
mount: typeof mount;
}
}
}
Cypress.Commands.add('mount', mount)
`
);
tree.write(
'libs/my-ng-lib/src/lib/with-import.component.cy.ts',
`import { MountConfig } from 'cypress/angular';
import { MyComponent } from './my.component';
import {TestBed} from '@angular/core/testing';
describe('MyComponent', () => {
const config: MountConfig = {
imports: [],
declarations: [],
providers: [{provide: 'foo', useValue: 'bar'}]
};
it('direct usage', () => {
cy.mount(MyComponent, config);
});
it('spread usage', () => {
cy.mount(MyComponent, {...config, providers: [{provide: 'foo', useValue: 'bar'}] });
});
it('inlined usage', () => {
cy.mount(MyComponent, {imports: [], declarations: [], providers: [{provide: 'foo', useValue: 'bar'}]});
});
});
`
);
tree.write(
'libs/my-ng-lib/src/lib/no-import.component.cy.ts',
`
import { MyComponent } from './my.component';
describe('MyComponent', () => {
const config = {
imports: [],
declarations: [],
providers: [{provide: 'foo', useValue: 'bar'}]
};
it('direct usage', () => {
cy.mount(MyComponent, config);
});
it('spread usage', () => {
cy.mount(MyComponent, {...config, providers: [{provide: 'foo', useValue: 'bar'}] });
});
it('inlined usage', () => {
cy.mount(MyComponent, {imports: [], declarations: [], providers: [{provide: 'foo', useValue: 'bar'}]});
});
});
`
);
}

View File

@ -1,180 +0,0 @@
import { CY_FILE_MATCHER } from '../../utils/ct-helpers';
import {
addDependenciesToPackageJson,
formatFiles,
getProjects,
joinPathFragments,
Tree,
visitNotIgnoredFiles,
} from '@nx/devkit';
import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
import { tsquery } from '@phenomnomnominal/tsquery';
import { extname } from 'path';
import * as ts from 'typescript';
import { CypressExecutorOptions } from '../../executors/cypress/cypress.impl';
import { installedCypressVersion } from '../../utils/cypress-version';
import { cypressVersion } from '../../utils/versions';
export async function updateToCypress11(tree: Tree) {
const installedVersion = installedCypressVersion();
if (installedVersion < 10) {
return;
}
const projects = getProjects(tree);
forEachExecutorOptions<CypressExecutorOptions>(
tree,
'@nrwl/cypress:cypress',
(options, projectName, targetName, configurationName) => {
if (
options.testingType !== 'component' ||
!(options.cypressConfig && tree.exists(options.cypressConfig))
) {
return;
}
const projectConfig = projects.get(projectName);
const commandsFile = joinPathFragments(
projectConfig.root,
'cypress',
'support',
'commands.ts'
);
const framework = getFramework(
tree.exists(commandsFile)
? tree.read(commandsFile, 'utf-8')
: tree.read(options.cypressConfig, 'utf-8')
);
visitNotIgnoredFiles(tree, projectConfig.sourceRoot, (filePath) => {
if (!CY_FILE_MATCHER.test(filePath)) {
return;
}
const frameworkFromFile = getFramework(tree.read(filePath, 'utf-8'));
if (framework === 'react' || frameworkFromFile === 'react') {
updateUnmountUsage(tree, filePath);
updateMountHookUsage(tree, filePath);
}
if (framework === 'angular' || frameworkFromFile === 'angular') {
updateProviderUsage(tree, filePath);
}
});
}
);
const installTask = addDependenciesToPackageJson(
tree,
{},
{ cypress: cypressVersion }
);
await formatFiles(tree);
return () => {
installTask();
};
}
export function updateMountHookUsage(tree: Tree, filePath: string) {
const originalContents = tree.read(filePath, 'utf-8');
const commentedMountHook = tsquery.replace(
originalContents,
':matches(ImportDeclaration, VariableStatement):has(Identifier[name="mountHook"]):has(StringLiteral[value="cypress/react"], StringLiteral[value="cypress/react18"])',
(node) => {
return `/** TODO: mountHook is deprecate.
* Use a wrapper component instead.
* See post for details: https://www.cypress.io/blog/2022/11/04/upcoming-changes-to-component-testing/#reactmounthook-removed
* */\n${node.getText()}`;
}
);
tree.write(filePath, commentedMountHook);
}
export function updateUnmountUsage(tree: Tree, filePath: string) {
const reactDomImport = extname(filePath).includes('ts')
? `import ReactDom from 'react-dom'`
: `const ReactDom = require('react-dom')`;
const originalContents = tree.read(filePath, 'utf-8');
const updatedImports = tsquery.replace(
originalContents,
':matches(ImportDeclaration, VariableStatement):has(Identifier[name="unmount"]):has(StringLiteral[value="cypress/react"], StringLiteral[value="cypress/react18"])',
(node) => {
return `${node.getText().replace('unmount', 'getContainerEl')}
${reactDomImport}`;
}
);
const updatedUnmountApi = tsquery.replace(
updatedImports,
'ExpressionStatement > CallExpression:has(Identifier[name="unmount"])',
(node: ts.ExpressionStatement) => {
if (node.expression.getText() === 'unmount') {
return `cy.then(() => ReactDom.unmountComponentAtNode(getContainerEl()))`;
}
}
);
tree.write(filePath, updatedUnmountApi);
}
export function updateProviderUsage(tree: Tree, filePath: string) {
const originalContents = tree.read(filePath, 'utf-8');
const isTestBedImported =
tsquery.query(
originalContents,
':matches(ImportDeclaration, VariableStatement):has(Identifier[name="TestBed"]):has(StringLiteral[value="@angular/core/testing"])'
)?.length > 0;
let updatedProviders = tsquery.replace(
originalContents,
'CallExpression:has(PropertyAccessExpression:has(Identifier[name="mount"]))',
(node: ts.CallExpression) => {
const expressionName = node.expression.getText();
if (expressionName === 'cy.mount' && node?.arguments?.length > 1) {
const component = node.arguments[0].getText();
if (ts.isObjectLiteralExpression(node.arguments[1])) {
const providers = node.arguments[1]?.properties
?.find((p) => p.name?.getText() === 'providers')
?.getText();
const noProviders = tsquery.replace(
node.getText(),
'PropertyAssignment:has(Identifier[name="providers"])',
(n) => {
// set it to undefined so we don't run into a hanging comma causing invalid syntax
return 'providers: undefined';
}
);
return `TestBed.overrideComponent(${component}, { add: { ${providers} }});\n${noProviders}`;
} else {
return `TestBed.overrideComponent(${component}, {add: { providers: ${node.arguments[1].getText()}.providers}});\n${node.getText()}`;
}
}
}
);
tree.write(
filePath,
`${
isTestBedImported
? ''
: "import {TestBed} from '@angular/core/testing';\n"
}${updatedProviders}`
);
}
function getFramework(contents: string): 'react' | 'angular' | null {
if (contents.includes('cypress/react') || contents.includes('@nrwl/react')) {
return 'react';
}
if (
contents.includes('cypress/angular') ||
contents.includes('@nrwl/angular')
) {
return 'angular';
}
return null;
}
export default updateToCypress11;

View File

@ -1,87 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Cypress 12 Migration should migrate to cy 12 1`] = `
"describe('something', () => {
it('should do the thing', () => {
// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
Cypress.Cookies.defaults()
// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
cy.server()
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
cy.route(/api/, () => {
return {
'test': 'Well',
}
}).as('getApi')
cy.visit('/index.html')
cy.window().then((win) => {
const xhr = new win.XMLHttpRequest
xhr.open('GET', '/api/v1/foo/bar?a=42')
xhr.send()
})
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
/**
* TODO(@nrwl/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.should(($s) => {
cy.get('@table').find('tr').should('have.length', 3)
})
})
})"
`;
exports[`Cypress 12 Migration should migrate to cy 12 2`] = `
"describe('something', () => {
it('should do the thing', () => {
// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
Cypress.Cookies.defaults()
// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
cy.server()
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
cy.route(/api/, () => {
return {
'test': 'Well',
}
}).as('getApi')
cy.visit('/index.html')
cy.window().then((win) => {
const xhr = new win.XMLHttpRequest
xhr.open('GET', '/api/v1/foo/bar?a=42')
xhr.send()
})
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
/**
* TODO(@nrwl/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.should(($s) => {
cy.get('@table').find('tr').should('have.length', 3)
})
})
})"
`;

View File

@ -1,717 +0,0 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
addProjectConfiguration,
stripIndents,
Tree,
readJson,
} from '@nx/devkit';
import {
shouldNotOverrideCommands,
shouldNotUseCyInShouldCB,
shouldUseCyIntercept,
shouldUseCySession,
turnOffTestIsolation,
updateToCypress12,
} from './update-to-cypress-12';
import { installedCypressVersion } from '../../utils/cypress-version';
jest.mock('../../utils/cypress-version');
describe('Cypress 12 Migration', () => {
let tree: Tree;
let mockInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
jest.resetAllMocks();
});
it('should migrate to cy 12', () => {
mockInstalledCypressVersion.mockReturnValue(11);
addCypressProject(tree, 'my-app-e2e');
addCypressProject(tree, 'my-other-app-e2e');
updateToCypress12(tree);
assertMigration(tree, 'my-app-e2e');
assertMigration(tree, 'my-other-app-e2e');
const pkgJson = readJson(tree, 'package.json');
expect(pkgJson.devDependencies['cypress']).toEqual('^12.2.0');
});
it('should not migrate if cypress version is < 11', () => {
mockInstalledCypressVersion.mockReturnValue(10);
addCypressProject(tree, 'my-app-e2e');
updateToCypress12(tree);
expect(tree.read('apps/my-app-e2e/cypress.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__filename)
})"
`);
});
describe('nest cypress commands in should callback', () => {
beforeEach(() => {
tree.write(
'should-callback.ts',
`describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
cy.server()
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
cy.should((b) => {
const a = 123;
// I'm not doing nested cy stuff
});
cy.should(($s) => {
cy.task("");
})
cy.should(function($el) {
cy.task("");
})
})
})
`
);
});
it('should comment', () => {
shouldNotUseCyInShouldCB(tree, 'should-callback.ts');
expect(tree.read('should-callback.ts', 'utf-8')).toMatchInlineSnapshot(`
"describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
cy.server()
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
cy.should((b) => {
const a = 123;
// I'm not doing nested cy stuff
});
/**
* TODO(@nrwl/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.should(($s) => {
cy.task("");
})
/**
* TODO(@nrwl/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.should(function($el) {
cy.task("");
})
})
})
"
`);
});
it('should be idempotent', () => {
const expected = `describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
cy.server()
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
cy.should((b) => {
const a = 123;
// I'm not doing nested cy stuff
});
/**
* TODO(@nrwl/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.should(($s) => {
cy.task("");
})
/**
* TODO(@nrwl/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.should(function($el) {
cy.task("");
})
})
})
`;
shouldNotUseCyInShouldCB(tree, 'should-callback.ts');
expect(tree.read('should-callback.ts', 'utf-8')).toEqual(expected);
shouldNotUseCyInShouldCB(tree, 'should-callback.ts');
expect(tree.read('should-callback.ts', 'utf-8')).toEqual(expected);
});
});
describe('banned Cypres.Commands.overwrite', () => {
beforeEach(() => {
tree.write(
'commands.ts',
`declare namespace Cypress {
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);
});
Cypress.Commands.overwrite('find', () => {});
`
);
});
it('should comment', () => {
shouldNotOverrideCommands(tree, 'commands.ts');
expect(tree.read('commands.ts', 'utf-8')).toMatchInlineSnapshot(`
"declare namespace Cypress {
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);
});
/**
* TODO(@nrwl/cypress): This command can no longer be overridden
* Consider using a different name like 'custom_find'
* More info: https://docs.cypress.io/guides/references/migration-guide#Cypress-Commands-overwrite
**/
Cypress.Commands.overwrite('find', () => {});
"
`);
});
it('should be idempotent', () => {
const expected = `declare namespace Cypress {
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);
});
/**
* TODO(@nrwl/cypress): This command can no longer be overridden
* Consider using a different name like 'custom_find'
* More info: https://docs.cypress.io/guides/references/migration-guide#Cypress-Commands-overwrite
**/
Cypress.Commands.overwrite('find', () => {});
`;
shouldNotOverrideCommands(tree, 'commands.ts');
expect(tree.read('commands.ts', 'utf-8')).toEqual(expected);
shouldNotOverrideCommands(tree, 'commands.ts');
expect(tree.read('commands.ts', 'utf-8')).toEqual(expected);
});
});
describe('api removal', () => {
it('should be idempotent', () => {
tree.write(
'my-cool-test.cy.ts',
`
describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
cy.server()
cy.route(/api/, () => {
return {
'test': 'Well',
}
}).as('getApi')
cy.visit('/index.html')
cy.window().then((win) => {
const xhr = new win.XMLHttpRequest
xhr.open('GET', '/api/v1/foo/bar?a=42')
xhr.send()
})
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
})
})
`
);
const expected = stripIndents`describe('something', () => {
it('should do the thing', () => {
// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
Cypress.Cookies.defaults()
// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
cy.server()
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
cy.route(/api/, () => {
return {
'test': 'Well',
}
}).as('getApi')
cy.visit('/index.html')
cy.window().then((win) => {
const xhr = new win.XMLHttpRequest
xhr.open('GET', '/api/v1/foo/bar?a=42')
xhr.send()
})
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
})
})`;
shouldUseCyIntercept(tree, 'my-cool-test.cy.ts');
shouldUseCySession(tree, 'my-cool-test.cy.ts');
expect(stripIndents`${tree.read('my-cool-test.cy.ts', 'utf-8')}`).toEqual(
expected
);
shouldUseCyIntercept(tree, 'my-cool-test.cy.ts');
shouldUseCySession(tree, 'my-cool-test.cy.ts');
expect(stripIndents`${tree.read('my-cool-test.cy.ts', 'utf-8')}`).toEqual(
expected
);
});
it('comment on cy.route,cy.server, & Cypress.Server.defaults usage', () => {
tree.write(
'my-cool-test.cy.ts',
`
describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
cy.server()
cy.route(/api/, () => {
return {
'test': 'Well',
}
}).as('getApi')
cy.visit('/index.html')
cy.window().then((win) => {
const xhr = new win.XMLHttpRequest
xhr.open('GET', '/api/v1/foo/bar?a=42')
xhr.send()
})
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
})
})
`
);
shouldUseCyIntercept(tree, 'my-cool-test.cy.ts');
expect(tree.read('my-cool-test.cy.ts', 'utf-8')).toMatchInlineSnapshot(`
"
describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
cy.server()
// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
cy.route(/api/, () => {
return {
'test': 'Well',
}
}).as('getApi')
cy.visit('/index.html')
cy.window().then((win) => {
const xhr = new win.XMLHttpRequest
xhr.open('GET', '/api/v1/foo/bar?a=42')
xhr.send()
})
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
})
})
"
`);
});
it('comment on Cypress.Cookies.defaults & Cypress.Cookies.preserveOnce', () => {
tree.write(
'my-cool-test.cy.ts',
`
describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
cy.server()
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
})
})
`
);
shouldUseCySession(tree, 'my-cool-test.cy.ts');
expect(tree.read('my-cool-test.cy.ts', 'utf-8')).toMatchInlineSnapshot(`
"
describe('something', () => {
it('should do the thing', () => {
// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
Cypress.Cookies.defaults()
// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
cy.server()
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
})
})
"
`);
});
});
describe('testIsolation', () => {
it('should be idempotent', () => {
const content = `
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename),
testIsolation: true,
})
`;
tree.write('my-cypress.config.ts', content);
turnOffTestIsolation(tree, 'my-cypress.config.ts');
expect(tree.read('my-cypress.config.ts', 'utf-8')).toEqual(content);
});
it('should add testIsolation: false to the default e2e config', () => {
tree.write(
'my-cypress.config.ts',
`
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__filename),
})
`
);
turnOffTestIsolation(tree, 'my-cypress.config.ts');
expect(tree.read('my-cypress.config.ts', 'utf-8')).toMatchInlineSnapshot(`
"
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename),
/**
* TODO(@nrwl/cypress): In Cypress v12,the testIsolation option is turned on by default.
* This can cause tests to start breaking where not indended.
* You should consider enabling this once you verify tests do not depend on each other
* More Info: https://docs.cypress.io/guides/references/migration-guide#Test-Isolation
**/
testIsolation: false,
},
})
"
`);
});
it('should add testIsolation: false to inline object e2e config', () => {
tree.write(
'my-cypress.config.ts',
`
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename),
video: false
}
})
`
);
turnOffTestIsolation(tree, 'my-cypress.config.ts');
expect(tree.read('my-cypress.config.ts', 'utf-8')).toMatchInlineSnapshot(`
"
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename),
video: false,
/**
* TODO(@nrwl/cypress): In Cypress v12,the testIsolation option is turned on by default.
* This can cause tests to start breaking where not indended.
* You should consider enabling this once you verify tests do not depend on each other
* More Info: https://docs.cypress.io/guides/references/migration-guide#Test-Isolation
**/
testIsolation: false,
}
})
"
`);
});
it('should add testIsolation: false for a variable e2e config', () => {
tree.write(
'my-cypress.config.ts',
`
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
const myConfig = {
...nxE2EPreset(__filename),
video: false
}
export default defineConfig({
e2e: myConfig,
})
`
);
turnOffTestIsolation(tree, 'my-cypress.config.ts');
expect(tree.read('my-cypress.config.ts', 'utf-8')).toMatchInlineSnapshot(`
"
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
const myConfig = {
...nxE2EPreset(__filename),
video: false
}
export default defineConfig({
e2e: {
...myConfig,
/**
* TODO(@nrwl/cypress): In Cypress v12,the testIsolation option is turned on by default.
* This can cause tests to start breaking where not indended.
* You should consider enabling this once you verify tests do not depend on each other
* More Info: https://docs.cypress.io/guides/references/migration-guide#Test-Isolation
**/
testIsolation: false,
},
})
"
`);
});
});
});
function addCypressProject(tree: Tree, name: string) {
const targets = {
e2e: {
executor: '@nrwl/cypress:cypress',
options: {
tsConfig: `apps/${name}/tsconfig.e2e.json`,
testingType: 'e2e',
browser: 'chrome',
},
configurations: {
dev: {
cypressConfig: `apps/${name}/cypress.config.ts`,
devServerTarget: 'client:serve:dev',
baseUrl: 'http://localhost:4206',
},
watch: {
cypressConfig: 'apps/client-e2e/cypress-custom.config.ts',
devServerTarget: 'client:serve:watch',
baseUrl: 'http://localhost:4204',
},
},
defaultConfiguration: 'dev',
},
};
addProjectConfiguration(tree, name, {
root: `apps/${name}`,
sourceRoot: `apps/${name}/src`,
projectType: 'application',
targets,
});
// testIsolation
tree.write(
`apps/${name}/cypress.config.ts`,
`import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__filename)
})`
);
// test Cypress.Commands.Override
tree.write(
`apps/${name}/src/support/commands.ts`,
`declare namespace Cypress {
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);
});
Cypress.Commands.overwrite('find', () => {});
`
);
// test .should(() => cy.<cmd>)
tree.write(
`apps/${name}/src/e2e/callback.spec.ts`,
`describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
cy.server()
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
cy.should((b) => {
const a = 123;
// I'm not doing nested cy stuff
});
cy.should(($s) => {
cy.task("");
})
cy.should(function($el) {
cy.task("");
})
})
})`
);
tree.write(
`apps/${name}/src/e2e/intercept-session.spec.ts`,
`describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
cy.server()
cy.route(/api/, () => {
return {
'test': 'Well',
}
}).as('getApi')
cy.visit('/index.html')
cy.window().then((win) => {
const xhr = new win.XMLHttpRequest
xhr.open('GET', '/api/v1/foo/bar?a=42')
xhr.send()
})
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
})
})`
);
tree.write(
`apps/${name}/src/e2e/combo.spec.ts`,
`describe('something', () => {
it('should do the thing', () => {
Cypress.Cookies.defaults()
Cypress.Cookies.preserveOnce('seesion_id', 'remember-token');
Cypress.blah.abc()
Cypress.Server.defaults({
delay: 500,
method: 'GET',
})
cy.server()
cy.route(/api/, () => {
return {
'test': 'Well',
}
}).as('getApi')
cy.visit('/index.html')
cy.window().then((win) => {
const xhr = new win.XMLHttpRequest
xhr.open('GET', '/api/v1/foo/bar?a=42')
xhr.send()
})
cy.wait('@getApi')
.its('url').should('include', 'api/v1')
cy.should(($s) => {
cy.get('@table').find('tr').should('have.length', 3)
})
})
})`
);
}
function assertMigration(tree: Tree, name: string) {
expect(tree.read(`apps/${name}/cypress.config.ts`, 'utf-8')).toContain(
'testIsolation: false'
);
// command overrides
expect(tree.read(`apps/${name}/src/support/commands.ts`, 'utf-8')).toContain(
'TODO(@nrwl/cypress): This command can no longer be overridden'
);
// test .should(() => cy.<cmd>)
expect(tree.read(`apps/${name}/src/e2e/callback.spec.ts`, 'utf-8')).toContain(
'TODO(@nrwl/cypress): Nesting Cypress commands in a should assertion now throws.'
);
// use cy.intercept, cy.session
const interceptSessionSpec = tree.read(
`apps/${name}/src/e2e/intercept-session.spec.ts`,
'utf-8'
);
expect(interceptSessionSpec).toContain(
'// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes'
);
expect(interceptSessionSpec).toContain(
'// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults'
);
// intercept,session & callback
expect(
tree.read(`apps/${name}/src/e2e/combo.spec.ts`, 'utf-8')
).toMatchSnapshot();
}

View File

@ -1,245 +0,0 @@
import {
GeneratorCallback,
getProjects,
installPackagesTask,
stripIndents,
Tree,
updateJson,
visitNotIgnoredFiles,
} from '@nx/devkit';
import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
import { tsquery } from '@phenomnomnominal/tsquery';
import {
CallExpression,
isArrowFunction,
isCallExpression,
isFunctionExpression,
isObjectLiteralExpression,
PropertyAccessExpression,
PropertyAssignment,
} from 'typescript';
import { CypressExecutorOptions } from '../../executors/cypress/cypress.impl';
import { installedCypressVersion } from '../../utils/cypress-version';
import { BANNED_COMMANDS, isAlreadyCommented } from './helpers';
const JS_TS_FILE_MATCHER = /\.[jt]sx?$/;
export function updateToCypress12(tree: Tree): GeneratorCallback {
if (installedCypressVersion() < 11) {
return;
}
const projects = getProjects(tree);
forEachExecutorOptions<CypressExecutorOptions>(
tree,
'@nrwl/cypress:cypress',
(options, projectName, targetName, configName) => {
if (!(options.cypressConfig && tree.exists(options.cypressConfig))) {
return;
}
const projectConfig = projects.get(projectName);
turnOffTestIsolation(tree, options.cypressConfig);
visitNotIgnoredFiles(tree, projectConfig.root, (filePath) => {
if (!JS_TS_FILE_MATCHER.test(filePath)) {
return;
}
shouldUseCyIntercept(tree, filePath);
shouldUseCySession(tree, filePath);
shouldNotUseCyInShouldCB(tree, filePath);
shouldNotOverrideCommands(tree, filePath);
});
}
);
console.warn(stripIndents`Cypress 12 has lots of breaking changes that might subtly break your tests.
This migration marked known issues that need to be manually migrated,
but there can still be runtime based errors that were not detected.
Please consult the offical Cypress v12 migration guide for more info on these changes and the next steps.
https://docs.cypress.io/guides/references/migration-guide
`);
updateJson(tree, 'package.json', (json) => {
json.devDependencies.cypress = '^12.2.0';
return json;
});
return () => {
installPackagesTask(tree);
};
}
export function turnOffTestIsolation(tree: Tree, configPath: string) {
const config = tree.read(configPath, 'utf-8');
const isTestIsolationSet = tsquery.query<PropertyAssignment>(
config,
'ExportAssignment ObjectLiteralExpression > PropertyAssignment:has(Identifier[name="testIsolation"])'
);
if (isTestIsolationSet.length > 0) {
return;
}
const testIsolationProperty = `/**
* TODO(@nrwl/cypress): In Cypress v12,the testIsolation option is turned on by default.
* This can cause tests to start breaking where not indended.
* You should consider enabling this once you verify tests do not depend on each other
* More Info: https://docs.cypress.io/guides/references/migration-guide#Test-Isolation
**/
testIsolation: false,`;
const updated = tsquery.replace(
config,
'ExportAssignment ObjectLiteralExpression > PropertyAssignment:has(Identifier[name="e2e"])',
(node: PropertyAssignment) => {
if (isObjectLiteralExpression(node.initializer)) {
const listOfProperties = node.initializer.properties
.map((j) => j.getText())
.join(',\n ');
return `e2e: {
${listOfProperties},
${testIsolationProperty}
}`;
}
return `e2e: {
...${node.initializer.getText()},
${testIsolationProperty}
}`;
}
);
tree.write(configPath, updated);
}
/**
* Leave a comment on all apis that have been removed andsuperseded by cy.intercept
* stating they these API are now removed and need to update.
* cy.route, cy.server, Cypress.Server.defaults
**/
export function shouldUseCyIntercept(tree: Tree, filePath: string) {
const content = tree.read(filePath, 'utf-8');
const markedRemovedCommands = tsquery.replace(
content,
':matches(PropertyAccessExpression:has(Identifier[name="cy"]):has(Identifier[name="server"], Identifier[name="route"]), PropertyAccessExpression:has(Identifier[name="defaults"]):has(Identifier[name="Cypress"], Identifier[name="Server"]))',
(node: PropertyAccessExpression) => {
if (isAlreadyCommented(node)) {
return;
}
const expression = node.expression.getText().trim();
// prevent extra chaining i.e. cy.route().as() will return 2 results
// cy.route and cy.route().as
// only need the first 1 so skip any extra chaining
if (expression === 'cy' || expression === 'Cypress.Server') {
return `// TODO(@nrwl/cypress): this command has been removed, use cy.intercept instead. https://docs.cypress.io/guides/references/migration-guide#cy-server-cy-route-and-Cypress-Server-defaults
${node.getText()}`;
}
}
);
tree.write(filePath, markedRemovedCommands);
}
/**
* Leave a comment on all apis that have been removed and superseded by cy.session
* stating they these API are now removed and need to update.
* Cypress.Cookies.defaults & Cypress.Cookies.preserveOnce
**/
export function shouldUseCySession(tree: Tree, filePath: string) {
const content = tree.read(filePath, 'utf-8');
const markedRemovedCommands = tsquery.replace(
content,
':matches(PropertyAccessExpression:has(Identifier[name="defaults"]):has(Identifier[name="Cypress"], Identifier[name="Cookies"]), PropertyAccessExpression:has(Identifier[name="preserveOnce"]):has(Identifier[name="Cypress"], Identifier[name="Cookies"]))',
(node: PropertyAccessExpression) => {
if (isAlreadyCommented(node)) {
return;
}
const expression = node.expression.getText().trim();
// prevent grabbing other Cypress.<something>.defaults
if (expression === 'Cypress.Cookies') {
return `// TODO(@nrwl/cypress): this command has been removed, use cy.session instead. https://docs.cypress.io/guides/references/migration-guide#Command-Cypress-API-Changes
${node.getText()}`;
}
}
);
tree.write(filePath, markedRemovedCommands);
}
/**
* leave a comment about nested cy commands in a cy.should callback
* */
export function shouldNotUseCyInShouldCB(tree: Tree, filePath: string) {
const content = tree.read(filePath, 'utf-8');
const markedNestedCyCommands = tsquery.replace(
content,
'CallExpression > PropertyAccessExpression:has(Identifier[name="cy"]):has(Identifier[name="should"])',
(node: PropertyAccessExpression) => {
if (
isAlreadyCommented(node) ||
(node.parent && !isCallExpression(node.parent))
) {
return;
}
const parentExpression = node.parent as CallExpression;
if (
parentExpression?.arguments?.[0] &&
(isArrowFunction(parentExpression.arguments[0]) ||
isFunctionExpression(parentExpression.arguments[0]))
) {
const isUsingNestedCyCommand =
tsquery.query<PropertyAccessExpression>(
parentExpression.arguments[0],
'CallExpression > PropertyAccessExpression:has(Identifier[name="cy"])'
)?.length > 0;
if (isUsingNestedCyCommand) {
return `/**
* TODO(@nrwl/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
${node.getText()}`;
}
return node.getText();
}
}
);
tree.write(filePath, markedNestedCyCommands);
}
/**
* leave a comment on all usages of overriding built-ins that are now banned
* */
export function shouldNotOverrideCommands(tree: Tree, filePath: string) {
const content = tree.read(filePath, 'utf-8');
const markedOverrideUsage = tsquery.replace(
content,
'PropertyAccessExpression:has(Identifier[name="overwrite"]):has(Identifier[name="Cypress"])',
(node: PropertyAccessExpression) => {
if (isAlreadyCommented(node)) {
return;
}
const expression = node.expression.getText().trim();
// prevent grabbing other Cypress.<something>.defaults
if (expression === 'Cypress.Commands') {
// get value.
const overwriteExpression = node.parent as CallExpression;
const command = (overwriteExpression.arguments?.[0] as any)?.text; // need string without quotes
if (BANNED_COMMANDS.includes(command)) {
// overwrite
return `/**
* TODO(@nrwl/cypress): This command can no longer be overridden
* Consider using a different name like 'custom_${command}'
* More info: https://docs.cypress.io/guides/references/migration-guide#Cypress-Commands-overwrite
**/
${node.getText()}`;
}
}
}
);
tree.write(filePath, markedOverrideUsage);
}
export default updateToCypress12;

View File

@ -20,42 +20,6 @@
} }
}, },
"packageJsonUpdates": { "packageJsonUpdates": {
"15.0.0": {
"version": "15.0.0-beta.0",
"packages": {
"detox": {
"version": "19.12.5",
"alwaysAddToPackageJson": false
}
}
},
"15.2.2": {
"version": "15.2.2-beta.0",
"packages": {
"detox": {
"version": "20.0.3",
"alwaysAddToPackageJson": false
}
}
},
"15.6.2": {
"version": "15.6.2-beta.0",
"packages": {
"detox": {
"version": "20.1.1",
"alwaysAddToPackageJson": false
}
}
},
"15.8.6": {
"version": "15.8.6-beta.0",
"packages": {
"detox": {
"version": "~20.5.0",
"alwaysAddToPackageJson": false
}
}
},
"16.0.0": { "16.0.0": {
"version": "16.0.0-beta.3", "version": "16.0.0-beta.3",
"packages": { "packages": {

View File

@ -17,9 +17,15 @@
], ],
"patterns": [ "patterns": [
{ {
"group": ["nx/**/*"], "group": [
"message": "Use requireNx() from packages/devkit/nx.ts OR use a type import instead.", "nx/bin/*",
"allowTypeImports": true "nx/src/*",
"nx/plugins/*",
"!nx/src/devkit-internals",
"!nx/src/devkit-exports"
],
"allowTypeImports": true,
"message": "Only import from nx/src/devkit-internals or nx/src/devkit-exports"
}, },
{ {
"group": ["@nx/devkit/**/*"], "group": ["@nx/devkit/**/*"],

View File

@ -1,15 +0,0 @@
// After Nx v19, this can be removed and replaced with either:
// - import {} from 'nx/src/devkit-exports'
// - import {} from 'nx/src/devkit-internals'
export function requireNx(): typeof import('nx/src/devkit-exports') &
Partial<typeof import('nx/src/devkit-internals')> {
let result = { ...require('nx/src/devkit-exports') };
try {
result = {
...result,
// Remove in Nx v19, devkit should not support Nx v16.0.2 at that point.
...require('nx/src/devkit-internals'),
};
} catch {}
return result;
}

View File

@ -38,7 +38,7 @@
"minimatch": "9.0.3" "minimatch": "9.0.3"
}, },
"peerDependencies": { "peerDependencies": {
"nx": ">= 16 <= 19" "nx": ">= 17 <= 20"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@ -1,15 +1,10 @@
import type { Target } from 'nx/src/command-line/run/run'; import {
import type { ProjectGraph } from 'nx/src/config/project-graph'; ExecutorContext,
import type { ExecutorContext } from 'nx/src/devkit-exports'; ProjectGraph,
readCachedProjectGraph,
import { requireNx } from '../../nx'; Target,
} from 'nx/src/devkit-exports';
let { readCachedProjectGraph, splitTarget, splitByColons } = requireNx(); import { splitByColons, splitTarget } from 'nx/src/devkit-internals';
// TODO: Remove this in Nx 19 when Nx 16.7.0 is no longer supported
splitTarget = splitTarget ?? require('nx/src/utils/split-target').splitTarget;
splitByColons =
splitByColons ?? ((s: string) => s.split(':') as [string, ...string[]]);
/** /**
* @deprecated(v17) A project graph should be passed to parseTargetString for best accuracy. * @deprecated(v17) A project graph should be passed to parseTargetString for best accuracy.

View File

@ -1,19 +1,12 @@
import type { Target } from 'nx/src/command-line/run/run';
import type { ExecutorContext } from 'nx/src/config/misc-interfaces';
import { requireNx } from '../../nx';
import { relative } from 'path'; import { relative } from 'path';
let { import type { ExecutorContext, Target } from 'nx/src/devkit-exports';
Workspaces,
getExecutorInformation, import {
calculateDefaultProjectName, calculateDefaultProjectName,
combineOptionsForExecutor, combineOptionsForExecutor,
} = requireNx(); getExecutorInformation,
} from 'nx/src/devkit-internals';
// TODO: Remove this in Nx 19 when Nx 16.7.0 is no longer supported
combineOptionsForExecutor =
combineOptionsForExecutor ??
require('nx/src/utils/params').combineOptionsForExecutor;
/** /**
* Reads and combines options for a given target. * Reads and combines options for a given target.
@ -38,32 +31,20 @@ export function readTargetOptions<T = any>(
throw new Error(`Unable to find target ${target} for project ${project}`); throw new Error(`Unable to find target ${target} for project ${project}`);
} }
// TODO(v19): remove Workspaces.
const ws = new Workspaces(context.root);
const [nodeModule, executorName] = targetConfiguration.executor.split(':'); const [nodeModule, executorName] = targetConfiguration.executor.split(':');
const { schema } = getExecutorInformation const { schema } = getExecutorInformation(
? getExecutorInformation( nodeModule,
nodeModule, executorName,
executorName, context.root,
context.root, context.projectsConfigurations?.projects ?? context.workspace.projects
context.projectsConfigurations?.projects ?? context.workspace.projects );
)
: // TODO(v19): remove readExecutor. This is to be backwards compatible with Nx 16.5 and below.
(ws as any).readExecutor(nodeModule, executorName);
const defaultProject = calculateDefaultProjectName const defaultProject = calculateDefaultProjectName(
? calculateDefaultProjectName( context.cwd,
context.cwd, context.root,
context.root, { version: 2, projects: context.projectsConfigurations.projects },
{ version: 2, projects: context.projectsConfigurations.projects }, context.nxJsonConfiguration
context.nxJsonConfiguration );
)
: // TODO(v19): remove calculateDefaultProjectName. This is to be backwards compatible with Nx 16.5 and below.
(ws as any).calculateDefaultProjectName(
context.cwd,
{ version: 2, projects: context.projectsConfigurations.projects },
context.nxJsonConfiguration
);
return combineOptionsForExecutor( return combineOptionsForExecutor(
{}, {},

View File

@ -1,7 +1,4 @@
import type { Tree } from 'nx/src/devkit-exports'; import { readNxJson, Tree, updateNxJson } from 'nx/src/devkit-exports';
import { requireNx } from '../../nx';
const { readNxJson, updateNxJson } = requireNx();
export function addBuildTargetDefaults( export function addBuildTargetDefaults(
tree: Tree, tree: Tree,

View File

@ -1,20 +1,22 @@
import { prompt } from 'enquirer'; import { prompt } from 'enquirer';
import type { ProjectConfiguration } from 'nx/src/config/workspace-json-project-json';
import type { Tree } from 'nx/src/generators/tree';
import { join, relative } from 'path'; import { join, relative } from 'path';
import { requireNx } from '../../nx';
import { names } from '../utils/names'; import { names } from '../utils/names';
const { import {
createProjectRootMappingsFromProjectConfigurations,
findProjectForPath,
getProjects, getProjects,
joinPathFragments, joinPathFragments,
logger, logger,
normalizePath, normalizePath,
output, output,
ProjectConfiguration,
Tree,
workspaceRoot, workspaceRoot,
} = requireNx(); } from 'nx/src/devkit-exports';
import {
createProjectRootMappingsFromProjectConfigurations,
findProjectForPath,
} from 'nx/src/devkit-internals';
export type NameAndDirectoryFormat = 'as-provided' | 'derived'; export type NameAndDirectoryFormat = 'as-provided' | 'derived';
export type ArtifactGenerationOptions = { export type ArtifactGenerationOptions = {
@ -142,7 +144,7 @@ function logDeprecationMessage(
formats: NameAndDirectoryFormats formats: NameAndDirectoryFormats
) { ) {
logger.warn(` logger.warn(`
In Nx 19, generating a ${options.artifactType} will no longer support providing a project and deriving the directory. In Nx 20, generating a ${options.artifactType} will no longer support providing a project and deriving the directory.
Please provide the exact directory in the future. Please provide the exact directory in the future.
Example: nx g ${options.callingGenerator} ${formats['derived'].artifactName} --directory ${formats['derived'].directory} Example: nx g ${options.callingGenerator} ${formats['derived'].artifactName} --directory ${formats['derived'].directory}
NOTE: The example above assumes the command is being run from the workspace root. If the command is being run from a subdirectory, the directory option should be adjusted accordingly. NOTE: The example above assumes the command is being run from the workspace root. If the command is being run from a subdirectory, the directory option should be adjusted accordingly.

View File

@ -1,9 +1,9 @@
import type { Tree } from 'nx/src/generators/tree'; import {
import type { ProjectGraph } from 'nx/src/config/project-graph'; getProjects,
import type { ProjectConfiguration } from 'nx/src/config/workspace-json-project-json'; ProjectConfiguration,
import { requireNx } from '../../nx'; ProjectGraph,
Tree,
const { getProjects } = requireNx(); } from 'nx/src/devkit-exports';
type CallBack<T> = ( type CallBack<T> = (
currentValue: T, currentValue: T,

View File

@ -1,13 +1,8 @@
import type { Tree } from 'nx/src/generators/tree';
import * as path from 'path'; import * as path from 'path';
import type * as Prettier from 'prettier'; import type * as Prettier from 'prettier';
import { requireNx } from '../../nx';
let { updateJson, readJson, sortObjectByKeys } = requireNx(); import { readJson, Tree, updateJson } from 'nx/src/devkit-exports';
import { sortObjectByKeys } from 'nx/src/devkit-internals';
// TODO: Remove this in Nx 19 when Nx 16.7.0 is no longer supported
sortObjectByKeys =
sortObjectByKeys ?? require('nx/src/utils/object-sort').sortObjectByKeys;
/** /**
* Formats all the created or updated files using Prettier * Formats all the created or updated files using Prettier

View File

@ -1,10 +1,8 @@
import { readFileSync, readdirSync, statSync } from 'fs'; import { readdirSync, readFileSync, statSync } from 'fs';
import * as path from 'path'; import * as path from 'path';
import type { Tree } from 'nx/src/generators/tree';
import { requireNx } from '../../nx';
import { isBinaryPath } from '../utils/binary-extensions'; import { isBinaryPath } from '../utils/binary-extensions';
const { logger } = requireNx(); import { logger, Tree } from 'nx/src/devkit-exports';
/** /**
* Generates a folder of files based on provided templates. * Generates a folder of files based on provided templates.

View File

@ -1,34 +1,30 @@
import type { TargetConfiguration } from 'nx/src/config/workspace-json-project-json';
import type {
ExpandedPluginConfiguration,
NxJsonConfiguration,
} from 'nx/src/config/nx-json';
import type { Tree } from 'nx/src/generators/tree';
import type {
CreateNodes,
CreateNodesContext,
} from 'nx/src/project-graph/plugins';
import type { ProjectGraph } from 'nx/src/config/project-graph';
import type { RunCommandsOptions } from 'nx/src/executors/run-commands/run-commands.impl'; import type { RunCommandsOptions } from 'nx/src/executors/run-commands/run-commands.impl';
import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils';
import { minimatch } from 'minimatch'; import { minimatch } from 'minimatch';
import { forEachExecutorOptions } from '../executor-options-utils'; import { forEachExecutorOptions } from '../executor-options-utils';
import { deleteMatchingProperties } from './plugin-migration-utils'; import { deleteMatchingProperties } from './plugin-migration-utils';
import { requireNx } from '../../../nx';
const { import {
glob,
readNxJson, readNxJson,
updateNxJson, updateNxJson,
mergeTargetConfigurations,
updateProjectConfiguration, updateProjectConfiguration,
readProjectConfiguration, readProjectConfiguration,
ProjectGraph,
ExpandedPluginConfiguration,
NxJsonConfiguration,
TargetConfiguration,
Tree,
CreateNodes,
} from 'nx/src/devkit-exports';
import {
mergeTargetConfigurations,
retrieveProjectConfigurations, retrieveProjectConfigurations,
LoadedNxPlugin, LoadedNxPlugin,
ProjectConfigurationsError, ProjectConfigurationsError,
} = requireNx(); } from 'nx/src/devkit-internals';
import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils';
type PluginOptionsBuilder<T> = (targetName: string) => T; type PluginOptionsBuilder<T> = (targetName: string) => T;
type PostTargetTransformer = ( type PostTargetTransformer = (

View File

@ -1,23 +1,22 @@
import { prompt } from 'enquirer'; import { prompt } from 'enquirer';
import type { ProjectType } from 'nx/src/config/workspace-json-project-json';
import type { Tree } from 'nx/src/generators/tree';
import { join, relative } from 'path'; import { join, relative } from 'path';
import { requireNx } from '../../nx';
import { import {
extractLayoutDirectory, extractLayoutDirectory,
getWorkspaceLayout, getWorkspaceLayout,
} from '../utils/get-workspace-layout'; } from '../utils/get-workspace-layout';
import { names } from '../utils/names'; import { names } from '../utils/names';
const { import {
joinPathFragments, joinPathFragments,
logger, logger,
normalizePath, normalizePath,
output, output,
ProjectType,
readJson, readJson,
stripIndents, stripIndents,
Tree,
workspaceRoot, workspaceRoot,
} = requireNx(); } from 'nx/src/devkit-exports';
export type ProjectNameAndRootFormat = 'as-provided' | 'derived'; export type ProjectNameAndRootFormat = 'as-provided' | 'derived';
export type ProjectGenerationOptions = { export type ProjectGenerationOptions = {
@ -130,7 +129,7 @@ function logDeprecationMessage(
formats: ProjectNameAndRootFormats formats: ProjectNameAndRootFormats
) { ) {
logger.warn(stripIndents` logger.warn(stripIndents`
In Nx 19, generating projects will no longer derive the name and root. In Nx 20, generating projects will no longer derive the name and root.
Please provide the exact project name and root in the future. Please provide the exact project name and root in the future.
Example: nx g ${callingGenerator} ${formats['derived'].projectName} --directory ${formats['derived'].projectRoot} Example: nx g ${callingGenerator} ${formats['derived'].projectName} --directory ${formats['derived'].projectRoot}
`); `);

View File

@ -1,4 +1,4 @@
import type { GeneratorCallback } from 'nx/src/config/misc-interfaces'; import type { GeneratorCallback } from 'nx/src/devkit-exports';
/** /**
* Run tasks in serial * Run tasks in serial

View File

@ -1,4 +1,4 @@
import type { Tree } from 'nx/src/generators/tree'; import type { Tree } from 'nx/src/devkit-exports';
import type { ScriptTarget, ModuleKind } from 'typescript'; import type { ScriptTarget, ModuleKind } from 'typescript';
import { typescriptVersion } from '../utils/versions'; import { typescriptVersion } from '../utils/versions';
import { ensurePackage } from '../utils/package-json'; import { ensurePackage } from '../utils/package-json';

View File

@ -1,7 +1,4 @@
import type { Tree } from 'nx/src/generators/tree'; import { Tree, updateJson } from 'nx/src/devkit-exports';
import { requireNx } from '../../nx';
const { updateJson } = requireNx();
export function updateTsConfigsToJs( export function updateTsConfigsToJs(
tree: Tree, tree: Tree,

View File

@ -1,4 +1,4 @@
import type { Tree } from 'nx/src/generators/tree'; import type { Tree } from 'nx/src/devkit-exports';
import ignore from 'ignore'; import ignore from 'ignore';
import { join, relative, sep } from 'path'; import { join, relative, sep } from 'path';

View File

@ -1,4 +1,4 @@
import type { Tree } from 'nx/src/generators/tree'; import type { Tree } from 'nx/src/devkit-exports';
import { formatFiles } from '../../generators/format-files'; import { formatFiles } from '../../generators/format-files';
import { replaceNrwlPackageWithNxPackage } from '../../utils/replace-package'; import { replaceNrwlPackageWithNxPackage } from '../../utils/replace-package';

View File

@ -1,16 +1,13 @@
import type { Tree } from 'nx/src/generators/tree';
import type { GeneratorCallback } from 'nx/src/config/misc-interfaces';
import type { Node, SyntaxKind } from 'typescript'; import type { Node, SyntaxKind } from 'typescript';
import { visitNotIgnoredFiles } from '../../generators/visit-not-ignored-files'; import { visitNotIgnoredFiles } from '../../generators/visit-not-ignored-files';
import { formatFiles } from '../../generators/format-files'; import { formatFiles } from '../../generators/format-files';
import { requireNx } from '../../../nx';
import { import {
addDependenciesToPackageJson, addDependenciesToPackageJson,
ensurePackage, ensurePackage,
} from '../../utils/package-json'; } from '../../utils/package-json';
import { typescriptVersion } from '../../utils/versions'; import { typescriptVersion } from '../../utils/versions';
const { readJson } = requireNx(); import { GeneratorCallback, readJson, Tree } from 'nx/src/devkit-exports';
let tsModule: typeof import('typescript'); let tsModule: typeof import('typescript');

View File

@ -1,11 +1,13 @@
import { type ExecSyncOptions, execSync } from 'child_process'; import { execSync, type ExecSyncOptions } from 'child_process';
import { join } from 'path'; import { join } from 'path';
import { requireNx } from '../../nx';
import type { Tree } from 'nx/src/generators/tree'; import {
import type { PackageManager } from 'nx/src/utils/package-manager'; detectPackageManager,
const { detectPackageManager, getPackageManagerCommand, joinPathFragments } = getPackageManagerCommand,
requireNx(); joinPathFragments,
PackageManager,
Tree,
} from 'nx/src/devkit-exports';
/** /**
* Runs `npm install` or `yarn install`. It will skip running the install if * Runs `npm install` or `yarn install`. It will skip running the install if

View File

@ -1,24 +1,23 @@
import {
type CreateNodes,
type ProjectConfiguration,
type ProjectGraph,
type Tree,
} from 'nx/src/devkit-exports';
import type { PackageJson } from 'nx/src/utils/package-json'; import type { PackageJson } from 'nx/src/utils/package-json';
import * as yargs from 'yargs-parser';
import { requireNx } from '../../nx';
const {
readJson,
writeJson,
readNxJson,
updateNxJson,
retrieveProjectConfigurations,
LoadedNxPlugin,
ProjectConfigurationsError,
} = requireNx();
import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils'; import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils';
import * as yargs from 'yargs-parser';
import {
CreateNodes,
ProjectConfiguration,
ProjectGraph,
readJson,
readNxJson,
Tree,
updateNxJson,
writeJson,
} from 'nx/src/devkit-exports';
import {
LoadedNxPlugin,
ProjectConfigurationsError,
retrieveProjectConfigurations,
} from 'nx/src/devkit-internals';
/** /**
* Iterates through various forms of plugin options to find the one which does not conflict with the current graph * Iterates through various forms of plugin options to find the one which does not conflict with the current graph

View File

@ -1,7 +1,7 @@
import type { CreateNodesContext } from 'nx/src/devkit-exports';
import { requireNx } from '../../nx';
import { join } from 'path'; import { join } from 'path';
const { hashWithWorkspaceContext, hashArray, hashObject } = requireNx(); import { CreateNodesContext, hashArray } from 'nx/src/devkit-exports';
import { hashObject, hashWithWorkspaceContext } from 'nx/src/devkit-internals';
export function calculateHashForCreateNodes( export function calculateHashForCreateNodes(
projectRoot: string, projectRoot: string,

View File

@ -1,9 +1,8 @@
import { dirname, extname, join, sep } from 'path'; import { dirname, extname, join, sep } from 'path';
import { existsSync, readdirSync } from 'fs'; import { existsSync, readdirSync } from 'fs';
import { requireNx } from '../../nx';
import { pathToFileURL } from 'node:url'; import { pathToFileURL } from 'node:url';
import { workspaceRoot } from 'nx/src/devkit-exports';
const { workspaceRoot, registerTsProject } = requireNx(); import { registerTsProject } from 'nx/src/devkit-internals';
export let dynamicImport = new Function( export let dynamicImport = new Function(
'modulePath', 'modulePath',

View File

@ -1,17 +1,17 @@
import type { Observable } from 'rxjs'; import type { Observable } from 'rxjs';
import type { Executor, ExecutorContext } from 'nx/src/config/misc-interfaces'; import type {
import type { ProjectsConfigurations } from 'nx/src/devkit-exports'; Executor,
ExecutorContext,
ProjectsConfigurations,
} from 'nx/src/devkit-exports';
import { requireNx } from '../../nx';
import { NX_VERSION } from './package-json'; import { NX_VERSION } from './package-json';
import { lt } from 'semver'; import { lt } from 'semver';
import {
const {
Workspaces,
readNxJsonFromDisk, readNxJsonFromDisk,
retrieveProjectConfigurationsWithAngularProjects,
readProjectConfigurationsFromRootMap, readProjectConfigurationsFromRootMap,
} = requireNx(); retrieveProjectConfigurationsWithAngularProjects,
} from 'nx/src/devkit-internals';
/** /**
* Convert an Nx Executor into an Angular Devkit Builder * Convert an Nx Executor into an Angular Devkit Builder
@ -21,43 +21,35 @@ const {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function convertNxExecutor(executor: Executor) { export function convertNxExecutor(executor: Executor) {
const builderFunction = (options, builderContext) => { const builderFunction = (options, builderContext) => {
const workspaces = new Workspaces(builderContext.workspaceRoot); const nxJsonConfiguration = readNxJsonFromDisk(
const nxJsonConfiguration = readNxJsonFromDisk builderContext.workspaceRoot
? readNxJsonFromDisk(builderContext.workspaceRoot) );
: // TODO(v19): remove readNxJson. This is to be backwards compatible with Nx 16.5 and below.
(workspaces as any).readNxJson();
const promise = async () => { const promise = async () => {
const projectsConfigurations: ProjectsConfigurations = const projectsConfigurations: ProjectsConfigurations = {
retrieveProjectConfigurationsWithAngularProjects version: 2,
? { projects: await retrieveProjectConfigurationsWithAngularProjects(
version: 2, builderContext.workspaceRoot,
projects: await retrieveProjectConfigurationsWithAngularProjects( nxJsonConfiguration
builderContext.workspaceRoot, ).then((p) => {
nxJsonConfiguration if ((p as any).projectNodes) {
).then((p) => { return (p as any).projectNodes;
if ((p as any).projectNodes) { }
return (p as any).projectNodes; // v18.3.4 changed projects to be keyed by root
} // rather than project name
// v18.3.4 changed projects to be keyed by root if (lt(NX_VERSION, '18.3.4')) {
// rather than project name return p.projects;
if (lt(NX_VERSION, '18.3.4')) { }
return p.projects;
}
if (readProjectConfigurationsFromRootMap) { if (readProjectConfigurationsFromRootMap) {
return readProjectConfigurationsFromRootMap(p.projects); return readProjectConfigurationsFromRootMap(p.projects);
} }
throw new Error( throw new Error(
'Unable to successfully map Nx executor -> Angular Builder' 'Unable to successfully map Nx executor -> Angular Builder'
); );
}), }),
} };
: // TODO(v19): remove retrieveProjectConfigurations. This is to be backwards compatible with Nx 16.5 and below.
(workspaces as any).readProjectsConfigurations({
_includeProjectsFromAngularJson: true,
});
const context: ExecutorContext = { const context: ExecutorContext = {
root: builderContext.workspaceRoot, root: builderContext.workspaceRoot,

View File

@ -1,15 +1,14 @@
import { join } from 'path'; import { join } from 'path';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import type {
CreateNodesContext,
ProjectConfiguration,
} from 'nx/src/devkit-exports';
import type { PackageJson } from 'nx/src/utils/package-json'; import type { PackageJson } from 'nx/src/utils/package-json';
import type { InputDefinition } from 'nx/src/config/workspace-json-project-json'; import type { InputDefinition } from 'nx/src/config/workspace-json-project-json';
import { requireNx } from '../../nx';
const { readJsonFile } = requireNx(); import {
CreateNodesContext,
ProjectConfiguration,
readJsonFile,
} from 'nx/src/devkit-exports';
/** /**
* Get the named inputs available for a directory * Get the named inputs available for a directory

View File

@ -1,7 +1,4 @@
import type { Tree } from 'nx/src/generators/tree'; import { readNxJson, Tree } from 'nx/src/devkit-exports';
import { requireNx } from '../../nx';
const { readNxJson } = requireNx();
/** /**
* Returns workspace defaults. It includes defaults folders for apps and libs, * Returns workspace defaults. It includes defaults folders for apps and libs,

View File

@ -1,20 +1,14 @@
import type {
FileChange,
Tree,
TreeWriteOptions,
} from 'nx/src/generators/tree';
import type {
Generator,
GeneratorCallback,
} from 'nx/src/config/misc-interfaces';
import { join, relative } from 'path'; import { join, relative } from 'path';
import type { Mode } from 'fs'; import type { Mode } from 'fs';
import { requireNx } from '../../nx'; import type { TreeWriteOptions } from 'nx/src/generators/tree';
import {
let { logger, stripIndent } = requireNx(); FileChange,
Generator,
// TODO: Remove this in Nx 19 when Nx 16.7.0 is no longer supported GeneratorCallback,
stripIndent = stripIndent ?? require('nx/src/utils/logger').stripIndent; logger,
Tree,
} from 'nx/src/devkit-exports';
import { stripIndent } from 'nx/src/devkit-internals';
class RunCallbackTask { class RunCallbackTask {
constructor(private callback: GeneratorCallback) {} constructor(private callback: GeneratorCallback) {}

View File

@ -1,7 +1,6 @@
import { requireNx } from '../../nx'; import { output } from 'nx/src/devkit-exports';
export function logShowProjectCommand(projectName: string): void { export function logShowProjectCommand(projectName: string): void {
const { output } = requireNx();
output.log({ output.log({
title: `👀 View Details of ${projectName}`, title: `👀 View Details of ${projectName}`,
bodyLines: [ bodyLines: [

View File

@ -1,9 +1,8 @@
import type { Tree } from 'nx/src/generators/tree';
import { relative } from 'path'; import { relative } from 'path';
import { visitNotIgnoredFiles } from '../generators/visit-not-ignored-files'; import { visitNotIgnoredFiles } from '../generators/visit-not-ignored-files';
import { requireNx } from '../../nx';
const { normalizePath } = requireNx(); import { normalizePath, Tree } from 'nx/src/devkit-exports';
/** /**
* Analogous to cp -r oldDir newDir * Analogous to cp -r oldDir newDir
*/ */

View File

@ -1,27 +1,23 @@
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { Module } from 'module'; import { Module } from 'module';
import type { Tree } from 'nx/src/generators/tree';
import type { GeneratorCallback } from 'nx/src/config/misc-interfaces';
import { clean, coerce, gt } from 'semver'; import { clean, coerce, gt } from 'semver';
import { installPackagesTask } from '../tasks/install-packages-task'; import { installPackagesTask } from '../tasks/install-packages-task';
import { requireNx } from '../../nx';
import { dirSync } from 'tmp'; import { dirSync } from 'tmp';
import { join } from 'path'; import { join } from 'path';
import type { PackageManager } from 'nx/src/utils/package-manager'; import {
import { writeFileSync } from 'fs';
const {
readJson,
updateJson,
getPackageManagerCommand,
workspaceRoot,
detectPackageManager, detectPackageManager,
createTempNpmDirectory, GeneratorCallback,
getPackageManagerCommand,
getPackageManagerVersion, getPackageManagerVersion,
} = requireNx(); PackageManager,
readJson,
Tree,
updateJson,
workspaceRoot,
} from 'nx/src/devkit-exports';
import { createTempNpmDirectory } from 'nx/src/devkit-internals';
import { writeFileSync } from 'fs';
const UNIDENTIFIED_VERSION = 'UNIDENTIFIED_VERSION'; const UNIDENTIFIED_VERSION = 'UNIDENTIFIED_VERSION';
const NON_SEMVER_TAGS = { const NON_SEMVER_TAGS = {

View File

@ -1,18 +1,16 @@
import type { Tree } from 'nx/src/generators/tree'; import {
getProjects,
logger,
readNxJson,
Tree,
updateJson,
updateNxJson,
updateProjectConfiguration,
} from 'nx/src/devkit-exports';
import type { PackageJson } from 'nx/src/utils/package-json'; import type { PackageJson } from 'nx/src/utils/package-json';
import { requireNx } from '../../nx';
import { visitNotIgnoredFiles } from '../generators/visit-not-ignored-files'; import { visitNotIgnoredFiles } from '../generators/visit-not-ignored-files';
import { basename } from 'path'; import { basename } from 'path';
import { isBinaryPath } from './binary-extensions'; import { isBinaryPath } from './binary-extensions';
const { logger } = requireNx();
const {
getProjects,
updateProjectConfiguration,
readNxJson,
updateNxJson,
updateJson,
} = requireNx();
export function replaceNrwlPackageWithNxPackage( export function replaceNrwlPackageWithNxPackage(
tree: Tree, tree: Tree,

View File

@ -1,19 +1,15 @@
import type { import {
Tree,
CreateNodes, CreateNodes,
ProjectConfiguration,
TargetConfiguration,
} from 'nx/src/devkit-exports';
import { requireNx } from '../../nx';
const {
readNxJson,
updateNxJson,
glob, glob,
hashObject, ProjectConfiguration,
findProjectForPath, readNxJson,
readProjectConfiguration, readProjectConfiguration,
TargetConfiguration,
Tree,
updateNxJson,
updateProjectConfiguration, updateProjectConfiguration,
} = requireNx(); } from 'nx/src/devkit-exports';
import { findProjectForPath, hashObject } from 'nx/src/devkit-internals';
export async function replaceProjectConfigurationsWithPlugin<T = unknown>( export async function replaceProjectConfigurationsWithPlugin<T = unknown>(
tree: Tree, tree: Tree,

View File

@ -1,11 +1,5 @@
{ {
"generators": { "generators": {
"set-generate-package-json": {
"cli": "nx",
"version": "15.8.7-beta.0",
"description": "Set generatePackageJson to true to maintain existing behavior of generating package.json in output path.",
"factory": "./src/migrations/update-15-8-7/set-generate-package-json"
},
"update-16-0-0-add-nx-packages": { "update-16-0-0-add-nx-packages": {
"cli": "nx", "cli": "nx",
"version": "16.0.0-beta.1", "version": "16.0.0-beta.1",
@ -20,15 +14,6 @@
} }
}, },
"packageJsonUpdates": { "packageJsonUpdates": {
"15.7.0": {
"version": "15.7.0-beta.0",
"packages": {
"esbuild": {
"version": "0.17.5",
"alwaysAddToPackageJson": false
}
}
},
"16.0.0": { "16.0.0": {
"version": "16.0.0-beta.5", "version": "16.0.0-beta.5",
"packages": { "packages": {

View File

@ -1,73 +0,0 @@
import {
addProjectConfiguration,
readProjectConfiguration,
Tree,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import update from './set-generate-package-json';
describe('Migration: Set generatePackageJson', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should keep existing generatePackageJson option if it exists', async () => {
addProjectConfiguration(tree, 'myapp', {
root: 'myapp',
targets: {
build: {
executor: '@nrwl/esbuild:esbuild',
options: {
generatePackageJson: false,
},
},
},
});
await update(tree);
const config = readProjectConfiguration(tree, 'myapp');
expect(config.targets.build.options).toEqual({
generatePackageJson: false,
});
});
it('should set generatePackageJson to true for esbuild targets', async () => {
addProjectConfiguration(tree, 'myapp', {
root: 'myapp',
targets: {
build: {
executor: '@nrwl/esbuild:esbuild',
},
},
});
await update(tree);
const config = readProjectConfiguration(tree, 'myapp');
expect(config.targets.build.options).toEqual({
generatePackageJson: true,
});
});
it('should ignore targets not using esbuild', async () => {
addProjectConfiguration(tree, 'myapp', {
root: 'myapp',
targets: {
build: {
executor: '@nrwl/webpack:webpack',
},
},
});
await update(tree);
const config = readProjectConfiguration(tree, 'myapp');
expect(config.targets.build.options).toBeUndefined();
});
});

View File

@ -1,32 +0,0 @@
import type { Tree } from '@nx/devkit';
import {
formatFiles,
getProjects,
updateProjectConfiguration,
} from '@nx/devkit';
export default async function update(tree: Tree): Promise<void> {
const projects = getProjects(tree);
projects.forEach((projectConfig, projectName) => {
let shouldUpdate = false;
Object.entries(projectConfig.targets).forEach(
([targetName, targetConfig]) => {
if (targetConfig.executor === '@nrwl/esbuild:esbuild') {
shouldUpdate = true;
projectConfig.targets[targetName].options ??= {};
projectConfig.targets[targetName].options.generatePackageJson ??=
true;
}
}
);
if (shouldUpdate) {
updateProjectConfiguration(tree, projectName, projectConfig);
}
});
await formatFiles(tree);
}

View File

@ -1,17 +1,5 @@
{ {
"generators": { "generators": {
"add-eslint-inputs": {
"cli": "nx",
"version": "15.0.0-beta.0",
"description": "Stop hashing eslint config files for build targets and dependent tasks",
"factory": "./src/migrations/update-15-0-0/add-eslint-inputs"
},
"add-eslint-ignore": {
"cli": "nx",
"version": "15.7.1-beta.0",
"description": "Add node_modules to root eslint ignore",
"factory": "./src/migrations/update-15-7-1/add-eslint-ignore"
},
"update-16-0-0-add-nx-packages": { "update-16-0-0-add-nx-packages": {
"cli": "nx", "cli": "nx",
"version": "16.0.0-beta.1", "version": "16.0.0-beta.1",

View File

@ -1,187 +0,0 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
Tree,
addProjectConfiguration,
readNxJson,
updateNxJson,
} from '@nx/devkit';
import addEslintInputs from './add-eslint-inputs';
import { ESLINT_CONFIG_FILENAMES } from '../../utils/config-file';
describe('15.0.0 migration (add-eslint-inputs)', () => {
let tree: Tree;
describe('production', () => {
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
updateNxJson(tree, {
namedInputs: {
default: ['{projectRoot}/**/*', 'sharedGlobals'],
sharedGlobals: [],
production: ['default'],
},
});
addProjectConfiguration(tree, 'proj', {
root: 'proj',
targets: {
lint: {
executor: '@nrwl/linter:eslint',
options: {},
},
lint2: {
executor: '@nrwl/linter:eslint',
options: {},
},
notTest: {
executor: 'nx:run-commands',
},
},
});
});
test.each(ESLINT_CONFIG_FILENAMES)(
'should ignore %p for production',
async (eslintConfigFilename) => {
tree.write(eslintConfigFilename, '{}');
await addEslintInputs(tree);
const updated = readNxJson(tree);
expect(updated.namedInputs.production).toEqual([
'default',
`!{projectRoot}/${eslintConfigFilename}`,
]);
}
);
test.each(ESLINT_CONFIG_FILENAMES)(
'should add %p to all lint targets',
async (eslintConfigFilename) => {
tree.write(eslintConfigFilename, '{}');
await addEslintInputs(tree);
const updated = readNxJson(tree);
const result = ['default', `{workspaceRoot}/${eslintConfigFilename}`];
expect(updated.targetDefaults.lint.inputs).toEqual(result);
expect(updated.targetDefaults.lint2.inputs).toEqual(result);
}
);
});
describe('development', () => {
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
addProjectConfiguration(tree, 'proj', {
root: 'proj',
targets: {
lint: {
executor: '@nrwl/linter:eslint',
options: {},
},
lint2: {
executor: '@nrwl/linter:eslint',
options: {},
},
notTest: {
executor: 'nx:run-commands',
},
},
});
});
test.each(ESLINT_CONFIG_FILENAMES)(
'should not add `!{projectRoot}/%s` if `workspaceConfiguration.namedInputs` is undefined',
async (eslintConfigFilename) => {
tree.write(eslintConfigFilename, '{}');
await addEslintInputs(tree);
const updated = readNxJson(tree);
expect(updated.namedInputs?.production).toBeUndefined();
}
);
test.each(ESLINT_CONFIG_FILENAMES)(
'should not add `!{projectRoot}/%s` if `workspaceConfiguration.namedInputs.production` is undefined',
async (eslintConfigFilename) => {
updateNxJson(tree, {
namedInputs: {},
});
tree.write(eslintConfigFilename, '{}');
await addEslintInputs(tree);
const updated = readNxJson(tree);
expect(updated.namedInputs?.production).toBeUndefined();
}
);
});
describe('lintTargetDefaults.input fallback values', () => {
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
addProjectConfiguration(tree, 'proj', {
root: 'proj',
targets: {
lint: {
executor: '@nrwl/linter:eslint',
options: {},
},
lint2: {
executor: '@nrwl/linter:eslint',
options: {},
},
notTest: {
executor: 'nx:run-commands',
},
},
});
});
test.each(ESLINT_CONFIG_FILENAMES)(
'should not override `targetDefaults.lint.inputs` with `%s` as there was a default target set in the workspace config',
async (eslintConfigFilename) => {
updateNxJson(tree, {
targetDefaults: {
lint: {
inputs: ['{workspaceRoot}/.eslintrc.default'],
},
},
});
tree.write(eslintConfigFilename, '{}');
await addEslintInputs(tree);
const updated = readNxJson(tree);
expect(updated.targetDefaults.lint.inputs).toEqual([
'{workspaceRoot}/.eslintrc.default',
]);
expect(updated.targetDefaults.lint2.inputs).toEqual([
'default',
`{workspaceRoot}/${eslintConfigFilename}`,
]);
}
);
it('should return `default` if there is no globalEslintFile', async () => {
await addEslintInputs(tree);
const updated = readNxJson(tree);
expect(updated.targetDefaults.lint.inputs).toEqual(['default']);
expect(updated.targetDefaults.lint2.inputs).toEqual(['default']);
});
});
});

View File

@ -1,42 +0,0 @@
import {
formatFiles,
joinPathFragments,
readNxJson,
Tree,
updateNxJson,
} from '@nx/devkit';
import { getEslintTargets } from '../../generators/utils/eslint-targets';
import { ESLINT_CONFIG_FILENAMES } from '../../utils/config-file';
export default async function addEslintInputs(tree: Tree) {
const nxJson = readNxJson(tree);
const globalEslintFile = ESLINT_CONFIG_FILENAMES.find((file) =>
tree.exists(file)
);
if (globalEslintFile && nxJson.namedInputs?.production) {
const productionFileset = new Set(nxJson.namedInputs.production);
productionFileset.add(`!{projectRoot}/${globalEslintFile}`);
nxJson.namedInputs.production = Array.from(productionFileset);
}
for (const targetName of getEslintTargets(tree)) {
nxJson.targetDefaults ??= {};
const lintTargetDefaults = (nxJson.targetDefaults[targetName] ??= {});
lintTargetDefaults.inputs ??= [
'default',
...(globalEslintFile
? [joinPathFragments('{workspaceRoot}', globalEslintFile)]
: []),
];
}
updateNxJson(tree, nxJson);
await formatFiles(tree);
}

View File

@ -1,107 +0,0 @@
import {
addProjectConfiguration,
readJson,
readNxJson,
Tree,
updateNxJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import addEslintIgnore from './add-eslint-ignore';
describe('15.7.1 migration (add-eslintignore)', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
updateNxJson(tree, {
namedInputs: {
default: ['{projectRoot}/**/*', 'sharedGlobals'],
sharedGlobals: [],
},
});
addProjectConfiguration(tree, 'proj', {
root: 'proj',
targets: {
lint: {
executor: '@nrwl/linter:eslint',
options: {},
},
lint2: {
executor: '@nrwl/linter:eslint',
options: {},
},
notTest: {
executor: 'nx:run-commands',
},
},
});
});
it('should not add .eslintignore if eslint config does not exist', async () => {
await addEslintIgnore(tree);
expect(tree.exists('.eslintignore')).toBeFalsy();
});
it('should add .eslintignore if it does not exist', async () => {
ensureGlobalConfig(tree);
await addEslintIgnore(tree);
expect(tree.exists('.eslintignore')).toBeTruthy();
expect(tree.read('.eslintignore', 'utf-8')).toEqual('node_modules\n');
});
it('should add node_modules if missing in .eslintignore', async () => {
ensureGlobalConfig(tree);
const original = 'dist\ntmp\n';
tree.write('.eslintignore', original);
await addEslintIgnore(tree);
expect(tree.read('.eslintignore', 'utf-8')).toEqual(
`node_modules\n${original}`
);
});
it('should not add node_modules if already in .eslintignore', async () => {
ensureGlobalConfig(tree);
const original = `dist\nnode_modules\ntmp\n`;
tree.write('.eslintignore', original);
await addEslintIgnore(tree);
expect(tree.read('.eslintignore', 'utf-8')).toEqual(original);
});
it('should add lint target', async () => {
ensureGlobalConfig(tree);
await addEslintIgnore(tree);
expect(tree.exists('.eslintignore')).toBeTruthy();
expect(readJson(tree, 'nx.json').targetDefaults).toMatchInlineSnapshot(`
{
"lint": {
"inputs": [
"default",
"{workspaceRoot}/.eslintrc.json",
],
},
"lint2": {
"inputs": [
"default",
"{workspaceRoot}/.eslintrc.json",
],
},
}
`);
});
});
function ensureGlobalConfig(tree: Tree) {
tree.write('.eslintrc.json', '{}');
}

View File

@ -1,49 +0,0 @@
import {
formatFiles,
joinPathFragments,
readJson,
Tree,
updateNxJson,
} from '@nx/devkit';
import { getEslintTargets } from '../../generators/utils/eslint-targets';
import { ESLINT_CONFIG_FILENAMES } from '../../utils/config-file';
export default async function addEslintIgnore(tree: Tree) {
const nxJson = readJson(tree, 'nx.json');
const globalEslintFile = ESLINT_CONFIG_FILENAMES.find((file) =>
tree.exists(file)
);
if (globalEslintFile) {
if (tree.exists('.eslintignore')) {
const content = tree.read('.eslintignore', 'utf-8');
if (!content.includes('node_modules')) {
tree.write('.eslintignore', `node_modules\n${content}`);
}
} else {
tree.write('.eslintignore', 'node_modules\n');
}
for (const targetName of getEslintTargets(tree)) {
nxJson.targetDefaults ??= {};
const lintTargetDefaults = (nxJson.targetDefaults[targetName] ??= {});
const lintIgnorePath = joinPathFragments(
'{workspaceRoot}',
globalEslintFile
);
if (lintTargetDefaults.inputs) {
if (!lintTargetDefaults.inputs.includes(lintIgnorePath)) {
lintTargetDefaults.inputs.push(lintIgnorePath);
}
} else {
lintTargetDefaults.inputs = ['default', lintIgnorePath];
}
}
updateNxJson(tree, nxJson);
await formatFiles(tree);
}
}

View File

@ -1,23 +1,5 @@
{ {
"generators": { "generators": {
"change-expo-jest-preset": {
"version": "15.0.3-beta.0",
"cli": "nx",
"description": "Change jest config preset tof expo projects",
"factory": "./src/migrations/update-15-0-3/change-jest-preset"
},
"add-new-expo-cli-targets": {
"version": "15.0.3-beta.0",
"cli": "nx",
"description": "Add targets for @expo/cli and replace eject target",
"factory": "./src/migrations/update-15-0-3/add-new-expo-cli-targets"
},
"change-webpack-to-metro": {
"version": "15.8.3-beta.0",
"cli": "nx",
"description": "Change webpack to metro in expo projects",
"factory": "./src/migrations/update-15-8-3/change-webpack-to-metro"
},
"remove-deprecated-expo-targets": { "remove-deprecated-expo-targets": {
"version": "16.0.0-beta.0", "version": "16.0.0-beta.0",
"cli": "nx", "cli": "nx",
@ -86,295 +68,6 @@
} }
}, },
"packageJsonUpdates": { "packageJsonUpdates": {
"15.0.0": {
"version": "15.0.0-beta.0",
"packages": {
"metro-resolver": {
"version": "0.72.3",
"alwaysAddToPackageJson": false
},
"@testing-library/react-native": {
"version": "11.2.0",
"alwaysAddToPackageJson": false
},
"@testing-library/jest-native": {
"version": "4.0.13",
"alwaysAddToPackageJson": false
},
"expo": {
"version": "46.0.13",
"alwaysAddToPackageJson": false
},
"expo-cli": {
"version": "6.0.6",
"alwaysAddToPackageJson": false
},
"react-native": {
"version": "0.69.6",
"alwaysAddToPackageJson": false
}
}
},
"15.0.3": {
"version": "15.0.3-beta.0",
"packages": {
"@testing-library/react-native": {
"version": "11.3.0",
"alwaysAddToPackageJson": false
},
"@testing-library/jest-native": {
"version": "5.0.0",
"alwaysAddToPackageJson": false
},
"expo": {
"version": "46.0.16",
"alwaysAddToPackageJson": false
},
"expo-cli": {
"version": "6.0.6",
"alwaysAddToPackageJson": false
},
"eas-cli": {
"version": "2.4.1",
"alwaysAddToPackageJson": false
},
"@expo/cli": {
"version": "0.3.2",
"alwaysAddToPackageJson": false,
"addToPackageJson": "devDependencies"
}
}
},
"15.2.2": {
"version": "15.2.2-beta.0",
"packages": {
"metro-resolver": {
"version": "0.73.3",
"alwaysAddToPackageJson": false
}
}
},
"15.3.0": {
"version": "15.3.0-beta.0",
"packages": {
"@testing-library/react-native": {
"version": "11.5.0",
"alwaysAddToPackageJson": false
},
"@testing-library/jest-native": {
"version": "5.3.0",
"alwaysAddToPackageJson": false
},
"expo": {
"version": "47.0.8",
"alwaysAddToPackageJson": false
},
"@expo/metro-config": {
"version": "0.5.1",
"alwaysAddToPackageJson": false
},
"expo-splash-screen": {
"version": "~0.17.5",
"alwaysAddToPackageJson": false
},
"expo-cli": {
"version": "6.0.8",
"alwaysAddToPackageJson": false
},
"@expo/cli": {
"version": "0.4.10",
"alwaysAddToPackageJson": false
},
"eas-cli": {
"version": "2.8.0",
"alwaysAddToPackageJson": false
},
"babel-preset-expo": {
"version": "~9.2.2",
"alwaysAddToPackageJson": false
},
"react-native": {
"version": "0.70.5",
"alwaysAddToPackageJson": false
},
"@types/react-native": {
"version": "0.70.7",
"alwaysAddToPackageJson": false
},
"react-native-svg": {
"version": "13.3.0",
"alwaysAddToPackageJson": false
},
"@svgr/webpack": {
"version": "^6.5.1",
"alwaysAddToPackageJson": false
}
}
},
"15.4.3": {
"version": "15.4.3-beta.0",
"packages": {
"expo": {
"version": "47.0.9",
"alwaysAddToPackageJson": false
},
"@expo/metro-config": {
"version": "0.5.2",
"alwaysAddToPackageJson": false
},
"@expo/webpack-config": {
"version": "~0.17.3",
"alwaysAddToPackageJson": false,
"addToPackageJson": "dependencies"
},
"eas-cli": {
"version": "3.1.1",
"alwaysAddToPackageJson": false
},
"react-native-web": {
"version": "~0.18.10",
"alwaysAddToPackageJson": false
},
"react-native-svg": {
"version": "13.4.0",
"alwaysAddToPackageJson": false
}
}
},
"15.6.2": {
"version": "15.6.2-beta.0",
"packages": {
"expo": {
"version": "47.0.13",
"alwaysAddToPackageJson": false
},
"@expo/webpack-config": {
"version": "~0.17.4",
"alwaysAddToPackageJson": false
},
"eas-cli": {
"version": "3.3.2",
"alwaysAddToPackageJson": false
},
"expo-cli": {
"version": "6.1.0",
"alwaysAddToPackageJson": false
},
"@expo/cli": {
"version": "0.4.11",
"alwaysAddToPackageJson": false
}
}
},
"15.7.2": {
"version": "15.7.2-beta.0",
"packages": {
"@expo/webpack-config": {
"version": "~18.0.1",
"alwaysAddToPackageJson": false
},
"eas-cli": {
"version": "3.5.2",
"alwaysAddToPackageJson": false
},
"expo-cli": {
"version": "6.3.0",
"alwaysAddToPackageJson": false
}
}
},
"15.8.3": {
"version": "15.8.3-beta.0",
"packages": {
"expo": {
"version": "48.0.4",
"alwaysAddToPackageJson": false
},
"@expo/metro-config": {
"version": "0.7.1",
"alwaysAddToPackageJson": false
},
"expo-splash-screen": {
"version": "~0.18.1",
"alwaysAddToPackageJson": false
},
"expo-status-bar": {
"version": "~1.4.4",
"alwaysAddToPackageJson": false
},
"expo-cli": {
"version": "6.3.2",
"alwaysAddToPackageJson": false
},
"@expo/cli": {
"version": "0.6.2",
"alwaysAddToPackageJson": false
},
"eas-cli": {
"version": "3.7.2",
"alwaysAddToPackageJson": false
},
"babel-preset-expo": {
"version": "~9.3.0",
"alwaysAddToPackageJson": false
},
"react": {
"version": "18.2.0",
"alwaysAddToPackageJson": false
},
"react-dom": {
"version": "18.2.0",
"alwaysAddToPackageJson": false
},
"react-test-renderer": {
"version": "18.2.0",
"alwaysAddToPackageJson": false
},
"@types/react": {
"version": "18.0.28",
"alwaysAddToPackageJson": false
},
"react-native": {
"version": "0.71.3",
"alwaysAddToPackageJson": false
},
"@types/react-native": {
"version": "0.71.3",
"alwaysAddToPackageJson": false
},
"react-native-web": {
"version": "~0.18.12",
"alwaysAddToPackageJson": false
},
"metro": {
"version": "0.74.1",
"alwaysAddToPackageJson": false,
"addToPackageJson": "devDependencies"
}
}
},
"15.8.6": {
"version": "15.8.6-beta.0",
"packages": {
"expo": {
"version": "48.0.6",
"alwaysAddToPackageJson": false
}
}
},
"15.9.0": {
"version": "15.9.0-beta.0",
"packages": {
"react-native": {
"version": "0.71.4",
"alwaysAddToPackageJson": false
},
"@types/react-native": {
"version": "0.71.4",
"alwaysAddToPackageJson": false
}
}
},
"16.0.0": { "16.0.0": {
"version": "16.0.0-beta.0", "version": "16.0.0-beta.0",
"packages": { "packages": {

View File

@ -1,45 +0,0 @@
import { addProjectConfiguration, getProjects, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import update from './add-new-expo-cli-targets';
describe('add-eas-update-target', () => {
let tree: Tree;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
addProjectConfiguration(tree, 'product', {
root: 'apps/product',
sourceRoot: 'apps/product/src',
targets: {
start: {
executor: '@nrwl/expo:start',
},
},
});
});
it(`should update project.json with target prebuild, install and eject`, async () => {
await update(tree);
getProjects(tree).forEach((project) => {
expect(project.targets['eject']).toEqual({
executor: 'nx:run-commands',
options: {
command: `nx prebuild product`,
},
});
expect(project.targets['install']).toEqual({
executor: '@nrwl/expo:install',
options: {},
});
expect(project.targets['prebuild']).toEqual({
executor: '@nrwl/expo:prebuild',
options: {},
});
expect(project.targets['update']).toEqual({
executor: '@nrwl/expo:update',
options: {},
});
});
});
});

View File

@ -1,68 +0,0 @@
import {
Tree,
formatFiles,
getProjects,
updateProjectConfiguration,
offsetFromRoot,
} from '@nx/devkit';
/**
* Add new @expo/cli targets:
* - add target prebuild
* - add target install
* - add target update
* - replace target eject
*/
export default async function update(tree: Tree) {
const projects = getProjects(tree);
for (const [name, config] of projects.entries()) {
if (config.targets?.['start']?.executor === '@nrwl/expo:start') {
if (!config.targets['prebuild']) {
config.targets['prebuild'] = {
executor: '@nrwl/expo:prebuild',
options: {},
};
}
if (!config.targets['install']) {
config.targets['install'] = {
executor: '@nrwl/expo:install',
options: {},
};
}
if (!config.targets['update']) {
config.targets['update'] = {
executor: '@nrwl/expo:update',
options: {},
};
}
if (!config.targets['export']) {
config.targets['export'] = {
executor: '@nrwl/expo:export',
options: {
platform: 'all',
outputDir: `${offsetFromRoot(config.root)}dist/${config.root}`,
},
};
}
if (!config.targets['export-web']) {
config.targets['export-web'] = {
executor: '@nrwl/expo:export',
options: {
bundler: 'webpack',
},
};
}
config.targets['eject'] = {
executor: 'nx:run-commands',
options: {
command: `nx prebuild ${name}`,
},
};
}
updateProjectConfiguration(tree, name, config);
}
await formatFiles(tree);
}

View File

@ -1,124 +0,0 @@
import {
addProjectConfiguration,
readJson,
Tree,
updateJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import update from './change-jest-preset';
describe('Change expo jest preset', () => {
let tree: Tree;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
updateJson(tree, 'package.json', (packageJson) => {
packageJson.devDependencies['jest-expo'] = '*';
return packageJson;
});
addProjectConfiguration(tree, 'products', {
root: 'apps/products',
sourceRoot: 'apps/products/src',
targets: {
test: {
executor: '@nrwl/jest:jest',
options: {
jestConfig: 'apps/products/jest.config.ts',
passWithNoTests: true,
},
},
},
});
});
it(`should not remove transfrom if the code does not contain existing preprocessor`, async () => {
tree.write(
'apps/products/jest.config.ts',
`module.exports = {
preset: 'jest-expo',
};`
);
await update(tree);
const jestConfig = tree.read('apps/products/jest.config.ts', 'utf-8');
expect(jestConfig).toContain(`preset: 'react-native'`);
});
it(`should remove transform if the code contains existing preprocessor`, async () => {
tree.write(
'apps/products/jest.config.ts',
`module.exports = {
preset: 'jest-expo',
testRunner: 'jest-jasmine2',
transform: {
'\\.(js|ts|tsx)$': require.resolve('react-native/jest/preprocessor.js'),
'^.+\\.(bmp|gif|jpg|jpeg|mp4|png|psd|svg|webp)$': require.resolve(
'react-native/jest/assetFileTransformer.js'
),
}
};`
);
await update(tree);
const jestConfig = tree.read('apps/products/jest.config.ts', 'utf-8');
expect(jestConfig).not.toContain('transfrom');
expect(jestConfig).not.toContain('testRunner');
});
it(`should rename .babelrc to babel.config.json`, async () => {
tree.write(
'apps/products/jest.config.ts',
`module.exports = {
preset: 'jest-expo',
};`
);
tree.write(
'apps/products/.babelrc',
`{
"presets": ["module:metro-react-native-babel-preset"]
}`
);
await update(tree);
expect(tree.exists('apps/products/.babelrc')).toBeFalsy();
expect(tree.exists('apps/products/babel.config.json')).toBeTruthy();
const babelConfigJson = tree.read(
'apps/products/babel.config.json',
'utf-8'
);
expect(babelConfigJson).toContain(
`"presets": ["module:metro-react-native-babel-preset"]`
);
});
it(`should not rename .babelrc to babel.config.json if app is not react native`, async () => {
tree.write(
'apps/products/jest.config.ts',
`module.exports = {
preset: 'other',
};`
);
tree.write(
'apps/products/.babelrc',
`{
"presets": ["module:metro-react-native-babel-preset"]
}`
);
await update(tree);
expect(tree.exists('apps/products/.babelrc')).toBeTruthy();
expect(tree.exists('apps/products/babel.config.json')).toBeFalsy();
});
it('should remove jest-expo from package.json', async () => {
tree.write(
'apps/products/jest.config.ts',
`module.exports = {
preset: 'jest-expo',
};`
);
await update(tree);
const packageJson = readJson(tree, 'package.json');
expect(packageJson.devDependencies['jest-expo']).toBeUndefined();
});
});

View File

@ -1,90 +0,0 @@
import {
formatFiles,
logger,
ProjectConfiguration,
readProjectConfiguration,
stripIndents,
Tree,
updateJson,
} from '@nx/devkit';
import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
import {
addPropertyToJestConfig,
removePropertyFromJestConfig,
} from '@nx/jest';
import { JestExecutorOptions } from '@nx/jest/src/executors/jest/schema';
import { join } from 'path';
/**
* Change the preset in expo's jest config
* - remove transform and testRunner from jest config
* - changes preset from jest-expo to react-native
* - change to babel.config.json
*/
export default async function update(tree: Tree) {
let useJestExpo = false;
forEachExecutorOptions<JestExecutorOptions>(
tree,
'@nrwl/jest:jest',
(options, projectName) => {
if (options.jestConfig && tree.exists(options.jestConfig)) {
const jestConfig = tree.read(options.jestConfig, 'utf-8');
if (jestConfig.includes(`preset: 'jest-expo'`)) {
const project = readProjectConfiguration(tree, projectName);
changePreset(tree, project);
renameBabelJson(tree, project);
useJestExpo = true;
}
}
}
);
if (useJestExpo) {
removeJestExpoFromPackageJson(tree);
}
await formatFiles(tree);
}
function changePreset(host: Tree, project: ProjectConfiguration) {
const jestConfigPath = project.targets?.test?.options?.jestConfig;
if (!jestConfigPath || !host.exists(jestConfigPath)) return;
try {
removePropertyFromJestConfig(host, jestConfigPath, 'transform');
removePropertyFromJestConfig(host, jestConfigPath, 'testRunner');
removePropertyFromJestConfig(host, jestConfigPath, 'preset');
addPropertyToJestConfig(host, jestConfigPath, 'preset', 'react-native');
} catch {
logger.error(
stripIndents`Unable to update ${jestConfigPath} for project ${project.root}.`
);
}
}
function renameBabelJson(host: Tree, project: ProjectConfiguration) {
const babelrcPath = join(project.root, '.babelrc');
const babelJsonPath = join(project.root, 'babel.config.json');
if (!host.exists(babelrcPath)) {
return;
}
try {
const buffer = host.read(babelrcPath);
if (!buffer) {
return;
}
host.write(babelJsonPath, buffer);
host.delete(babelrcPath);
} catch {
logger.error(
stripIndents`Unable to rename from ${babelrcPath} to ${babelJsonPath} for project ${project.root}.`
);
}
}
function removeJestExpoFromPackageJson(tree: Tree) {
updateJson(tree, 'package.json', (packageJson) => {
delete packageJson.devDependencies['jest-expo'];
return packageJson;
});
}

View File

@ -1,56 +0,0 @@
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: '@nrwl/expo:start',
},
'export-web': {
executor: '@nrwl/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: '@nrwl/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

@ -1,39 +0,0 @@
import {
Tree,
formatFiles,
getProjects,
updateProjectConfiguration,
updateJson,
} from '@nx/devkit';
/**
* Change webpack to metro
* - change target export-web
* - delete webpack.config.js
* - delete @expo/webpack-config dependency
*/
export default async function update(tree: Tree) {
const projects = getProjects(tree);
for (const [name, config] of projects.entries()) {
if (config.targets?.['start']?.executor === '@nrwl/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

@ -1,23 +1,5 @@
{ {
"generators": { "generators": {
"add-jest-inputs": {
"version": "15.0.0-beta.0",
"cli": "nx",
"description": "Stop hashing jest spec files and config files for build targets and dependent tasks",
"factory": "./src/migrations/update-15-0-0/add-jest-inputs"
},
"update-configs-jest-29": {
"version": "15.8.0-beta.0",
"cli": "nx",
"description": "Update jest configs to support jest 29 changes (https://jestjs.io/docs/upgrading-to-jest29)",
"factory": "./src/migrations/update-15-8-0/update-configs-jest-29"
},
"update-tests-jest-29": {
"version": "15.8.0-beta.0",
"cli": "nx",
"description": "Update jest test files to support jest 29 changes (https://jestjs.io/docs/upgrading-to-jest29)",
"factory": "./src/migrations/update-15-8-0/update-tests-jest-29"
},
"update-16-0-0-add-nx-packages": { "update-16-0-0-add-nx-packages": {
"cli": "nx", "cli": "nx",
"version": "16.0.0-beta.1", "version": "16.0.0-beta.1",
@ -37,52 +19,6 @@
} }
}, },
"packageJsonUpdates": { "packageJsonUpdates": {
"15.0.1-beta.3": {
"version": "15.0.1-beta.3",
"packages": {
"jest-environment-jsdom": {
"version": "~28.1.1",
"addToPackageJson": "devDependencies"
}
}
},
"15.8.0": {
"version": "15.8.0-beta.0",
"packages": {
"jest": {
"version": "~29.4.1",
"alwaysAddToPackageJson": false
},
"@types/jest": {
"version": "~29.4.0",
"alwaysAddToPackageJson": false
},
"expect": {
"version": "~29.4.1",
"alwaysAddToPackageJson": false
},
"@jest/globals": {
"version": "~29.4.1",
"alwaysAddToPackageJson": false
},
"jest-jasmine2": {
"version": "~29.4.1",
"alwaysAddToPackageJson": false
},
"jest-environment-jsdom": {
"version": "~29.4.1",
"alwaysAddToPackageJson": false
},
"ts-jest": {
"version": "~29.0.5",
"alwaysAddToPackageJson": false
},
"babel-jest": {
"version": "~29.4.1",
"alwaysAddToPackageJson": false
}
}
},
"16.0.0": { "16.0.0": {
"version": "16.0.0-beta.4", "version": "16.0.0-beta.4",
"packages": { "packages": {

View File

@ -1,80 +0,0 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
addProjectConfiguration,
readNxJson,
Tree,
updateNxJson,
} from '@nx/devkit';
import addJestInputs from './add-jest-inputs';
describe('15.0.0 migration (add-jest-inputs)', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
it('should add inputs configuration for jest targets', async () => {
updateNxJson(tree, {
namedInputs: {
default: ['{projectRoot}/**/*', 'sharedGlobals'],
sharedGlobals: [],
production: ['default'],
},
});
addProjectConfiguration(tree, 'proj', {
root: 'proj',
targets: {
test: {
executor: '@nrwl/jest:jest',
options: {},
},
test2: {
executor: '@nrwl/jest:jest',
options: {},
},
notTest: {
executor: 'nx:run-commands',
},
},
});
tree.write('jest.preset.js', '');
await addJestInputs(tree);
const updated = readNxJson(tree);
expect(updated).toMatchInlineSnapshot(`
{
"namedInputs": {
"default": [
"{projectRoot}/**/*",
"sharedGlobals",
],
"production": [
"default",
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/tsconfig.spec.json",
"!{projectRoot}/jest.config.[jt]s",
],
"sharedGlobals": [],
},
"targetDefaults": {
"test": {
"inputs": [
"default",
"^production",
"{workspaceRoot}/jest.preset.js",
],
},
"test2": {
"inputs": [
"default",
"^production",
"{workspaceRoot}/jest.preset.js",
],
},
},
}
`);
});
});

View File

@ -1,46 +0,0 @@
import { formatFiles, readNxJson, Tree, updateNxJson } from '@nx/devkit';
import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
export default async function (tree: Tree) {
const nxJson = readNxJson(tree);
const jestTargets = getJestTargetNames(tree);
const hasProductionFileset = !!nxJson.namedInputs?.production;
if (jestTargets.size > 0 && hasProductionFileset) {
const productionFileset = new Set(nxJson.namedInputs.production);
for (const exclusion of [
'!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)',
'!{projectRoot}/tsconfig.spec.json',
'!{projectRoot}/jest.config.[jt]s',
]) {
productionFileset.add(exclusion);
}
nxJson.namedInputs.production = Array.from(productionFileset);
}
for (const targetName of jestTargets) {
nxJson.targetDefaults ??= {};
const jestTargetDefaults = (nxJson.targetDefaults[targetName] ??= {});
jestTargetDefaults.inputs ??= [
'default',
hasProductionFileset ? '^production' : '^default',
...(tree.exists('jest.preset.js')
? ['{workspaceRoot}/jest.preset.js']
: []),
];
}
updateNxJson(tree, nxJson);
await formatFiles(tree);
}
function getJestTargetNames(tree: Tree) {
const jestTargetNames = new Set<string>();
forEachExecutorOptions(tree, '@nrwl/jest:jest', (_, __, target) => {
jestTargetNames.add(target);
});
return jestTargetNames;
}

View File

@ -1,493 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Jest Migration - jest 29 update configs should NOT update ts-jest with no globals are preset 1`] = `
"const nxPreset = require('@nrwl/jest/preset').default;
module.exports = {
...nxPreset,
testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
transform: {
'^.+.(ts|js|html)$': 'ts-jest',
},
resolver: '@nrwl/jest/plugins/resolver',
moduleFileExtensions: ['ts', 'js', 'html'],
coverageReporters: ['html'],
/* TODO: Update to latest Jest snapshotFormat
* By default Nx has kept the older style of Jest Snapshot formats
* to prevent breaking of any existing tests with snapshots.
* It's recommend you update to the latest format.
* You can do this by removing snapshotFormat property
* and running tests with --update-snapshot flag.
* Example: "nx affected --targets=test --update-snapshot"
* More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format
*/
snapshotFormat: { escapeString: true, printBasicPrototype: true },
};
"
`;
exports[`Jest Migration - jest 29 update configs should add snapshot config with no root preset 1`] = `
"/* eslint-disable */
export default {
displayName: 'my-lib',
preset: '../../jest.preset.js',
globals: {},
transform: {
'^.+\\\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/my-lib',
/* TODO: Update to latest Jest snapshotFormat
* By default Nx has kept the older style of Jest Snapshot formats
* to prevent breaking of any existing tests with snapshots.
* It's recommend you update to the latest format.
* You can do this by removing snapshotFormat property
* and running tests with --update-snapshot flag.
* Example: From within the project directory, run "nx test --update-snapshot"
* More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format
*/
snapshotFormat: { escapeString: true, printBasicPrototype: true },
};
"
`;
exports[`Jest Migration - jest 29 update configs should add snapshot config with no root preset 2`] = `
"module.exports = {
transform: {
'^.+\\\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
// I am a comment and shouldn't be removed
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
globals: { something: 'else', abc: [1234, true, { abc: 'yes' }] },
/**
* Multi-line comment shouldn't be removed
*/
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
/* TODO: Update to latest Jest snapshotFormat
* By default Nx has kept the older style of Jest Snapshot formats
* to prevent breaking of any existing tests with snapshots.
* It's recommend you update to the latest format.
* You can do this by removing snapshotFormat property
* and running tests with --update-snapshot flag.
* Example: From within the project directory, run "nx test --update-snapshot"
* More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format
*/
snapshotFormat: { escapeString: true, printBasicPrototype: true },
};
"
`;
exports[`Jest Migration - jest 29 update configs should be idempotent 1`] = `
"/* eslint-disable */
export default {
displayName: 'my-lib',
preset: '../../jest.preset.js',
globals: {},
transform: {
'^.+\\\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/my-lib',
};
"
`;
exports[`Jest Migration - jest 29 update configs should be idempotent 2`] = `
"module.exports = {
transform: {
'^.+\\\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
// I am a comment and shouldn't be removed
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
globals: { something: 'else', abc: [1234, true, { abc: 'yes' }] },
/**
* Multi-line comment shouldn't be removed
*/
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};
"
`;
exports[`Jest Migration - jest 29 update configs should update globalThis.ngJest.teardown to testEnvironmentOptions 1`] = `
"globalThis.ngJest = {};
export default {
globals: {},
transform: {
'^.+.(ts|mjs|js|html)$': [
'jest-preset-angular',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '.(html|svg)$',
},
],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
testEnvironmentOptions: { teardown: true },
};
"
`;
exports[`Jest Migration - jest 29 update configs should update globalThis.ngJest.teardown to testEnvironmentOptions 2`] = `
"globalThis.ngJest = {
ngcc: true,
};
module.exports = {
globals: {},
transform: {
'^.+.(ts|mjs|js|html)$': [
'jest-preset-angular',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '.(html|svg)$',
},
],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
testEnvironmentOptions: {
blah: 123,
teardown: false,
},
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};
"
`;
exports[`Jest Migration - jest 29 update configs should update jest.config.ts 1`] = `
"/* eslint-disable */
export default {
displayName: 'my-lib',
preset: '../../jest.preset.js',
globals: {},
transform: {
'^.+\\\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/my-lib',
};
"
`;
exports[`Jest Migration - jest 29 update configs should update jest.config.ts 2`] = `
"module.exports = {
transform: {
'^.+\\\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
// I am a comment and shouldn't be removed
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
globals: { something: 'else', abc: [1234, true, { abc: 'yes' }] },
/**
* Multi-line comment shouldn't be removed
*/
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};
"
`;
exports[`Jest Migration - jest 29 update configs should update root preset 1`] = `
"const nxPreset = require('@nrwl/jest/preset').default;
module.exports = {
...nxPreset,
/* TODO: Update to latest Jest snapshotFormat
* By default Nx has kept the older style of Jest Snapshot formats
* to prevent breaking of any existing tests with snapshots.
* It's recommend you update to the latest format.
* You can do this by removing snapshotFormat property
* and running tests with --update-snapshot flag.
* Example: "nx affected --targets=test --update-snapshot"
* More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format
*/
snapshotFormat: { escapeString: true, printBasicPrototype: true },
};
"
`;
exports[`Jest Migration - jest 29 update configs should update root preset 2`] = `
"/* eslint-disable */
export default {
displayName: 'my-lib',
preset: '../../jest.preset.js',
globals: {},
transform: {
'^.+\\\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/my-lib',
};
"
`;
exports[`Jest Migration - jest 29 update configs should update root preset 3`] = `
"module.exports = {
transform: {
'^.+\\\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
// I am a comment and shouldn't be removed
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
globals: { something: 'else', abc: [1234, true, { abc: 'yes' }] },
/**
* Multi-line comment shouldn't be removed
*/
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};
"
`;
exports[`Jest Migration - jest 29 update configs should update root preset if ts-jest is preset 1`] = `
"const nxPreset = require('@nrwl/jest/preset').default;
module.exports = {
...nxPreset,
testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
globals: { something: 'else', abc: [1234, true, { abc: 'yes' }] },
transform: {
'^.+.(ts|js|html)$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
resolver: '@nrwl/jest/plugins/resolver',
moduleFileExtensions: ['ts', 'js', 'html'],
coverageReporters: ['html'],
/* TODO: Update to latest Jest snapshotFormat
* By default Nx has kept the older style of Jest Snapshot formats
* to prevent breaking of any existing tests with snapshots.
* It's recommend you update to the latest format.
* You can do this by removing snapshotFormat property
* and running tests with --update-snapshot flag.
* Example: "nx affected --targets=test --update-snapshot"
* More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format
*/
snapshotFormat: { escapeString: true, printBasicPrototype: true },
};
"
`;
exports[`Jest Migration - jest 29 update configs should work if not using ts-jest transformer 1`] = `
"export default {
transform: {
'^.+\\\\.[tj]sx?$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html']
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};"
`;
exports[`Jest Migration - jest 29 update configs should work if not using ts-jest transformer 2`] = `
"module.exports = {
transform: {
'^.+\\\\.[tj]sx?$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html']
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};"
`;
exports[`Jest Migration - jest 29 update configs should work snapshotFormat is defined 1`] = `
"export default {
transform: {
'^.+\\\\.[tj]sx?$': 'babel-jest',
},
globals: { something: 'else',
abc: [1234, true, {abc: 'yes'}] },
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html']
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
snapshotFormat: {escapeString: false, printBasicPrototype: true}
};"
`;
exports[`Jest Migration - jest 29 update configs should work snapshotFormat is defined 2`] = `
"module.exports = {
transform: {
'^.+\\\\.[tj]sx?$': ['ts-jest', {
tsconfig: '<rootDir>/tsconfig.spec.json'
}],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html']
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
globals: { something: 'else',
abc: [1234, true, {abc: 'yes'}] },
snapshotFormat: {escapeString: false, printBasicPrototype: true}
};"
`;
exports[`Jest Migration - jest 29 update configs should work with jest-preset-angular 1`] = `
"export default {
globals: { },
transform: {
'^.+.(ts|mjs|js|html)$': ['jest-preset-angular', {
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
}],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html']
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};"
`;
exports[`Jest Migration - jest 29 update configs should work with jest-preset-angular 2`] = `
"module.exports = {
globals: { },
transform: {
'^.+.(ts|mjs|js|html)$': ['jest-preset-angular', {
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
}],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html']
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};"
`;
exports[`Jest Migration - jest 29 update configs should work with multiple projects + configs 1`] = `
"/* eslint-disable */
export default {
displayName: 'my-lib',
preset: '../../jest.preset.js',
globals: {},
transform: {
'^.+\\\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/my-lib',
};
"
`;
exports[`Jest Migration - jest 29 update configs should work with multiple projects + configs 2`] = `
"module.exports = {
transform: {
'^.+\\\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
// I am a comment and shouldn't be removed
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
globals: { something: 'else', abc: [1234, true, { abc: 'yes' }] },
/**
* Multi-line comment shouldn't be removed
*/
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};
"
`;
exports[`Jest Migration - jest 29 update configs should work with multiple projects + configs 3`] = `
"/* eslint-disable */
export default {
displayName: 'another-lib',
preset: '../../jest.preset.js',
globals: {},
transform: {
'^.+\\\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/another-lib',
};
"
`;
exports[`Jest Migration - jest 29 update configs should work with multiple projects + configs 4`] = `
"module.exports = {
transform: {
'^.+\\\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
// I am a comment and shouldn't be removed
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
globals: { something: 'else', abc: [1234, true, { abc: 'yes' }] },
/**
* Multi-line comment shouldn't be removed
*/
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};
"
`;

View File

@ -1,199 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Jest Migration - jest 29 mocked usage in tests should be idempotent 1`] = `
"import { Mocked, MockedShallow } from 'jest-mock';
import { expect, jest, test } from '@jest/globals';
import { song } from './song';
jest.mock('./song');
jest.spyOn(console, 'log');
const mockedSong = jest.mocked(song);
// or through \`jest.Mocked<Source>\`
// const mockedSong = song as jest.Mocked<typeof song>;
test('deep method is typed correctly', () => {
mockedSong.one.more.time.mockReturnValue(12);
expect(mockedSong.one.more.time(10)).toBe(12);
expect(mockedSong.one.more.time.mock.calls).toHaveLength(1);
});
test('direct usage', () => {
jest.mocked(console.log).mockImplementation(() => {
return;
});
console.log('one more time');
expect(jest.mocked(console.log, { shallow: true }).mock.calls).toHaveLength(
1
);
});
"
`;
exports[`Jest Migration - jest 29 mocked usage in tests should be idempotent 2`] = `
"const { Mocked, MockedShallow } = require('jest-mock');
const { expect, jest, test } = require('@jest/globals');
const { song } = require('./song');
jest.mock('./song');
jest.spyOn(console, 'log');
const mockedSong = jest.mocked(song);
// or through \`jest.Mocked<Source>\`
// const mockedSong = song as jest.Mocked<typeof song>;
test('deep method is typed correctly', () => {
mockedSong.one.more.time.mockReturnValue(12);
expect(mockedSong.one.more.time(10)).toBe(12);
expect(mockedSong.one.more.time.mock.calls).toHaveLength(1);
});
test('direct usage', () => {
jest.mocked(console.log).mockImplementation(() => {
return;
});
console.log('one more time');
expect(jest.mocked(console.log, { shallow: true }).mock.calls).toHaveLength(
1
);
});
"
`;
exports[`Jest Migration - jest 29 mocked usage in tests should be idempotent 3`] = `
"import { Mocked, MockedShallow } from 'jest-mock';
import { expect, jest, test } from '@jest/globals';
import { song } from './song';
jest.mock('./song');
jest.spyOn(console, 'log');
const mockedSong = jest.mocked(song);
// or through \`jest.Mocked<Source>\`
// const mockedSong = song as jest.Mocked<typeof song>;
test('deep method is typed correctly', () => {
mockedSong.one.more.time.mockReturnValue(12);
expect(mockedSong.one.more.time(10)).toBe(12);
expect(mockedSong.one.more.time.mock.calls).toHaveLength(1);
});
test('direct usage', () => {
jest.mocked(console.log).mockImplementation(() => {
return;
});
console.log('one more time');
expect(jest.mocked(console.log, { shallow: true }).mock.calls).toHaveLength(
1
);
});
"
`;
exports[`Jest Migration - jest 29 mocked usage in tests should be idempotent 4`] = `
"const { Mocked, MockedShallow } = require('jest-mock');
const { expect, jest, test } = require('@jest/globals');
const { song } = require('./song');
jest.mock('./song');
jest.spyOn(console, 'log');
const mockedSong = jest.mocked(song);
// or through \`jest.Mocked<Source>\`
// const mockedSong = song as jest.Mocked<typeof song>;
test('deep method is typed correctly', () => {
mockedSong.one.more.time.mockReturnValue(12);
expect(mockedSong.one.more.time(10)).toBe(12);
expect(mockedSong.one.more.time.mock.calls).toHaveLength(1);
});
test('direct usage', () => {
jest.mocked(console.log).mockImplementation(() => {
return;
});
console.log('one more time');
expect(jest.mocked(console.log, { shallow: true }).mock.calls).toHaveLength(
1
);
});
"
`;
exports[`Jest Migration - jest 29 mocked usage in tests should not update anything if there are no tests 1`] = `
"import { Mocked, MockedShallow } from 'jest-mock';
import { expect, jest, test } from '@jest/globals';
import { song } from './song';
jest.mock('./song');
jest.spyOn(console, 'log');
const mockedSong = jest.mocked(song);
// or through \`jest.Mocked<Source>\`
// const mockedSong = song as jest.Mocked<typeof song>;
test('deep method is typed correctly', () => {
mockedSong.one.more.time.mockReturnValue(12);
expect(mockedSong.one.more.time(10)).toBe(12);
expect(mockedSong.one.more.time.mock.calls).toHaveLength(1);
});
test('direct usage', () => {
jest.mocked(console.log).mockImplementation(() => {
return;
});
console.log('one more time');
expect(jest.mocked(console.log, { shallow: true }).mock.calls).toHaveLength(
1
);
});
"
`;
exports[`Jest Migration - jest 29 mocked usage in tests should not update anything if there are no tests 2`] = `
"const { Mocked, MockedShallow } = require('jest-mock');
const { expect, jest, test } = require('@jest/globals');
const { song } = require('./song');
jest.mock('./song');
jest.spyOn(console, 'log');
const mockedSong = jest.mocked(song);
// or through \`jest.Mocked<Source>\`
// const mockedSong = song as jest.Mocked<typeof song>;
test('deep method is typed correctly', () => {
mockedSong.one.more.time.mockReturnValue(12);
expect(mockedSong.one.more.time(10)).toBe(12);
expect(mockedSong.one.more.time.mock.calls).toHaveLength(1);
});
test('direct usage', () => {
jest.mocked(console.log).mockImplementation(() => {
return;
});
console.log('one more time');
expect(jest.mocked(console.log, { shallow: true }).mock.calls).toHaveLength(
1
);
});
"
`;

View File

@ -1,490 +0,0 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
ProjectGraph,
readProjectConfiguration,
Tree,
updateProjectConfiguration,
} from '@nx/devkit';
import { updateConfigsJest29 } from './update-configs-jest-29';
import { libraryGenerator } from '@nx/js';
let projectGraph: ProjectGraph;
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
return projectGraph;
}),
}));
describe('Jest Migration - jest 29 update configs', () => {
let tree: Tree;
let originalEnv: string;
beforeEach(() => {
originalEnv = process.env.NX_ADD_PLUGINS;
process.env.NX_ADD_PLUGINS = 'false';
});
afterEach(() => {
process.env.NX_ADD_PLUGINS = originalEnv;
});
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
afterAll(() => {
jest.resetAllMocks();
});
it('should update jest.config.ts', async () => {
await setup(tree, 'my-lib');
await updateConfigsJest29(tree);
const actualJestConfigTs = tree.read('libs/my-lib/jest.config.ts', 'utf-8');
expect(actualJestConfigTs).toMatchSnapshot();
const actualJestConfigJs = tree.read('libs/my-lib/jest.config.js', 'utf-8');
expect(actualJestConfigJs).toMatchSnapshot();
});
it('should update root preset', async () => {
await setup(tree, 'my-lib');
await updateConfigsJest29(tree);
const actualPreset = tree.read('jest.preset.js', 'utf-8');
expect(actualPreset).toMatchSnapshot();
const actualJestConfigTs = tree.read('libs/my-lib/jest.config.ts', 'utf-8');
expect(actualJestConfigTs).toMatchSnapshot();
const actualJestConfigJs = tree.read('libs/my-lib/jest.config.js', 'utf-8');
expect(actualJestConfigJs).toMatchSnapshot();
});
it('should update root preset if ts-jest is preset', async () => {
await setup(tree, 'my-lib');
tree.write(
'jest.preset.js',
`const nxPreset = require('@nrwl/jest/preset').default;
module.exports = {
...nxPreset,
testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json'
},
something: 'else',
abc: [1234, true, {abc: 'yes'}]
},
transform: {
'^.+\\.(ts|js|html)$': 'ts-jest',
},
resolver: '@nrwl/jest/plugins/resolver',
moduleFileExtensions: ['ts', 'js', 'html'],
coverageReporters: ['html'],
};
`
);
await updateConfigsJest29(tree);
const actualPreset = tree.read('jest.preset.js', 'utf-8');
expect(actualPreset).toMatchSnapshot();
});
it('should NOT update ts-jest with no globals are preset', async () => {
await setup(tree, 'my-lib');
tree.write(
'jest.preset.js',
`const nxPreset = require('@nrwl/jest/preset').default;
module.exports = {
...nxPreset,
testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
transform: {
'^.+\\.(ts|js|html)$': 'ts-jest',
},
resolver: '@nrwl/jest/plugins/resolver',
moduleFileExtensions: ['ts', 'js', 'html'],
coverageReporters: ['html'],
};
`
);
await updateConfigsJest29(tree);
const actualPreset = tree.read('jest.preset.js', 'utf-8');
expect(actualPreset).toMatchSnapshot();
});
it('should add snapshot config with no root preset', async () => {
await setup(tree, 'my-lib');
tree.delete('jest.preset.js');
await updateConfigsJest29(tree);
const actualJestConfigTs = tree.read('libs/my-lib/jest.config.ts', 'utf-8');
expect(actualJestConfigTs).toMatchSnapshot();
const actualJestConfigJs = tree.read('libs/my-lib/jest.config.js', 'utf-8');
expect(actualJestConfigJs).toMatchSnapshot();
});
it('should work with multiple projects + configs', async () => {
await setup(tree, 'my-lib');
await setup(tree, 'another-lib', projectGraph);
await updateConfigsJest29(tree);
const actualJestConfigTs1 = tree.read(
'libs/my-lib/jest.config.ts',
'utf-8'
);
expect(actualJestConfigTs1).toMatchSnapshot();
const actualJestConfigJs1 = tree.read(
'libs/my-lib/jest.config.js',
'utf-8'
);
expect(actualJestConfigJs1).toMatchSnapshot();
const actualJestConfigTs2 = tree.read(
'libs/another-lib/jest.config.ts',
'utf-8'
);
expect(actualJestConfigTs2).toMatchSnapshot();
const actualJestConfigJs2 = tree.read(
'libs/another-lib/jest.config.js',
'utf-8'
);
expect(actualJestConfigJs2).toMatchSnapshot();
});
it('should update globalThis.ngJest.teardown to testEnvironmentOptions ', async () => {
await setup(tree, 'jest-preset-angular');
tree.write(
`libs/jest-preset-angular/jest.config.ts`,
`globalThis.ngJest = {
teardown: true
}
export default {
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
}
},
transform: {
'^.+.(ts|mjs|js|html)$': 'jest-preset-angular',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};`
);
tree.write(
`libs/jest-preset-angular/jest.config.js`,
`
globalThis.ngJest = {
ngcc: true,
teardown: false
}
module.exports = {
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
}
},
transform: {
'^.+.(ts|mjs|js|html)$': 'jest-preset-angular',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
testEnvironmentOptions: {
blah: 123,
},
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};`
);
await updateConfigsJest29(tree);
const jpaJestConfigTs = tree.read(
`libs/jest-preset-angular/jest.config.ts`,
'utf-8'
);
expect(jpaJestConfigTs).toMatchSnapshot();
const jpaJestConfigJs = tree.read(
`libs/jest-preset-angular/jest.config.js`,
'utf-8'
);
expect(jpaJestConfigJs).toMatchSnapshot();
});
it('should work with jest-preset-angular', async () => {
await setup(tree, 'jest-preset-angular');
tree.write(
`libs/jest-preset-angular/jest.config.ts`,
`export default {
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
}
},
transform: {
'^.+.(ts|mjs|js|html)$': 'jest-preset-angular',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html']
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};`
);
tree.write(
`libs/jest-preset-angular/jest.config.js`,
`module.exports = {
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
}
},
transform: {
'^.+.(ts|mjs|js|html)$': 'jest-preset-angular',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html']
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};`
);
await updateConfigsJest29(tree);
const jpaJestConfigTs = tree.read(
`libs/jest-preset-angular/jest.config.ts`,
'utf-8'
);
expect(jpaJestConfigTs).toMatchSnapshot();
const jpaJestConfigJs = tree.read(
`libs/jest-preset-angular/jest.config.js`,
'utf-8'
);
expect(jpaJestConfigJs).toMatchSnapshot();
});
it('should work if not using ts-jest transformer', async () => {
await setup(tree, 'no-ts-jest');
tree.write(
`libs/no-ts-jest/jest.config.ts`,
`export default {
transform: {
'^.+\\\\.[tj]sx?$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html']
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};`
);
tree.write(
`libs/no-ts-jest/jest.config.js`,
`module.exports = {
transform: {
'^.+\\\\.[tj]sx?$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html']
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};`
);
await updateConfigsJest29(tree);
const noTsJestConfigTs = tree.read(
`libs/no-ts-jest/jest.config.ts`,
'utf-8'
);
expect(noTsJestConfigTs).toMatchSnapshot();
const noTsJestConfigJs = tree.read(
`libs/no-ts-jest/jest.config.js`,
'utf-8'
);
expect(noTsJestConfigJs).toMatchSnapshot();
});
it('should work snapshotFormat is defined', async () => {
await setup(tree, 'no-ts-jest');
tree.write(
`libs/no-ts-jest/jest.config.ts`,
`export default {
transform: {
'^.+\\\\.[tj]sx?$': 'babel-jest',
},
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json'
},
something: 'else',
abc: [1234, true, {abc: 'yes'}]
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html']
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
snapshotFormat: {escapeString: false, printBasicPrototype: true}
};`
);
tree.write(
`libs/no-ts-jest/jest.config.js`,
`module.exports = {
transform: {
'^.+\\\\.[tj]sx?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html']
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js',
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json'
},
something: 'else',
abc: [1234, true, {abc: 'yes'}]
},
snapshotFormat: {escapeString: false, printBasicPrototype: true}
};`
);
await updateConfigsJest29(tree);
const snapshotJestConfigTs = tree.read(
'libs/no-ts-jest/jest.config.ts',
'utf-8'
);
expect(snapshotJestConfigTs).toMatchSnapshot();
const snapshotJestConfigJs = tree.read(
`libs/no-ts-jest/jest.config.js`,
'utf-8'
);
expect(snapshotJestConfigJs).toMatchSnapshot();
});
it('should be idempotent', async () => {
await setup(tree, 'my-lib');
await updateConfigsJest29(tree);
const actualJestConfigTs1 = tree.read(
'libs/my-lib/jest.config.ts',
'utf-8'
);
expect(actualJestConfigTs1).toMatchSnapshot();
const actualJestConfigJs1 = tree.read(
'libs/my-lib/jest.config.js',
'utf-8'
);
expect(actualJestConfigJs1).toMatchSnapshot();
await updateConfigsJest29(tree);
const actualJestConfigTs2 = tree.read(
'libs/my-lib/jest.config.ts',
'utf-8'
);
expect(actualJestConfigTs2).toEqual(actualJestConfigTs1);
const actualJestConfigJs2 = tree.read(
'libs/my-lib/jest.config.js',
'utf-8'
);
expect(actualJestConfigJs2).toEqual(actualJestConfigJs1);
});
});
async function setup(tree: Tree, name: string, existingGraph?: ProjectGraph) {
await libraryGenerator(tree, {
name,
});
const projectConfig = readProjectConfiguration(tree, name);
projectConfig.targets['test'] = {
...projectConfig.targets['test'],
executor: '@nrwl/jest:jest',
configurations: {
ci: {
ci: true,
},
other: {
jestConfig: `libs/${name}/jest.config.js`,
},
},
};
updateProjectConfiguration(tree, name, projectConfig);
tree.write(
`libs/${name}/jest.config.ts`,
`/* eslint-disable */
export default {
displayName: '${name}',
preset: '../../jest.preset.js',
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
}
},
transform: {
'^.+\\\\.[tj]sx?$': 'ts-jest'
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/${name}'
};
`
);
tree.write(
`libs/${name}/jest.config.js`,
`module.exports = {
transform: {
'^.+\\\\.[tj]sx?$': 'ts-jest'
},
// I am a comment and shouldn't be removed
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json'
},
something: 'else',
abc: [1234, true, {abc: 'yes'}]
},
/**
* Multi-line comment shouldn't be removed
*/
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js'
};
`
);
tree.write(
'jest.config.ts',
tree
.read('jest.config.ts')
.toString()
.replace(new RegExp('@nx/jest', 'g'), '@nrwl/jest')
);
tree.write(
'jest.preset.js',
tree
.read('jest.preset.js')
.toString()
.replace(new RegExp('@nx/jest', 'g'), '@nrwl/jest')
);
projectGraph = {
dependencies: {
...existingGraph?.dependencies,
},
nodes: {
...existingGraph?.nodes,
[name]: {
name,
type: 'lib',
data: projectConfig,
} as any,
},
};
}

View File

@ -1,230 +0,0 @@
import {
createProjectGraphAsync,
formatFiles,
logger,
stripIndents,
Tree,
} from '@nx/devkit';
import { forEachExecutorOptionsInGraph } from '@nx/devkit/src/generators/executor-options-utils';
import { TS_QUERY_JEST_CONFIG_PREFIX } from '../../utils/ast-utils';
import { tsquery } from '@phenomnomnominal/tsquery';
import * as ts from 'typescript';
import { JestExecutorOptions } from '../../executors/jest/schema';
import { findRootJestPreset } from '../../utils/config/find-root-jest-files';
export async function updateConfigsJest29(tree: Tree) {
const rootPreset = findRootJestPreset(tree);
const targetsWithJest = new Set<string>();
// have to use graph so the negative configuration targets are expanded
const graph = await createProjectGraphAsync();
forEachExecutorOptionsInGraph<JestExecutorOptions>(
graph,
'@nrwl/jest:jest',
(options, projectName, targetName) => {
if (options.jestConfig && tree.exists(options.jestConfig)) {
targetsWithJest.add(targetName);
// if the default root preset exists or if the project doesn't have a 'preset' configured
// -> update snapshot config
if (!rootPreset || !hasPresetConfigured(tree, options.jestConfig)) {
addSnapshotOptionsToConfig(
tree,
options.jestConfig,
`From within the project directory, run "nx test --update-snapshot"`
);
}
updateTsJestOptions(tree, options.jestConfig);
updateNgJestOptions(tree, options.jestConfig);
}
}
);
if (rootPreset && tree.exists(rootPreset)) {
const cmd = `"nx affected --targets=${Array.from(targetsWithJest).join(
','
)} --update-snapshot"`;
addSnapshotOptionsToConfig(tree, rootPreset, cmd);
updateTsJestOptions(tree, rootPreset);
updateNgJestOptions(tree, rootPreset);
}
await formatFiles(tree);
logger.info(stripIndents`NX Jest Snapshot format changed in v29.
By default Nx kept the older style to prevent breaking of existing tests with snapshots.
It's recommend you update to the latest format.
You can do this in your project's jest config file.
Remove the snapshotFormat property and re-run tests with the --update-snapshot flag.
More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format`);
}
function addSnapshotOptionsToConfig(
tree: Tree,
configPath: string,
updateSnapshotExample: string
) {
const config = tree.read(configPath, 'utf-8');
const hasSnapshotOptions = tsquery.query(
config,
`${TS_QUERY_JEST_CONFIG_PREFIX} > ObjectLiteralExpression PropertyAssignment:has(Identifier[name="snapshotFormat"])`
);
if (hasSnapshotOptions.length > 0) {
return;
}
const updatedConfig = tsquery.replace(
config,
`${TS_QUERY_JEST_CONFIG_PREFIX} > ObjectLiteralExpression`,
(node: ts.ObjectLiteralExpression) => {
return `{
${node.properties.map((p) => getNodeWithComments(config, p)).join(',\n')},
/* TODO: Update to latest Jest snapshotFormat
* By default Nx has kept the older style of Jest Snapshot formats
* to prevent breaking of any existing tests with snapshots.
* It's recommend you update to the latest format.
* You can do this by removing snapshotFormat property
* and running tests with --update-snapshot flag.
* Example: ${updateSnapshotExample}
* More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format
*/
snapshotFormat: { escapeString: true, printBasicPrototype: true }
}`;
},
{ visitAllChildren: false }
);
tree.write(configPath, updatedConfig);
}
function hasPresetConfigured(tree: Tree, configPath: string): boolean {
const contents = tree.read(configPath, 'utf-8');
return (
tsquery.query(
contents,
`${TS_QUERY_JEST_CONFIG_PREFIX} > ObjectLiteralExpression PropertyAssignment:has(Identifier[name="preset"])`
)?.length > 0
);
}
function updateTsJestOptions(tree: Tree, configPath: string) {
// query for the globals property, if they don't have one then there's nothing to modify.
const contents = tree.read(configPath, 'utf-8');
let tsJestGlobalsConfig: string;
const noTsJestGlobals = tsquery.replace(
contents,
`${TS_QUERY_JEST_CONFIG_PREFIX} > ObjectLiteralExpression PropertyAssignment:has(Identifier[name="globals"])`,
(node: ts.PropertyAssignment) => {
if (tsJestGlobalsConfig) {
logger.warn(
stripIndents`Found more than one "globals" object in the jest config, ${configPath}
Will use the first one`
);
return;
}
tsJestGlobalsConfig = getGlobalTsJestConfig(node);
return getGlobalConfigWithoutTsJest(node);
}
);
if (!tsJestGlobalsConfig) {
return;
}
const updatedTsJestTransformer = tsquery.replace(
noTsJestGlobals,
`${TS_QUERY_JEST_CONFIG_PREFIX}> ObjectLiteralExpression PropertyAssignment:has(Identifier[name="transform"]) PropertyAssignment > :has(StringLiteral[value="ts-jest"], StringLiteral[value="jest-preset-angular"])`,
(node: ts.StringLiteral) => {
return `[${node.getText()}, ${tsJestGlobalsConfig}]`;
}
);
tree.write(configPath, updatedTsJestTransformer);
}
function updateNgJestOptions(tree: Tree, configPath: string) {
const contents = tree.read(configPath, 'utf-8');
let ngJestTeardownConfig: string;
const noTeardownConfig = tsquery.replace(
contents,
'BinaryExpression:has(PropertyAccessExpression:has(Identifier[name=ngJest])) PropertyAssignment:has(Identifier[name=teardown])',
(node: ts.PropertyAssignment) => {
ngJestTeardownConfig = node.initializer.getText();
return ' ';
}
);
if (!ngJestTeardownConfig) {
return;
}
let maybeUpdatedTestEnvOpts = tsquery.replace(
noTeardownConfig,
`${TS_QUERY_JEST_CONFIG_PREFIX} > ObjectLiteralExpression PropertyAssignment:has(Identifier[name="testEnvironmentOptions"]) ObjectLiteralExpression`,
(node: ts.ObjectLiteralExpression) => {
return `{
${node.properties
.map((p) => getNodeWithComments(noTeardownConfig, p))
.join(',\n')},
teardown: ${ngJestTeardownConfig}
}`;
}
);
if (maybeUpdatedTestEnvOpts !== noTeardownConfig) {
tree.write(configPath, maybeUpdatedTestEnvOpts);
return;
}
// didn't find existing testEnvironmentOptions, so add the new property
const updatedConfig = tsquery.replace(
maybeUpdatedTestEnvOpts,
`${TS_QUERY_JEST_CONFIG_PREFIX} > ObjectLiteralExpression`,
(node: ts.ObjectLiteralExpression) => {
return `{
${node.properties
.map((p) => getNodeWithComments(maybeUpdatedTestEnvOpts, p))
.join(',\n')},
testEnvironmentOptions: { teardown: ${ngJestTeardownConfig} },
}`;
},
{ visitAllChildren: false }
);
tree.write(configPath, updatedConfig);
}
function getGlobalTsJestConfig(node: ts.PropertyAssignment): string {
const globalObject = node.initializer as ts.ObjectLiteralExpression;
const foundConfig = globalObject.properties.find(
(p) => ts.isPropertyAssignment(p) && p.name.getText().includes('ts-jest')
) as ts.PropertyAssignment;
return foundConfig?.initializer?.getText() || '';
}
function getGlobalConfigWithoutTsJest(node: ts.PropertyAssignment): string {
const globalObject = node?.initializer as ts.ObjectLiteralExpression;
const withoutTsJest = globalObject?.properties?.filter((p) => {
return !(
ts.isPropertyAssignment(p) && p.name.getText().includes('ts-jest')
);
});
const globalConfigs = withoutTsJest.map((c) => c.getText()).join(',\n');
return `globals: { ${globalConfigs} }`;
}
function getNodeWithComments(fullText: string, node: ts.Node) {
const commentRanges = ts.getLeadingCommentRanges(
fullText,
node.getFullStart()
);
if (commentRanges?.length > 0) {
const withComments = `${commentRanges
.map((r) => fullText.slice(r.pos, r.end))
.join('\n')}\n${node.getText()}`;
return withComments;
}
return node.getText();
}
export default updateConfigsJest29;

View File

@ -1,182 +0,0 @@
import {
ProjectGraph,
readProjectConfiguration,
Tree,
updateProjectConfiguration,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { libraryGenerator } from '@nx/js';
import { updateTestsJest29 } from './update-tests-jest-29';
let projectGraph: ProjectGraph;
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest
.fn()
.mockImplementation(async () => projectGraph),
}));
describe('Jest Migration - jest 29 mocked usage in tests', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
it('should not update anything if there are no tests', async () => {
await setup(tree, 'my-lib');
const expected = tree.read('libs/my-lib/src/lib/my-lib.spec.ts', 'utf-8');
await updateTestsJest29(tree);
expect(
tree.read('libs/my-lib/src/file-one.spec.ts', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('libs/my-lib/src/file-two.spec.ts', 'utf-8')
).toMatchSnapshot();
expect(tree.read('libs/my-lib/src/lib/my-lib.spec.ts', 'utf-8')).toEqual(
expected
);
});
it('should be idempotent', async () => {
await setup(tree, 'my-lib');
const expected = tree.read('libs/my-lib/src/lib/my-lib.spec.ts', 'utf-8');
await updateTestsJest29(tree);
expect(
tree.read('libs/my-lib/src/file-one.spec.ts', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('libs/my-lib/src/file-two.spec.ts', 'utf-8')
).toMatchSnapshot();
expect(tree.read('libs/my-lib/src/lib/my-lib.spec.ts', 'utf-8')).toEqual(
expected
);
await updateTestsJest29(tree);
expect(
tree.read('libs/my-lib/src/file-one.spec.ts', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('libs/my-lib/src/file-two.spec.ts', 'utf-8')
).toMatchSnapshot();
expect(tree.read('libs/my-lib/src/lib/my-lib.spec.ts', 'utf-8')).toEqual(
expected
);
});
});
async function setup(tree: Tree, name: string) {
await libraryGenerator(tree, {
name,
});
const projectConfig = readProjectConfiguration(tree, name);
projectConfig.targets['test'] = {
...projectConfig.targets['test'],
executor: '@nrwl/jest:jest',
configurations: {
ci: {
ci: true,
},
other: {
jestConfig: `libs/${name}/jest.config.js`,
},
},
};
updateProjectConfiguration(tree, name, projectConfig);
tree.write(
`libs/${name}/jest.config.js`,
`module.exports = {
transform: {
'^.+\\\\.[tj]sx?$': 'ts-jest'
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json'
}
},
displayName: 'jest',
testEnvironment: 'node',
preset: '../../jest.preset.js'
};
`
);
tree.write(
`libs/${name}/src/file-one.spec.ts`,
`
import{ MaybeMockedDeep, MaybeMocked } from 'jest-mock';
import {expect, jest, test} from '@jest/globals';
import {song} from './song';
jest.mock('./song');
jest.spyOn(console, 'log');
const mockedSong = jest.mocked(song, true);
// or through \`jest.Mocked<Source>\`
// const mockedSong = song as jest.Mocked<typeof song>;
test('deep method is typed correctly', () => {
mockedSong.one.more.time.mockReturnValue(12);
expect(mockedSong.one.more.time(10)).toBe(12);
expect(mockedSong.one.more.time.mock.calls).toHaveLength(1);
});
test('direct usage', () => {
jest.mocked(console.log).mockImplementation(() => {
return;
});
console.log('one more time');
expect(jest.mocked(console.log, false).mock.calls).toHaveLength(1);
});
`
);
tree.write(
`libs/${name}/src/file-two.spec.ts`,
`
const { MaybeMockedDeep, MaybeMocked } = require('jest-mock');
const {expect, jest, test} = require('@jest/globals');
const {song} = require('./song');
jest.mock('./song');
jest.spyOn(console, 'log');
const mockedSong = jest.mocked(song, true);
// or through \`jest.Mocked<Source>\`
// const mockedSong = song as jest.Mocked<typeof song>;
test('deep method is typed correctly', () => {
mockedSong.one.more.time.mockReturnValue(12);
expect(mockedSong.one.more.time(10)).toBe(12);
expect(mockedSong.one.more.time.mock.calls).toHaveLength(1);
});
test('direct usage', () => {
jest.mocked(console.log).mockImplementation(() => {
return;
});
console.log('one more time');
expect(jest.mocked(console.log, false).mock.calls).toHaveLength(1);
});
`
);
projectGraph = {
dependencies: {},
nodes: {
[name]: {
name,
type: 'lib',
data: projectConfig,
} as any,
},
};
}

View File

@ -1,96 +0,0 @@
import {
createProjectGraphAsync,
formatFiles,
readProjectConfiguration,
Tree,
visitNotIgnoredFiles,
} from '@nx/devkit';
import { forEachExecutorOptionsInGraph } from '@nx/devkit/src/generators/executor-options-utils';
import { tsquery } from '@phenomnomnominal/tsquery';
import {
CallExpression,
ImportDeclaration,
VariableStatement,
} from 'typescript';
import type { JestExecutorOptions } from '../../executors/jest/schema';
import { TEST_FILE_PATTERN } from '../../utils/ast-utils';
export async function updateTestsJest29(tree: Tree) {
const graph = await createProjectGraphAsync();
forEachExecutorOptionsInGraph<JestExecutorOptions>(
graph,
'@nrwl/jest:jest',
(options, projectName) => {
const projectConfig = readProjectConfiguration(tree, projectName);
visitNotIgnoredFiles(
tree,
projectConfig.sourceRoot || projectConfig.root,
(file) => {
if (!TEST_FILE_PATTERN.test(file)) {
return;
}
updateJestMockTypes(tree, file);
updateJestMocked(tree, file);
}
);
}
);
await formatFiles(tree);
}
export function updateJestMockTypes(tree: Tree, filePath: string) {
const contents = tree.read(filePath, 'utf-8');
const updatedContent = tsquery.replace(
contents,
':matches(ImportDeclaration, VariableStatement):has(Identifier[name="MaybeMockedDeep"], Identifier[name="MaybeMocked"]):has(StringLiteral[value="jest-mock"])',
(node: ImportDeclaration | VariableStatement) => {
const text = node.getText();
return (
text
// MaybeMockedDeep and MaybeMocked now are exported as Mocked and MockedShallow
.replace('MaybeMockedDeep', 'Mocked')
.replace('MaybeMocked', 'MockedShallow')
);
}
);
tree.write(filePath, updatedContent);
}
export function updateJestMocked(tree: Tree, filePath: string) {
const contents = tree.read(filePath, 'utf-8');
const jestGlobalNodes = tsquery.query(
contents,
':matches(ImportDeclaration, VariableStatement):has(Identifier[name="jest"]):has(StringLiteral[value="@jest/globals"])'
);
// this only applies if using jest from @jest/globals
if (jestGlobalNodes.length === 0) {
return;
}
const updatedJestMockTypes = tsquery.replace(
contents,
'CallExpression:has(Identifier[name="jest"]):has(Identifier[name="mocked"])',
(node: CallExpression) => {
if (
node.arguments.length === 2 &&
node.getText().startsWith('jest.mocked(')
) {
const text = node.getText();
// jest.mocked(someObject, true); => jest.mocked(someObject);
if (node.arguments[1].getText() === 'true') {
return text.replace(/,\s*true/g, '');
}
// jest.mocked(someObject, false); => jest.mocked(someObject, {shallow: true});
// opt into the new behavior unless explicitly opting out
if (node.arguments[1].getText() === 'false') {
return text.replace('false', '{shallow: true}');
}
}
}
);
tree.write(filePath, updatedJestMockTypes);
}
export default updateTestsJest29;

View File

@ -1,11 +1,5 @@
{ {
"generators": { "generators": {
"rename-swcrc-config": {
"cli": "nx",
"version": "15.8.0-beta.0",
"description": "Rename .lib.swcrc to .swcrc for better SWC support throughout the workspace",
"factory": "./src/migrations/update-15-8-0/rename-swcrc-config"
},
"update-16-0-0-add-nx-packages": { "update-16-0-0-add-nx-packages": {
"cli": "nx", "cli": "nx",
"version": "16.0.0-beta.1", "version": "16.0.0-beta.1",
@ -32,18 +26,6 @@
} }
}, },
"packageJsonUpdates": { "packageJsonUpdates": {
"15.8.0": {
"version": "15.8.0-beta.2",
"x-prompt": "Do you want to update to TypeScript v4.9?",
"requires": {
"typescript": ">=4.8.2 <4.9.0"
},
"packages": {
"typescript": {
"version": "~4.9.5"
}
}
},
"16.0.0": { "16.0.0": {
"version": "16.0.0-beta.3", "version": "16.0.0-beta.3",
"packages": { "packages": {

View File

@ -37,7 +37,6 @@
"@babel/preset-env": "^7.23.2", "@babel/preset-env": "^7.23.2",
"@babel/preset-typescript": "^7.22.5", "@babel/preset-typescript": "^7.22.5",
"@babel/runtime": "^7.22.6", "@babel/runtime": "^7.22.6",
"@phenomnomnominal/tsquery": "~5.0.1",
"babel-plugin-const-enum": "^1.0.1", "babel-plugin-const-enum": "^1.0.1",
"babel-plugin-macros": "^2.8.0", "babel-plugin-macros": "^2.8.0",
"babel-plugin-transform-typescript-metadata": "^0.3.1", "babel-plugin-transform-typescript-metadata": "^0.3.1",

View File

@ -1,119 +0,0 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
readProjectConfiguration,
Tree,
updateProjectConfiguration,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { libraryGenerator } from '../../generators/library/library';
import renameSwcrcConfig from './rename-swcrc-config';
describe('Rename swcrc file migration', () => {
let tree: Tree;
let originalEnv: string;
beforeEach(() => {
originalEnv = process.env.NX_ADD_PLUGINS;
process.env.NX_ADD_PLUGINS = 'false';
});
afterEach(() => {
process.env.NX_ADD_PLUGINS = originalEnv;
});
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
it('should migrate .lib.swcrc to .swcrc', async () => {
await setup(tree);
await renameSwcrcConfig(tree);
expect(tree.exists('libs/my-lib/.lib.swcrc')).toBeFalsy();
expect(tree.exists('libs/my-lib/.swcrc')).toBeTruthy();
const jestConfig = tree.read('libs/my-lib/jest.config.ts', 'utf-8');
expect(jestConfig).toContain(`fs.readFileSync(\`\${__dirname}/.swcrc\``);
expect(jestConfig).toContain('swcJestConfig.swcrc = false');
});
it('should migrate custom path .lib.swcrc', async () => {
await setup(tree);
customSwcrcPath(tree);
await renameSwcrcConfig(tree);
expect(tree.exists('libs/my-lib/src/.lib.swcrc')).toBeFalsy();
expect(tree.exists('libs/my-lib/src/.swcrc')).toBeTruthy();
const projectConfig = readProjectConfiguration(tree, 'my-lib');
expect(projectConfig.targets.build.options.swcrc).toEqual(
'libs/my-lib/src/.swcrc'
);
});
it('should do nothing if custom path swcrc is not named .lib.swcrc', async () => {
await setup(tree);
customSwcrcPath(tree, true);
await renameSwcrcConfig(tree);
expect(tree.exists('libs/my-lib/src/.custom.swcrc')).toBeTruthy();
expect(tree.exists('libs/my-lib/src/.swcrc')).toBeFalsy();
const projectConfig = readProjectConfiguration(tree, 'my-lib');
expect(projectConfig.targets.build.options.swcrc).toEqual(
'libs/my-lib/src/.custom.swcrc'
);
});
});
async function setup(tree: Tree) {
await libraryGenerator(tree, {
name: 'my-lib',
bundler: 'swc',
unitTestRunner: 'jest',
config: 'project',
});
const projectConfig = readProjectConfiguration(tree, 'my-lib');
projectConfig.targets.build.executor = '@nrwl/js:swc';
projectConfig.targets.test.executor = '@nrwl/jest:jest';
updateProjectConfiguration(tree, 'my-lib', projectConfig);
tree.rename('libs/my-lib/.swcrc', 'libs/my-lib/.lib.swcrc');
tree.write(
'libs/my-lib/jest.config.ts',
`
const fs = require('fs');
// Reading the SWC compilation config and remove the "exclude"
// for the test files to be compiled by SWC
const { exclude: _, ...swcJestConfig } = JSON.parse(
fs.readFileSync(\`\${__dirname}/.lib.swcrc\`, 'utf-8')
);
module.exports = {
displayName: 'my-lib',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]s$': ['@swc/jest', swcJestConfig],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/packages/vite',
};
`
);
}
function customSwcrcPath(tree: Tree, custom = false) {
tree.rename(
'libs/my-lib/.lib.swcrc',
custom ? 'libs/my-lib/src/.custom.swcrc' : 'libs/my-lib/src/.lib.swcrc'
);
const projectConfig = readProjectConfiguration(tree, 'my-lib');
projectConfig.targets.build.options.swcrc = custom
? 'libs/my-lib/src/.custom.swcrc'
: 'libs/my-lib/src/.lib.swcrc';
updateProjectConfiguration(tree, 'my-lib', projectConfig);
}

View File

@ -1,119 +0,0 @@
import {
formatFiles,
getProjects,
joinPathFragments,
Tree,
updateProjectConfiguration,
} from '@nx/devkit';
import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
import { SwcExecutorOptions } from '../../utils/schema';
import { tsquery } from '@phenomnomnominal/tsquery';
import type { TemplateSpan } from 'typescript';
export default async function (tree: Tree) {
let changesMade = false;
const projects = getProjects(tree);
forEachExecutorOptions(
tree,
'@nrwl/js:swc',
(_, projectName, target, configurationName) => {
const projectConfiguration = projects.get(projectName);
const executorOptions: SwcExecutorOptions = configurationName
? projectConfiguration.targets[target].configurations[configurationName]
: projectConfiguration.targets[target].options;
// if the project uses a custom path to swcrc file
// and only if it's the default name
if (
executorOptions.swcrc &&
executorOptions.swcrc.includes('.lib.swcrc')
) {
const newSwcrc = executorOptions.swcrc.replace('.lib.swcrc', '.swcrc');
// rename the swcrc file first
tree.rename(executorOptions.swcrc, newSwcrc);
// then update the executor options
executorOptions.swcrc = newSwcrc;
changesMade = true;
}
const libSwcrcPath =
joinPathFragments(projectConfiguration.root, '.lib.swcrc') ||
joinPathFragments(projectConfiguration.sourceRoot, '.lib.swcrc');
const isLibSwcrcExist = tree.exists(libSwcrcPath);
if (isLibSwcrcExist) {
tree.rename(libSwcrcPath, libSwcrcPath.replace('.lib.swcrc', '.swcrc'));
changesMade = true;
}
updateProjectConfiguration(tree, projectName, projectConfiguration);
}
);
forEachExecutorOptions(
tree,
'@nrwl/jest:jest',
(_, projectName, target, configurationName) => {
const projectConfiguration = projects.get(projectName);
const executorOptions = configurationName
? projectConfiguration.targets[target].configurations[configurationName]
: projectConfiguration.targets[target].options;
const isJestConfigExist =
executorOptions.jestConfig && tree.exists(executorOptions.jestConfig);
if (isJestConfigExist) {
const jestConfig = tree.read(executorOptions.jestConfig, 'utf-8');
const jsonParseNodes = tsquery.query(
jestConfig,
':matches(CallExpression:has(Identifier[name="JSON"]):has(Identifier[name="parse"]))'
);
if (jsonParseNodes.length) {
// if we already assign false to swcrc, skip
if (jestConfig.includes('.swcrc = false')) {
return;
}
let updatedJestConfig = tsquery.replace(
jestConfig,
'CallExpression:has(Identifier[name="JSON"]):has(Identifier[name="parse"]) TemplateSpan',
(templateSpan: TemplateSpan) => {
if (templateSpan.literal.text === '/.lib.swcrc') {
return templateSpan
.getFullText()
.replace('.lib.swcrc', '.swcrc');
}
return '';
}
);
updatedJestConfig = tsquery.replace(
updatedJestConfig,
':matches(ExportAssignment, BinaryExpression:has(Identifier[name="module"]):has(Identifier[name="exports"]))',
(node) => {
return `
// disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves.
// If we do not disable this, SWC Core will read .swcrc and won't transform our test files due to "exclude"
if (swcJestConfig.swcrc === undefined) {
swcJestConfig.swcrc = false;
}
${node.getFullText()}
`;
}
);
tree.write(executorOptions.jestConfig, updatedJestConfig);
changesMade = true;
}
}
}
);
if (changesMade) {
await formatFiles(tree);
}
}

View File

@ -1,11 +1,5 @@
{ {
"generators": { "generators": {
"add-style-packages": {
"cli": "nx",
"version": "15.8.8-beta.0",
"description": "Add less and stylus packages if used.",
"factory": "./src/migrations/update-15-8-8/add-style-packages"
},
"update-16-0-0-add-nx-packages": { "update-16-0-0-add-nx-packages": {
"cli": "nx", "cli": "nx",
"version": "16.0.0-beta.1", "version": "16.0.0-beta.1",
@ -32,40 +26,6 @@
} }
}, },
"packageJsonUpdates": { "packageJsonUpdates": {
"15.0.4": {
"version": "15.0.4-beta.0",
"packages": {
"next": {
"version": "13.0.0",
"alwaysAddToPackageJson": false
},
"eslint-config-next": {
"version": "13.0.0",
"alwaysAddToPackageJson": false
},
"less-loader": {
"version": "11.1.0",
"alwaysAddToPackageJson": false
},
"stylus-loader": {
"version": "7.1.0",
"alwaysAddToPackageJson": false
}
}
},
"15.4.5": {
"version": "15.4.5-beta.0",
"packages": {
"next": {
"version": "13.1.1",
"alwaysAddToPackageJson": false
},
"eslint-config-next": {
"version": "13.1.1",
"alwaysAddToPackageJson": false
}
}
},
"16.0.0": { "16.0.0": {
"version": "16.0.0-beta.0", "version": "16.0.0-beta.0",
"packages": { "packages": {

View File

@ -16,7 +16,7 @@ import {
writeFileSync, writeFileSync,
} from 'fs-extra'; } from 'fs-extra';
import { dirname, extname, join, relative } from 'path'; import { dirname, extname, join, relative } from 'path';
import { findNodes } from 'nx/src/utils/typescript'; import { findNodes } from '@nx/js';
import type { NextBuildBuilderOptions } from '../../../utils/types'; import type { NextBuildBuilderOptions } from '../../../utils/types';

View File

@ -1,85 +0,0 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { writeJson, readJson, Tree, addProjectConfiguration } from '@nx/devkit';
import update from './add-style-packages';
describe('Add less if needed', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should add less if used', async () => {
writeJson(tree, 'package.json', {
dependencies: {},
devDependencies: {},
});
addProjectConfiguration(tree, 'myapp', {
root: 'myapp',
targets: {
build: {
executor: '@nrwl/next:build',
},
},
});
tree.write(
`myapp/next.config.js`,
`require('@nrwl/next/plugins/with-less')`
);
await update(tree);
const packageJson = readJson(tree, 'package.json');
expect(packageJson).toEqual({
dependencies: {},
devDependencies: { less: '3.12.2' },
});
});
it('should not add anything if less not used by Next.js app', async () => {
writeJson(tree, 'package.json', {
dependencies: {},
devDependencies: {},
});
addProjectConfiguration(tree, 'myapp', {
root: 'myapp',
targets: {
build: {
executor: '@nrwl/next:build',
},
},
});
tree.write(`myapp/next.config.js`, `require('@nrwl/next/plugins/with-nx')`);
await update(tree);
const packageJson = readJson(tree, 'package.json');
expect(packageJson).toEqual({
dependencies: {},
devDependencies: {},
});
});
it('should not add anything if no Next.js apps are in workspace', async () => {
writeJson(tree, 'package.json', {
dependencies: {},
devDependencies: {},
});
addProjectConfiguration(tree, 'myapp', {
root: 'myapp',
targets: {
build: {
executor: '@nrwl/webpack:webpack',
},
},
});
await update(tree);
const packageJson = readJson(tree, 'package.json');
expect(packageJson).toEqual({
dependencies: {},
devDependencies: {},
});
});
});

View File

@ -1,35 +0,0 @@
import {
addDependenciesToPackageJson,
getProjects,
joinPathFragments,
Tree,
} from '@nx/devkit';
export async function update(tree: Tree) {
const projects = getProjects(tree);
const missingDeps = {};
for (const [, config] of projects) {
if (
config.targets?.build?.executor === '@nrwl/next:build' &&
tree.exists(joinPathFragments(config.root, 'next.config.js'))
) {
const nextConfigContent = tree.read(
joinPathFragments(config.root, 'next.config.js'),
'utf-8'
);
if (nextConfigContent.includes('@nrwl/next/plugins/with-less')) {
missingDeps['less'] = '3.12.2';
}
if (nextConfigContent.includes('@nrwl/next/plugins/with-stylus')) {
missingDeps['stylus'] = '^0.55.0';
}
}
}
return addDependenciesToPackageJson(tree, {}, missingDeps);
}
export default update;

View File

@ -1,29 +1,5 @@
{ {
"generators": { "generators": {
"15.0.0-migrate-to-inputs": {
"cli": "nx",
"version": "15.0.0-beta.1",
"description": "Replace implicitDependencies with namedInputs + target inputs",
"implementation": "./src/migrations/update-15-0-0/migrate-to-inputs"
},
"15.0.0-prefix-outputs": {
"cli": "nx",
"version": "15.0.0-beta.1",
"description": "Prefix outputs with {workspaceRoot}/{projectRoot} if needed",
"implementation": "./src/migrations/update-15-0-0/prefix-outputs"
},
"15.1.0-set-project-names": {
"cli": "nx",
"version": "15.0.12-beta.1",
"description": "Set project names in project.json files",
"implementation": "./src/migrations/update-15-1-0/set-project-names"
},
"15.8.2-update-nx-wrapper": {
"cli": "nx",
"version": "15.8.2-beta.0",
"description": "Updates the nx wrapper.",
"implementation": "./src/migrations/update-15-8-2/update-nxw"
},
"16.0.0-remove-nrwl-cli": { "16.0.0-remove-nrwl-cli": {
"cli": "nx", "cli": "nx",
"version": "16.0.0-beta.0", "version": "16.0.0-beta.0",

View File

@ -21,7 +21,6 @@ import { isCI } from '../../utils/is-ci';
import { output } from '../../utils/output'; import { output } from '../../utils/output';
import { handleErrors } from '../../utils/params'; import { handleErrors } from '../../utils/params';
import { joinPathFragments } from '../../utils/path'; import { joinPathFragments } from '../../utils/path';
import { getRootTsConfigPath } from '../../utils/typescript';
import { workspaceRoot } from '../../utils/workspace-root'; import { workspaceRoot } from '../../utils/workspace-root';
import { ChangelogOptions } from './command-object'; import { ChangelogOptions } from './command-object';
import { import {
@ -58,6 +57,7 @@ import {
handleDuplicateGitTags, handleDuplicateGitTags,
noDiffInChangelogMessage, noDiffInChangelogMessage,
} from './utils/shared'; } from './utils/shared';
import { getRootTsConfigPath } from '../../plugins/js/utils/typescript';
export interface NxReleaseChangelogResult { export interface NxReleaseChangelogResult {
workspaceChangelog?: { workspaceChangelog?: {

View File

@ -1,5 +1,4 @@
import { Argv, CommandModule, showHelp } from 'yargs'; import { Argv, CommandModule, showHelp } from 'yargs';
import { readNxJson } from '../../project-graph/file-utils';
import { logger } from '../../utils/logger'; import { logger } from '../../utils/logger';
import { import {
OutputStyle, OutputStyle,
@ -10,6 +9,7 @@ import {
withRunManyOptions, withRunManyOptions,
} from '../yargs-utils/shared-options'; } from '../yargs-utils/shared-options';
import { VersionData } from './utils/shared'; import { VersionData } from './utils/shared';
import { readNxJson } from '../../config/nx-json';
export interface NxReleaseArgs { export interface NxReleaseArgs {
groups?: string[]; groups?: string[];

View File

@ -43,6 +43,7 @@ export const patternsWeIgnoreInCommunityReport: Array<string | RegExp> = [
]; ];
const LINE_SEPARATOR = '---------------------------------------'; const LINE_SEPARATOR = '---------------------------------------';
/** /**
* Reports relevant version numbers for adding to an Nx issue report * Reports relevant version numbers for adding to an Nx issue report
* *
@ -294,7 +295,7 @@ export function findRegisteredPluginsBeingUsed(nxJson: NxJsonConfiguration) {
export function findInstalledPackagesWeCareAbout() { export function findInstalledPackagesWeCareAbout() {
const packagesWeMayCareAbout: Record<string, string> = {}; const packagesWeMayCareAbout: Record<string, string> = {};
// TODO (v19): Remove workaround for hiding @nrwl packages when matching @nx package is found. // TODO (v20): Remove workaround for hiding @nrwl packages when matching @nx package is found.
const packageChangeMap: Record<string, string> = { const packageChangeMap: Record<string, string> = {
'@nrwl/nx-plugin': '@nx/plugin', '@nrwl/nx-plugin': '@nx/plugin',
'@nx/plugin': '@nrwl/nx-plugin', '@nx/plugin': '@nrwl/nx-plugin',

View File

@ -1,4 +1,4 @@
import { toProjectName } from './workspaces'; import { toProjectName } from './to-project-name';
import { TempFs } from '../internal-testing-utils/temp-fs'; import { TempFs } from '../internal-testing-utils/temp-fs';
import { withEnvironmentVariables } from '../internal-testing-utils/with-environment'; import { withEnvironmentVariables } from '../internal-testing-utils/with-environment';
import { retrieveProjectConfigurations } from '../project-graph/utils/retrieve-workspace-files'; import { retrieveProjectConfigurations } from '../project-graph/utils/retrieve-workspace-files';

View File

@ -0,0 +1,10 @@
import { dirname } from 'path';
/**
* Pulled from toFileName in names from @nx/devkit.
* Todo: Should refactor, not duplicate.
*/
export function toProjectName(fileName: string): string {
const parts = dirname(fileName).split(/[\/\\]/g);
return parts[parts.length - 1].toLowerCase();
}

View File

@ -1,38 +0,0 @@
import { dirname } from 'path';
import {
readCachedProjectGraph,
readProjectsConfigurationFromProjectGraph,
} from '../project-graph/project-graph';
import type { NxJsonConfiguration } from './nx-json';
import { readNxJson } from './nx-json';
import { ProjectsConfigurations } from './workspace-json-project-json';
// TODO(v19): remove this class
/**
* @deprecated This will be removed in v19. Use {@link readProjectsConfigurationFromProjectGraph} instead.
*/
export class Workspaces {
constructor(private root: string) {}
/**
* @deprecated Use {@link readProjectsConfigurationFromProjectGraph} instead.
*/
readWorkspaceConfiguration(): ProjectsConfigurations & NxJsonConfiguration {
const nxJson = readNxJson(this.root);
return {
...readProjectsConfigurationFromProjectGraph(readCachedProjectGraph()),
...nxJson,
};
}
}
/**
* Pulled from toFileName in names from @nx/devkit.
* Todo: Should refactor, not duplicate.
*/
export function toProjectName(fileName: string): string {
const parts = dirname(fileName).split(/[\/\\]/g);
return parts[parts.length - 1].toLowerCase();
}

View File

@ -37,12 +37,6 @@ export type {
HasherContext, HasherContext,
} from './config/misc-interfaces'; } from './config/misc-interfaces';
// TODO(v19): Remove this export
/**
* @category Workspace
*/
export { Workspaces } from './config/workspaces';
export { workspaceLayout } from './config/configuration'; export { workspaceLayout } from './config/configuration';
export type { export type {

View File

@ -40,13 +40,13 @@ export interface Hash {
export interface TaskHasher { export interface TaskHasher {
/** /**
* @deprecated use hashTask(task:Task, taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. This will be removed in v19 * @deprecated use hashTask(task:Task, taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. This will be removed in v20
* @param task * @param task
*/ */
hashTask(task: Task): Promise<Hash>; hashTask(task: Task): Promise<Hash>;
/** /**
* @deprecated use hashTask(task:Task, taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. This will be removed in v19 * @deprecated use hashTask(task:Task, taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. This will be removed in v20
*/ */
hashTask(task: Task, taskGraph: TaskGraph): Promise<Hash>; hashTask(task: Task, taskGraph: TaskGraph): Promise<Hash>;
@ -57,13 +57,13 @@ export interface TaskHasher {
): Promise<Hash>; ): Promise<Hash>;
/** /**
* @deprecated use hashTasks(tasks:Task[], taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. This will be removed in v19 * @deprecated use hashTasks(tasks:Task[], taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. This will be removed in v20
* @param tasks * @param tasks
*/ */
hashTasks(tasks: Task[]): Promise<Hash[]>; hashTasks(tasks: Task[]): Promise<Hash[]>;
/** /**
* @deprecated use hashTasks(tasks:Task[], taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. This will be removed in v19 * @deprecated use hashTasks(tasks:Task[], taskGraph: TaskGraph, env: NodeJS.ProcessEnv) instead. This will be removed in v20
*/ */
hashTasks(tasks: Task[], taskGraph: TaskGraph): Promise<Hash[]>; hashTasks(tasks: Task[], taskGraph: TaskGraph): Promise<Hash[]>;

View File

@ -1,38 +0,0 @@
import { Tree } from '../../generators/tree';
import { readNxJson } from '../../generators/utils/nx-json';
import { dirname } from 'path';
import { readJson, writeJson } from '../../generators/utils/json';
import { formatChangedFilesWithPrettierIfAvailable } from '../../generators/internal-utils/format-changed-files-with-prettier-if-available';
import { retrieveProjectConfigurationPaths } from '../../project-graph/utils/retrieve-workspace-files';
import { loadNxPlugins } from '../../project-graph/plugins/internal-api';
export default async function (tree: Tree) {
const nxJson = readNxJson(tree);
const [plugins, cleanup] = await loadNxPlugins(
nxJson?.plugins ?? [],
tree.root
);
const projectFiles = retrieveProjectConfigurationPaths(tree.root, plugins);
const projectJsons = projectFiles.filter((f) => f.endsWith('project.json'));
for (let f of projectJsons) {
const projectJson = readJson(tree, f);
if (!projectJson.name) {
projectJson.name = toProjectName(dirname(f), nxJson);
writeJson(tree, f, projectJson);
}
}
await formatChangedFilesWithPrettierIfAvailable(tree);
cleanup();
}
function toProjectName(directory: string, nxJson: any): string {
let { appsDir, libsDir } = nxJson?.workspaceLayout || {};
appsDir ??= 'apps';
libsDir ??= 'libs';
const parts = directory.split(/[\/\\]/g);
if ([appsDir, libsDir].includes(parts[0])) {
parts.splice(0, 1);
}
return parts.join('-').toLowerCase();
}

View File

@ -1,6 +0,0 @@
import { Tree } from '../../generators/tree';
import { updateNxw } from '../../utils/update-nxw';
export default async function (tree: Tree) {
updateNxw(tree);
}

View File

@ -4,7 +4,7 @@ import { dirname, join } from 'node:path';
import { NxJsonConfiguration, readNxJson } from '../../config/nx-json'; import { NxJsonConfiguration, readNxJson } from '../../config/nx-json';
import { ProjectConfiguration } from '../../config/workspace-json-project-json'; import { ProjectConfiguration } from '../../config/workspace-json-project-json';
import { toProjectName } from '../../config/workspaces'; import { toProjectName } from '../../config/to-project-name';
import { readJsonFile, readYamlFile } from '../../utils/fileutils'; import { readJsonFile, readYamlFile } from '../../utils/fileutils';
import { combineGlobPatterns } from '../../utils/globs'; import { combineGlobPatterns } from '../../utils/globs';
import { NX_PREFIX } from '../../utils/logger'; import { NX_PREFIX } from '../../utils/logger';

View File

@ -1,7 +1,7 @@
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
import { ProjectConfiguration } from '../../../config/workspace-json-project-json'; import { ProjectConfiguration } from '../../../config/workspace-json-project-json';
import { toProjectName } from '../../../config/workspaces'; import { toProjectName } from '../../../config/to-project-name';
import { readJsonFile } from '../../../utils/fileutils'; import { readJsonFile } from '../../../utils/fileutils';
import { NxPluginV2 } from '../../../project-graph/plugins'; import { NxPluginV2 } from '../../../project-graph/plugins';
import { CreateNodesError } from '../../../project-graph/error-types'; import { CreateNodesError } from '../../../project-graph/error-types';

Some files were not shown because too many files have changed in this diff Show More