import { getProjects, readJson, readWorkspaceConfiguration, Tree, } from '@nrwl/devkit'; import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { applicationGenerator } from './application'; import { Schema } from './schema'; import { Linter } from '@nrwl/linter'; describe('app', () => { let appTree: Tree; let schema: Schema = { babelJest: false, e2eTestRunner: 'cypress', skipFormat: false, unitTestRunner: 'jest', name: 'myApp', linter: Linter.EsLint, style: 'css', strict: false, standaloneConfig: false, }; beforeEach(() => { appTree = createTreeWithEmptyWorkspace(); }); describe('not nested', () => { it('should update workspace.json', async () => { await applicationGenerator(appTree, schema); const workspaceJson = readWorkspaceConfiguration(appTree); const projects = getProjects(appTree); expect(projects.get('my-app').root).toEqual('apps/my-app'); expect(projects.get('my-app-e2e').root).toEqual('apps/my-app-e2e'); expect(workspaceJson.defaultProject).toEqual('my-app'); }); it('should update tags and implicit dependencies', async () => { await applicationGenerator(appTree, { ...schema, tags: 'one,two' }); const projects = Object.fromEntries(getProjects(appTree)); expect(projects).toMatchObject({ 'my-app': { tags: ['one', 'two'], }, 'my-app-e2e': { tags: [], implicitDependencies: ['my-app'], }, }); }); it('should generate files', async () => { await applicationGenerator(appTree, schema); expect(appTree.exists('apps/my-app/.babelrc')).toBeTruthy(); expect(appTree.exists('apps/my-app/.browserslistrc')).toBeTruthy(); expect(appTree.exists('apps/my-app/src/main.tsx')).toBeTruthy(); expect(appTree.exists('apps/my-app/src/app/app.tsx')).toBeTruthy(); expect(appTree.exists('apps/my-app/src/app/app.spec.tsx')).toBeTruthy(); expect(appTree.exists('apps/my-app/src/app/app.module.css')).toBeTruthy(); const jestConfig = appTree.read('apps/my-app/jest.config.js').toString(); expect(jestConfig).toContain('@nrwl/react/plugins/jest'); const tsconfig = readJson(appTree, 'apps/my-app/tsconfig.json'); expect(tsconfig.references).toEqual([ { path: './tsconfig.app.json', }, { path: './tsconfig.spec.json', }, ]); expect(tsconfig.compilerOptions.strict).not.toBeDefined(); expect( tsconfig.compilerOptions.forceConsistentCasingInFileNames ).not.toBeDefined(); expect(tsconfig.compilerOptions.noImplicitReturns).not.toBeDefined(); expect( tsconfig.compilerOptions.noFallthroughCasesInSwitch ).not.toBeDefined(); const tsconfigApp = readJson(appTree, 'apps/my-app/tsconfig.app.json'); expect(tsconfigApp.compilerOptions.outDir).toEqual('../../dist/out-tsc'); expect(tsconfigApp.extends).toEqual('./tsconfig.json'); expect(tsconfigApp.exclude).toEqual([ '**/*.spec.ts', '**/*.test.ts', '**/*.spec.tsx', '**/*.test.tsx', '**/*.spec.js', '**/*.test.js', '**/*.spec.jsx', '**/*.test.jsx', ]); const eslintJson = readJson(appTree, 'apps/my-app/.eslintrc.json'); expect(eslintJson.extends).toEqual([ 'plugin:@nrwl/nx/react', '../../.eslintrc.json', ]); expect(appTree.exists('apps/my-app-e2e/cypress.json')).toBeTruthy(); const tsconfigE2E = readJson(appTree, 'apps/my-app-e2e/tsconfig.json'); expect(tsconfigE2E).toMatchInlineSnapshot(` Object { "compilerOptions": Object { "allowJs": true, "outDir": "../../dist/out-tsc", "sourceMap": false, "types": Array [ "cypress", "node", ], }, "extends": "../../tsconfig.base.json", "include": Array [ "src/**/*.ts", "src/**/*.js", ], } `); }); }); describe('nested', () => { it('should update workspace.json', async () => { await applicationGenerator(appTree, { ...schema, directory: 'myDir' }); const workspaceJson = getProjects(appTree); expect(workspaceJson.get('my-dir-my-app').root).toEqual( 'apps/my-dir/my-app' ); expect(workspaceJson.get('my-dir-my-app-e2e').root).toEqual( 'apps/my-dir/my-app-e2e' ); }); it('should update tags and implicit deps', async () => { await applicationGenerator(appTree, { ...schema, directory: 'myDir', tags: 'one,two', }); const projects = Object.fromEntries(getProjects(appTree)); expect(projects).toMatchObject({ 'my-dir-my-app': { tags: ['one', 'two'], }, 'my-dir-my-app-e2e': { tags: [], implicitDependencies: ['my-dir-my-app'], }, }); }); it('should generate files', async () => { const hasJsonValue = ({ path, expectedValue, lookupFn }) => { const config = readJson(appTree, path); expect(lookupFn(config)).toEqual(expectedValue); }; await applicationGenerator(appTree, { ...schema, directory: 'myDir' }); // Make sure these exist [ 'apps/my-dir/my-app/src/main.tsx', 'apps/my-dir/my-app/src/app/app.tsx', 'apps/my-dir/my-app/src/app/app.spec.tsx', 'apps/my-dir/my-app/src/app/app.module.css', ].forEach((path) => { expect(appTree.exists(path)).toBeTruthy(); }); // Make sure these have properties [ { path: 'apps/my-dir/my-app/tsconfig.app.json', lookupFn: (json) => json.compilerOptions.outDir, expectedValue: '../../../dist/out-tsc', }, { path: 'apps/my-dir/my-app/tsconfig.app.json', lookupFn: (json) => json.exclude, expectedValue: [ '**/*.spec.ts', '**/*.test.ts', '**/*.spec.tsx', '**/*.test.tsx', '**/*.spec.js', '**/*.test.js', '**/*.spec.jsx', '**/*.test.jsx', ], }, { path: 'apps/my-dir/my-app-e2e/tsconfig.json', lookupFn: (json) => json.compilerOptions.outDir, expectedValue: '../../../dist/out-tsc', }, { path: 'apps/my-dir/my-app/.eslintrc.json', lookupFn: (json) => json.extends, expectedValue: ['plugin:@nrwl/nx/react', '../../../.eslintrc.json'], }, ].forEach(hasJsonValue); }); }); it('should create Nx specific template', async () => { await applicationGenerator(appTree, { ...schema, directory: 'myDir' }); expect( appTree.read('apps/my-dir/my-app/src/app/app.tsx').toString() ).toContain(``); expect( appTree.read('apps/my-dir/my-app/src/app/nx-welcome.tsx').toString() ).toContain('Hello there'); }); it.each` style ${'styled-components'} ${'styled-jsx'} ${'@emotion/styled'} `( 'should generate valid .babelrc JSON config for CSS-in-JS solutions', async ({ style }) => { await applicationGenerator(appTree, { ...schema, style, }); expect(() => { readJson(appTree, `apps/my-app/.babelrc`); }).not.toThrow(); } ); describe('--style scss', () => { it('should generate scss styles', async () => { await applicationGenerator(appTree, { ...schema, style: 'scss' }); expect(appTree.exists('apps/my-app/src/app/app.module.scss')).toEqual( true ); }); }); it('should setup jest with tsx support', async () => { await applicationGenerator(appTree, { ...schema, name: 'my-app' }); expect(appTree.read('apps/my-app/jest.config.js').toString()).toContain( `moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],` ); }); it('should setup jest without serializers', async () => { await applicationGenerator(appTree, { ...schema, name: 'my-app' }); expect(appTree.read('apps/my-app/jest.config.js').toString()).not.toContain( `'jest-preset-angular/build/AngularSnapshotSerializer.js',` ); }); it('should setup the nrwl web build builder', async () => { await applicationGenerator(appTree, { ...schema, name: 'my-app' }); const workspaceJson = getProjects(appTree); const targetConfig = workspaceJson.get('my-app').targets; expect(targetConfig.build.executor).toEqual('@nrwl/web:webpack'); expect(targetConfig.build.outputs).toEqual(['{options.outputPath}']); expect(targetConfig.build.options).toEqual({ assets: ['apps/my-app/src/favicon.ico', 'apps/my-app/src/assets'], index: 'apps/my-app/src/index.html', main: 'apps/my-app/src/main.tsx', baseHref: '/', outputPath: 'dist/apps/my-app', polyfills: 'apps/my-app/src/polyfills.ts', scripts: [], styles: ['apps/my-app/src/styles.css'], tsConfig: 'apps/my-app/tsconfig.app.json', webpackConfig: '@nrwl/react/plugins/webpack', }); expect(targetConfig.build.configurations.production).toEqual({ optimization: true, extractLicenses: true, fileReplacements: [ { replace: 'apps/my-app/src/environments/environment.ts', with: 'apps/my-app/src/environments/environment.prod.ts', }, ], namedChunks: false, outputHashing: 'all', sourceMap: false, vendorChunk: false, }); }); it('should setup the nrwl web dev server builder', async () => { await applicationGenerator(appTree, { ...schema, name: 'my-app' }); const workspaceJson = getProjects(appTree); const targetConfig = workspaceJson.get('my-app').targets; expect(targetConfig.serve.executor).toEqual('@nrwl/web:dev-server'); expect(targetConfig.serve.options).toEqual({ buildTarget: 'my-app:build', hmr: true, }); expect(targetConfig.serve.configurations.production).toEqual({ buildTarget: 'my-app:build:production', hmr: false, }); }); it('should setup the eslint builder', async () => { await applicationGenerator(appTree, { ...schema, name: 'my-app' }); const workspaceJson = getProjects(appTree); expect(workspaceJson.get('my-app').targets.lint).toEqual({ executor: '@nrwl/linter:eslint', outputs: ['{options.outputFile}'], options: { lintFilePatterns: ['apps/my-app/**/*.{ts,tsx,js,jsx}'], }, }); }); describe('--unit-test-runner none', () => { it('should not generate test configuration', async () => { await applicationGenerator(appTree, { ...schema, unitTestRunner: 'none', }); expect(appTree.exists('jest.config.js')).toBeFalsy(); expect(appTree.exists('apps/my-app/src/app/app.spec.tsx')).toBeFalsy(); expect(appTree.exists('apps/my-app/tsconfig.spec.json')).toBeFalsy(); expect(appTree.exists('apps/my-app/jest.config.js')).toBeFalsy(); const workspaceJson = getProjects(appTree); expect(workspaceJson.get('my-app').targets.test).toBeUndefined(); expect(workspaceJson.get('my-app').targets.lint).toMatchInlineSnapshot(` Object { "executor": "@nrwl/linter:eslint", "options": Object { "lintFilePatterns": Array [ "apps/my-app/**/*.{ts,tsx,js,jsx}", ], }, "outputs": Array [ "{options.outputFile}", ], } `); }); }); describe('--e2e-test-runner none', () => { it('should not generate test configuration', async () => { await applicationGenerator(appTree, { ...schema, e2eTestRunner: 'none' }); expect(appTree.exists('apps/my-app-e2e')).toBeFalsy(); const workspaceJson = getProjects(appTree); expect(workspaceJson.get('my-app-e2e')).toBeUndefined(); }); }); describe('--pascalCaseFiles', () => { it('should use upper case app file', async () => { await applicationGenerator(appTree, { ...schema, pascalCaseFiles: true }); expect(appTree.exists('apps/my-app/src/app/App.tsx')).toBeTruthy(); expect(appTree.exists('apps/my-app/src/app/App.spec.tsx')).toBeTruthy(); expect(appTree.exists('apps/my-app/src/app/App.module.css')).toBeTruthy(); }); }); it('should generate functional components by default', async () => { await applicationGenerator(appTree, schema); const appContent = appTree.read('apps/my-app/src/app/app.tsx').toString(); expect(appContent).not.toMatch(/extends Component/); }); it('should add .eslintrc.json and dependencies', async () => { await applicationGenerator(appTree, { ...schema, linter: Linter.EsLint }); const packageJson = readJson(appTree, '/package.json'); expect(packageJson.devDependencies.eslint).toBeDefined(); expect(packageJson.devDependencies['@nrwl/linter']).toBeDefined(); expect(packageJson.devDependencies['@nrwl/eslint-plugin-nx']).toBeDefined(); expect(packageJson.devDependencies['eslint-plugin-react']).toBeDefined(); expect( packageJson.devDependencies['eslint-plugin-react-hooks'] ).toBeDefined(); expect( packageJson.devDependencies['@typescript-eslint/parser'] ).toBeDefined(); expect( packageJson.devDependencies['@typescript-eslint/eslint-plugin'] ).toBeDefined(); expect(packageJson.devDependencies['eslint-config-prettier']).toBeDefined(); const eslintJson = readJson(appTree, '/apps/my-app/.eslintrc.json'); expect(eslintJson).toMatchInlineSnapshot(` Object { "extends": Array [ "plugin:@nrwl/nx/react", "../../.eslintrc.json", ], "ignorePatterns": Array [ "!**/*", ], "overrides": Array [ Object { "files": Array [ "*.ts", "*.tsx", "*.js", "*.jsx", ], "rules": Object {}, }, Object { "files": Array [ "*.ts", "*.tsx", ], "rules": Object {}, }, Object { "files": Array [ "*.js", "*.jsx", ], "rules": Object {}, }, ], } `); }); describe('--class-component', () => { it('should generate class components', async () => { await applicationGenerator(appTree, { ...schema, classComponent: true }); const appContent = appTree.read('apps/my-app/src/app/app.tsx').toString(); expect(appContent).toMatch(/extends Component/); }); }); describe('--style none', () => { it('should not generate any styles', async () => { await applicationGenerator(appTree, { ...schema, style: 'none' }); expect(appTree.exists('apps/my-app/src/app/app.tsx')).toBeTruthy(); expect(appTree.exists('apps/my-app/src/app/app.spec.tsx')).toBeTruthy(); expect(appTree.exists('apps/my-app/src/app/app.css')).toBeFalsy(); expect(appTree.exists('apps/my-app/src/app/app.scss')).toBeFalsy(); expect(appTree.exists('apps/my-app/src/app/app.styl')).toBeFalsy(); expect(appTree.exists('apps/my-app/src/app/app.module.css')).toBeFalsy(); expect(appTree.exists('apps/my-app/src/app/app.module.scss')).toBeFalsy(); expect(appTree.exists('apps/my-app/src/app/app.module.styl')).toBeFalsy(); const content = appTree.read('apps/my-app/src/app/app.tsx').toString(); expect(content).not.toContain('styled-components'); expect(content).not.toContain(''); expect(content).not.toContain('@emotion/styled'); expect(content).not.toContain(''); //for imports expect(content).not.toContain('app.styl'); expect(content).not.toContain('app.css'); expect(content).not.toContain('app.scss'); expect(content).not.toContain('app.module.styl'); expect(content).not.toContain('app.module.css'); expect(content).not.toContain('app.module.scss'); }); it('should set defaults when style: none', async () => { await applicationGenerator(appTree, { ...schema, style: 'none' }); const workspaceJson = readWorkspaceConfiguration(appTree); expect(workspaceJson.generators['@nrwl/react']).toMatchObject({ application: { style: 'none', }, component: { style: 'none', }, library: { style: 'none', }, }); }); it('should exclude styles from workspace.json', async () => { await applicationGenerator(appTree, { ...schema, style: 'none' }); const workspaceJson = getProjects(appTree); expect(workspaceJson.get('my-app').targets.build.options.styles).toEqual( [] ); }); }); describe('--style styled-components', () => { it('should use styled-components as the styled API library', async () => { await applicationGenerator(appTree, { ...schema, style: 'styled-components', }); expect( appTree.exists('apps/my-app/src/app/app.styled-components') ).toBeFalsy(); expect(appTree.exists('apps/my-app/src/app/app.tsx')).toBeTruthy(); expect( appTree.exists('apps/my-app/src/styles.styled-components') ).toBeFalsy(); const content = appTree.read('apps/my-app/src/app/app.tsx').toString(); expect(content).toContain('styled-component'); expect(content).toContain(''); }); it('should add dependencies to package.json', async () => { await applicationGenerator(appTree, { ...schema, style: 'styled-components', }); const packageJSON = readJson(appTree, 'package.json'); expect(packageJSON.dependencies['styled-components']).toBeDefined(); }); }); describe('--style @emotion/styled', () => { it('should use @emotion/styled as the styled API library', async () => { await applicationGenerator(appTree, { ...schema, style: '@emotion/styled', }); expect( appTree.exists('apps/my-app/src/app/app.@emotion/styled') ).toBeFalsy(); expect(appTree.exists('apps/my-app/src/app/app.tsx')).toBeTruthy(); const content = appTree.read('apps/my-app/src/app/app.tsx').toString(); expect(content).toContain('@emotion/styled'); expect(content).toContain(''); }); it('should add jsxImportSource to tsconfig.json', async () => { await applicationGenerator(appTree, { ...schema, style: '@emotion/styled', }); const tsconfigJson = readJson(appTree, 'apps/my-app/tsconfig.json'); expect(tsconfigJson.compilerOptions['jsxImportSource']).toEqual( '@emotion/react' ); }); it('should exclude styles from workspace.json', async () => { await applicationGenerator(appTree, { ...schema, style: '@emotion/styled', }); const workspaceJson = getProjects(appTree); expect(workspaceJson.get('my-app').targets.build.options.styles).toEqual( [] ); }); it('should add dependencies to package.json', async () => { await applicationGenerator(appTree, { ...schema, style: '@emotion/styled', }); const packageJSON = readJson(appTree, 'package.json'); expect(packageJSON.dependencies['@emotion/react']).toBeDefined(); expect(packageJSON.dependencies['@emotion/styled']).toBeDefined(); }); }); describe('--style styled-jsx', () => { it('should use styled-jsx as the styled API library', async () => { await applicationGenerator(appTree, { ...schema, style: 'styled-jsx', }); expect(appTree.exists('apps/my-app/src/app/app.styled-jsx')).toBeFalsy(); expect(appTree.exists('apps/my-app/src/app/app.tsx')).toBeTruthy(); const content = appTree.read('apps/my-app/src/app/app.tsx').toString(); expect(content).toContain('