cleanup(misc): clean up init generators (#21088)

This commit is contained in:
Leosvel Pérez Espinosa 2024-01-16 15:29:44 +01:00 committed by GitHub
parent dcef077032
commit 047dc22aed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
199 changed files with 3474 additions and 4409 deletions

View File

@ -8,28 +8,7 @@
"title": "Init Angular Plugin",
"description": "Initializes the `@nx/angular` plugin. NOTE: Does not work in the `--dry-run` mode.",
"type": "object",
"examples": [
{
"command": "nx g @nx/angular:init --style=scss",
"description": "Installs angular dependencies and initializes the `@nx/angular` plugin with the `scss` stylesheet format."
}
],
"properties": {
"unitTestRunner": {
"type": "string",
"enum": ["jest", "none"],
"description": "Test runner to use for unit tests.",
"default": "jest",
"x-priority": "important"
},
"e2eTestRunner": {
"type": "string",
"enum": ["cypress", "playwright", "none"],
"x-prompt": "Which E2E test runner would you like to use?",
"description": "Test runner to use for end to end (e2e) tests.",
"default": "cypress",
"x-priority": "important"
},
"skipInstall": {
"type": "boolean",
"description": "Skip installing after adding `@nx/workspace`.",
@ -42,38 +21,6 @@
"default": false,
"x-priority": "internal"
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint",
"x-priority": "important"
},
"style": {
"description": "The file extension to be used for style files.",
"type": "string",
"default": "css",
"enum": ["css", "scss", "sass", "less"],
"x-prompt": {
"message": "Which stylesheet format would you like to use?",
"type": "list",
"items": [
{ "value": "css", "label": "CSS" },
{
"value": "scss",
"label": "SASS(.scss) [ http://sass-lang.com ]"
},
{
"value": "sass",
"label": "SASS(.sass) [ http://sass-lang.com ]"
},
{
"value": "less",
"label": "LESS [ http://lesscss.org ]"
}
]
}
},
"skipPackageJson": {
"type": "boolean",
"default": false,

View File

@ -9,6 +9,12 @@
"description": "Add Cypress Configuration to the workspace.",
"type": "object",
"properties": {
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"skipPackageJson": {
"type": "boolean",
"default": false,

View File

@ -18,12 +18,6 @@
"default": false,
"description": "Do not add dependencies to `package.json`.",
"x-priority": "internal"
},
"framework": {
"type": "string",
"description": "App framework to test",
"enum": ["react-native", "expo"],
"default": "react-native"
}
},
"required": [],

View File

@ -9,16 +9,15 @@
"description": "Init Webpack Plugin.",
"type": "object",
"properties": {
"compiler": {
"type": "string",
"enum": ["babel", "swc", "tsc"],
"description": "The compiler to initialize for.",
"default": "babel"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
},
"skipPackageJson": {
"description": "Do not add dependencies to `package.json`.",
"type": "boolean",
"default": false
}
},
"required": [],

View File

@ -8,33 +8,16 @@
"description": "Add Nx Expo Schematics.",
"type": "object",
"properties": {
"unitTestRunner": {
"description": "Adds the specified unit test runner",
"type": "string",
"enum": ["jest", "none"],
"default": "jest"
},
"skipFormat": {
"description": "Skip formatting files",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"e2eTestRunner": {
"description": "Adds the specified e2e test runner",
"type": "string",
"enum": ["detox", "none"],
"default": "detox"
},
"skipPackageJson": {
"type": "boolean",
"default": false,
"description": "Do not add dependencies to `package.json`."
},
"js": {
"type": "boolean",
"default": false,
"description": "Use JavaScript instead of TypeScript"
}
},
"required": [],

View File

@ -9,16 +9,15 @@
"description": "Init Express Plugin.",
"type": "object",
"properties": {
"unitTestRunner": {
"description": "Adds the specified unit test runner.",
"type": "string",
"enum": ["jest", "none"],
"default": "jest"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
},
"skipPackageJson": {
"type": "boolean",
"default": false,
"description": "Do not add dependencies to `package.json`."
}
},
"required": [],

View File

@ -9,36 +9,17 @@
"description": "Add Jest Configuration to a workspace.",
"type": "object",
"properties": {
"babelJest": {
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"alias": "babel-jest",
"description": "Use `babel-jest` instead of `ts-jest`.",
"default": false
"default": false,
"x-priority": "internal"
},
"skipPackageJson": {
"type": "boolean",
"default": false,
"description": "Do not add dependencies to `package.json`.",
"x-priority": "internal"
},
"testEnvironment": {
"type": "string",
"enum": ["jsdom", "node", "none"],
"description": "The test environment for jest. This controls which jest-environment-* package is installed",
"default": "jsdom",
"x-priority": "important"
},
"js": {
"type": "boolean",
"default": false,
"description": "Use JavaScript instead of TypeScript for config files"
},
"rootProject": {
"description": "initialize Jest for an application at the root of the workspace",
"type": "boolean",
"default": false,
"hidden": true,
"x-priority": "internal"
}
},
"required": [],

View File

@ -9,12 +9,6 @@
"cli": "nx",
"type": "object",
"properties": {
"unitTestRunner": {
"description": "Adds the specified unit test runner.",
"type": "string",
"enum": ["jest", "none"],
"default": "jest"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",

View File

@ -9,40 +9,16 @@
"description": "Init Next Plugin.",
"type": "object",
"properties": {
"unitTestRunner": {
"description": "Adds the specified unit test runner.",
"type": "string",
"enum": ["jest", "none"],
"default": "jest"
},
"e2eTestRunner": {
"description": "Adds the specified e2e test runner.",
"type": "string",
"enum": ["cypress", "none"],
"default": "cypress"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
},
"js": {
"type": "boolean",
"default": false,
"description": "Use JavaScript instead of TypeScript"
},
"skipPackageJson": {
"type": "boolean",
"default": false,
"description": "Do not add dependencies to `package.json`.",
"x-priority": "internal"
},
"rootProject": {
"description": "Create an application at the root of the workspace.",
"type": "boolean",
"default": false,
"hidden": true,
"x-priority": "internal"
}
},
"required": [],

View File

@ -9,21 +9,15 @@
"description": "Init Node Plugin.",
"type": "object",
"properties": {
"unitTestRunner": {
"description": "Adds the specified unit test runner.",
"type": "string",
"enum": ["jest", "none"],
"default": "jest"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
},
"js": {
"skipPackageJson": {
"description": "Do not add dependencies to `package.json`.",
"type": "boolean",
"default": false,
"description": "Use JavaScript instead of TypeScript"
"default": false
}
},
"required": [],

View File

@ -14,20 +14,10 @@
"type": "boolean",
"default": false
},
"rootProject": {
"description": "Create a project at the root of the workspace",
"type": "boolean",
"default": false
},
"skipPackageJson": {
"description": "Do not add dependencies to `package.json`.",
"type": "boolean",
"default": false
},
"style": {
"description": "The file extension to be used for style files.",
"type": "string",
"default": "css"
}
},
"required": [],

View File

@ -63,6 +63,11 @@
"default": false,
"hidden": true,
"x-priority": "internal"
},
"skipInstall": {
"type": "boolean",
"description": "Skip running `playwright install`. This is to ensure that playwright browsers are installed.",
"default": false
}
},
"required": ["project"],

View File

@ -19,11 +19,6 @@
"default": false,
"description": "Do not add dependencies to `package.json`.",
"x-priority": "internal"
},
"skipInstall": {
"type": "boolean",
"description": "Skip running `playwright install`. This is to ensure that playwright browsers are installed.",
"default": false
}
},
"required": [],

View File

@ -9,29 +9,12 @@
"description": "Add Nx React native schematics.",
"type": "object",
"properties": {
"unitTestRunner": {
"description": "Adds the specified unit test runner.",
"type": "string",
"enum": ["jest", "none"],
"default": "jest"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"e2eTestRunner": {
"description": "Adds the specified E2E test runner.",
"type": "string",
"enum": ["detox", "none"],
"default": "detox"
},
"js": {
"type": "boolean",
"default": false,
"description": "Use JavaScript instead of TypeScript"
},
"skipPackageJson": {
"description": "Do not add dependencies to `package.json`.",
"type": "boolean",

View File

@ -9,18 +9,6 @@
"cli": "nx",
"type": "object",
"properties": {
"unitTestRunner": {
"description": "Adds the specified unit test runner.",
"type": "string",
"enum": ["jest", "none"],
"default": "jest"
},
"e2eTestRunner": {
"description": "Adds the specified E2E test runner.",
"type": "string",
"enum": ["cypress", "playwright", "none"],
"default": "cypress"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
@ -30,17 +18,6 @@
"description": "Do not add dependencies to `package.json`.",
"type": "boolean",
"default": false
},
"skipHelperLibs": {
"description": "Do not install tslib.",
"type": "boolean",
"default": false,
"hidden": true
},
"js": {
"type": "boolean",
"default": false,
"description": "Use JavaScript instead of TypeScript"
}
},
"required": [],

View File

@ -9,16 +9,15 @@
"description": "Init Webpack Plugin.",
"type": "object",
"properties": {
"compiler": {
"type": "string",
"enum": ["babel", "swc", "tsc"],
"description": "The compiler to initialize for.",
"default": "babel"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
},
"skipPackageJson": {
"description": "Do not add dependencies to `package.json`.",
"type": "boolean",
"default": false
}
},
"required": [],

View File

@ -8,41 +8,14 @@
"$id": "init-storybook-plugin",
"type": "object",
"properties": {
"uiFramework": {
"type": "string",
"description": "Storybook UI Framework to use.",
"enum": [
"@storybook/angular",
"@storybook/html-webpack5",
"@storybook/nextjs",
"@storybook/preact-webpack5",
"@storybook/react-webpack5",
"@storybook/react-vite",
"@storybook/server-webpack5",
"@storybook/svelte-webpack5",
"@storybook/svelte-vite",
"@storybook/sveltekit",
"@storybook/vue-webpack5",
"@storybook/vue-vite",
"@storybook/vue3-webpack5",
"@storybook/vue3-vite",
"@storybook/web-components-webpack5",
"@storybook/web-components-vite",
"@storybook/react",
"@storybook/html",
"@storybook/web-components",
"@storybook/vue",
"@storybook/vue3",
"@storybook/svelte",
"@storybook/react-native"
],
"x-prompt": "What UI framework plugin should storybook use?",
"x-priority": "important",
"aliases": ["storybook7UiFramework"]
},
"js": {
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
},
"skipPackageJson": {
"description": "Do not add dependencies to `package.json`.",
"type": "boolean",
"description": "Generate JavaScript story files rather than TypeScript story files.",
"default": false
}
},

View File

@ -8,32 +8,17 @@
"$id": "init-vite-plugin",
"type": "object",
"properties": {
"uiFramework": {
"type": "string",
"description": "UI Framework to use for Vite.",
"enum": ["react", "none"],
"default": "react",
"x-prompt": "What UI framework plugin should Vite use?"
},
"compiler": {
"type": "string",
"description": "Compiler to use for Vite when UI Framework is React.",
"enum": ["babel", "swc"],
"default": "babel"
},
"includeLib": {
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"description": "Add dependencies needed to build libraries.",
"default": false
},
"testEnvironment": {
"description": "The vitest environment to use. See https://vitest.dev/config/#environment.",
"type": "string",
"enum": ["node", "jsdom", "happy-dom", "edge-runtime"],
"default": "jsdom"
"skipPackageJson": {
"description": "Do not add dependencies to `package.json`.",
"type": "boolean",
"default": false
}
},
"examplesFile": "---\ntitle: Examples for the Vite init generator\ndescription: This page contains examples for the Vite @nx/vite:init generator, which helps you initialize vite in your Nx workspace, by installing the necessary dependencies.\n---\n\nThis is a generator will initialize Vite.js in your workspace. It will install all the necessary dependencies. You can read more about how this generator works, in the [Vite package overview page](/packages/vite).\n\n{% callout type=\"note\" title=\"string\" %}\nYou don't need to use this generator on its own.\n{% /callout %}\n\nThis generator will be called automatically when you are either converting an existing React or Web app to use Vite, using the [`@nx/vite:configuration` generator](/packages/vite/generators/configuration), or when you are creating a new React or Web app using the [`@nx/react:app`](/packages/react/generators/application) or [`@nx/web:app`](/packages/web/generators/application) generators, if you choose `vite` as the `bundler`.\n\nIf you need to for some reason, you can use it on its own like this:\n\n```bash\nnx g @nx/vite:init\n```\n\n## Examples\n\n### Install all the necessary dependencies for Vite and the React plugin\n\n```bash\nnx g @nx/vite:init --uiFramework=react\n```\n\n### Install all the necessary dependencies for Vite\n\n```bash\nnx g @nx/vite:init --uiFramework=none\n```\n",
"presets": []
},
"description": "Initialize Vite in the workspace.",

View File

@ -14,26 +14,10 @@
"type": "boolean",
"default": false
},
"js": {
"type": "boolean",
"description": "Use JavaScript instead of TypeScript",
"default": false
},
"rootProject": {
"description": "Create a project at the root of the workspace",
"skipPackageJson": {
"description": "Do not add dependencies to `package.json`.",
"type": "boolean",
"default": false
},
"routing": {
"type": "boolean",
"description": "Generate application with routes.",
"x-prompt": "Would you like to add Vue Router to this application?",
"default": false
},
"style": {
"description": "The file extension to be used for style files.",
"type": "string",
"default": "css"
}
},
"required": [],

View File

@ -9,25 +9,6 @@
"description": "Init Web Plugin.",
"type": "object",
"properties": {
"bundler": {
"type": "string",
"description": "The bundler to use.",
"enum": ["webpack", "none", "vite"],
"default": "webpack"
},
"unitTestRunner": {
"description": "Adds the specified unit test runner",
"type": "string",
"enum": ["jest", "none"],
"default": "jest"
},
"e2eTestRunner": {
"description": "Adds the specified e2e test runner",
"type": "string",
"enum": ["cypress", "playwright", "none"],
"x-prompt": "Which E2E test runner would you like to use?",
"default": "cypress"
},
"skipFormat": {
"description": "Skip formatting files",
"type": "boolean",

View File

@ -9,22 +9,15 @@
"description": "Initialize the Webpack Plugin.",
"type": "object",
"properties": {
"uiFramework": {
"type": "string",
"description": "UI Framework to use for Webpack.",
"enum": ["react", "none"],
"x-prompt": "What UI framework plugin should Webpack use?"
},
"compiler": {
"type": "string",
"enum": ["babel", "swc", "tsc"],
"description": "The compiler to initialize for.",
"default": "babel"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
},
"skipPackageJson": {
"description": "Do not add dependencies to `package.json`.",
"type": "boolean",
"default": false
}
},
"required": [],

View File

@ -11,7 +11,10 @@ import {
describe('Storybook executors for Angular', () => {
const angularStorybookLib = uniq('test-ui-ng-lib');
beforeAll(() => {
newProject();
newProject({
packages: ['@nx/angular'],
unsetProjectNameAndRootFormat: false,
});
runCLI(
`g @nx/angular:library ${angularStorybookLib} --project-name-and-root-format=as-provided --no-interactive`
);

View File

@ -12,8 +12,11 @@ import {
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/eslint';
import * as enquirer from 'enquirer';
import { backwardCompatibleVersions } from '../../utils/backward-compatible-versions';
import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners';
import {
angularDevkitVersion,
angularVersion,
autoprefixerVersion,
postcssVersion,
tailwindVersion,
@ -48,6 +51,36 @@ describe('app', () => {
appTree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
it('should add angular dependencies', async () => {
// ACT
await generateApp(appTree);
// ASSERT
const { dependencies, devDependencies } = readJson(appTree, 'package.json');
expect(dependencies['@angular/animations']).toBe(angularVersion);
expect(dependencies['@angular/common']).toBe(angularVersion);
expect(dependencies['@angular/compiler']).toBe(angularVersion);
expect(dependencies['@angular/core']).toBe(angularVersion);
expect(dependencies['@angular/platform-browser']).toBe(angularVersion);
expect(dependencies['@angular/platform-browser-dynamic']).toBe(
angularVersion
);
expect(dependencies['@angular/router']).toBe(angularVersion);
expect(dependencies['rxjs']).toBeDefined();
expect(dependencies['tslib']).toBeDefined();
expect(dependencies['zone.js']).toBeDefined();
expect(devDependencies['@angular/cli']).toBe(angularDevkitVersion);
expect(devDependencies['@angular/compiler-cli']).toBe(angularVersion);
expect(devDependencies['@angular/language-service']).toBe(angularVersion);
expect(devDependencies['@angular-devkit/build-angular']).toBe(
angularDevkitVersion
);
// codelyzer should no longer be there by default
expect(devDependencies['codelyzer']).toBeUndefined();
});
describe('not nested', () => {
it('should create project configs', async () => {
// ACT
@ -1120,6 +1153,60 @@ describe('app', () => {
}));
});
it('should add angular dependencies', async () => {
// ACT
await generateApp(appTree, 'my-app');
// ASSERT
const { dependencies, devDependencies } = readJson(
appTree,
'package.json'
);
expect(dependencies['@angular/animations']).toEqual(
backwardCompatibleVersions.angularV15.angularVersion
);
expect(dependencies['@angular/common']).toEqual(
backwardCompatibleVersions.angularV15.angularVersion
);
expect(dependencies['@angular/compiler']).toEqual(
backwardCompatibleVersions.angularV15.angularVersion
);
expect(dependencies['@angular/core']).toEqual(
backwardCompatibleVersions.angularV15.angularVersion
);
expect(dependencies['@angular/platform-browser']).toEqual(
backwardCompatibleVersions.angularV15.angularVersion
);
expect(dependencies['@angular/platform-browser-dynamic']).toEqual(
backwardCompatibleVersions.angularV15.angularVersion
);
expect(dependencies['@angular/router']).toEqual(
backwardCompatibleVersions.angularV15.angularVersion
);
expect(dependencies['rxjs']).toEqual(
backwardCompatibleVersions.angularV15.rxjsVersion
);
expect(dependencies['zone.js']).toEqual(
backwardCompatibleVersions.angularV15.zoneJsVersion
);
expect(devDependencies['@angular/cli']).toEqual(
backwardCompatibleVersions.angularV15.angularDevkitVersion
);
expect(devDependencies['@angular/compiler-cli']).toEqual(
backwardCompatibleVersions.angularV15.angularDevkitVersion
);
expect(devDependencies['@angular/language-service']).toEqual(
backwardCompatibleVersions.angularV15.angularVersion
);
expect(devDependencies['@angular-devkit/build-angular']).toEqual(
backwardCompatibleVersions.angularV15.angularDevkitVersion
);
// codelyzer should no longer be there by default
expect(devDependencies['codelyzer']).toBeUndefined();
});
it('should import "ApplicationConfig" from "@angular/platform-browser"', async () => {
await generateApp(appTree, 'my-app', { standalone: true });

View File

@ -7,9 +7,11 @@ import {
Tree,
updateNxJson,
} from '@nx/devkit';
import { initGenerator as jsInitGenerator } from '@nx/js';
import { angularInitGenerator } from '../init/init';
import { setupSsr } from '../setup-ssr/setup-ssr';
import { setupTailwindGenerator } from '../setup-tailwind/setup-tailwind';
import { ensureAngularDependencies } from '../utils/ensure-angular-dependencies';
import {
addE2e,
addLinting,
@ -20,6 +22,7 @@ import {
enableStrictTypeChecking,
normalizeOptions,
setApplicationStrictDefault,
setGeneratorDefaults,
updateEditorTsConfig,
} from './lib';
import type { Schema } from './schema';
@ -41,10 +44,17 @@ export async function applicationGeneratorInternal(
const options = await normalizeOptions(tree, schema);
const rootOffset = offsetFromRoot(options.appProjectRoot);
await jsInitGenerator(tree, {
...options,
tsConfigName: options.rootProject ? 'tsconfig.json' : 'tsconfig.base.json',
js: false,
skipFormat: true,
});
await angularInitGenerator(tree, {
...options,
skipFormat: true,
});
ensureAngularDependencies(tree);
createProject(tree, options);
@ -62,6 +72,7 @@ export async function applicationGeneratorInternal(
await addUnitTestRunner(tree, options);
await addE2e(tree, options);
updateEditorTsConfig(tree, options);
setGeneratorDefaults(tree, options);
if (options.rootProject) {
const nxJson = readNxJson(tree);

View File

@ -1,37 +1,15 @@
import { Tree, joinPathFragments } from '@nx/devkit';
import { configurationGenerator } from '@nx/jest';
import { Tree } from '@nx/devkit';
import { UnitTestRunner } from '../../../utils/test-runners';
import { addJest } from '../../utils/add-jest';
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,
skipSerializers: false,
await addJest(host, {
name: options.name,
projectRoot: options.appProjectRoot,
skipPackageJson: options.skipPackageJson,
skipFormat: true,
strict: options.strict,
});
const setupFile = joinPathFragments(
options.appProjectRoot,
'src',
'test-setup.ts'
);
if (options.strict && host.exists(setupFile)) {
const contents = host.read(setupFile, 'utf-8');
host.write(
setupFile,
`// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment
globalThis.ngJest = {
testEnvironmentOptions: {
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
},
};
${contents}`
);
}
}
}

View File

@ -8,4 +8,5 @@ export * from './enable-strict-type-checking';
export * from './normalize-options';
export * from './normalized-schema';
export * from './set-app-strict-default';
export * from './set-generator-defaults';
export * from './update-editor-tsconfig';

View File

@ -0,0 +1,20 @@
import { readNxJson, updateNxJson, type Tree } from '@nx/devkit';
import type { NormalizedSchema } from './normalized-schema';
export function setGeneratorDefaults(
tree: Tree,
options: NormalizedSchema
): void {
const nxJson = readNxJson(tree);
nxJson.generators = nxJson.generators ?? {};
nxJson.generators['@nx/angular:application'] = {
e2eTestRunner: options.e2eTestRunner,
linter: options.linter,
style: options.style,
unitTestRunner: options.unitTestRunner,
...(nxJson.generators['@nx/angular:application'] || {}),
};
updateNxJson(tree, nxJson);
}

View File

@ -6,6 +6,7 @@ import {
exportComponentInEntryPoint,
findModuleFromOptions,
normalizeOptions,
setGeneratorDefaults,
} from './lib';
import type { Schema } from './schema';
@ -92,6 +93,7 @@ export async function componentGeneratorInternal(
}
exportComponentInEntryPoint(tree, options);
setGeneratorDefaults(tree, options);
if (!options.skipFormat) {
await formatFiles(tree);

View File

@ -1,3 +1,4 @@
export * from './component';
export * from './module';
export * from './normalize-options';
export * from './set-generator-defaults';

View File

@ -0,0 +1,17 @@
import { readNxJson, updateNxJson, type Tree } from '@nx/devkit';
import type { NormalizedSchema } from '../schema';
export function setGeneratorDefaults(
tree: Tree,
options: NormalizedSchema
): void {
const nxJson = readNxJson(tree);
nxJson.generators = nxJson.generators ?? {};
nxJson.generators['@nx/angular:component'] = {
style: options.style,
...(nxJson.generators['@nx/angular:component'] || {}),
};
updateNxJson(tree, nxJson);
}

View File

@ -1,26 +1,5 @@
jest.mock('@nx/devkit', () => ({
...jest.requireActual('@nx/devkit'),
// need to mock so it doesn't resolve what the workspace has installed
// and be able to test with different versions
ensurePackage: jest.fn(() => ({
cypressInitGenerator: jest.fn(),
initGenerator: jest.fn(),
})),
}));
import {
ensurePackage,
NxJsonConfiguration,
readJson,
readNxJson,
Tree,
updateJson,
updateNxJson,
} from '@nx/devkit';
import { readNxJson, updateJson, updateNxJson, type Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/eslint';
import { backwardCompatibleVersions } from '../../utils/backward-compatible-versions';
import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners';
import { angularDevkitVersion, angularVersion } from '../../utils/versions';
import init from './init';
describe('init', () => {
@ -30,262 +9,11 @@ describe('init', () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
it('should add angular dependencies', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
linter: Linter.EsLint,
skipFormat: true,
});
// ASSERT
const { dependencies, devDependencies } = readJson(tree, 'package.json');
expect(dependencies['@angular/animations']).toBe(angularVersion);
expect(dependencies['@angular/common']).toBe(angularVersion);
expect(dependencies['@angular/compiler']).toBe(angularVersion);
expect(dependencies['@angular/core']).toBe(angularVersion);
expect(dependencies['@angular/platform-browser']).toBe(angularVersion);
expect(dependencies['@angular/platform-browser-dynamic']).toBe(
angularVersion
);
expect(dependencies['@angular/router']).toBe(angularVersion);
expect(dependencies['rxjs']).toBeDefined();
expect(dependencies['tslib']).toBeDefined();
expect(dependencies['zone.js']).toBeDefined();
expect(devDependencies['@angular/cli']).toBe(angularDevkitVersion);
expect(devDependencies['@angular/compiler-cli']).toBe(angularVersion);
expect(devDependencies['@angular/language-service']).toBe(angularVersion);
expect(devDependencies['@angular-devkit/build-angular']).toBe(
angularDevkitVersion
);
// codelyzer should no longer be there by default
expect(devDependencies['codelyzer']).toBeUndefined();
});
it('should add angular dependencies respecting base packages versions', async () => {
// ARRANGE
updateJson(tree, 'package.json', (json) => ({
...json,
dependencies: {
...json.dependencies,
'@angular/core': '~15.0.0',
},
devDependencies: {
...json.devDependencies,
'@angular-devkit/build-angular': '~15.0.0',
},
}));
// ACT
await init(tree, { skipFormat: true });
// ASSERT
const { dependencies, devDependencies } = readJson(tree, 'package.json');
expect(dependencies['@angular/animations']).toBe('~15.0.0');
expect(dependencies['@angular/common']).toBe('~15.0.0');
expect(dependencies['@angular/compiler']).toBe('~15.0.0');
expect(dependencies['@angular/core']).toBe('~15.0.0');
expect(dependencies['@angular/platform-browser']).toBe('~15.0.0');
expect(dependencies['@angular/platform-browser-dynamic']).toBe('~15.0.0');
expect(dependencies['@angular/router']).toBe('~15.0.0');
expect(dependencies['rxjs']).toBeDefined();
expect(dependencies['tslib']).toBeDefined();
expect(dependencies['zone.js']).toBeDefined();
expect(devDependencies['@angular/cli']).toBe('~15.0.0');
expect(devDependencies['@angular/compiler-cli']).toBe('~15.0.0');
expect(devDependencies['@angular/language-service']).toBe('~15.0.0');
expect(devDependencies['@angular-devkit/build-angular']).toBe('~15.0.0');
});
it('should not overwrite already installed dependencies', async () => {
// ARRANGE
updateJson(tree, 'package.json', (json) => ({
...json,
dependencies: {
...json.dependencies,
'@angular/animations': '~15.0.1',
'@angular/core': '~15.0.0',
},
}));
// ACT
await init(tree, { skipFormat: true });
// ASSERT
const { dependencies } = readJson(tree, 'package.json');
expect(dependencies['@angular/animations']).toBe('~15.0.1');
expect(dependencies['@angular/core']).toBe('~15.0.0');
});
describe('--unit-test-runner', () => {
describe('jest', () => {
it('should add jest dependencies', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
linter: Linter.EsLint,
skipFormat: true,
});
const { devDependencies } = readJson(tree, 'package.json');
// ASSERT
expect(devDependencies['@nx/jest']).toBeDefined();
expect(devDependencies['jest']).toBeDefined();
expect(devDependencies['jest-preset-angular']).toBeDefined();
});
it('should add jest configuration', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
linter: Linter.EsLint,
skipFormat: true,
});
const hasJestConfigFile = tree.exists('jest.config.ts');
// ASSERT
expect(hasJestConfigFile).toBeTruthy();
});
it('should set defaults', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
linter: Linter.EsLint,
skipFormat: true,
});
const { generators } = readJson<NxJsonConfiguration>(tree, 'nx.json');
// ASSERT
expect(generators['@nx/angular:application'].unitTestRunner).toEqual(
'jest'
);
expect(generators['@nx/angular:library'].unitTestRunner).toEqual(
'jest'
);
});
});
});
describe('--e2e-test-runner', () => {
describe('playwright', () => {
it('should call @nx/playwright:init', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.None,
e2eTestRunner: E2eTestRunner.Playwright,
linter: Linter.EsLint,
skipFormat: true,
});
expect(ensurePackage).toHaveBeenLastCalledWith(
'@nx/playwright',
'0.0.1'
);
});
it('should set defaults', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.None,
e2eTestRunner: E2eTestRunner.Playwright,
linter: Linter.EsLint,
skipFormat: true,
});
const { generators } = readJson<NxJsonConfiguration>(tree, 'nx.json');
// ASSERT
expect(generators['@nx/angular:application'].e2eTestRunner).toEqual(
'playwright'
);
});
});
describe('cypress', () => {
it('should call @nx/cypress:init', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.None,
e2eTestRunner: E2eTestRunner.Cypress,
linter: Linter.EsLint,
skipFormat: true,
});
expect(ensurePackage).toHaveBeenLastCalledWith('@nx/cypress', '0.0.1');
});
it('should set defaults', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.None,
e2eTestRunner: E2eTestRunner.Cypress,
linter: Linter.EsLint,
skipFormat: true,
});
const { generators } = readJson<NxJsonConfiguration>(tree, 'nx.json');
// ASSERT
expect(generators['@nx/angular:application'].e2eTestRunner).toEqual(
'cypress'
);
});
});
});
describe('--linter', () => {
describe('eslint', () => {
it('should set the default to eslint', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.None,
linter: Linter.EsLint,
skipFormat: true,
});
const { generators } = readJson<NxJsonConfiguration>(tree, 'nx.json');
// ASSERT
expect(generators['@nx/angular:application'].linter).toEqual('eslint');
expect(generators['@nx/angular:library'].linter).toEqual('eslint');
});
});
describe('none', () => {
it('should set the default to none', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.None,
linter: Linter.None,
skipFormat: true,
});
const { generators } = readJson<NxJsonConfiguration>(tree, 'nx.json');
// ASSERT
expect(generators['@nx/angular:application'].linter).toEqual('none');
expect(generators['@nx/angular:library'].linter).toEqual('none');
});
});
});
describe('angular cache dir', () => {
it('should add .angular to .gitignore', async () => {
tree.write('.gitignore', '');
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
e2eTestRunner: E2eTestRunner.Cypress,
linter: Linter.EsLint,
skipFormat: true,
});
await init(tree, { skipFormat: true });
expect(tree.read('.gitignore', 'utf-8')).toContain('.angular');
});
@ -301,12 +29,7 @@ bar
`
);
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
e2eTestRunner: E2eTestRunner.Cypress,
linter: Linter.EsLint,
skipFormat: true,
});
await init(tree, { skipFormat: true });
const angularEntries = tree
.read('.gitignore', 'utf-8')
@ -317,12 +40,7 @@ bar
it('should add .angular to .prettierignore', async () => {
tree.write('.prettierignore', '');
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
e2eTestRunner: E2eTestRunner.Cypress,
linter: Linter.EsLint,
skipFormat: true,
});
await init(tree, { skipFormat: true });
expect(tree.read('.prettierignore', 'utf-8')).toContain('.angular');
});
@ -338,12 +56,7 @@ bar
`
);
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
e2eTestRunner: E2eTestRunner.Cypress,
linter: Linter.EsLint,
skipFormat: true,
});
await init(tree, { skipFormat: true });
const angularEntries = tree
.read('.prettierignore', 'utf-8')
@ -353,18 +66,14 @@ bar
it('should add configured angular cache dir to .gitignore and .prettierignore', async () => {
tree.write('.gitignore', '');
tree.write('.prettierignore', '');
const nxJson = readNxJson(tree);
updateNxJson(tree, {
...nxJson,
cli: { cache: { path: 'node_modules/.cache/angular' } },
} as any);
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
e2eTestRunner: E2eTestRunner.Cypress,
linter: Linter.EsLint,
skipFormat: true,
});
await init(tree, { skipFormat: true });
expect(tree.read('.gitignore', 'utf-8')).toContain(
'node_modules/.cache/angular'
@ -388,289 +97,11 @@ bar
}));
});
it('should add angular dependencies', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
linter: Linter.EsLint,
skipFormat: true,
});
// ASSERT
const { dependencies, devDependencies } = readJson(tree, 'package.json');
expect(dependencies['@angular/animations']).toEqual(
backwardCompatibleVersions.angularV15.angularVersion
);
expect(dependencies['@angular/common']).toEqual(
backwardCompatibleVersions.angularV15.angularVersion
);
expect(dependencies['@angular/compiler']).toEqual(
backwardCompatibleVersions.angularV15.angularVersion
);
expect(dependencies['@angular/core']).toEqual(
backwardCompatibleVersions.angularV15.angularVersion
);
expect(dependencies['@angular/platform-browser']).toEqual(
backwardCompatibleVersions.angularV15.angularVersion
);
expect(dependencies['@angular/platform-browser-dynamic']).toEqual(
backwardCompatibleVersions.angularV15.angularVersion
);
expect(dependencies['@angular/router']).toEqual(
backwardCompatibleVersions.angularV15.angularVersion
);
expect(dependencies['rxjs']).toEqual(
backwardCompatibleVersions.angularV15.rxjsVersion
);
expect(dependencies['zone.js']).toEqual(
backwardCompatibleVersions.angularV15.zoneJsVersion
);
expect(devDependencies['@angular/cli']).toEqual(
backwardCompatibleVersions.angularV15.angularDevkitVersion
);
expect(devDependencies['@angular/compiler-cli']).toEqual(
backwardCompatibleVersions.angularV15.angularDevkitVersion
);
expect(devDependencies['@angular/language-service']).toEqual(
backwardCompatibleVersions.angularV15.angularVersion
);
expect(devDependencies['@angular-devkit/build-angular']).toEqual(
backwardCompatibleVersions.angularV15.angularDevkitVersion
);
// codelyzer should no longer be there by default
expect(devDependencies['codelyzer']).toBeUndefined();
});
it('should add angular dependencies respecting base packages versions', async () => {
// ARRANGE
updateJson(tree, 'package.json', (json) => ({
...json,
dependencies: {
...json.dependencies,
'@angular/core': '~15.0.0',
},
devDependencies: {
...json.devDependencies,
'@angular-devkit/build-angular': '~15.0.0',
},
}));
// ACT
await init(tree, { skipFormat: true });
// ASSERT
const { dependencies, devDependencies } = readJson(tree, 'package.json');
expect(dependencies['@angular/animations']).toBe('~15.0.0');
expect(dependencies['@angular/common']).toBe('~15.0.0');
expect(dependencies['@angular/compiler']).toBe('~15.0.0');
expect(dependencies['@angular/core']).toBe('~15.0.0');
expect(dependencies['@angular/platform-browser']).toBe('~15.0.0');
expect(dependencies['@angular/platform-browser-dynamic']).toBe('~15.0.0');
expect(dependencies['@angular/router']).toBe('~15.0.0');
expect(dependencies['rxjs']).toBeDefined();
expect(dependencies['tslib']).toBeDefined();
expect(dependencies['zone.js']).toBeDefined();
expect(devDependencies['@angular/cli']).toBe('~15.0.0');
expect(devDependencies['@angular/compiler-cli']).toBe('~15.0.0');
expect(devDependencies['@angular/language-service']).toBe('~15.0.0');
expect(devDependencies['@angular-devkit/build-angular']).toBe('~15.0.0');
});
it('should not overwrite already installed dependencies', async () => {
// ARRANGE
updateJson(tree, 'package.json', (json) => ({
...json,
dependencies: {
...json.dependencies,
'@angular/animations': '~15.0.1',
'@angular/core': '~15.0.0',
},
}));
// ACT
await init(tree, { skipFormat: true });
// ASSERT
const { dependencies } = readJson(tree, 'package.json');
expect(dependencies['@angular/animations']).toBe('~15.0.1');
expect(dependencies['@angular/core']).toBe('~15.0.0');
});
describe('--unit-test-runner', () => {
describe('jest', () => {
it('should add jest dependencies', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
linter: Linter.EsLint,
skipFormat: true,
});
const { devDependencies } = readJson(tree, 'package.json');
// ASSERT
expect(devDependencies['@nx/jest']).toBeDefined();
expect(devDependencies['jest']).toBeDefined();
expect(devDependencies['jest-preset-angular']).toEqual(
backwardCompatibleVersions.angularV15.jestPresetAngularVersion
);
});
it('should add jest configuration', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
linter: Linter.EsLint,
skipFormat: true,
});
const hasJestConfigFile = tree.exists('jest.config.ts');
// ASSERT
expect(hasJestConfigFile).toBeTruthy();
});
it('should set defaults', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
linter: Linter.EsLint,
skipFormat: true,
});
const { generators } = readJson<NxJsonConfiguration>(tree, 'nx.json');
// ASSERT
expect(generators['@nx/angular:application'].unitTestRunner).toEqual(
'jest'
);
expect(generators['@nx/angular:library'].unitTestRunner).toEqual(
'jest'
);
});
});
});
describe('--e2e-test-runner', () => {
it('should call @nx/playwright:init', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.None,
e2eTestRunner: E2eTestRunner.Playwright,
linter: Linter.EsLint,
skipFormat: true,
});
expect(ensurePackage).toHaveBeenLastCalledWith(
'@nx/playwright',
'0.0.1'
);
});
it('should set defaults', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.None,
e2eTestRunner: E2eTestRunner.Playwright,
linter: Linter.EsLint,
skipFormat: true,
});
const { generators } = readJson<NxJsonConfiguration>(tree, 'nx.json');
// ASSERT
expect(generators['@nx/angular:application'].e2eTestRunner).toEqual(
'playwright'
);
});
describe('cypress', () => {
it('should add cypress dependencies', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.None,
e2eTestRunner: E2eTestRunner.Cypress,
linter: Linter.EsLint,
skipFormat: true,
});
// ASSERT
expect(ensurePackage).toHaveBeenLastCalledWith(
'@nx/cypress',
'0.0.1'
);
});
it('should set defaults', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.None,
e2eTestRunner: E2eTestRunner.Cypress,
linter: Linter.EsLint,
skipFormat: true,
});
const { generators } = readJson<NxJsonConfiguration>(tree, 'nx.json');
// ASSERT
expect(generators['@nx/angular:application'].e2eTestRunner).toEqual(
'cypress'
);
});
});
});
describe('--linter', () => {
describe('eslint', () => {
it('should set the default to eslint', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.None,
linter: Linter.EsLint,
skipFormat: true,
});
const { generators } = readJson<NxJsonConfiguration>(tree, 'nx.json');
// ASSERT
expect(generators['@nx/angular:application'].linter).toEqual(
'eslint'
);
expect(generators['@nx/angular:library'].linter).toEqual('eslint');
});
});
describe('none', () => {
it('should set the default to none', async () => {
// ACT
await init(tree, {
unitTestRunner: UnitTestRunner.None,
linter: Linter.None,
skipFormat: true,
});
const { generators } = readJson<NxJsonConfiguration>(tree, 'nx.json');
// ASSERT
expect(generators['@nx/angular:application'].linter).toEqual('none');
expect(generators['@nx/angular:library'].linter).toEqual('none');
});
});
});
describe('angular cache dir', () => {
it('should add .angular to .gitignore', async () => {
tree.write('.gitignore', '');
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
e2eTestRunner: E2eTestRunner.Cypress,
linter: Linter.EsLint,
skipFormat: true,
});
await init(tree, { skipFormat: true });
expect(tree.read('.gitignore', 'utf-8')).toContain('.angular');
});
@ -686,12 +117,7 @@ bar
`
);
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
e2eTestRunner: E2eTestRunner.Cypress,
linter: Linter.EsLint,
skipFormat: true,
});
await init(tree, { skipFormat: true });
const angularEntries = tree
.read('.gitignore', 'utf-8')
@ -702,12 +128,7 @@ bar
it('should add .angular to .prettierignore', async () => {
tree.write('.prettierignore', '');
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
e2eTestRunner: E2eTestRunner.Cypress,
linter: Linter.EsLint,
skipFormat: true,
});
await init(tree, { skipFormat: true });
expect(tree.read('.prettierignore', 'utf-8')).toContain('.angular');
});
@ -723,12 +144,7 @@ bar
`
);
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
e2eTestRunner: E2eTestRunner.Cypress,
linter: Linter.EsLint,
skipFormat: true,
});
await init(tree, { skipFormat: true });
const angularEntries = tree
.read('.prettierignore', 'utf-8')
@ -738,18 +154,14 @@ bar
it('should add configured angular cache dir to .gitignore and .prettierignore', async () => {
tree.write('.gitignore', '');
tree.write('.prettierignore', '');
const nxJson = readNxJson(tree);
updateNxJson(tree, {
...nxJson,
cli: { cache: { path: 'node_modules/.cache/angular' } },
} as any);
await init(tree, {
unitTestRunner: UnitTestRunner.Jest,
e2eTestRunner: E2eTestRunner.Cypress,
linter: Linter.EsLint,
skipFormat: true,
});
await init(tree, { skipFormat: true });
expect(tree.read('.gitignore', 'utf-8')).toContain(
'node_modules/.cache/angular'

View File

@ -2,222 +2,59 @@ import {
addDependenciesToPackageJson,
ensurePackage,
formatFiles,
GeneratorCallback,
logger,
readNxJson,
runTasksInSerial,
Tree,
updateNxJson,
type GeneratorCallback,
type Tree,
} from '@nx/devkit';
import { jestInitGenerator } from '@nx/jest';
import { Linter } from '@nx/eslint';
import { initGenerator as jsInitGenerator } from '@nx/js';
import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners';
import {
addDependenciesToPackageJsonIfDontExist,
getInstalledPackageVersion,
versions,
} from '../utils/version-utils';
import type {
PackageCompatVersions,
PackageLatestVersions,
} from '../../utils/backward-compatible-versions';
import { getInstalledPackageVersion, versions } from '../utils/version-utils';
import { Schema } from './schema';
import { nxVersion } from '../../utils/versions';
export async function angularInitGenerator(
tree: Tree,
rawOptions: Schema
options: Schema
): Promise<GeneratorCallback> {
const tasks: GeneratorCallback[] = [];
const options = normalizeOptions(rawOptions);
const pkgVersions = versions(tree);
const peerDepsToInstall = ['@angular-devkit/core'];
let devkitVersion: string;
peerDepsToInstall.forEach((pkg) => {
const packageVersion = getInstalledPackageVersion(tree, pkg);
if (!packageVersion) {
devkitVersion ??=
getInstalledPackageVersion(tree, '@angular-devkit/build-angular') ??
pkgVersions.angularDevkitVersion;
try {
ensurePackage(pkg, devkitVersion);
} catch {
// @schematics/angular cannot be required so this fails but this will still allow wrapping the schematic later on
}
if (!options.skipPackageJson) {
tasks.push(
addDependenciesToPackageJson(tree, {}, { [pkg]: devkitVersion })
);
}
}
});
setDefaults(tree, options);
const jsTask = await jsInitGenerator(tree, {
...options,
tsConfigName: options.rootProject ? 'tsconfig.json' : 'tsconfig.base.json',
js: false,
skipFormat: true,
});
tasks.push(jsTask);
if (!options.skipPackageJson) {
tasks.push(updateDependencies(tree, pkgVersions));
}
const unitTestTask = await addUnitTestRunner(
tree,
options,
pkgVersions.jestPresetAngularVersion
);
tasks.push(unitTestTask);
const e2eTask = await addE2ETestRunner(tree, options);
tasks.push(e2eTask);
ignoreAngularCacheDirectory(tree);
const installTask = installAngularDevkitCoreIfMissing(tree, options);
if (!options.skipFormat) {
await formatFiles(tree);
}
return runTasksInSerial(...tasks);
return installTask;
}
function normalizeOptions(options: Schema): Required<Schema> {
return {
e2eTestRunner: options.e2eTestRunner ?? E2eTestRunner.Cypress,
linter: options.linter ?? Linter.EsLint,
skipFormat: options.skipFormat ?? false,
skipInstall: options.skipInstall ?? false,
skipPackageJson: options.skipPackageJson ?? false,
style: options.style ?? 'css',
unitTestRunner: options.unitTestRunner ?? UnitTestRunner.Jest,
rootProject: options.rootProject,
};
}
function setDefaults(host: Tree, options: Schema) {
const nxJson = readNxJson(host);
nxJson.generators = nxJson.generators || {};
nxJson.generators['@nx/angular:application'] = {
style: options.style,
linter: options.linter,
unitTestRunner: options.unitTestRunner,
e2eTestRunner: options.e2eTestRunner,
...(nxJson.generators['@nx/angular:application'] || {}),
};
nxJson.generators['@nx/angular:library'] = {
linter: options.linter,
unitTestRunner: options.unitTestRunner,
...(nxJson.generators['@nx/angular:library'] || {}),
};
nxJson.generators['@nx/angular:component'] = {
style: options.style,
...(nxJson.generators['@nx/angular:component'] || {}),
};
updateNxJson(host, nxJson);
}
function updateDependencies(
tree: Tree,
versions: PackageLatestVersions | PackageCompatVersions
): GeneratorCallback {
const angularVersion =
getInstalledPackageVersion(tree, '@angular/core') ??
versions.angularVersion;
const angularDevkitVersion =
getInstalledPackageVersion(tree, '@angular-devkit/build-angular') ??
versions.angularDevkitVersion;
const rxjsVersion =
getInstalledPackageVersion(tree, 'rxjs') ?? versions.rxjsVersion;
const tsLibVersion =
getInstalledPackageVersion(tree, 'tslib') ?? versions.tsLibVersion;
const zoneJsVersion =
getInstalledPackageVersion(tree, 'zone.js') ?? versions.zoneJsVersion;
return addDependenciesToPackageJsonIfDontExist(
tree,
{
'@angular/animations': angularVersion,
'@angular/common': angularVersion,
'@angular/compiler': angularVersion,
'@angular/core': angularVersion,
'@angular/forms': angularVersion,
'@angular/platform-browser': angularVersion,
'@angular/platform-browser-dynamic': angularVersion,
'@angular/router': angularVersion,
rxjs: rxjsVersion,
tslib: tsLibVersion,
'zone.js': zoneJsVersion,
},
{
'@angular/cli': angularDevkitVersion,
'@angular/compiler-cli': angularVersion,
'@angular/language-service': angularVersion,
'@angular-devkit/build-angular': angularDevkitVersion,
'@angular-devkit/schematics': angularDevkitVersion,
'@schematics/angular': angularDevkitVersion,
}
);
}
async function addUnitTestRunner(
tree: Tree,
options: Schema,
jestPresetAngularVersion: string
): Promise<GeneratorCallback> {
switch (options.unitTestRunner) {
case UnitTestRunner.Jest:
if (!options.skipPackageJson) {
process.env.npm_config_legacy_peer_deps ??= 'true';
addDependenciesToPackageJsonIfDontExist(
tree,
{},
{
'jest-preset-angular': jestPresetAngularVersion,
}
);
}
return jestInitGenerator(tree, {
skipPackageJson: options.skipPackageJson,
});
default:
return () => {};
}
}
async function addE2ETestRunner(
function installAngularDevkitCoreIfMissing(
tree: Tree,
options: Schema
): Promise<GeneratorCallback> {
switch (options.e2eTestRunner) {
case E2eTestRunner.Cypress:
const { cypressInitGenerator } = ensurePackage<
typeof import('@nx/cypress')
>('@nx/cypress', nxVersion);
return cypressInitGenerator(tree, {
skipPackageJson: options.skipPackageJson,
});
case E2eTestRunner.Playwright:
const { initGenerator: playwrightInitGenerator } = ensurePackage<
typeof import('@nx/playwright')
>('@nx/playwright', nxVersion);
return playwrightInitGenerator(tree, {
skipFormat: true,
skipPackageJson: options.skipPackageJson,
});
default:
return () => {};
): GeneratorCallback {
const packageVersion = getInstalledPackageVersion(
tree,
'@angular-devkit/core'
);
if (!packageVersion) {
const pkgVersions = versions(tree);
const devkitVersion =
getInstalledPackageVersion(tree, '@angular-devkit/build-angular') ??
pkgVersions.angularDevkitVersion;
try {
ensurePackage('@angular-devkit/core', devkitVersion);
} catch {
// @schematics/angular cannot be required so this fails but this will still allow wrapping the schematic later on
}
if (!options.skipPackageJson) {
return addDependenciesToPackageJson(
tree,
{},
{ ['@angular-devkit/core']: devkitVersion }
);
}
}
return () => {};
}
function ignoreAngularCacheDirectory(tree: Tree): void {

View File

@ -1,14 +1,5 @@
import { Linter } from '@nx/eslint';
import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners';
import type { Styles } from '../utils/types';
export interface Schema {
unitTestRunner?: UnitTestRunner;
e2eTestRunner?: E2eTestRunner;
skipFormat?: boolean;
skipInstall?: boolean;
style?: Styles;
linter?: Linter;
skipPackageJson?: boolean;
rootProject?: boolean;
}

View File

@ -5,28 +5,7 @@
"title": "Init Angular Plugin",
"description": "Initializes the `@nx/angular` plugin. NOTE: Does not work in the `--dry-run` mode.",
"type": "object",
"examples": [
{
"command": "nx g @nx/angular:init --style=scss",
"description": "Installs angular dependencies and initializes the `@nx/angular` plugin with the `scss` stylesheet format."
}
],
"properties": {
"unitTestRunner": {
"type": "string",
"enum": ["jest", "none"],
"description": "Test runner to use for unit tests.",
"default": "jest",
"x-priority": "important"
},
"e2eTestRunner": {
"type": "string",
"enum": ["cypress", "playwright", "none"],
"x-prompt": "Which E2E test runner would you like to use?",
"description": "Test runner to use for end to end (e2e) tests.",
"default": "cypress",
"x-priority": "important"
},
"skipInstall": {
"type": "boolean",
"description": "Skip installing after adding `@nx/workspace`.",
@ -39,41 +18,6 @@
"default": false,
"x-priority": "internal"
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint",
"x-priority": "important"
},
"style": {
"description": "The file extension to be used for style files.",
"type": "string",
"default": "css",
"enum": ["css", "scss", "sass", "less"],
"x-prompt": {
"message": "Which stylesheet format would you like to use?",
"type": "list",
"items": [
{
"value": "css",
"label": "CSS"
},
{
"value": "scss",
"label": "SASS(.scss) [ http://sass-lang.com ]"
},
{
"value": "sass",
"label": "SASS(.sass) [ http://sass-lang.com ]"
},
{
"value": "less",
"label": "LESS [ http://lesscss.org ]"
}
]
}
},
"skipPackageJson": {
"type": "boolean",
"default": false,

View File

@ -0,0 +1,18 @@
import { readNxJson, updateNxJson, type Tree } from '@nx/devkit';
import type { NormalizedSchema } from './normalized-schema';
export function setGeneratorDefaults(
tree: Tree,
options: NormalizedSchema
): void {
const nxJson = readNxJson(tree);
nxJson.generators = nxJson.generators ?? {};
nxJson.generators['@nx/angular:library'] = {
linter: options.libraryOptions.linter,
unitTestRunner: options.libraryOptions.unitTestRunner,
...(nxJson.generators['@nx/angular:library'] || {}),
};
updateNxJson(tree, nxJson);
}

View File

@ -13,6 +13,8 @@ import { Linter } from '@nx/eslint';
import { createApp } from '../../utils/nx-devkit/testing';
import { UnitTestRunner } from '../../utils/test-runners';
import {
angularDevkitVersion,
angularVersion,
autoprefixerVersion,
postcssVersion,
tailwindVersion,
@ -62,6 +64,36 @@ describe('lib', () => {
).resolves.not.toThrow();
});
it('should add angular dependencies', async () => {
// ACT
await runLibraryGeneratorWithOpts();
// ASSERT
const { dependencies, devDependencies } = readJson(tree, 'package.json');
expect(dependencies['@angular/animations']).toBe(angularVersion);
expect(dependencies['@angular/common']).toBe(angularVersion);
expect(dependencies['@angular/compiler']).toBe(angularVersion);
expect(dependencies['@angular/core']).toBe(angularVersion);
expect(dependencies['@angular/platform-browser']).toBe(angularVersion);
expect(dependencies['@angular/platform-browser-dynamic']).toBe(
angularVersion
);
expect(dependencies['@angular/router']).toBe(angularVersion);
expect(dependencies['rxjs']).toBeDefined();
expect(dependencies['tslib']).toBeDefined();
expect(dependencies['zone.js']).toBeDefined();
expect(devDependencies['@angular/cli']).toBe(angularDevkitVersion);
expect(devDependencies['@angular/compiler-cli']).toBe(angularVersion);
expect(devDependencies['@angular/language-service']).toBe(angularVersion);
expect(devDependencies['@angular-devkit/build-angular']).toBe(
angularDevkitVersion
);
// codelyzer should no longer be there by default
expect(devDependencies['codelyzer']).toBeUndefined();
});
describe('not nested', () => {
it('should update ng-package.json', async () => {
// ACT

View File

@ -5,11 +5,9 @@ import {
joinPathFragments,
Tree,
} from '@nx/devkit';
import { configurationGenerator } from '@nx/jest';
import { Linter } from '@nx/eslint';
import { addTsConfigPath } from '@nx/js';
import { addTsConfigPath, initGenerator as jsInitGenerator } from '@nx/js';
import init from '../../generators/init/init';
import { E2eTestRunner } from '../../utils/test-runners';
import addLintingGenerator from '../add-linting/add-linting';
import setupTailwindGenerator from '../setup-tailwind/setup-tailwind';
import {
@ -30,6 +28,9 @@ import { updateTsConfig } from './lib/update-tsconfig';
import { Schema } from './schema';
import { createFiles } from './lib/create-files';
import { addProject } from './lib/add-project';
import { addJest } from '../utils/add-jest';
import { setGeneratorDefaults } from './lib/set-generator-defaults';
import { ensureAngularDependencies } from '../utils/ensure-angular-dependencies';
export async function libraryGenerator(
tree: Tree,
@ -69,11 +70,9 @@ export async function libraryGeneratorInternal(
const pkgVersions = versions(tree);
await init(tree, {
...libraryOptions,
skipFormat: true,
e2eTestRunner: E2eTestRunner.None,
});
await jsInitGenerator(tree, { ...options, js: false, skipFormat: true });
await init(tree, { ...libraryOptions, skipFormat: true });
ensureAngularDependencies(tree);
const project = addProject(tree, libraryOptions);
@ -81,6 +80,7 @@ export async function libraryGeneratorInternal(
updateTsConfig(tree, libraryOptions);
await addUnitTestRunner(tree, libraryOptions);
updateNpmScopeIfBuildableOrPublishable(tree, libraryOptions);
setGeneratorDefaults(tree, options);
if (!libraryOptions.standalone) {
addModule(tree, libraryOptions);
@ -127,33 +127,12 @@ async function addUnitTestRunner(
options: NormalizedSchema['libraryOptions']
) {
if (options.unitTestRunner === 'jest') {
await configurationGenerator(host, {
project: options.name,
setupFile: 'angular',
supportTsx: false,
skipSerializers: false,
skipFormat: true,
await addJest(host, {
name: options.name,
projectRoot: options.projectRoot,
skipPackageJson: options.skipPackageJson,
strict: options.strict,
});
const setupFile = joinPathFragments(
options.projectRoot,
'src',
'test-setup.ts'
);
if (options.strict && host.exists(setupFile)) {
const contents = host.read(setupFile, 'utf-8');
host.write(
setupFile,
`// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment
globalThis.ngJest = {
testEnvironmentOptions: {
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
},
};
${contents}`
);
}
}
}

View File

@ -1,36 +0,0 @@
import * as angularCliMigrator from './migrate-from-angular-cli';
import * as initGenerator from '../init/init';
import { ngAddGenerator } from './ng-add';
import type { Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
describe('ngAdd generator', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
jest
.spyOn(angularCliMigrator, 'migrateFromAngularCli')
.mockImplementation(() => Promise.resolve(() => {}));
jest
.spyOn(initGenerator, 'angularInitGenerator')
.mockImplementation(() => Promise.resolve(() => {}));
jest.clearAllMocks();
});
it('should initialize the Angular plugin when in an Nx workspace', async () => {
await ngAddGenerator(tree, { skipFormat: true });
expect(initGenerator.angularInitGenerator).toHaveBeenCalled();
expect(angularCliMigrator.migrateFromAngularCli).not.toHaveBeenCalled();
});
it('should perform a migration when in an Angular CLI workspace', async () => {
tree.delete('nx.json');
await ngAddGenerator(tree, { skipFormat: true });
expect(angularCliMigrator.migrateFromAngularCli).toHaveBeenCalled();
expect(initGenerator.angularInitGenerator).not.toHaveBeenCalled();
});
});

View File

@ -1,18 +1,9 @@
import type { Tree } from '@nx/devkit';
import { angularInitGenerator } from '../init/init';
import { migrateFromAngularCli } from './migrate-from-angular-cli';
import type { GeneratorOptions } from './schema';
function getWorkspaceType(tree: Tree): 'angular' | 'nx' {
return tree.exists('nx.json') ? 'nx' : 'angular';
}
export async function ngAddGenerator(tree: Tree, options: GeneratorOptions) {
if (getWorkspaceType(tree) === 'angular') {
return await migrateFromAngularCli(tree, options);
}
return await angularInitGenerator(tree, options);
return await migrateFromAngularCli(tree, options);
}
export default ngAddGenerator;

View File

@ -10,6 +10,7 @@ import {
writeJson,
} from '@nx/devkit';
import { Linter, lintInitGenerator } from '@nx/eslint';
import { setupRootEsLint } from '@nx/eslint/src/generators/lint-project/setup-root-eslint';
import {
getRootTsConfigPathInTree,
initGenerator as jsInitGenerator,
@ -189,25 +190,16 @@ export function updateRootEsLintConfig(
existingEsLintConfig: any | undefined,
unitTestRunner?: string
): void {
if (tree.exists('.eslintrc.json')) {
/**
* If it still exists it means that there was no project at the root of the
* workspace, so it was not moved. In that case, we remove the file so the
* init generator do its work. We still receive the content of the file,
* so we update it after the init generator has run.
*/
tree.delete('.eslintrc.json');
}
lintInitGenerator(tree, { linter: Linter.EsLint, unitTestRunner });
lintInitGenerator(tree, {});
if (!existingEsLintConfig) {
// There was no eslint config in the root, so we keep the generated one as-is.
// There was no eslint config in the root, so we set it up and use it as-is
setupRootEsLint(tree, { unitTestRunner });
return;
}
existingEsLintConfig.ignorePatterns = ['**/*'];
if (!(existingEsLintConfig.plugins ?? []).includes('@nrwl/nx')) {
if (!(existingEsLintConfig.plugins ?? []).includes('@nx')) {
existingEsLintConfig.plugins = Array.from(
new Set([...(existingEsLintConfig.plugins ?? []), '@nx'])
);

View File

@ -34,9 +34,7 @@ export async function storybookConfigurationGenerator(
await formatFiles(tree);
}
return () => {
storybookGeneratorInstallTask();
};
return storybookGeneratorInstallTask;
}
export default storybookConfigurationGenerator;

View File

@ -0,0 +1,55 @@
import { joinPathFragments, type Tree } from '@nx/devkit';
import { configurationGenerator } from '@nx/jest';
import { jestPresetAngularVersion } from '../../utils/versions';
import { addDependenciesToPackageJsonIfDontExist } from './version-utils';
export type AddJestOptions = {
name: string;
projectRoot: string;
skipPackageJson: boolean;
strict: boolean;
};
export async function addJest(
tree: Tree,
options: AddJestOptions
): Promise<void> {
if (!options.skipPackageJson) {
process.env.npm_config_legacy_peer_deps ??= 'true';
addDependenciesToPackageJsonIfDontExist(
tree,
{},
{ 'jest-preset-angular': jestPresetAngularVersion }
);
}
await configurationGenerator(tree, {
project: options.name,
setupFile: 'angular',
supportTsx: false,
skipSerializers: false,
skipPackageJson: options.skipPackageJson,
skipFormat: true,
});
const setupFile = joinPathFragments(
options.projectRoot,
'src',
'test-setup.ts'
);
if (options.strict && tree.exists(setupFile)) {
const contents = tree.read(setupFile, 'utf-8');
tree.write(
setupFile,
`// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment
globalThis.ngJest = {
testEnvironmentOptions: {
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
},
};
${contents}`
);
}
}

View File

@ -0,0 +1,99 @@
import { readJson, updateJson, type Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { angularDevkitVersion, angularVersion } from '../../utils/versions';
import { ensureAngularDependencies } from './ensure-angular-dependencies';
describe('ensureAngularDependencies', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should add angular dependencies', () => {
// ACT
ensureAngularDependencies(tree);
// ASSERT
const { dependencies, devDependencies } = readJson(tree, 'package.json');
expect(dependencies['@angular/animations']).toBe(angularVersion);
expect(dependencies['@angular/common']).toBe(angularVersion);
expect(dependencies['@angular/compiler']).toBe(angularVersion);
expect(dependencies['@angular/core']).toBe(angularVersion);
expect(dependencies['@angular/platform-browser']).toBe(angularVersion);
expect(dependencies['@angular/platform-browser-dynamic']).toBe(
angularVersion
);
expect(dependencies['@angular/router']).toBe(angularVersion);
expect(dependencies['rxjs']).toBeDefined();
expect(dependencies['tslib']).toBeDefined();
expect(dependencies['zone.js']).toBeDefined();
expect(devDependencies['@angular/cli']).toBe(angularDevkitVersion);
expect(devDependencies['@angular/compiler-cli']).toBe(angularVersion);
expect(devDependencies['@angular/language-service']).toBe(angularVersion);
expect(devDependencies['@angular-devkit/build-angular']).toBe(
angularDevkitVersion
);
// codelyzer should no longer be there by default
expect(devDependencies['codelyzer']).toBeUndefined();
});
it('should add angular dependencies respecting base packages versions', () => {
// ARRANGE
updateJson(tree, 'package.json', (json) => ({
...json,
dependencies: {
...json.dependencies,
'@angular/core': '~15.0.0',
},
devDependencies: {
...json.devDependencies,
'@angular-devkit/build-angular': '~15.0.0',
},
}));
// ACT
ensureAngularDependencies(tree);
// ASSERT
const { dependencies, devDependencies } = readJson(tree, 'package.json');
expect(dependencies['@angular/animations']).toBe('~15.0.0');
expect(dependencies['@angular/common']).toBe('~15.0.0');
expect(dependencies['@angular/compiler']).toBe('~15.0.0');
expect(dependencies['@angular/core']).toBe('~15.0.0');
expect(dependencies['@angular/platform-browser']).toBe('~15.0.0');
expect(dependencies['@angular/platform-browser-dynamic']).toBe('~15.0.0');
expect(dependencies['@angular/router']).toBe('~15.0.0');
expect(dependencies['rxjs']).toBeDefined();
expect(dependencies['tslib']).toBeDefined();
expect(dependencies['zone.js']).toBeDefined();
expect(devDependencies['@angular/cli']).toBe('~15.0.0');
expect(devDependencies['@angular/compiler-cli']).toBe('~15.0.0');
expect(devDependencies['@angular/language-service']).toBe('~15.0.0');
expect(devDependencies['@angular-devkit/build-angular']).toBe('~15.0.0');
});
it('should not overwrite already installed dependencies', () => {
// ARRANGE
updateJson(tree, 'package.json', (json) => ({
...json,
dependencies: {
...json.dependencies,
'@angular/animations': '~15.0.1',
'@angular/core': '~15.0.0',
},
}));
// ACT
ensureAngularDependencies(tree);
// ASSERT
const { dependencies } = readJson(tree, 'package.json');
expect(dependencies['@angular/animations']).toBe('~15.0.1');
expect(dependencies['@angular/core']).toBe('~15.0.0');
});
});

View File

@ -0,0 +1,48 @@
import type { GeneratorCallback, Tree } from '@nx/devkit';
import {
addDependenciesToPackageJsonIfDontExist,
getInstalledPackageVersion,
versions,
} from './version-utils';
export function ensureAngularDependencies(tree: Tree): GeneratorCallback {
const pkgVersions = versions(tree);
const angularVersion =
getInstalledPackageVersion(tree, '@angular/core') ??
pkgVersions.angularVersion;
const angularDevkitVersion =
getInstalledPackageVersion(tree, '@angular-devkit/build-angular') ??
pkgVersions.angularDevkitVersion;
const rxjsVersion =
getInstalledPackageVersion(tree, 'rxjs') ?? pkgVersions.rxjsVersion;
const tsLibVersion =
getInstalledPackageVersion(tree, 'tslib') ?? pkgVersions.tsLibVersion;
const zoneJsVersion =
getInstalledPackageVersion(tree, 'zone.js') ?? pkgVersions.zoneJsVersion;
return addDependenciesToPackageJsonIfDontExist(
tree,
{
'@angular/animations': angularVersion,
'@angular/common': angularVersion,
'@angular/compiler': angularVersion,
'@angular/core': angularVersion,
'@angular/forms': angularVersion,
'@angular/platform-browser': angularVersion,
'@angular/platform-browser-dynamic': angularVersion,
'@angular/router': angularVersion,
rxjs: rxjsVersion,
tslib: tsLibVersion,
'zone.js': zoneJsVersion,
},
{
'@angular/cli': angularDevkitVersion,
'@angular/compiler-cli': angularVersion,
'@angular/language-service': angularVersion,
'@angular-devkit/build-angular': angularDevkitVersion,
'@angular-devkit/schematics': angularDevkitVersion,
'@schematics/angular': angularDevkitVersion,
}
);
}

View File

@ -16,13 +16,16 @@ import {
updateJson,
updateProjectConfiguration,
} from '@nx/devkit';
import { getRelativePathToRootTsConfig } from '@nx/js';
import {
getRelativePathToRootTsConfig,
initGenerator as jsInitGenerator,
} from '@nx/js';
import { Linter } from '@nx/eslint';
import { join } from 'path';
import { addLinterToCyProject } from '../../utils/add-linter';
import { addDefaultE2EConfig } from '../../utils/config';
import { installedCypressVersion } from '../../utils/cypress-version';
import { viteVersion } from '../../utils/versions';
import { typesNodeVersion, viteVersion } from '../../utils/versions';
import cypressInitGenerator from '../init/init';
import { addBaseCypressSetup } from '../base-setup/base-setup';
@ -56,7 +59,8 @@ export async function configurationGenerator(
const tasks: GeneratorCallback[] = [];
if (!installedCypressVersion()) {
tasks.push(await cypressInitGenerator(tree, opts));
tasks.push(await jsInitGenerator(tree, { ...options, skipFormat: true }));
tasks.push(await cypressInitGenerator(tree, { ...opts, skipFormat: true }));
}
const projectGraph = await createProjectGraphAsync();
const nxJson = readNxJson(tree);
@ -65,9 +69,7 @@ export async function configurationGenerator(
? p === '@nx/cypress/plugin'
: p.plugin === '@nx/cypress/plugin'
);
if (opts.bundler === 'vite') {
tasks.push(addDependenciesToPackageJson(tree, {}, { vite: viteVersion }));
}
await addFiles(tree, opts, projectGraph, hasPlugin);
if (!hasPlugin) {
addTarget(tree, opts);
@ -79,6 +81,10 @@ export async function configurationGenerator(
});
tasks.push(linterTask);
if (!opts.skipPackageJson) {
tasks.push(ensureDependencies(tree, opts));
}
if (!opts.skipFormat) {
await formatFiles(tree);
}
@ -86,6 +92,18 @@ export async function configurationGenerator(
return runTasksInSerial(...tasks);
}
function ensureDependencies(tree: Tree, options: NormalizedSchema) {
const devDependencies: Record<string, string> = {
'@types/node': typesNodeVersion,
};
if (options.bundler === 'vite') {
devDependencies['vite'] = viteVersion;
}
return addDependenciesToPackageJson(tree, {}, devDependencies);
}
function normalizeOptions(tree: Tree, options: CypressE2EConfigSchema) {
const projectConfig = readProjectConfiguration(tree, options.project);
if (projectConfig?.targets?.e2e) {

View File

@ -18,13 +18,20 @@ import {
} from '@nx/devkit';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { checkAndCleanWithSemver } from '@nx/devkit/src/utils/semver';
import { getRelativePathToRootTsConfig } from '@nx/js';
import {
getRelativePathToRootTsConfig,
initGenerator as jsInitGenerator,
} from '@nx/js';
import { Linter } from '@nx/eslint';
import { join } from 'path';
import { major } from 'semver';
import { addLinterToCyProject } from '../../utils/add-linter';
import { installedCypressVersion } from '../../utils/cypress-version';
import { cypressVersion, viteVersion } from '../../utils/versions';
import {
cypressVersion,
typesNodeVersion,
viteVersion,
} from '../../utils/versions';
import { cypressInitGenerator } from '../init/init';
import { Schema } from './schema';
@ -186,18 +193,9 @@ export async function cypressProjectGeneratorInternal(
// if there is an installed cypress version, then we don't call
// init since we want to keep the existing version that is installed
if (!cypressVersion) {
tasks.push(await cypressInitGenerator(host, options));
}
if (schema.bundler === 'vite') {
tasks.push(await jsInitGenerator(host, { ...options, skipFormat: true }));
tasks.push(
addDependenciesToPackageJson(
host,
{},
{
vite: viteVersion,
}
)
await cypressInitGenerator(host, { ...options, skipFormat: true })
);
}
@ -211,12 +209,29 @@ export async function cypressProjectGeneratorInternal(
overwriteExisting: true,
});
tasks.push(installTask);
if (!options.skipPackageJson) {
tasks.push(ensureDependencies(host, options));
}
if (!options.skipFormat) {
await formatFiles(host);
}
return runTasksInSerial(...tasks);
}
function ensureDependencies(tree: Tree, options: CypressProjectSchema) {
const devDependencies: Record<string, string> = {
'@types/node': typesNodeVersion,
};
if (options.bundler === 'vite') {
devDependencies['vite'] = viteVersion;
}
return addDependenciesToPackageJson(tree, {}, devDependencies);
}
async function normalizeOptions(
host: Tree,
options: Schema

View File

@ -26,7 +26,6 @@ describe('init', () => {
expect(packageJson.devDependencies.cypress).toBeDefined();
expect(packageJson.devDependencies['@nx/cypress']).toBeDefined();
expect(packageJson.devDependencies['@types/node']).toBeDefined();
expect(packageJson.devDependencies[existing]).toBeDefined();
expect(packageJson.dependencies['@nx/cypress']).toBeUndefined();
expect(packageJson.dependencies[existing]).toBeDefined();

View File

@ -1,5 +1,6 @@
import {
addDependenciesToPackageJson,
formatFiles,
GeneratorCallback,
readNxJson,
removeDependenciesFromPackageJson,
@ -7,13 +8,8 @@ import {
Tree,
updateNxJson,
} from '@nx/devkit';
import {
cypressVersion,
nxVersion,
typesNodeVersion,
} from '../../utils/versions';
import { cypressVersion, nxVersion } from '../../utils/versions';
import { Schema } from './schema';
import { initGenerator } from '@nx/js';
import { CypressPluginOptions } from '../../plugins/plugin';
function setupE2ETargetDefaults(tree: Tree) {
@ -38,17 +34,21 @@ function setupE2ETargetDefaults(tree: Tree) {
}
function updateDependencies(tree: Tree) {
removeDependenciesFromPackageJson(tree, ['@nx/cypress'], []);
const tasks: GeneratorCallback[] = [];
tasks.push(removeDependenciesFromPackageJson(tree, ['@nx/cypress'], []));
return addDependenciesToPackageJson(
tree,
{},
{
['@nx/cypress']: nxVersion,
cypress: cypressVersion,
'@types/node': typesNodeVersion,
}
tasks.push(
addDependenciesToPackageJson(
tree,
{},
{
['@nx/cypress']: nxVersion,
cypress: cypressVersion,
}
)
);
return runTasksInSerial(...tasks);
}
function addPlugin(tree: Tree) {
@ -93,30 +93,24 @@ function updateProductionFileset(tree: Tree) {
}
export async function cypressInitGenerator(tree: Tree, options: Schema) {
const addPlugins = process.env.NX_PCV3 === 'true';
updateProductionFileset(tree);
if (!addPlugins) {
if (process.env.NX_PCV3 === 'true') {
addPlugin(tree);
} else {
setupE2ETargetDefaults(tree);
}
const tasks: GeneratorCallback[] = [];
tasks.push(
await initGenerator(tree, {
...options,
skipFormat: true,
})
);
if (addPlugins) {
addPlugin(tree);
}
let installTask: GeneratorCallback = () => {};
if (!options.skipPackageJson) {
tasks.push(updateDependencies(tree));
installTask = updateDependencies(tree);
}
return runTasksInSerial(...tasks);
if (!options.skipFormat) {
await formatFiles(tree);
}
return installTask;
}
export default cypressInitGenerator;

View File

@ -1,7 +1,4 @@
export interface Schema {
skipPackageJson?: boolean;
skipFormat?: boolean;
addPlugins?: boolean;
skipPackageJson?: boolean;
}

View File

@ -6,6 +6,12 @@
"description": "Add Cypress Configuration to the workspace.",
"type": "object",
"properties": {
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"skipPackageJson": {
"type": "boolean",
"default": false,

View File

@ -7,6 +7,7 @@ import { addProject } from './lib/add-project';
import { createFiles } from './lib/create-files';
import { normalizeOptions } from './lib/normalize-options';
import { Schema } from './schema';
import { ensureDependencies } from './lib/ensure-dependencies';
export async function detoxApplicationGenerator(host: Tree, schema: Schema) {
return await detoxApplicationGeneratorInternal(host, {
@ -30,12 +31,13 @@ export async function detoxApplicationGeneratorInternal(
addGitIgnoreEntry(host, options);
const lintingTask = await addLinting(host, options);
const depsTask = ensureDependencies(host, options);
if (!options.skipFormat) {
await formatFiles(host);
}
return runTasksInSerial(initTask, lintingTask);
return runTasksInSerial(initTask, lintingTask, depsTask);
}
export default detoxApplicationGenerator;

View File

@ -0,0 +1,21 @@
import { addDependenciesToPackageJson, type Tree } from '@nx/devkit';
import { jestVersion, typesNodeVersion } from '@nx/jest/src/utils/versions';
import {
configPluginsDetoxVersion,
testingLibraryJestDom,
} from '../../../utils/versions';
import type { NormalizedSchema } from './normalize-options';
export function ensureDependencies(tree: Tree, options: NormalizedSchema) {
const devDependencies: Record<string, string> = {
'@testing-library/jest-dom': testingLibraryJestDom,
'@types/node': typesNodeVersion,
'jest-circus': jestVersion,
};
if (options.framework === 'expo') {
devDependencies['@config-plugins/detox'] = configPluginsDetoxVersion;
}
return addDependenciesToPackageJson(tree, {}, devDependencies);
}

View File

@ -13,7 +13,6 @@ describe('init', () => {
await detoxInitGenerator(tree, {});
const packageJson = readJson(tree, 'package.json');
expect(packageJson.devDependencies['@nx/detox']).toBeDefined();
expect(packageJson.devDependencies['@types/node']).toBeDefined();
expect(packageJson.devDependencies['detox']).toBeDefined();
});
});

View File

@ -8,16 +8,9 @@ import {
Tree,
updateNxJson,
} from '@nx/devkit';
import { jestVersion, typesNodeVersion } from '@nx/jest/src/utils/versions';
import { Schema } from './schema';
import {
configPluginsDetoxVersion,
detoxVersion,
nxVersion,
testingLibraryJestDom,
} from '../../utils/versions';
import { DetoxPluginOptions } from '../../plugins/plugin';
import { detoxVersion, nxVersion } from '../../utils/versions';
import { Schema } from './schema';
export async function detoxInitGenerator(host: Tree, schema: Schema) {
const tasks: GeneratorCallback[] = [];
@ -45,12 +38,6 @@ export function updateDependencies(host: Tree, schema: Schema) {
{
'@nx/detox': nxVersion,
detox: detoxVersion,
'@testing-library/jest-dom': testingLibraryJestDom,
'@types/node': typesNodeVersion,
'jest-circus': jestVersion,
...(schema.framework === 'expo'
? { '@config-plugins/detox': configPluginsDetoxVersion }
: {}),
}
);
}

View File

@ -1,5 +1,4 @@
export interface Schema {
skipFormat?: boolean;
skipPackageJson?: boolean; //default is false
framework?: 'react-native' | 'expo';
}

View File

@ -15,12 +15,6 @@
"default": false,
"description": "Do not add dependencies to `package.json`.",
"x-priority": "internal"
},
"framework": {
"type": "string",
"description": "App framework to test",
"enum": ["react-native", "expo"],
"default": "react-native"
}
},
"required": []

View File

@ -1,23 +1,31 @@
import { addDependenciesToPackageJson, formatFiles, Tree } from '@nx/devkit';
import {
addDependenciesToPackageJson,
formatFiles,
GeneratorCallback,
Tree,
} from '@nx/devkit';
import { Schema } from './schema';
import { esbuildVersion } from '@nx/js/src/utils/versions';
import { nxVersion } from '../../utils/versions';
export async function esbuildInitGenerator(tree: Tree, schema: Schema) {
const task = addDependenciesToPackageJson(
tree,
{},
{
'@nx/esbuild': nxVersion,
esbuild: esbuildVersion,
}
);
let installTask: GeneratorCallback = () => {};
if (!schema.skipPackageJson) {
installTask = addDependenciesToPackageJson(
tree,
{},
{
'@nx/esbuild': nxVersion,
esbuild: esbuildVersion,
}
);
}
if (!schema.skipFormat) {
await formatFiles(tree);
}
return task;
return installTask;
}
export default esbuildInitGenerator;

View File

@ -1,4 +1,4 @@
export interface Schema {
compiler?: 'babel' | 'swc' | 'tsc';
skipFormat?: boolean;
skipPackageJson?: boolean;
}

View File

@ -6,16 +6,15 @@
"description": "Init Webpack Plugin.",
"type": "object",
"properties": {
"compiler": {
"type": "string",
"enum": ["babel", "swc", "tsc"],
"description": "The compiler to initialize for.",
"default": "babel"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
},
"skipPackageJson": {
"description": "Do not add dependencies to `package.json`.",
"type": "boolean",
"default": false
}
},
"required": []

View File

@ -1,61 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`@nx/eslint:init should generate the global eslint config 1`] = `
"{
"root": true,
"ignorePatterns": [
"**/*"
],
"plugins": [
"@nx"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": [
"*"
]
}
]
}
]
}
},
{
"files": [
"*.ts",
"*.tsx"
],
"extends": [
"plugin:@nx/typescript"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"extends": [
"plugin:@nx/javascript"
],
"rules": {}
}
]
}
"
`;

View File

@ -16,22 +16,8 @@ describe('@nx/eslint:init', () => {
process.env.NX_PCV3 = envV3;
});
it('should generate the global eslint config', async () => {
await lintInitGenerator(tree, {
linter: Linter.EsLint,
});
expect(tree.read('.eslintrc.json', 'utf-8')).toMatchSnapshot();
expect(tree.read('.eslintignore', 'utf-8')).toMatchInlineSnapshot(`
"node_modules
"
`);
});
it('should add the root eslint config to the lint targetDefaults for lint', async () => {
await lintInitGenerator(tree, {
linter: Linter.EsLint,
});
it('should add the root eslint config to the lint targetDefaults for lint', () => {
lintInitGenerator(tree, {});
expect(readJson(tree, 'nx.json').targetDefaults['@nx/eslint:lint']).toEqual(
{
@ -46,24 +32,22 @@ describe('@nx/eslint:init', () => {
);
});
it('should not generate the global eslint config if it already exist', async () => {
it('should not generate the global eslint config if it already exist', () => {
tree.write('.eslintrc.js', '{}');
await lintInitGenerator(tree, {
linter: Linter.EsLint,
});
lintInitGenerator(tree, {});
expect(tree.exists('.eslintrc.json')).toBe(false);
});
it('should setup lint target defaults', async () => {
it('should setup lint target defaults', () => {
updateJson<NxJsonConfiguration>(tree, 'nx.json', (json) => {
json.namedInputs ??= {};
json.namedInputs.production = ['default'];
return json;
});
await lintInitGenerator(tree, {});
lintInitGenerator(tree, {});
expect(
readJson<NxJsonConfiguration>(tree, 'nx.json').targetDefaults[
@ -80,7 +64,7 @@ describe('@nx/eslint:init', () => {
});
});
it('should setup @nx/eslint/plugin', async () => {
it('should setup @nx/eslint/plugin', () => {
process.env.NX_PCV3 = 'true';
updateJson<NxJsonConfiguration>(tree, 'nx.json', (json) => {
json.namedInputs ??= {};
@ -88,7 +72,7 @@ describe('@nx/eslint:init', () => {
return json;
});
await lintInitGenerator(tree, {});
lintInitGenerator(tree, {});
expect(
readJson<NxJsonConfiguration>(tree, 'nx.json').targetDefaults[
@ -108,20 +92,20 @@ describe('@nx/eslint:init', () => {
`);
});
it('should add @nx/eslint/plugin in subsequent step', async () => {
it('should add @nx/eslint/plugin in subsequent step', () => {
updateJson<NxJsonConfiguration>(tree, 'nx.json', (json) => {
json.namedInputs ??= {};
json.namedInputs.production = ['default'];
return json;
});
await lintInitGenerator(tree, {});
lintInitGenerator(tree, {});
expect(
readJson<NxJsonConfiguration>(tree, 'nx.json').plugins
).not.toBeDefined();
process.env.NX_PCV3 = 'true';
await lintInitGenerator(tree, {});
lintInitGenerator(tree, {});
expect(readJson<NxJsonConfiguration>(tree, 'nx.json').plugins)
.toMatchInlineSnapshot(`
[

View File

@ -3,28 +3,16 @@ import {
addDependenciesToPackageJson,
readNxJson,
removeDependenciesFromPackageJson,
updateJson,
runTasksInSerial,
updateNxJson,
writeJson,
} from '@nx/devkit';
import {
eslintConfigPrettierVersion,
eslintVersion,
nxVersion,
typescriptESLintVersion,
} from '../../utils/versions';
import { Linter } from '../utils/linter';
import { eslintVersion, nxVersion } from '../../utils/versions';
import { findEslintFile } from '../utils/eslint-file';
import { getGlobalEsLintConfiguration } from './global-eslint-config';
import { EslintPluginOptions } from '../../plugins/plugin';
import { hasEslintPlugin } from '../utils/plugin';
export interface LinterInitOptions {
linter?: Linter;
unitTestRunner?: string;
skipPackageJson?: boolean;
rootProject?: boolean;
}
function updateProductionFileset(tree: Tree) {
@ -78,22 +66,6 @@ function addPlugin(tree: Tree) {
updateNxJson(tree, nxJson);
}
function updateVSCodeExtensions(tree: Tree) {
if (tree.exists('.vscode/extensions.json')) {
updateJson(tree, '.vscode/extensions.json', (json) => {
json.recommendations ||= [];
const extension = 'dbaeumer.vscode-eslint';
if (!json.recommendations.includes(extension)) {
json.recommendations.push(extension);
}
return json;
});
}
}
/**
* Initializes ESLint configuration in a workspace and adds necessary dependencies.
*/
function initEsLint(tree: Tree, options: LinterInitOptions): GeneratorCallback {
const addPlugins = process.env.NX_PCV3 === 'true';
const hasPlugin = hasEslintPlugin(tree);
@ -108,17 +80,6 @@ function initEsLint(tree: Tree, options: LinterInitOptions): GeneratorCallback {
return () => {};
}
if (!options.skipPackageJson) {
removeDependenciesFromPackageJson(tree, ['@nx/eslint'], []);
}
writeJson(
tree,
'.eslintrc.json',
getGlobalEsLintConfiguration(options.unitTestRunner, options.rootProject)
);
tree.write('.eslintignore', 'node_modules\n');
updateProductionFileset(tree);
if (addPlugins) {
@ -127,22 +88,22 @@ function initEsLint(tree: Tree, options: LinterInitOptions): GeneratorCallback {
addTargetDefaults(tree);
}
updateVSCodeExtensions(tree);
return !options.skipPackageJson
? addDependenciesToPackageJson(
const tasks: GeneratorCallback[] = [];
if (!options.skipPackageJson) {
tasks.push(removeDependenciesFromPackageJson(tree, ['@nx/eslint'], []));
tasks.push(
addDependenciesToPackageJson(
tree,
{},
{
'@nx/eslint': nxVersion,
'@nx/eslint-plugin': nxVersion,
'@typescript-eslint/parser': typescriptESLintVersion,
'@typescript-eslint/eslint-plugin': typescriptESLintVersion,
eslint: eslintVersion,
'eslint-config-prettier': eslintConfigPrettierVersion,
}
)
: () => {};
);
}
return runTasksInSerial(...tasks);
}
export function lintInitGenerator(tree: Tree, options: LinterInitOptions) {

View File

@ -9,7 +9,6 @@ import {
import { Linter } from '../utils/linter';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { lintProjectGenerator } from './lint-project';
import { eslintVersion } from '../../utils/versions';
describe('@nx/eslint:lint-project', () => {
let tree: Tree;
@ -272,4 +271,55 @@ describe('@nx/eslint:lint-project', () => {
const eslintConfig = readJson(tree, 'libs/test-lib/.eslintrc.json');
expect(eslintConfig.extends).toBeUndefined();
});
it('should generate the global eslint config', async () => {
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
});
expect(tree.read('.eslintrc.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"root": true,
"ignorePatterns": ["**/*"],
"plugins": ["@nx"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": ["*"]
}
]
}
]
}
},
{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nx/typescript"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nx/javascript"],
"rules": {}
}
]
}
"
`);
expect(tree.read('.eslintignore', 'utf-8')).toMatchInlineSnapshot(`
"node_modules
"
`);
});
});

View File

@ -1,4 +1,5 @@
import type {
GeneratorCallback,
NxJsonConfiguration,
ProjectConfiguration,
Tree,
@ -8,6 +9,7 @@ import {
offsetFromRoot,
readJson,
readProjectConfiguration,
runTasksInSerial,
updateJson,
updateProjectConfiguration,
writeJson,
@ -35,6 +37,7 @@ import {
baseEsLintFlatConfigFile,
} from '../../utils/config-file';
import { hasEslintPlugin } from '../utils/plugin';
import { setupRootEsLint } from './setup-root-eslint';
interface LintProjectOptions {
project: string;
@ -52,12 +55,17 @@ export async function lintProjectGenerator(
tree: Tree,
options: LintProjectOptions
) {
const installTask = lintInitGenerator(tree, {
linter: options.linter,
const tasks: GeneratorCallback[] = [];
const initTask = lintInitGenerator(tree, {
skipPackageJson: options.skipPackageJson,
});
tasks.push(initTask);
const rootEsLintTask = setupRootEsLint(tree, {
unitTestRunner: options.unitTestRunner,
skipPackageJson: options.skipPackageJson,
rootProject: options.rootProject,
});
tasks.push(rootEsLintTask);
const projectConfig = readProjectConfiguration(tree, options.project);
let lintFilePatterns = options.eslintFilePatterns;
@ -154,7 +162,7 @@ export async function lintProjectGenerator(
await formatFiles(tree);
}
return installTask;
return runTasksInSerial(...tasks);
}
function createEsLintConfiguration(

View File

@ -0,0 +1,58 @@
import {
addDependenciesToPackageJson,
writeJson,
type GeneratorCallback,
type Tree,
} from '@nx/devkit';
import {
eslintConfigPrettierVersion,
nxVersion,
typescriptESLintVersion,
} from '../../utils/versions';
import { getGlobalEsLintConfiguration } from '../init/global-eslint-config';
import { findEslintFile } from '../utils/eslint-file';
export type SetupRootEsLintOptions = {
unitTestRunner?: string;
skipPackageJson?: boolean;
rootProject?: boolean;
};
export function setupRootEsLint(
tree: Tree,
options: SetupRootEsLintOptions
): GeneratorCallback {
const rootEslintFile = findEslintFile(tree);
if (rootEslintFile) {
return () => {};
}
writeJson(
tree,
'.eslintrc.json',
getGlobalEsLintConfiguration(options.unitTestRunner, options.rootProject)
);
if (tree.exists('.eslintignore')) {
let content = tree.read('.eslintignore', 'utf-8');
if (!/^node_modules$/gm.test(content)) {
content = `${content}\nnode_modules\n`;
tree.write('.eslintignore', content);
}
} else {
tree.write('.eslintignore', 'node_modules\n');
}
return !options.skipPackageJson
? addDependenciesToPackageJson(
tree,
{},
{
'@nx/eslint-plugin': nxVersion,
'@typescript-eslint/parser': typescriptESLintVersion,
'@typescript-eslint/eslint-plugin': typescriptESLintVersion,
'eslint-config-prettier': eslintConfigPrettierVersion,
}
)
: () => {};
}

View File

@ -5,6 +5,7 @@ import {
runTasksInSerial,
Tree,
} from '@nx/devkit';
import { initGenerator as jsInitGenerator } from '@nx/js';
import { runSymlink } from '../../utils/symlink-task';
import { addLinting } from '../../utils/add-linting';
@ -17,6 +18,8 @@ import { createApplicationFiles } from './lib/create-application-files';
import { addEasScripts } from './lib/add-eas-scripts';
import { addDetox } from './lib/add-detox';
import { Schema } from './schema';
import { ensureDependencies } from '../../utils/ensure-dependencies';
import { initRootBabelConfig } from '../../utils/init-root-babel-config';
export async function expoApplicationGenerator(
host: Tree,
@ -34,7 +37,18 @@ export async function expoApplicationGeneratorInternal(
): Promise<GeneratorCallback> {
const options = await normalizeOptions(host, schema);
const tasks: GeneratorCallback[] = [];
const jsInitTask = await jsInitGenerator(host, {
...schema,
skipFormat: true,
});
tasks.push(jsInitTask);
const initTask = await initGenerator(host, { ...options, skipFormat: true });
tasks.push(initTask);
if (!options.skipPackageJson) {
tasks.push(ensureDependencies(host));
}
initRootBabelConfig(host);
createApplicationFiles(host, options);
addProject(host, options);
@ -46,6 +60,7 @@ export async function expoApplicationGeneratorInternal(
joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
],
});
tasks.push(lintTask);
const jestTask = await addJest(
host,
@ -55,15 +70,18 @@ export async function expoApplicationGeneratorInternal(
options.js,
options.skipPackageJson
);
tasks.push(jestTask);
const detoxTask = await addDetox(host, options);
tasks.push(detoxTask);
const symlinkTask = runSymlink(host.root, options.appProjectRoot);
tasks.push(symlinkTask);
addEasScripts(host);
if (!options.skipFormat) {
await formatFiles(host);
}
return runTasksInSerial(initTask, lintTask, jestTask, detoxTask, symlinkTask);
return runTasksInSerial(...tasks);
}
export default expoApplicationGenerator;

View File

@ -11,12 +11,11 @@ describe('init', () => {
});
it('should add react native dependencies', async () => {
await expoInitGenerator(tree, { e2eTestRunner: 'none' });
await expoInitGenerator(tree, {});
const packageJson = readJson(tree, 'package.json');
expect(packageJson.dependencies['react']).toBeDefined();
expect(packageJson.dependencies['expo']).toBeDefined();
expect(packageJson.dependencies['react-native']).toBeDefined();
expect(packageJson.devDependencies['@types/react']).toBeDefined();
});
it('should add .gitignore entries for React native files and directories', async () => {
@ -26,7 +25,7 @@ describe('init', () => {
/node_modules
`
);
await expoInitGenerator(tree, { e2eTestRunner: 'none' });
await expoInitGenerator(tree, {});
const content = tree.read('/.gitignore').toString();

View File

@ -8,73 +8,32 @@ import {
Tree,
updateNxJson,
} from '@nx/devkit';
import { Schema } from './schema';
import { ExpoPluginOptions } from '../../../plugins/plugin';
import {
babelPresetExpoVersion,
easCliVersion,
expoCliVersion,
expoMetroConfigVersion,
expoSplashScreenVersion,
expoStatusBarVersion,
expoVersion,
jestExpoVersion,
metroVersion,
nxVersion,
reactDomVersion,
reactNativeSvgTransformerVersion,
reactNativeSvgVersion,
reactNativeVersion,
reactNativeWebVersion,
reactTestRendererVersion,
reactVersion,
testingLibraryJestNativeVersion,
testingLibraryReactNativeVersion,
typesReactVersion,
} from '../../utils/versions';
import { ExpoPluginOptions } from '../../../plugins/plugin';
import { jestInitGenerator } from '@nx/jest';
import { detoxInitGenerator } from '@nx/detox';
import { initGenerator as jsInitGenerator } from '@nx/js';
import { addGitIgnoreEntry } from './lib/add-git-ignore-entry';
import { initRootBabelConfig } from './lib/init-root-babel-config';
import { Schema } from './schema';
export async function expoInitGenerator(host: Tree, schema: Schema) {
addGitIgnoreEntry(host);
initRootBabelConfig(host);
const tasks: GeneratorCallback[] = [];
tasks.push(
await jsInitGenerator(host, {
...schema,
skipFormat: true,
})
);
if (!schema.skipPackageJson) {
tasks.push(moveDependency(host));
tasks.push(updateDependencies(host));
}
if (!schema.unitTestRunner || schema.unitTestRunner === 'jest') {
const jestTask = await jestInitGenerator(host, schema);
tasks.push(jestTask);
}
if (!schema.e2eTestRunner || schema.e2eTestRunner === 'detox') {
const detoxTask = await detoxInitGenerator(host, {
...schema,
skipFormat: true,
});
tasks.push(detoxTask);
}
if (process.env.NX_PCV3 === 'true') {
addPlugin(host);
}
const tasks: GeneratorCallback[] = [];
if (!schema.skipPackageJson) {
tasks.push(moveDependency(host));
tasks.push(updateDependencies(host));
}
if (!schema.skipFormat) {
await formatFiles(host);
}
@ -90,25 +49,11 @@ export function updateDependencies(host: Tree) {
'react-dom': reactDomVersion,
'react-native': reactNativeVersion,
expo: expoVersion,
'expo-splash-screen': expoSplashScreenVersion,
'expo-status-bar': expoStatusBarVersion,
'react-native-web': reactNativeWebVersion,
'@expo/metro-config': expoMetroConfigVersion,
'react-native-svg-transformer': reactNativeSvgTransformerVersion,
'react-native-svg': reactNativeSvgVersion,
},
{
'@nx/expo': nxVersion,
'@types/react': typesReactVersion,
metro: metroVersion,
'metro-resolver': metroVersion,
'react-test-renderer': reactTestRendererVersion,
'@testing-library/react-native': testingLibraryReactNativeVersion,
'@testing-library/jest-native': testingLibraryJestNativeVersion,
'jest-expo': jestExpoVersion,
'@expo/cli': expoCliVersion,
'eas-cli': easCliVersion,
'babel-preset-expo': babelPresetExpoVersion,
}
);
}

View File

@ -1,7 +1,4 @@
export interface Schema {
unitTestRunner?: 'jest' | 'none';
skipFormat?: boolean;
e2eTestRunner?: 'detox' | 'none';
skipPackageJson?: boolean; // default is false
js?: boolean;
}

View File

@ -5,33 +5,16 @@
"description": "Add Nx Expo Schematics.",
"type": "object",
"properties": {
"unitTestRunner": {
"description": "Adds the specified unit test runner",
"type": "string",
"enum": ["jest", "none"],
"default": "jest"
},
"skipFormat": {
"description": "Skip formatting files",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"e2eTestRunner": {
"description": "Adds the specified e2e test runner",
"type": "string",
"enum": ["detox", "none"],
"default": "detox"
},
"skipPackageJson": {
"type": "boolean",
"default": false,
"description": "Do not add dependencies to `package.json`."
},
"js": {
"type": "boolean",
"default": false,
"description": "Use JavaScript instead of TypeScript"
}
},
"required": []

View File

@ -7,14 +7,19 @@ import {
joinPathFragments,
names,
offsetFromRoot,
ProjectConfiguration,
runTasksInSerial,
TargetConfiguration,
toJS,
Tree,
updateJson,
updateProjectConfiguration,
} from '@nx/devkit';
import { addTsConfigPath, getRelativePathToRootTsConfig } from '@nx/js';
import {
addTsConfigPath,
getRelativePathToRootTsConfig,
initGenerator as jsInitGenerator,
} from '@nx/js';
import init from '../init/init';
import { addLinting } from '../../utils/add-linting';
import { addJest } from '../../utils/add-jest';
@ -25,6 +30,8 @@ import {
} from '../../utils/versions';
import { NormalizedSchema, normalizeOptions } from './lib/normalize-options';
import { Schema } from './schema';
import { ensureDependencies } from '../../utils/ensure-dependencies';
import { initRootBabelConfig } from '../../utils/init-root-babel-config';
export async function expoLibraryGenerator(
host: Tree,
@ -49,12 +56,17 @@ export async function expoLibraryGeneratorInternal(
const tasks: GeneratorCallback[] = [];
const initTask = await init(host, {
...options,
const jsInitTask = await jsInitGenerator(host, {
...schema,
skipFormat: true,
e2eTestRunner: 'none',
});
tasks.push(jsInitTask);
const initTask = await init(host, { ...options, skipFormat: true });
tasks.push(initTask);
if (!options.skipPackageJson) {
tasks.push(ensureDependencies(host));
}
initRootBabelConfig(host);
const addProjectTask = await addProject(host, options);
if (addProjectTask) {
@ -103,54 +115,58 @@ export async function expoLibraryGeneratorInternal(
return runTasksInSerial(...tasks);
}
async function addProject(host: Tree, options: NormalizedSchema) {
const targets: { [key: string]: TargetConfiguration } = {};
let task: GeneratorCallback;
if (options.publishable || options.buildable) {
const { rollupInitGenerator } = ensurePackage<typeof import('@nx/rollup')>(
'@nx/rollup',
nxVersion
);
const external = [
'react/jsx-runtime',
'react-native',
'react',
'react-dom',
];
targets.build = {
executor: '@nx/rollup:rollup',
outputs: ['{options.outputPath}'],
options: {
outputPath: `dist/${options.projectRoot}`,
tsConfig: `${options.projectRoot}/tsconfig.lib.json`,
project: `${options.projectRoot}/package.json`,
entryFile: maybeJs(options, `${options.projectRoot}/src/index.ts`),
external,
rollupConfig: `@nx/react/plugins/bundle-rollup`,
assets: [
{
glob: `${options.projectRoot}/README.md`,
input: '.',
output: '.',
},
],
},
};
task = await rollupInitGenerator(host, { ...options, skipFormat: true });
}
addProjectConfiguration(host, options.name, {
async function addProject(
host: Tree,
options: NormalizedSchema
): Promise<GeneratorCallback> {
const project: ProjectConfiguration = {
root: options.projectRoot,
sourceRoot: joinPathFragments(options.projectRoot, 'src'),
projectType: 'library',
tags: options.parsedTags,
targets,
targets: {},
};
addProjectConfiguration(host, options.name, project);
if (!options.publishable && !options.buildable) {
return () => {};
}
const { configurationGenerator } = ensurePackage<typeof import('@nx/rollup')>(
'@nx/rollup',
nxVersion
);
const rollupConfigTask = await configurationGenerator(host, {
...options,
project: options.name,
skipFormat: true,
});
return task;
const external = ['react/jsx-runtime', 'react-native', 'react', 'react-dom'];
project.targets.build = {
executor: '@nx/rollup:rollup',
outputs: ['{options.outputPath}'],
options: {
outputPath: `dist/${options.projectRoot}`,
tsConfig: `${options.projectRoot}/tsconfig.lib.json`,
project: `${options.projectRoot}/package.json`,
entryFile: maybeJs(options, `${options.projectRoot}/src/index.ts`),
external,
rollupConfig: `@nx/react/plugins/bundle-rollup`,
assets: [
{
glob: `${options.projectRoot}/README.md`,
input: '.',
output: '.',
},
],
},
};
updateProjectConfiguration(host, options.name, project);
return rollupConfigTask;
}
function updateTsConfig(tree: Tree, options: NormalizedSchema) {

View File

@ -0,0 +1,44 @@
import {
addDependenciesToPackageJson,
type GeneratorCallback,
type Tree,
} from '@nx/devkit';
import {
babelPresetExpoVersion,
expoMetroConfigVersion,
expoSplashScreenVersion,
expoStatusBarVersion,
jestExpoVersion,
metroVersion,
reactNativeSvgTransformerVersion,
reactNativeSvgVersion,
reactNativeWebVersion,
reactTestRendererVersion,
testingLibraryJestNativeVersion,
testingLibraryReactNativeVersion,
typesReactVersion,
} from './versions';
export function ensureDependencies(host: Tree): GeneratorCallback {
return addDependenciesToPackageJson(
host,
{
'expo-splash-screen': expoSplashScreenVersion,
'expo-status-bar': expoStatusBarVersion,
'react-native-web': reactNativeWebVersion,
'@expo/metro-config': expoMetroConfigVersion,
'react-native-svg-transformer': reactNativeSvgTransformerVersion,
'react-native-svg': reactNativeSvgVersion,
},
{
'@types/react': typesReactVersion,
metro: metroVersion,
'metro-resolver': metroVersion,
'react-test-renderer': reactTestRendererVersion,
'@testing-library/react-native': testingLibraryReactNativeVersion,
'@testing-library/jest-native': testingLibraryJestNativeVersion,
'jest-expo': jestExpoVersion,
'babel-preset-expo': babelPresetExpoVersion,
}
);
}

View File

@ -1,8 +1,16 @@
import type { Tree } from '@nx/devkit';
import { formatFiles, toJS, updateJson } from '@nx/devkit';
import type { GeneratorCallback, Tree } from '@nx/devkit';
import {
addDependenciesToPackageJson,
formatFiles,
runTasksInSerial,
toJS,
updateJson,
} from '@nx/devkit';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { applicationGenerator as nodeApplicationGenerator } from '@nx/node';
import { tslibVersion } from '@nx/node/src/utils/versions';
import { join } from 'path';
import { nxVersion } from '../../utils/versions';
import { initGenerator } from '../init/init';
import type { Schema } from './schema';
@ -63,23 +71,28 @@ export async function applicationGenerator(tree: Tree, schema: Schema) {
export async function applicationGeneratorInternal(tree: Tree, schema: Schema) {
const options = await normalizeOptions(tree, schema);
const tasks: GeneratorCallback[] = [];
const initTask = await initGenerator(tree, { ...options, skipFormat: true });
tasks.push(initTask);
const applicationTask = await nodeApplicationGenerator(tree, {
...schema,
bundler: 'webpack',
skipFormat: true,
});
tasks.push(applicationTask);
addMainFile(tree, options);
addTypes(tree, options);
if (!options.skipPackageJson) {
tasks.push(ensureDependencies(tree));
}
if (!options.skipFormat) {
await formatFiles(tree);
}
return async () => {
await initTask();
await applicationTask();
};
return runTasksInSerial(...tasks);
}
export default applicationGenerator;
@ -107,3 +120,11 @@ async function normalizeOptions(
appProjectRoot,
};
}
function ensureDependencies(tree: Tree): GeneratorCallback {
return addDependenciesToPackageJson(
tree,
{ tslib: tslibVersion },
{ '@nx/express': nxVersion }
);
}

View File

@ -22,22 +22,11 @@ describe('init', () => {
const packageJson = readJson(tree, 'package.json');
// add express
expect(packageJson.dependencies['express']).toBeDefined();
// add tslib
expect(packageJson.dependencies['tslib']).toBeDefined();
// move `@nx/express` to dev
expect(packageJson.dependencies['@nx/express']).toBeUndefined();
expect(packageJson.devDependencies['@nx/express']).toBeDefined();
// add express types
expect(packageJson.devDependencies['@types/express']).toBeDefined();
// keep existing packages
expect(packageJson.devDependencies[existing]).toBeDefined();
expect(packageJson.dependencies[existing]).toBeDefined();
});
it('should not add jest config if unitTestRunner is none', async () => {
await initGenerator(tree, {
unitTestRunner: 'none',
});
expect(tree.exists('jest.config.js')).toEqual(false);
});
});

View File

@ -1,48 +1,41 @@
import {
addDependenciesToPackageJson,
formatFiles,
GeneratorCallback,
removeDependenciesFromPackageJson,
runTasksInSerial,
Tree,
} from '@nx/devkit';
import { initGenerator as nodeInitGenerator } from '@nx/node';
import { tslibVersion } from '@nx/node/src/utils/versions';
import {
expressTypingsVersion,
expressVersion,
nxVersion,
} from '../../utils/versions';
import { expressVersion, nxVersion } from '../../utils/versions';
import type { Schema } from './schema';
function updateDependencies(tree: Tree) {
removeDependenciesFromPackageJson(tree, ['@nx/express'], []);
const tasks: GeneratorCallback[] = [];
return addDependenciesToPackageJson(
tree,
{
express: expressVersion,
tslib: tslibVersion,
},
{
'@types/express': expressTypingsVersion,
'@nx/express': nxVersion,
}
tasks.push(removeDependenciesFromPackageJson(tree, ['@nx/express'], []));
tasks.push(
addDependenciesToPackageJson(
tree,
{ express: expressVersion },
{ '@nx/express': nxVersion }
)
);
return runTasksInSerial(...tasks);
}
export async function initGenerator(tree: Tree, schema: Schema) {
const initTask = await nodeInitGenerator(tree, {
...schema,
skipFormat: true,
});
const installTask = updateDependencies(tree);
let installTask: GeneratorCallback = () => {};
if (!schema.skipPackageJson) {
installTask = updateDependencies(tree);
}
if (!schema.skipFormat) {
await formatFiles(tree);
}
return async () => {
await initTask();
await installTask();
};
return installTask;
}
export default initGenerator;

View File

@ -1,4 +1,4 @@
export interface Schema {
unitTestRunner?: 'jest' | 'none';
skipFormat?: boolean;
skipPackageJson?: boolean;
}

View File

@ -6,16 +6,15 @@
"description": "Init Express Plugin.",
"type": "object",
"properties": {
"unitTestRunner": {
"description": "Adds the specified unit test runner.",
"type": "string",
"enum": ["jest", "none"],
"default": "jest"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
},
"skipPackageJson": {
"type": "boolean",
"default": false,
"description": "Do not add dependencies to `package.json`."
}
},
"required": []

View File

@ -1,7 +1,10 @@
import init from '../init/init';
import { jestInitGenerator } from '../init/init';
import { checkForTestTarget } from './lib/check-for-test-target';
import { createFiles } from './lib/create-files';
import { createJestConfig } from './lib/create-jest-config';
import { ensureDependencies } from './lib/ensure-dependencies';
import { updateTsConfig } from './lib/update-tsconfig';
import { updateVsCodeRecommendedExtensions } from './lib/update-vscode-recommended-extensions';
import { updateWorkspace } from './lib/update-workspace';
import { JestProjectSchema, NormalizedJestProjectSchema } from './schema';
import {
@ -10,7 +13,9 @@ import {
GeneratorCallback,
readProjectConfiguration,
readNxJson,
runTasksInSerial,
} from '@nx/devkit';
import { initGenerator as jsInitGenerator } from '@nx/js';
const schemaDefaults = {
setupFile: 'none',
@ -61,11 +66,20 @@ export async function configurationGenerator(
schema: JestProjectSchema
): Promise<GeneratorCallback> {
const options = normalizeOptions(tree, schema);
const installTask = await init(tree, options);
const tasks: GeneratorCallback[] = [];
tasks.push(await jsInitGenerator(tree, { ...schema, skipFormat: true }));
tasks.push(await jestInitGenerator(tree, options));
if (!schema.skipPackageJson) {
tasks.push(ensureDependencies(tree, options));
}
await createJestConfig(tree, options);
checkForTestTarget(tree, options);
createFiles(tree, options);
updateTsConfig(tree, options);
updateVsCodeRecommendedExtensions(tree);
const nxJson = readNxJson(tree);
const hasPlugin = nxJson.plugins?.some((p) =>
@ -80,7 +94,8 @@ export async function configurationGenerator(
if (!schema.skipFormat) {
await formatFiles(tree);
}
return installTask;
return runTasksInSerial(...tasks);
}
export default configurationGenerator;

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`jest should generate files 1`] = `
exports[`createJestConfig should generate files 1`] = `
"import { getJestProjects } from '@nx/jest';
export default {
@ -8,13 +8,13 @@ projects: getJestProjects()
};"
`;
exports[`jest should generate files 2`] = `
exports[`createJestConfig should generate files 2`] = `
"const nxPreset = require('@nx/jest/preset').default;
module.exports = { ...nxPreset }"
`;
exports[`jest should generate files with --js flag 1`] = `
exports[`createJestConfig should generate files with --js flag 1`] = `
"const { getJestProjects } = require('@nx/jest');
module.exports = {
@ -22,7 +22,7 @@ projects: getJestProjects()
};"
`;
exports[`jest should generate files with --js flag 2`] = `
exports[`createJestConfig should generate files with --js flag 2`] = `
"const nxPreset = require('@nx/jest/preset').default;
module.exports = { ...nxPreset }"

View File

@ -0,0 +1,224 @@
let projectGraph: ProjectGraph;
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
return projectGraph;
}),
}));
import {
addProjectConfiguration as _addProjectConfiguration,
readProjectConfiguration,
stripIndents,
type ProjectConfiguration,
type ProjectGraph,
type Tree,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { createJestConfig } from './create-jest-config';
function addProjectConfiguration(
tree: Tree,
name: string,
project: ProjectConfiguration
) {
_addProjectConfiguration(tree, name, project);
projectGraph.nodes[name] = {
name: name,
type: 'lib',
data: {
root: project.root,
targets: project.targets,
},
};
}
describe('createJestConfig', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
projectGraph = {
nodes: {},
dependencies: {},
externalNodes: {},
};
});
it('should generate files with --js flag', async () => {
await createJestConfig(tree, { js: true });
expect(tree.exists('jest.config.js')).toBeTruthy();
expect(
stripIndents`${tree.read('jest.config.js', 'utf-8')}`
).toMatchSnapshot();
expect(
stripIndents`${tree.read('jest.preset.js', 'utf-8')}`
).toMatchSnapshot();
});
it('should generate files ', async () => {
await createJestConfig(tree, {});
expect(tree.exists('jest.config.ts')).toBeTruthy();
expect(
stripIndents`${tree.read('jest.config.ts', 'utf-8')}`
).toMatchSnapshot();
expect(
stripIndents`${tree.read('jest.preset.js', 'utf-8')}`
).toMatchSnapshot();
});
it('should not override existing files', async () => {
addProjectConfiguration(tree, 'my-project', {
root: 'apps/my-app',
name: 'my-app',
sourceRoot: 'apps/my-app/src',
targets: {
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'apps/my-app/jest.config.ts',
},
},
},
});
const expected = stripIndents`
import { getJestProjects } from '@nx/jest';
export default {
projects: getJestProjects(),
extraThing: "Goes Here"
}
`;
tree.write('jest.config.ts', expected);
await createJestConfig(tree, {});
expect(tree.read('jest.config.ts', 'utf-8')).toEqual(expected);
});
it('should make js jest files', async () => {
await createJestConfig(tree, { js: true });
expect(tree.exists('jest.config.js')).toBeTruthy();
expect(tree.exists('jest.preset.js')).toBeTruthy();
});
describe('root project', () => {
it('should not add a monorepo jest.config.ts to the project', async () => {
await createJestConfig(tree, { rootProject: true });
expect(tree.exists('jest.config.ts')).toBeFalsy();
});
it('should rename the project jest.config.ts to project jest config', async () => {
addProjectConfiguration(tree, 'my-project', {
root: '.',
name: 'my-project',
projectType: 'application',
sourceRoot: 'src',
targets: {
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.ts',
},
},
},
});
tree.write(
'jest.config.ts',
`
/* 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',
};
`
);
await createJestConfig(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';
export default {
projects: getJestProjects()
};`);
expect(readProjectConfiguration(tree, 'my-project').targets.test)
.toMatchInlineSnapshot(`
{
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "jest.config.app.ts",
},
}
`);
});
it('should work with --js', async () => {
addProjectConfiguration(tree, 'my-project', {
root: '.',
name: 'my-project',
sourceRoot: 'src',
projectType: 'application',
targets: {
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.js',
},
},
},
});
tree.write(
'jest.config.js',
`
/* eslint-disable */
module.exports = {
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',
};
`
);
await createJestConfig(tree, { js: true, rootProject: false });
expect(tree.exists('jest.config.app.js')).toBeTruthy();
expect(tree.read('jest.config.js', 'utf-8'))
.toEqual(`const { getJestProjects } = require('@nx/jest');
module.exports = {
projects: getJestProjects()
};`);
});
});
});

View File

@ -0,0 +1,143 @@
import {
createProjectGraphAsync,
readNxJson,
readProjectConfiguration,
stripIndents,
updateProjectConfiguration,
type TargetConfiguration,
type Tree,
} from '@nx/devkit';
import { readTargetDefaultsForTarget } from 'nx/src/project-graph/utils/project-configuration-utils';
import { findRootJestConfig } from '../../../utils/config/find-root-jest-files';
import type { NormalizedJestProjectSchema } from '../schema';
export async function createJestConfig(
tree: Tree,
options: Partial<NormalizedJestProjectSchema>
) {
if (!tree.exists('jest.preset.js')) {
// preset is always js file.
tree.write(
`jest.preset.js`,
`
const nxPreset = require('@nx/jest/preset').default;
module.exports = { ...nxPreset }`
);
}
if (options.rootProject) {
// we don't want any config to be made because the `configurationGenerator` will do it.
// will copy the template config file
return;
}
const rootJestPath = findRootJestConfig(tree);
if (!rootJestPath) {
// if there's not root jest config, we will create one and return
// this can happen when:
// - root jest config was renamed => in which case there is migration needed
// - root project didn't have jest setup => again, no migration is needed
generateGlobalConfig(tree, options.js);
return;
}
if (tree.exists(rootJestPath)) {
// moving from root project config to monorepo-style config
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 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')
);
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);
}
}
}
function generateGlobalConfig(tree: Tree, isJS: boolean) {
const contents = isJS
? stripIndents`
const { getJestProjects } = require('@nx/jest');
module.exports = {
projects: getJestProjects()
};`
: stripIndents`
import { getJestProjects } from '@nx/jest';
export default {
projects: getJestProjects()
};`;
tree.write(`jest.config.${isJS ? 'js' : 'ts'}`, contents);
}

View File

@ -0,0 +1,71 @@
import { readJson, type Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { ensureDependencies } from './ensure-dependencies';
describe('ensureDependencies', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
describe('Deprecated: --babelJest', () => {
it('should add babel dependencies', () => {
ensureDependencies(tree, { babelJest: true });
const packageJson = readJson(tree, 'package.json');
expect(packageJson.devDependencies['babel-jest']).toBeDefined();
});
});
describe('--compiler', () => {
it('should support tsc compiler', () => {
ensureDependencies(tree, { compiler: 'tsc' });
const packageJson = readJson(tree, 'package.json');
expect(packageJson.devDependencies['ts-jest']).toBeDefined();
});
it('should support babel compiler', () => {
ensureDependencies(tree, { compiler: 'babel' });
const packageJson = readJson(tree, 'package.json');
expect(packageJson.devDependencies['babel-jest']).toBeDefined();
});
it('should support swc compiler', () => {
ensureDependencies(tree, { compiler: 'swc' });
const packageJson = readJson(tree, 'package.json');
expect(packageJson.devDependencies['@swc/jest']).toBeDefined();
});
});
it('should add dependencies --testEnvironment=node', () => {
ensureDependencies(tree, { testEnvironment: 'node' });
const packageJson = readJson(tree, 'package.json');
expect(packageJson.devDependencies['@types/jest']).toBeDefined();
expect(packageJson.devDependencies['ts-jest']).toBeDefined();
expect(packageJson.devDependencies['ts-node']).toBeDefined();
expect(packageJson.devDependencies['jest-environment-node']).toBeDefined();
expect(
packageJson.devDependencies['jest-environment-jsdom']
).not.toBeDefined();
});
it('should add dependencies --testEnvironment=none', () => {
ensureDependencies(tree, { testEnvironment: 'none' });
const packageJson = readJson(tree, 'package.json');
expect(packageJson.devDependencies['@types/jest']).toBeDefined();
expect(packageJson.devDependencies['ts-jest']).toBeDefined();
expect(packageJson.devDependencies['ts-node']).toBeDefined();
expect(
packageJson.devDependencies['jest-environment-jsdom']
).not.toBeDefined();
expect(
packageJson.devDependencies['jest-environment-node']
).not.toBeDefined();
});
});

View File

@ -0,0 +1,48 @@
import { addDependenciesToPackageJson, type Tree } from '@nx/devkit';
import {
babelJestVersion,
jestTypesVersion,
jestVersion,
nxVersion,
swcJestVersion,
tsJestVersion,
tslibVersion,
tsNodeVersion,
typesNodeVersion,
} from '../../../utils/versions';
import type { NormalizedJestProjectSchema } from '../schema';
export function ensureDependencies(
tree: Tree,
options: Partial<NormalizedJestProjectSchema>
) {
const dependencies: Record<string, string> = {
tslib: tslibVersion,
};
const devDeps: Record<string, string> = {
// because the default jest-preset uses ts-jest,
// jest will throw an error if it's not installed
// even if not using it in overriding transformers
'ts-jest': tsJestVersion,
};
if (options.testEnvironment !== 'none') {
devDeps[`jest-environment-${options.testEnvironment}`] = jestVersion;
}
if (!options.js) {
devDeps['ts-node'] = tsNodeVersion;
devDeps['@types/jest'] = jestTypesVersion;
devDeps['@types/node'] = typesNodeVersion;
}
if (options.compiler === 'babel' || options.babelJest) {
devDeps['babel-jest'] = babelJestVersion;
// in some cases @nx/js will not already be present i.e. node only projects
devDeps['@nx/js'] = nxVersion;
} else if (options.compiler === 'swc') {
devDeps['@swc/jest'] = swcJestVersion;
}
return addDependenciesToPackageJson(tree, dependencies, devDeps);
}

View File

@ -0,0 +1,37 @@
import { readJson, writeJson, type Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { updateVsCodeRecommendedExtensions } from './update-vscode-recommended-extensions';
describe('updateVsCodeRecommendedExtensions', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
it('should add the jest extension to the recommended property', () => {
writeJson(tree, '.vscode/extensions.json', {
recommendations: [
'nrwl.angular-console',
'angular.ng-template',
'dbaeumer.vscode-eslint',
'esbenp.prettier-vscode',
],
});
updateVsCodeRecommendedExtensions(tree);
const extensionsJson = readJson(tree, '.vscode/extensions.json');
expect(extensionsJson).toMatchInlineSnapshot(`
{
"recommendations": [
"nrwl.angular-console",
"angular.ng-template",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"firsttris.vscode-jest-runner",
],
}
`);
});
});

View File

@ -0,0 +1,16 @@
import { updateJson, type Tree } from '@nx/devkit';
export function updateVsCodeRecommendedExtensions(host: Tree) {
if (!host.exists('.vscode/extensions.json')) {
return;
}
updateJson(host, '.vscode/extensions.json', (json) => {
json.recommendations = json.recommendations || [];
const extension = 'firsttris.vscode-jest-runner';
if (!json.recommendations.includes(extension)) {
json.recommendations.push(extension);
}
return json;
});
}

View File

@ -1,98 +1,17 @@
let projectGraph: ProjectGraph;
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
return projectGraph;
}),
}));
import {
addProjectConfiguration as _addProjectConfiguration,
NxJsonConfiguration,
ProjectGraph,
readJson,
readProjectConfiguration,
stripIndents,
Tree,
updateJson,
writeJson,
type NxJsonConfiguration,
type Tree,
} from '@nx/devkit';
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 () => {
await jestInitGenerator(tree, { js: true });
expect(tree.exists('jest.config.js')).toBeTruthy();
expect(
stripIndents`${tree.read('jest.config.js', 'utf-8')}`
).toMatchSnapshot();
expect(
stripIndents`${tree.read('jest.preset.js', 'utf-8')}`
).toMatchSnapshot();
});
it('should generate files ', async () => {
await jestInitGenerator(tree, {});
expect(tree.exists('jest.config.ts')).toBeTruthy();
expect(
stripIndents`${tree.read('jest.config.ts', 'utf-8')}`
).toMatchSnapshot();
expect(
stripIndents`${tree.read('jest.preset.js', 'utf-8')}`
).toMatchSnapshot();
});
it('should not override existing files', async () => {
addProjectConfiguration(tree, 'my-project', {
root: 'apps/my-app',
name: 'my-app',
sourceRoot: 'apps/my-app/src',
targets: {
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'apps/my-app/jest.config.ts',
},
},
},
});
const expected = stripIndents`
import { getJestProjects } from '@nx/jest';
export default {
projects: getJestProjects(),
extraThing: "Goes Here"
}
`;
tree.write('jest.config.ts', expected);
await jestInitGenerator(tree, {});
expect(tree.read('jest.config.ts', 'utf-8')).toEqual(expected);
});
it('should add target defaults for test', async () => {
@ -135,11 +54,10 @@ export default {
json.namedInputs.production = ['default', '^production'];
return json;
});
await jestInitGenerator(tree, {});
let nxJson: NxJsonConfiguration;
updateJson<NxJsonConfiguration>(tree, 'nx.json', (json) => {
json.namedInputs ??= {};
json.namedInputs.production = [
'default',
'^production',
@ -157,226 +75,18 @@ export default {
nxJson = json;
return json;
});
tree.write('jest.preset.js', '');
await jestInitGenerator(tree, {});
expect(readJson<NxJsonConfiguration>(tree, 'nx.json')).toEqual(nxJson);
});
it('should add dependencies', async () => {
await jestInitGenerator(tree, {});
const packageJson = readJson(tree, 'package.json');
expect(packageJson.devDependencies.jest).toBeDefined();
expect(packageJson.devDependencies['@nx/jest']).toBeDefined();
expect(packageJson.devDependencies['@types/jest']).toBeDefined();
expect(packageJson.devDependencies['ts-jest']).toBeDefined();
expect(packageJson.devDependencies['ts-node']).toBeDefined();
expect(packageJson.devDependencies['jest-environment-jsdom']).toBeDefined();
expect(
packageJson.devDependencies['jest-environment-node']
).not.toBeDefined();
});
it('should add dependencies --testEnvironment=node', async () => {
await jestInitGenerator(tree, { testEnvironment: 'node' });
const packageJson = readJson(tree, 'package.json');
expect(packageJson.devDependencies.jest).toBeDefined();
expect(packageJson.devDependencies['@nx/jest']).toBeDefined();
expect(packageJson.devDependencies['@types/jest']).toBeDefined();
expect(packageJson.devDependencies['ts-jest']).toBeDefined();
expect(packageJson.devDependencies['ts-node']).toBeDefined();
expect(packageJson.devDependencies['jest-environment-node']).toBeDefined();
expect(
packageJson.devDependencies['jest-environment-jsdom']
).not.toBeDefined();
});
it('should add dependencies --testEnvironment=none', async () => {
await jestInitGenerator(tree, { testEnvironment: 'none' });
const packageJson = readJson(tree, 'package.json');
expect(packageJson.devDependencies.jest).toBeDefined();
expect(packageJson.devDependencies['@nx/jest']).toBeDefined();
expect(packageJson.devDependencies['@types/jest']).toBeDefined();
expect(packageJson.devDependencies['ts-jest']).toBeDefined();
expect(packageJson.devDependencies['ts-node']).toBeDefined();
expect(
packageJson.devDependencies['jest-environment-jsdom']
).not.toBeDefined();
expect(
packageJson.devDependencies['jest-environment-node']
).not.toBeDefined();
});
it('should make js jest files', async () => {
await jestInitGenerator(tree, { js: true });
expect(tree.exists('jest.config.js')).toBeTruthy();
expect(tree.exists('jest.preset.js')).toBeTruthy();
});
describe('Deprecated: --babelJest', () => {
it('should add babel dependencies', async () => {
await jestInitGenerator(tree, { babelJest: true });
const packageJson = readJson(tree, 'package.json');
expect(packageJson.devDependencies['babel-jest']).toBeDefined();
});
});
describe('--compiler', () => {
it('should support tsc compiler', async () => {
await jestInitGenerator(tree, { compiler: 'tsc' });
const packageJson = readJson(tree, 'package.json');
expect(packageJson.devDependencies['ts-jest']).toBeDefined();
});
it('should support babel compiler', async () => {
await jestInitGenerator(tree, { compiler: 'babel' });
const packageJson = readJson(tree, 'package.json');
expect(packageJson.devDependencies['babel-jest']).toBeDefined();
});
it('should support swc compiler', async () => {
await jestInitGenerator(tree, { compiler: 'swc' });
const packageJson = readJson(tree, 'package.json');
expect(packageJson.devDependencies['@swc/jest']).toBeDefined();
});
});
describe('root project', () => {
it('should not add a monorepo jest.config.ts to the project', async () => {
await jestInitGenerator(tree, { rootProject: true });
expect(tree.exists('jest.config.ts')).toBeFalsy();
});
it('should rename the project jest.config.ts to project jest config', async () => {
addProjectConfiguration(tree, 'my-project', {
root: '.',
name: 'my-project',
projectType: 'application',
sourceRoot: 'src',
targets: {
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.ts',
},
},
},
});
tree.write(
'jest.config.ts',
`
/* 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',
};
`
);
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';
export default {
projects: getJestProjects()
};`);
expect(readProjectConfiguration(tree, 'my-project').targets.test)
.toMatchInlineSnapshot(`
{
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "jest.config.app.ts",
},
}
`);
});
it('should work with --js', async () => {
addProjectConfiguration(tree, 'my-project', {
root: '.',
name: 'my-project',
sourceRoot: 'src',
projectType: 'application',
targets: {
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'jest.config.js',
},
},
},
});
tree.write(
'jest.config.js',
`
/* eslint-disable */
module.exports = {
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',
};
`
);
await jestInitGenerator(tree, { js: true, rootProject: false });
expect(tree.exists('jest.config.app.js')).toBeTruthy();
expect(tree.read('jest.config.js', 'utf-8'))
.toEqual(`const { getJestProjects } = require('@nx/jest');
module.exports = {
projects: getJestProjects()
};`);
});
});
describe('adds jest extension', () => {
beforeEach(async () => {
writeJson(tree, '.vscode/extensions.json', {
recommendations: [
'nrwl.angular-console',
'angular.ng-template',
'dbaeumer.vscode-eslint',
'esbenp.prettier-vscode',
],
});
});
it('should add the jest extension to the recommended property', async () => {
await jestInitGenerator(tree, {});
const extensionsJson = readJson(tree, '.vscode/extensions.json');
expect(extensionsJson).toMatchInlineSnapshot(`
{
"recommendations": [
"nrwl.angular-console",
"angular.ng-template",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"firsttris.vscode-jest-runner",
],
}
`);
});
});
});

View File

@ -1,60 +1,15 @@
import {
addDependenciesToPackageJson,
createProjectGraphAsync,
GeneratorCallback,
formatFiles,
readNxJson,
readProjectConfiguration,
removeDependenciesFromPackageJson,
runTasksInSerial,
stripIndents,
TargetConfiguration,
Tree,
updateJson,
updateNxJson,
updateProjectConfiguration,
type GeneratorCallback,
type Tree,
} from '@nx/devkit';
import { initGenerator as jsInitGenerator } from '@nx/js';
import { findRootJestConfig } from '../../utils/config/find-root-jest-files';
import {
babelJestVersion,
jestTypesVersion,
jestVersion,
nxVersion,
swcJestVersion,
tsJestVersion,
tslibVersion,
tsNodeVersion,
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> {}
const schemaDefaults = {
compiler: 'tsc',
js: false,
rootProject: false,
testEnvironment: 'jsdom',
} as const;
function generateGlobalConfig(tree: Tree, isJS: boolean) {
const contents = isJS
? stripIndents`
const { getJestProjects } = require('@nx/jest');
module.exports = {
projects: getJestProjects()
};`
: stripIndents`
import { getJestProjects } from '@nx/jest';
export default {
projects: getJestProjects()
};`;
tree.write(`jest.config.${isJS ? 'js' : 'ts'}`, contents);
}
import { jestVersion, nxVersion } from '../../utils/versions';
import type { JestInitSchema } from './schema';
function addPlugin(tree: Tree) {
const nxJson = readNxJson(tree);
@ -77,127 +32,6 @@ function addPlugin(tree: Tree) {
updateNxJson(tree, nxJson);
}
async function createJestConfig(tree: Tree, options: NormalizedSchema) {
if (!tree.exists('jest.preset.js')) {
// preset is always js file.
tree.write(
`jest.preset.js`,
`
const nxPreset = require('@nx/jest/preset').default;
module.exports = { ...nxPreset }`
);
const shouldAddPlugin = process.env.NX_PCV3 === 'true';
if (shouldAddPlugin) {
addPlugin(tree);
}
updateProductionFileSet(tree);
if (!shouldAddPlugin) {
addJestTargetDefaults(tree, shouldAddPlugin);
}
}
if (options.rootProject) {
// we don't want any config to be made because the `configurationGenerator` will do it.
// will copy the template config file
return;
}
const rootJestPath = findRootJestConfig(tree);
if (!rootJestPath) {
// if there's not root jest config, we will create one and return
// this can happen when:
// - root jest config was renamed => in which case there is migration needed
// - root project didn't have jest setup => again, no migration is needed
generateGlobalConfig(tree, options.js);
return;
}
if (tree.exists(rootJestPath)) {
// moving from root project config to monorepo-style config
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 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')
);
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);
}
}
}
function updateProductionFileSet(tree: Tree) {
const nxJson = readNxJson(tree);
@ -223,23 +57,21 @@ function updateProductionFileSet(tree: Tree) {
updateNxJson(tree, nxJson);
}
function addJestTargetDefaults(tree: Tree, hasPlugin: boolean) {
function addJestTargetDefaults(tree: Tree) {
const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['@nx/jest:jest'] ??= {};
if (!hasPlugin) {
const productionFileSet = nxJson.namedInputs?.production;
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'].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,
@ -254,87 +86,41 @@ function addJestTargetDefaults(tree: Tree, hasPlugin: boolean) {
updateNxJson(tree, nxJson);
}
function updateDependencies(tree: Tree, options: NormalizedSchema) {
const dependencies = {
tslib: tslibVersion,
};
const devDeps = {
'@nx/jest': nxVersion,
jest: jestVersion,
// because the default jest-preset uses ts-jest,
// jest will throw an error if it's not installed
// even if not using it in overriding transformers
'ts-jest': tsJestVersion,
};
if (options.testEnvironment !== 'none') {
devDeps[`jest-environment-${options.testEnvironment}`] = jestVersion;
}
if (!options.js) {
devDeps['ts-node'] = tsNodeVersion;
devDeps['@types/jest'] = jestTypesVersion;
devDeps['@types/node'] = typesNodeVersion;
}
if (options.compiler === 'babel' || options.babelJest) {
devDeps['babel-jest'] = babelJestVersion;
// in some cases @nx/js will not already be present i.e. node only projects
devDeps['@nx/js'] = nxVersion;
} else if (options.compiler === 'swc') {
devDeps['@swc/jest'] = swcJestVersion;
}
return addDependenciesToPackageJson(tree, dependencies, devDeps);
}
function updateExtensions(host: Tree) {
if (!host.exists('.vscode/extensions.json')) {
return;
}
updateJson(host, '.vscode/extensions.json', (json) => {
json.recommendations = json.recommendations || [];
const extension = 'firsttris.vscode-jest-runner';
if (!json.recommendations.includes(extension)) {
json.recommendations.push(extension);
function updateDependencies(tree: Tree) {
return addDependenciesToPackageJson(
tree,
{},
{
'@nx/jest': nxVersion,
jest: jestVersion,
}
return json;
});
);
}
export async function jestInitGenerator(
tree: Tree,
schema: JestInitSchema
options: JestInitSchema
): Promise<GeneratorCallback> {
const options = normalizeOptions(schema);
const tasks: GeneratorCallback[] = [];
tasks.push(
await jsInitGenerator(tree, {
...schema,
skipFormat: true,
})
);
await createJestConfig(tree, options);
if (!options.skipPackageJson) {
removeDependenciesFromPackageJson(tree, ['@nx/jest'], []);
const installTask = updateDependencies(tree, options);
tasks.push(installTask);
if (!tree.exists('jest.preset.js')) {
updateProductionFileSet(tree);
if (process.env.NX_PCV3 === 'true') {
addPlugin(tree);
} else {
addJestTargetDefaults(tree);
}
}
const tasks: GeneratorCallback[] = [];
if (!options.skipPackageJson) {
tasks.push(removeDependenciesFromPackageJson(tree, ['@nx/jest'], []));
tasks.push(updateDependencies(tree));
}
if (!options.skipFormat) {
await formatFiles(tree);
}
updateExtensions(tree);
return runTasksInSerial(...tasks);
}
function normalizeOptions(options: JestInitSchema) {
return {
...schemaDefaults,
...options,
};
}
export default jestInitGenerator;

View File

@ -1,11 +1,4 @@
export interface JestInitSchema {
compiler?: 'tsc' | 'babel' | 'swc';
js?: boolean;
skipFormat?: boolean;
skipPackageJson?: boolean;
testEnvironment?: 'node' | 'jsdom' | 'none';
/**
* @deprecated
*/
babelJest?: boolean;
rootProject?: boolean;
}

View File

@ -6,36 +6,17 @@
"description": "Add Jest Configuration to a workspace.",
"type": "object",
"properties": {
"babelJest": {
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"alias": "babel-jest",
"description": "Use `babel-jest` instead of `ts-jest`.",
"default": false
"default": false,
"x-priority": "internal"
},
"skipPackageJson": {
"type": "boolean",
"default": false,
"description": "Do not add dependencies to `package.json`.",
"x-priority": "internal"
},
"testEnvironment": {
"type": "string",
"enum": ["jsdom", "node", "none"],
"description": "The test environment for jest. This controls which jest-environment-* package is installed",
"default": "jsdom",
"x-priority": "important"
},
"js": {
"type": "boolean",
"default": false,
"description": "Use JavaScript instead of TypeScript for config files"
},
"rootProject": {
"description": "initialize Jest for an application at the root of the workspace",
"type": "boolean",
"default": false,
"hidden": true,
"x-priority": "internal"
}
},
"required": []

View File

@ -5,6 +5,7 @@ import { applicationGenerator as nodeApplicationGenerator } from '@nx/node';
import { initGenerator } from '../init/init';
import {
createFiles,
ensureDependencies,
normalizeOptions,
toNodeApplicationGeneratorOptions,
updateTsConfig,
@ -26,23 +27,30 @@ export async function applicationGeneratorInternal(
rawOptions: ApplicationGeneratorOptions
): Promise<GeneratorCallback> {
const options = await normalizeOptions(tree, rawOptions);
const tasks: GeneratorCallback[] = [];
const initTask = await initGenerator(tree, {
skipPackageJson: options.skipPackageJson,
unitTestRunner: options.unitTestRunner,
skipFormat: true,
});
tasks.push(initTask);
const nodeApplicationTask = await nodeApplicationGenerator(
tree,
toNodeApplicationGeneratorOptions(options)
);
tasks.push(nodeApplicationTask);
createFiles(tree, options);
updateTsConfig(tree, options);
if (!options.skipPackageJson) {
tasks.push(ensureDependencies(tree));
}
if (!options.skipFormat) {
await formatFiles(tree);
}
return runTasksInSerial(initTask, nodeApplicationTask);
return runTasksInSerial(...tasks);
}
export default applicationGenerator;

View File

@ -0,0 +1,25 @@
import type { GeneratorCallback, Tree } from '@nx/devkit';
import { addDependenciesToPackageJson } from '@nx/devkit';
import {
nestJsVersion,
reflectMetadataVersion,
rxjsVersion,
tsLibVersion,
} from '../../../utils/versions';
export function ensureDependencies(tree: Tree): GeneratorCallback {
return addDependenciesToPackageJson(
tree,
{
'@nestjs/common': nestJsVersion,
'@nestjs/core': nestJsVersion,
'@nestjs/platform-express': nestJsVersion,
'reflect-metadata': reflectMetadataVersion,
rxjs: rxjsVersion,
tslib: tsLibVersion,
},
{
'@nestjs/testing': nestJsVersion,
}
);
}

View File

@ -1,3 +1,4 @@
export * from './create-files';
export * from './ensure-dependencies';
export * from './normalize-options';
export * from './update-tsconfig';

View File

@ -1,11 +1,7 @@
import type { Tree } from '@nx/devkit';
import * as devkit from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
nestJsSchematicsVersion,
nestJsVersion,
nxVersion,
} from '../../utils/versions';
import { nestJsSchematicsVersion, nxVersion } from '../../utils/versions';
import { initGenerator } from './init';
describe('init generator', () => {
@ -20,34 +16,12 @@ describe('init generator', () => {
await initGenerator(tree, {});
const packageJson = devkit.readJson(tree, 'package.json');
expect(packageJson.dependencies['@nestjs/common']).toBe(nestJsVersion);
expect(packageJson.dependencies['@nestjs/core']).toBe(nestJsVersion);
expect(packageJson.dependencies['@nestjs/platform-express']).toBe(
nestJsVersion
);
expect(packageJson.dependencies['reflect-metadata']).toBeDefined();
expect(packageJson.dependencies['rxjs']).toBeDefined();
expect(packageJson.dependencies['tslib']).toBeDefined();
expect(packageJson.dependencies['@nx/nest']).toBeUndefined();
expect(packageJson.devDependencies['@nestjs/schematics']).toBe(
nestJsSchematicsVersion
);
expect(packageJson.devDependencies['@nestjs/testing']).toBe(nestJsVersion);
expect(packageJson.devDependencies['@nx/nest']).toBe(nxVersion);
});
it('should add jest config when unitTestRunner is jest', async () => {
await initGenerator(tree, { unitTestRunner: 'jest' });
expect(tree.exists('jest.config.ts')).toBe(true);
});
it('should not add jest config when unitTestRunner is none', async () => {
await initGenerator(tree, { unitTestRunner: 'none' });
expect(tree.exists('jest.config.ts')).toBe(false);
});
describe('--skipFormat', () => {
it('should format files by default', async () => {
jest.spyOn(devkit, 'formatFiles');

View File

@ -1,33 +1,23 @@
import type { GeneratorCallback, Tree } from '@nx/devkit';
import { formatFiles, runTasksInSerial } from '@nx/devkit';
import { initGenerator as nodeInitGenerator } from '@nx/node';
import { formatFiles } from '@nx/devkit';
import { addDependencies, normalizeOptions } from './lib';
import { addDependencies } from './lib';
import type { InitGeneratorOptions } from './schema';
export async function initGenerator(
tree: Tree,
rawOptions: InitGeneratorOptions
options: InitGeneratorOptions
): Promise<GeneratorCallback> {
const options = normalizeOptions(rawOptions);
const tasks: GeneratorCallback[] = [];
const nodeInitTask = await nodeInitGenerator(tree, {
...options,
skipFormat: true,
});
tasks.push(nodeInitTask);
let installPackagesTask: GeneratorCallback = () => {};
if (!options.skipPackageJson) {
const installPackagesTask = addDependencies(tree);
tasks.push(installPackagesTask);
installPackagesTask = addDependencies(tree);
}
if (!options.skipFormat) {
await formatFiles(tree);
}
return runTasksInSerial(...tasks);
return installPackagesTask;
}
export default initGenerator;

View File

@ -1,28 +1,13 @@
import type { GeneratorCallback, Tree } from '@nx/devkit';
import { addDependenciesToPackageJson } from '@nx/devkit';
import {
nestJsSchematicsVersion,
nestJsVersion,
nxVersion,
reflectMetadataVersion,
rxjsVersion,
tsLibVersion,
} from '../../../utils/versions';
import { nestJsSchematicsVersion, nxVersion } from '../../../utils/versions';
export function addDependencies(tree: Tree): GeneratorCallback {
return addDependenciesToPackageJson(
tree,
{
'@nestjs/common': nestJsVersion,
'@nestjs/core': nestJsVersion,
'@nestjs/platform-express': nestJsVersion,
'reflect-metadata': reflectMetadataVersion,
rxjs: rxjsVersion,
tslib: tsLibVersion,
},
{},
{
'@nestjs/schematics': nestJsSchematicsVersion,
'@nestjs/testing': nestJsVersion,
'@nx/nest': nxVersion,
}
);

View File

@ -1,2 +1 @@
export * from './add-dependencies';
export * from './normalize-options';

View File

@ -1,10 +0,0 @@
import type { InitGeneratorOptions } from '../schema';
export function normalizeOptions(
options: InitGeneratorOptions
): InitGeneratorOptions {
return {
...options,
unitTestRunner: options.unitTestRunner ?? 'jest',
};
}

View File

@ -2,6 +2,5 @@ import { UnitTestRunner } from '../utils';
export interface InitGeneratorOptions {
skipFormat?: boolean;
unitTestRunner?: UnitTestRunner;
skipPackageJson?: boolean;
}

View File

@ -6,12 +6,6 @@
"cli": "nx",
"type": "object",
"properties": {
"unitTestRunner": {
"description": "Adds the specified unit test runner.",
"type": "string",
"enum": ["jest", "none"],
"default": "jest"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",

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