feat(testing): add jest create-nodes plugin (#20045)
This commit is contained in:
parent
595a7743b6
commit
5d9b4c5224
@ -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}`);
|
||||
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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
5
packages/jest/plugin.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export {
|
||||
createNodes,
|
||||
createDependencies,
|
||||
JestPluginOptions,
|
||||
} from './src/plugins/plugin';
|
||||
@ -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);
|
||||
|
||||
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);
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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 (!jestTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [jestTargetName, jestTargetConfigInGraph] = jestTarget;
|
||||
// if root project doesn't have jest target, there's nothing to migrate
|
||||
if (isProjectConfig) {
|
||||
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);
|
||||
jestTarget.options.jestConfig = jestProjectConfig;
|
||||
updateProjectConfiguration(tree, rootProject, rootProjectConfig);
|
||||
|
||||
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;
|
||||
|
||||
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'], []);
|
||||
|
||||
82
packages/jest/src/plugins/plugin.spec.ts
Normal file
82
packages/jest/src/plugins/plugin.spec.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
200
packages/jest/src/plugins/plugin.ts
Normal file
200
packages/jest/src/plugins/plugin.ts
Normal 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;
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user