feat(testing): add jest create-nodes plugin (#20045)

This commit is contained in:
Jason Jean 2024-01-11 14:54:04 -05:00 committed by GitHub
parent 595a7743b6
commit 5d9b4c5224
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 536 additions and 360 deletions

View File

@ -6,13 +6,16 @@ describe('Jest root projects', () => {
describe('angular', () => {
beforeAll(() => {
newProject({ packages: ['@nx/angular', '@nx/react'] });
newProject({
packages: ['@nx/angular'],
unsetProjectNameAndRootFormat: false,
});
runCLI(
`generate @nx/angular:app ${myapp} --directory . --rootProject --projectNameAndRootFormat as-provided --no-interactive`
);
});
it('should test root level app projects', async () => {
runCLI(
`generate @nx/angular:app ${myapp} --rootProject=true --no-interactive`
);
const rootProjectTestResults = await runCLIAsync(`test ${myapp}`);
expect(rootProjectTestResults.combinedOutput).toContain(
'Test Suites: 1 passed, 1 total'
@ -20,9 +23,8 @@ describe('Jest root projects', () => {
}, 300_000);
it('should add lib project and tests should still work', async () => {
runCLI(`generate @nx/angular:lib ${mylib} --no-interactive`);
runCLI(
`generate @nx/angular:component ${mylib} --export --standalone --project=${mylib} --no-interactive`
`generate @nx/angular:lib ${mylib} --projectNameAndRootFormat as-provided --no-interactive`
);
const libProjectTestResults = await runCLIAsync(`test ${mylib}`);
@ -41,12 +43,16 @@ describe('Jest root projects', () => {
describe('react', () => {
beforeAll(() => {
newProject();
newProject({
packages: ['@nx/react'],
unsetProjectNameAndRootFormat: false,
});
runCLI(
`generate @nx/react:app ${myapp} --directory . --rootProject --projectNameAndRootFormat as-provided`
);
});
it('should test root level app projects', async () => {
runCLI(`generate @nx/react:app ${myapp} --rootProject=true`);
const rootProjectTestResults = await runCLIAsync(`test ${myapp}`);
expect(rootProjectTestResults.combinedOutput).toContain(
@ -55,7 +61,9 @@ describe('Jest root projects', () => {
}, 300_000);
it('should add lib project and tests should still work', async () => {
runCLI(`generate @nx/react:lib ${mylib} --unitTestRunner=jest`);
runCLI(
`generate @nx/react:lib ${mylib} --unitTestRunner=jest --projectNameAndRootFormat as-provided`
);
const libProjectTestResults = await runCLIAsync(`test ${mylib}`);

View File

@ -17,7 +17,9 @@ describe('@nx/next/plugin', () => {
let appName: string;
beforeAll(() => {
project = newProject();
project = newProject({
packages: ['@nx/next'],
});
appName = uniq('app');
runCLI(
`generate @nx/next:app ${appName} --project-name-and-root-format=as-provided --no-interactive`,

View File

@ -24,7 +24,7 @@ describe('@nx/workspace:convert-to-monorepo', () => {
afterEach(() => cleanupProject());
it('should convert a standalone project to a monorepo', async () => {
it('should convert a standalone webpack and jest react project to a monorepo', async () => {
const reactApp = uniq('reactapp');
runCLI(
`generate @nx/react:app ${reactApp} --rootProject=true --bundler=webpack --unitTestRunner=jest --e2eTestRunner=cypress --no-interactive`
@ -43,6 +43,26 @@ describe('@nx/workspace:convert-to-monorepo', () => {
expect(() => runCLI(`lint e2e`)).not.toThrow();
expect(() => runCLI(`e2e e2e`)).not.toThrow();
});
it('should be convert a standalone vite and playwright react project to a monorepo', async () => {
const reactApp = uniq('reactapp');
runCLI(
`generate @nx/react:app ${reactApp} --rootProject=true --bundler=vite --unitTestRunner vitest --e2eTestRunner=playwright --no-interactive`
);
runCLI('generate @nx/workspace:convert-to-monorepo --no-interactive');
checkFilesExist(
`apps/${reactApp}/src/main.tsx`,
`apps/e2e/playwright.config.ts`
);
expect(() => runCLI(`build ${reactApp}`)).not.toThrow();
expect(() => runCLI(`test ${reactApp}`)).not.toThrow();
expect(() => runCLI(`lint ${reactApp}`)).not.toThrow();
expect(() => runCLI(`lint e2e`)).not.toThrow();
expect(() => runCLI(`e2e e2e`)).not.toThrow();
});
});
describe('Workspace Tests', () => {

View File

@ -11,7 +11,10 @@ describe('Webpack Plugin (PCv3)', () => {
beforeAll(() => {
originalPcv3 = process.env.NX_PCV3;
process.env.NX_PCV3 = 'true';
newProject();
newProject({
packages: ['@nx/react'],
unsetProjectNameAndRootFormat: false,
});
});
afterAll(() => {
@ -19,13 +22,21 @@ describe('Webpack Plugin (PCv3)', () => {
cleanupProject();
});
it('should generate, build, and serve React applications', () => {
it('should generate, build, and serve React applications and libraries', () => {
const appName = uniq('app');
const libName = uniq('lib');
runCLI(
`generate @nx/react:app ${appName} --bundler webpack --e2eTestRunner=cypress --no-interactive`
`generate @nx/react:app ${appName} --bundler webpack --e2eTestRunner=cypress --rootProject --no-interactive`
);
expect(true).toBe(true);
expect(() => runCLI(`test ${appName}`)).not.toThrow();
runCLI(
`generate @nx/react:lib ${libName} --unitTestRunner jest --no-interactive`
);
expect(() => runCLI(`test ${appName}`)).not.toThrow();
expect(() => runCLI(`test ${libName}`)).not.toThrow();
// TODO: figure out why this test hangs in CI (maybe down to sudo prompt?)
// expect(() => runCLI(`build ${appName}`)).not.toThrow();

View File

@ -6,6 +6,7 @@ import type { NormalizedSchema } from './normalized-schema';
export async function addUnitTestRunner(host: Tree, options: NormalizedSchema) {
if (options.unitTestRunner === UnitTestRunner.Jest) {
await configurationGenerator(host, {
...options,
project: options.name,
setupFile: 'angular',
supportTsx: false,

View File

@ -37,6 +37,11 @@ describe('Cypress Component Testing Configuration', () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
tree.write('.gitignore', '');
mockedInstalledCypressVersion.mockReturnValue(10);
projectGraph = {
dependencies: {},
nodes: {},
};
});
afterEach(() => {
@ -191,6 +196,9 @@ describe('Cypress Component Testing Configuration', () => {
export: true,
skipFormat: true,
});
jest.clearAllMocks();
const appConfig = readProjectConfiguration(tree, 'fancy-app');
appConfig.targets['build'].executor = 'something/else';
updateProjectConfiguration(tree, 'fancy-app', appConfig);

View File

@ -48,6 +48,11 @@ describe('lib', () => {
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
projectGraph = {
dependencies: {},
nodes: {},
};
});
it('should run the library generator without erroring if the directory has a trailing slash', async () => {

View File

@ -26,6 +26,11 @@ describe('Jest+Ng - 15.9.0 - tsconfig updates', () => {
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
tree.write('.gitignore', '');
projectGraph = {
dependencies: {},
nodes: {},
};
});
it('should update tsconfig.spec.json with target es2016', async () => {

View File

@ -42,6 +42,7 @@
"jest-config": "^29.4.1",
"jest-resolve": "^29.4.1",
"jest-util": "^29.4.1",
"minimatch": "3.0.5",
"resolve.exports": "1.1.0",
"tslib": "^2.3.0",
"@nx/devkit": "file:../devkit",

5
packages/jest/plugin.ts Normal file
View File

@ -0,0 +1,5 @@
export {
createNodes,
createDependencies,
JestPluginOptions,
} from './src/plugins/plugin';

View File

@ -9,6 +9,7 @@ import {
Tree,
GeneratorCallback,
readProjectConfiguration,
readNxJson,
} from '@nx/devkit';
const schemaDefaults = {
@ -65,7 +66,16 @@ export async function configurationGenerator(
checkForTestTarget(tree, options);
createFiles(tree, options);
updateTsConfig(tree, options);
updateWorkspace(tree, options);
const nxJson = readNxJson(tree);
const hasPlugin = nxJson.plugins?.some((p) =>
typeof p === 'string'
? p === '@nx/jest/plugin'
: p.plugin === '@nx/jest/plugin'
);
if (!hasPlugin) {
updateWorkspace(tree, options);
}
if (!schema.skipFormat) {
await formatFiles(tree);

View File

@ -1,6 +1,15 @@
let projectGraph: ProjectGraph;
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
return projectGraph;
}),
}));
import {
addProjectConfiguration,
addProjectConfiguration as _addProjectConfiguration,
NxJsonConfiguration,
ProjectGraph,
readJson,
readProjectConfiguration,
stripIndents,
@ -11,11 +20,29 @@ import {
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { jestInitGenerator } from './init';
function addProjectConfiguration(tree, name, project) {
_addProjectConfiguration(tree, name, project);
projectGraph.nodes[name] = {
name: name,
type: 'lib',
data: {
root: project.root,
targets: project.targets,
},
};
}
describe('jest', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
projectGraph = {
nodes: {},
dependencies: {},
externalNodes: {},
};
});
it('should generate files with --js flag', async () => {
@ -250,6 +277,21 @@ export default {
);
await jestInitGenerator(tree, { rootProject: false });
expect(tree.exists('jest.config.app.ts')).toBeTruthy();
expect(tree.read('jest.config.app.ts', 'utf-8')).toMatchInlineSnapshot(`
"
/* eslint-disable */
export default {
transform: {
'^.+\\.[tj]sx?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
globals: { 'ts-jest': { tsconfig: '<rootDir>/tsconfig.spec.json' } },
displayName: 'my-project',
testEnvironment: 'node',
preset: './jest.preset.js',
};
"
`);
expect(tree.read('jest.config.ts', 'utf-8'))
.toEqual(`import { getJestProjects } from '@nx/jest';

View File

@ -1,11 +1,13 @@
import {
addDependenciesToPackageJson,
createProjectGraphAsync,
GeneratorCallback,
getProjects,
readNxJson,
readProjectConfiguration,
removeDependenciesFromPackageJson,
runTasksInSerial,
stripIndents,
TargetConfiguration,
Tree,
updateJson,
updateNxJson,
@ -26,6 +28,7 @@ import {
typesNodeVersion,
} from '../../utils/versions';
import { JestInitSchema } from './schema';
import { readTargetDefaultsForTarget } from 'nx/src/project-graph/utils/project-configuration-utils';
interface NormalizedSchema extends ReturnType<typeof normalizeOptions> {}
@ -53,7 +56,28 @@ function generateGlobalConfig(tree: Tree, isJS: boolean) {
tree.write(`jest.config.${isJS ? 'js' : 'ts'}`, contents);
}
function createJestConfig(tree: Tree, options: NormalizedSchema) {
function addPlugin(tree: Tree) {
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
if (
!nxJson.plugins.some((p) =>
typeof p === 'string'
? p === '@nx/jest/plugin'
: p.plugin === '@nx/jest/plugin'
)
) {
nxJson.plugins.push({
plugin: '@nx/jest/plugin',
options: {
targetName: 'test',
},
});
}
updateNxJson(tree, nxJson);
}
async function createJestConfig(tree: Tree, options: NormalizedSchema) {
if (!tree.exists('jest.preset.js')) {
// preset is always js file.
tree.write(
@ -64,8 +88,15 @@ function createJestConfig(tree: Tree, options: NormalizedSchema) {
module.exports = { ...nxPreset }`
);
const shouldAddPlugin = process.env.NX_PCV3 === 'true';
if (shouldAddPlugin) {
addPlugin(tree);
}
updateProductionFileSet(tree);
addJestTargetDefaults(tree);
if (!shouldAddPlugin) {
addJestTargetDefaults(tree, shouldAddPlugin);
}
}
if (options.rootProject) {
// we don't want any config to be made because the `configurationGenerator` will do it.
@ -84,30 +115,83 @@ function createJestConfig(tree: Tree, options: NormalizedSchema) {
if (tree.exists(rootJestPath)) {
// moving from root project config to monorepo-style config
const projects = getProjects(tree);
const projectNames = Array.from(projects.keys());
const rootProject = projectNames.find(
(projectName) => projects.get(projectName)?.root === '.'
const { nodes: projects } = await createProjectGraphAsync();
const projectConfigurations = Object.values(projects);
const rootProject = projectConfigurations.find(
(projectNode) => projectNode.data?.root === '.'
);
// root project might have been removed,
// if it's missing there's nothing to migrate
if (rootProject) {
const rootProjectConfig = projects.get(rootProject);
const jestTarget = Object.values(rootProjectConfig.targets || {}).find(
(t) =>
t?.executor === '@nx/jest:jest' || t?.executor === '@nrwl/jest:jest'
const jestTarget = Object.entries(rootProject.data?.targets ?? {}).find(
([_, t]) =>
((t?.executor === '@nx/jest:jest' ||
t?.executor === '@nrwl/jest:jest') &&
t?.options?.jestConfig === rootJestPath) ||
(t?.executor === 'nx:run-commands' && t?.options?.command === 'jest')
);
const isProjectConfig = jestTarget?.options?.jestConfig === rootJestPath;
// if root project doesn't have jest target, there's nothing to migrate
if (isProjectConfig) {
const jestProjectConfig = `jest.config.${
rootProjectConfig.projectType === 'application' ? 'app' : 'lib'
}.${options.js ? 'js' : 'ts'}`;
tree.rename(rootJestPath, jestProjectConfig);
jestTarget.options.jestConfig = jestProjectConfig;
updateProjectConfiguration(tree, rootProject, rootProjectConfig);
if (!jestTarget) {
return;
}
const [jestTargetName, jestTargetConfigInGraph] = jestTarget;
// if root project doesn't have jest target, there's nothing to migrate
const rootProjectConfig = readProjectConfiguration(
tree,
rootProject.name
);
if (
rootProjectConfig.targets['test']?.executor === 'nx:run-commands'
? rootProjectConfig.targets['test']?.command !== 'jest'
: rootProjectConfig.targets['test']?.options?.jestConfig !==
rootJestPath
) {
// Jest target has already been updated
return;
}
const jestProjectConfig = `jest.config.${
rootProjectConfig.projectType === 'application' ? 'app' : 'lib'
}.${options.js ? 'js' : 'ts'}`;
tree.rename(rootJestPath, jestProjectConfig);
const nxJson = readNxJson(tree);
const targetDefaults = readTargetDefaultsForTarget(
jestTargetName,
nxJson.targetDefaults,
jestTargetConfigInGraph.executor
);
const target: TargetConfiguration = (rootProjectConfig.targets[
jestTargetName
] ??=
jestTargetConfigInGraph.executor === 'nx:run-commands'
? { command: `jest --config ${jestProjectConfig}` }
: {
executor: jestTargetConfigInGraph.executor,
options: {},
});
if (target.executor === '@nx/jest:jest') {
target.options.jestConfig = jestProjectConfig;
}
if (targetDefaults?.cache === undefined) {
target.cache = jestTargetConfigInGraph.cache;
}
if (targetDefaults?.inputs === undefined) {
target.inputs = jestTargetConfigInGraph.inputs;
}
if (targetDefaults?.outputs === undefined) {
target.outputs = jestTargetConfigInGraph.outputs;
}
if (targetDefaults?.dependsOn === undefined) {
target.dependsOn = jestTargetConfigInGraph.dependsOn;
}
updateProjectConfiguration(tree, rootProject.name, rootProjectConfig);
// generate new global config as it was move to project config or is missing
generateGlobalConfig(tree, options.js);
}
@ -139,20 +223,23 @@ function updateProductionFileSet(tree: Tree) {
updateNxJson(tree, nxJson);
}
function addJestTargetDefaults(tree: Tree) {
function addJestTargetDefaults(tree: Tree, hasPlugin: boolean) {
const nxJson = readNxJson(tree);
const productionFileSet = nxJson.namedInputs?.production;
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['@nx/jest:jest'] ??= {};
nxJson.targetDefaults['@nx/jest:jest'].cache ??= true;
// Test targets depend on all their project's sources + production sources of dependencies
nxJson.targetDefaults['@nx/jest:jest'].inputs ??= [
'default',
productionFileSet ? '^production' : '^default',
'{workspaceRoot}/jest.preset.js',
];
if (!hasPlugin) {
const productionFileSet = nxJson.namedInputs?.production;
nxJson.targetDefaults['@nx/jest:jest'].cache ??= true;
// Test targets depend on all their project's sources + production sources of dependencies
nxJson.targetDefaults['@nx/jest:jest'].inputs ??= [
'default',
productionFileSet ? '^production' : '^default',
'{workspaceRoot}/jest.preset.js',
];
}
nxJson.targetDefaults['@nx/jest:jest'].options ??= {
passWithNoTests: true,
@ -231,7 +318,7 @@ export async function jestInitGenerator(
})
);
createJestConfig(tree, options);
await createJestConfig(tree, options);
if (!options.skipPackageJson) {
removeDependenciesFromPackageJson(tree, ['@nx/jest'], []);

View File

@ -0,0 +1,82 @@
import { CreateNodesContext } from '@nx/devkit';
import { join } from 'path';
import { createNodes } from './plugin';
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
describe('@nx/jest/plugin', () => {
let createNodesFunction = createNodes[1];
let context: CreateNodesContext;
let tempFs: TempFs;
beforeEach(async () => {
tempFs = new TempFs('test');
context = {
nxJsonConfiguration: {
namedInputs: {
default: ['{projectRoot}/**/*'],
production: ['!{projectRoot}/**/*.spec.ts'],
},
},
workspaceRoot: tempFs.tempDir,
};
await tempFs.createFiles({
'proj/jest.config.js': '',
'proj/project.json': '{}',
});
});
afterEach(() => {
jest.resetModules();
});
it('should create nodes based on jest.config.ts', async () => {
mockJestConfig(
{
coverageDirectory: '../coverage',
},
context
);
const nodes = await createNodesFunction(
'proj/jest.config.js',
{
targetName: 'test',
},
context
);
expect(nodes.projects.proj).toMatchInlineSnapshot(`
{
"root": "proj",
"targets": {
"test": {
"cache": true,
"command": "jest",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"jest",
],
},
],
"options": {
"cwd": "proj",
},
"outputs": [
"{workspaceRoot}/coverage",
],
},
},
}
`);
});
});
function mockJestConfig(config: any, context: CreateNodesContext) {
jest.mock(join(context.workspaceRoot, 'proj/jest.config.js'), () => config, {
virtual: true,
});
}

View File

@ -0,0 +1,200 @@
import {
CreateDependencies,
CreateNodes,
CreateNodesContext,
joinPathFragments,
NxJsonConfiguration,
readJsonFile,
TargetConfiguration,
writeJsonFile,
} from '@nx/devkit';
import { dirname, join, relative, resolve } from 'path';
import { readTargetDefaultsForTarget } from 'nx/src/project-graph/utils/project-configuration-utils';
import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
import { existsSync, readdirSync } from 'fs';
import { readConfig } from 'jest-config';
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
import { getGlobPatternsFromPackageManagerWorkspaces } from 'nx/plugins/package-json-workspaces';
import { combineGlobPatterns } from 'nx/src/utils/globs';
import * as minimatch from 'minimatch';
export interface JestPluginOptions {
targetName?: string;
}
const cachePath = join(projectGraphCacheDirectory, 'jest.hash');
const targetsCache = existsSync(cachePath) ? readTargetsCache() : {};
const calculatedTargets: Record<
string,
Record<string, TargetConfiguration>
> = {};
function readTargetsCache(): Record<
string,
Record<string, TargetConfiguration>
> {
return readJsonFile(cachePath);
}
function writeTargetsToCache(
targets: Record<string, Record<string, TargetConfiguration>>
) {
writeJsonFile(cachePath, targets);
}
export const createDependencies: CreateDependencies = () => {
writeTargetsToCache(calculatedTargets);
return [];
};
export const createNodes: CreateNodes<JestPluginOptions> = [
'**/jest.config.{cjs,mjs,js,cts,mts,ts}',
async (configFilePath, options, context) => {
const projectRoot = dirname(configFilePath);
const packageManagerWorkspacesGlob = combineGlobPatterns(
getGlobPatternsFromPackageManagerWorkspaces(context.workspaceRoot)
);
// Do not create a project if package.json and project.json isn't there.
const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot));
if (
!siblingFiles.includes('package.json') &&
!siblingFiles.includes('project.json')
) {
return {};
} else if (
!siblingFiles.includes('project.json') &&
siblingFiles.includes('package.json')
) {
const path = joinPathFragments(projectRoot, 'package.json');
const isPackageJsonProject = minimatch(
path,
packageManagerWorkspacesGlob
);
if (!isPackageJsonProject) {
return {};
}
}
options = normalizeOptions(options);
const hash = calculateHashForCreateNodes(projectRoot, options, context);
const targets =
targetsCache[hash] ??
(await buildJestTargets(configFilePath, projectRoot, options, context));
calculatedTargets[hash] = targets;
return {
projects: {
[projectRoot]: {
root: projectRoot,
targets: targets,
},
},
};
},
];
async function buildJestTargets(
configFilePath: string,
projectRoot: string,
options: JestPluginOptions,
context: CreateNodesContext
) {
const config = await readConfig(
{
_: [],
$0: undefined,
},
resolve(context.workspaceRoot, configFilePath),
true,
null,
undefined,
true
);
const targetDefaults = readTargetDefaultsForTarget(
options.targetName,
context.nxJsonConfiguration.targetDefaults,
'nx:run-commands'
);
const namedInputs = getNamedInputs(projectRoot, context);
const targets: Record<string, TargetConfiguration> = {};
const target: TargetConfiguration = (targets[options.targetName] = {
command: 'jest',
options: {
cwd: projectRoot,
},
});
if (!targetDefaults?.cache) {
target.cache = true;
}
if (!targetDefaults?.inputs) {
target.inputs = getInputs(namedInputs);
}
if (!targetDefaults?.outputs) {
target.outputs = getOutputs(projectRoot, config, context);
}
return targets;
}
function getInputs(
namedInputs: NxJsonConfiguration['namedInputs']
): TargetConfiguration['inputs'] {
return [
...('production' in namedInputs
? ['default', '^production']
: ['default', '^default']),
{
externalDependencies: ['jest'],
},
];
}
function getOutputs(
projectRoot: string,
{ globalConfig }: Awaited<ReturnType<typeof readConfig>>,
context: CreateNodesContext
): string[] {
function getOutput(path: string): string {
const relativePath = relative(
join(context.workspaceRoot, projectRoot),
path
);
if (relativePath.startsWith('..')) {
return join('{workspaceRoot}', join(projectRoot, relativePath));
} else {
return join('{projectRoot}', relativePath);
}
}
const outputs = [];
for (const outputOption of [
globalConfig.coverageDirectory,
globalConfig.outputFile,
]) {
if (outputOption) {
outputs.push(getOutput(outputOption));
}
}
return outputs;
}
function normalizeOptions(options: JestPluginOptions): JestPluginOptions {
options ??= {};
options.targetName ??= 'test';
return options;
}

View File

@ -104,44 +104,6 @@ describe('monorepo generator', () => {
expect(tree.exists('libs/inner/my-lib/src/index.ts')).toBeTruthy();
});
it('should convert root React app (Webpack, Jest)', async () => {
await reactAppGenerator(tree, {
name: 'demo',
style: 'css',
bundler: 'webpack',
unitTestRunner: 'jest',
e2eTestRunner: 'none',
linter: 'eslint',
rootProject: true,
});
await monorepoGenerator(tree, {});
expect(readProjectConfiguration(tree, 'demo')).toMatchObject({
sourceRoot: 'apps/demo/src',
targets: {
build: {
executor: '@nx/webpack:webpack',
options: {
main: 'apps/demo/src/main.tsx',
tsConfig: 'apps/demo/tsconfig.app.json',
webpackConfig: 'apps/demo/webpack.config.js',
},
},
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'apps/demo/jest.config.app.ts',
},
},
},
});
// Extracted base config files
expect(tree.exists('tsconfig.base.json')).toBeTruthy();
expect(tree.exists('jest.config.ts')).toBeTruthy();
});
it('should convert root Next.js app with existing libraries', async () => {
await nextAppGenerator(tree, {
name: 'demo',

View File

@ -136,279 +136,6 @@ describe('move', () => {
);
});
it('should support moving root projects', async () => {
// Test that these are not moved
tree.write('.gitignore', '');
tree.write('README.md', '');
await libraryGenerator(tree, {
name: 'my-lib',
rootProject: true,
bundler: 'tsc',
buildable: true,
unitTestRunner: 'jest',
linter: 'eslint',
projectNameAndRootFormat: 'as-provided',
});
updateJson(tree, 'tsconfig.json', (json) => {
json.extends = './tsconfig.base.json';
json.files = ['./node_modules/@foo/bar/index.d.ts'];
return json;
});
let projectJson = readJson(tree, 'project.json');
expect(projectJson['$schema']).toEqual(
'node_modules/nx/schemas/project-schema.json'
);
// Test that this does not get moved
tree.write('other-lib/index.ts', '');
await moveGenerator(tree, {
projectName: 'my-lib',
importPath: '@proj/my-lib',
updateImportPath: true,
destination: 'my-lib',
projectNameAndRootFormat: 'as-provided',
});
expect(readJson(tree, 'my-lib/project.json')).toMatchObject({
name: 'my-lib',
$schema: '../node_modules/nx/schemas/project-schema.json',
sourceRoot: 'my-lib/src',
projectType: 'library',
targets: {
build: {
executor: '@nx/js:tsc',
outputs: ['{options.outputPath}'],
options: {
outputPath: 'dist/my-lib',
main: 'my-lib/src/index.ts',
tsConfig: 'my-lib/tsconfig.lib.json',
},
},
lint: {
executor: '@nx/eslint:lint',
},
test: {
executor: '@nx/jest:jest',
outputs: ['{workspaceRoot}/coverage/{projectName}'],
},
},
});
expect(readJson(tree, 'my-lib/tsconfig.json')).toMatchObject({
extends: '../tsconfig.base.json',
files: ['../node_modules/@foo/bar/index.d.ts'],
references: [
{ path: './tsconfig.lib.json' },
{ path: './tsconfig.spec.json' },
],
});
const jestConfig = tree.read('my-lib/jest.config.lib.ts', 'utf-8');
expect(jestConfig).toContain(`preset: '../jest.preset.js'`);
expect(tree.exists('my-lib/tsconfig.lib.json')).toBeTruthy();
expect(tree.exists('my-lib/tsconfig.spec.json')).toBeTruthy();
expect(tree.exists('my-lib/.eslintrc.json')).toBeTruthy();
expect(tree.exists('my-lib/src/index.ts')).toBeTruthy();
// Test that other libs and workspace files are not moved.
expect(tree.exists('package.json')).toBeTruthy();
expect(tree.exists('README.md')).toBeTruthy();
expect(tree.exists('.gitignore')).toBeTruthy();
expect(tree.exists('other-lib/index.ts')).toBeTruthy();
// Test that root configs are extracted
expect(tree.exists('tsconfig.base.json')).toBeTruthy();
expect(tree.exists('jest.config.ts')).toBeTruthy();
expect(tree.exists('.eslintrc.base.json')).not.toBeTruthy();
expect(tree.exists('.eslintrc.json')).toBeTruthy();
// Test that eslint migration was done
expect(readJson(tree, 'my-lib/.eslintrc.json').extends)
.toMatchInlineSnapshot(`
[
"../.eslintrc.json",
]
`);
expect(readJson(tree, 'my-lib/.eslintrc.json').plugins).not.toBeDefined();
expect(readJson(tree, '.eslintrc.json').plugins).toEqual(['@nx']);
});
it('should support moving standalone repos', async () => {
// Test that these are not moved
tree.write('.gitignore', '');
tree.write('README.md', '');
await applicationGenerator(tree, {
name: 'react-app',
rootProject: true,
unitTestRunner: 'jest',
e2eTestRunner: 'cypress',
linter: 'eslint',
style: 'css',
projectNameAndRootFormat: 'as-provided',
});
expect(readJson(tree, '.eslintrc.json').plugins).toEqual(['@nx']);
expect(readJson(tree, 'e2e/.eslintrc.json').plugins).toEqual(['@nx']);
// Test that this does not get moved
tree.write('other-lib/index.ts', '');
await moveGenerator(tree, {
projectName: 'react-app',
updateImportPath: false,
destination: 'apps/react-app',
projectNameAndRootFormat: 'as-provided',
});
// expect both eslint configs to have been changed
expect(tree.exists('.eslintrc.json')).toBeDefined();
expect(
readJson(tree, 'apps/react-app/.eslintrc.json').plugins
).toBeUndefined();
expect(readJson(tree, 'e2e/.eslintrc.json').plugins).toBeUndefined();
await moveGenerator(tree, {
projectName: 'e2e',
updateImportPath: false,
destination: 'apps/react-app-e2e',
projectNameAndRootFormat: 'as-provided',
});
expect(tree.read('apps/react-app-e2e/cypress.config.ts').toString())
.toMatchInlineSnapshot(`
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename, { cypressDir: 'src' }),
baseUrl: 'http://localhost:4200',
},
});
"
`);
});
it('should correctly move standalone repos that have migrated eslint config', async () => {
// Test that these are not moved
tree.write('.gitignore', '');
tree.write('README.md', '');
await applicationGenerator(tree, {
name: 'react-app',
rootProject: true,
unitTestRunner: 'jest',
e2eTestRunner: 'cypress',
linter: 'eslint',
style: 'css',
projectNameAndRootFormat: 'as-provided',
});
await libraryGenerator(tree, {
name: 'my-lib',
bundler: 'tsc',
buildable: true,
unitTestRunner: 'jest',
linter: 'eslint',
directory: 'my-lib',
projectNameAndRootFormat: 'as-provided',
});
// assess the correct starting position
expect(tree.exists('.eslintrc.base.json')).toBeTruthy();
expect(readJson(tree, '.eslintrc.json').plugins).not.toBeDefined();
expect(readJson(tree, '.eslintrc.json').extends).toEqual([
'plugin:@nx/react',
'./.eslintrc.base.json',
]);
expect(readJson(tree, 'e2e/.eslintrc.json').plugins).not.toBeDefined();
expect(readJson(tree, 'e2e/.eslintrc.json').extends).toEqual([
'plugin:cypress/recommended',
'../.eslintrc.base.json',
]);
await moveGenerator(tree, {
projectName: 'react-app',
updateImportPath: false,
destination: 'apps/react-app',
projectNameAndRootFormat: 'as-provided',
});
// expect both eslint configs to have been changed
expect(tree.exists('.eslintrc.json')).toBeTruthy();
expect(tree.exists('.eslintrc.base.json')).toBeFalsy();
expect(readJson(tree, 'apps/react-app/.eslintrc.json').extends).toEqual([
'plugin:@nx/react',
'../../.eslintrc.json',
]);
expect(readJson(tree, 'e2e/.eslintrc.json').extends).toEqual([
'plugin:cypress/recommended',
'../.eslintrc.json',
]);
});
it('should support scoped new project name for libraries', async () => {
await libraryGenerator(tree, {
name: 'my-lib',
projectNameAndRootFormat: 'as-provided',
});
await moveGenerator(tree, {
projectName: 'my-lib',
newProjectName: '@proj/shared-my-lib',
updateImportPath: true,
destination: 'shared/my-lib',
projectNameAndRootFormat: 'as-provided',
});
expect(tree.exists('shared/my-lib/package.json')).toBeTruthy();
expect(tree.exists('shared/my-lib/tsconfig.lib.json')).toBeTruthy();
expect(tree.exists('shared/my-lib/src/index.ts')).toBeTruthy();
expect(readProjectConfiguration(tree, '@proj/shared-my-lib'))
.toMatchInlineSnapshot(`
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"name": "@proj/shared-my-lib",
"projectType": "library",
"root": "shared/my-lib",
"sourceRoot": "shared/my-lib/src",
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"options": {
"assets": [
"shared/my-lib/*.md",
],
"main": "shared/my-lib/src/index.ts",
"outputPath": "dist/shared/my-lib",
"tsConfig": "shared/my-lib/tsconfig.lib.json",
},
"outputs": [
"{options.outputPath}",
],
},
"lint": {
"executor": "@nx/eslint:lint",
},
"test": {
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "shared/my-lib/jest.config.ts",
},
"outputs": [
"{workspaceRoot}/coverage/{projectRoot}",
],
},
},
}
`);
});
it('should move project correctly when --project-name-and-root-format=derived', async () => {
await libraryGenerator(tree, {
name: 'my-lib',