1075 lines
35 KiB
TypeScript
1075 lines
35 KiB
TypeScript
import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
|
|
import type { Tree } from '@nrwl/devkit';
|
|
import * as devkit from '@nrwl/devkit';
|
|
import {
|
|
NxJsonConfiguration,
|
|
parseJson,
|
|
readJson,
|
|
readProjectConfiguration,
|
|
readWorkspaceConfiguration,
|
|
updateJson,
|
|
} from '@nrwl/devkit';
|
|
import {
|
|
createTreeWithEmptyV1Workspace,
|
|
createTreeWithEmptyWorkspace,
|
|
} from '@nrwl/devkit/testing';
|
|
import { Linter } from '@nrwl/linter';
|
|
import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners';
|
|
import {
|
|
autoprefixerVersion,
|
|
postcssVersion,
|
|
tailwindVersion,
|
|
} from '../../utils/versions';
|
|
import { applicationGenerator } from './application';
|
|
import type { Schema } from './schema';
|
|
// need to mock cypress otherwise it'll use the nx installed version from package.json
|
|
// which is v9 while we are testing for the new v10 version
|
|
jest.mock('@nrwl/cypress/src/utils/cypress-version');
|
|
describe('app', () => {
|
|
let appTree: Tree;
|
|
let mockedInstalledCypressVersion: jest.Mock<
|
|
ReturnType<typeof installedCypressVersion>
|
|
> = installedCypressVersion as never;
|
|
beforeEach(() => {
|
|
mockedInstalledCypressVersion.mockReturnValue(10);
|
|
appTree = createTreeWithEmptyV1Workspace();
|
|
});
|
|
|
|
describe('not nested', () => {
|
|
it('should update workspace.json', async () => {
|
|
// ACT
|
|
await generateApp(appTree);
|
|
|
|
// ASSERT
|
|
const workspaceJson = readJson(appTree, '/workspace.json');
|
|
|
|
expect(workspaceJson.projects['my-app']).toMatchSnapshot();
|
|
expect(workspaceJson.projects['my-app-e2e']).toMatchSnapshot();
|
|
});
|
|
|
|
it('should remove the e2e target on the application', async () => {
|
|
// ACT
|
|
await generateApp(appTree);
|
|
|
|
// ASSERT
|
|
const workspaceJson = readJson(appTree, '/workspace.json');
|
|
expect(workspaceJson.projects['my-app'].architect.e2e).not.toBeDefined();
|
|
});
|
|
|
|
it('should update tags + implicit dependencies', async () => {
|
|
// ACT
|
|
await generateApp(appTree, 'myApp', { tags: 'one,two,my-app' });
|
|
|
|
// ASSERT
|
|
const projects = devkit.getProjects(appTree);
|
|
expect(projects).toEqual(
|
|
new Map(
|
|
Object.entries({
|
|
'my-app': expect.objectContaining({
|
|
tags: ['one', 'two', 'my-app'],
|
|
}),
|
|
'my-app-e2e': expect.objectContaining({
|
|
implicitDependencies: ['my-app'],
|
|
tags: [],
|
|
}),
|
|
})
|
|
)
|
|
);
|
|
});
|
|
|
|
it('should generate files', async () => {
|
|
await generateApp(appTree);
|
|
|
|
expect(appTree.exists(`apps/my-app/jest.config.ts`)).toBeTruthy();
|
|
expect(appTree.exists('apps/my-app/src/main.ts')).toBeTruthy();
|
|
expect(appTree.exists('apps/my-app/src/app/app.module.ts')).toBeTruthy();
|
|
expect(
|
|
appTree.exists('apps/my-app/src/app/app.component.ts')
|
|
).toBeTruthy();
|
|
expect(
|
|
appTree.read('apps/my-app/src/app/app.module.ts', 'utf-8')
|
|
).toContain('class AppModule');
|
|
|
|
const tsconfig = readJson(appTree, 'apps/my-app/tsconfig.json');
|
|
expect(tsconfig.references).toContainEqual({
|
|
path: './tsconfig.app.json',
|
|
});
|
|
expect(tsconfig.references).toContainEqual({
|
|
path: './tsconfig.spec.json',
|
|
});
|
|
expect(tsconfig.references).toContainEqual({
|
|
path: './tsconfig.editor.json',
|
|
});
|
|
|
|
const tsconfigApp = parseJson(
|
|
appTree.read('apps/my-app/tsconfig.app.json', 'utf-8')
|
|
);
|
|
expect(tsconfigApp.compilerOptions.outDir).toEqual('../../dist/out-tsc');
|
|
expect(tsconfigApp.extends).toEqual('./tsconfig.json');
|
|
expect(tsconfigApp.exclude).toEqual([
|
|
'jest.config.ts',
|
|
'**/*.test.ts',
|
|
'**/*.spec.ts',
|
|
]);
|
|
|
|
const eslintrcJson = parseJson(
|
|
appTree.read('apps/my-app/.eslintrc.json', 'utf-8')
|
|
);
|
|
expect(eslintrcJson.extends).toEqual(['../../.eslintrc.json']);
|
|
|
|
expect(appTree.exists('apps/my-app-e2e/cypress.config.ts')).toBeTruthy();
|
|
const tsconfigE2E = parseJson(
|
|
appTree.read('apps/my-app-e2e/tsconfig.json', 'utf-8')
|
|
);
|
|
expect(tsconfigE2E).toMatchSnapshot();
|
|
});
|
|
|
|
it('should setup jest with serializers', async () => {
|
|
await generateApp(appTree);
|
|
|
|
expect(appTree.read('apps/my-app/jest.config.ts', 'utf-8')).toContain(
|
|
`'jest-preset-angular/build/serializers/no-ng-attributes'`
|
|
);
|
|
expect(appTree.read('apps/my-app/jest.config.ts', 'utf-8')).toContain(
|
|
`'jest-preset-angular/build/serializers/ng-snapshot'`
|
|
);
|
|
expect(appTree.read('apps/my-app/jest.config.ts', 'utf-8')).toContain(
|
|
`'jest-preset-angular/build/serializers/html-comment'`
|
|
);
|
|
});
|
|
|
|
it('should default the prefix to npmScope', async () => {
|
|
// Testing without prefix
|
|
await generateApp(appTree, 'myApp', {
|
|
e2eTestRunner: E2eTestRunner.Protractor,
|
|
});
|
|
|
|
const appE2eSpec = appTree.read(
|
|
'apps/my-app-e2e/src/app.e2e-spec.ts',
|
|
'utf-8'
|
|
);
|
|
const workspaceJson = parseJson(appTree.read('workspace.json', 'utf-8'));
|
|
const myAppPrefix = workspaceJson.projects['my-app'].prefix;
|
|
|
|
expect(myAppPrefix).toEqual('proj');
|
|
expect(appE2eSpec).toContain('Welcome my-app');
|
|
});
|
|
|
|
it('should set a new prefix and use it', async () => {
|
|
// Testing WITH prefix
|
|
await generateApp(appTree, 'myAppWithPrefix', {
|
|
prefix: 'custom',
|
|
e2eTestRunner: E2eTestRunner.Protractor,
|
|
});
|
|
|
|
const appE2eSpec = appTree.read(
|
|
'apps/my-app-with-prefix-e2e/src/app.e2e-spec.ts',
|
|
'utf-8'
|
|
);
|
|
const workspaceJson = parseJson(appTree.read('workspace.json', 'utf-8'));
|
|
const myAppPrefix = workspaceJson.projects['my-app-with-prefix'].prefix;
|
|
|
|
expect(myAppPrefix).toEqual('custom');
|
|
expect(appE2eSpec).toContain('Welcome my-app-with-prefix');
|
|
});
|
|
|
|
it('should work if the new project root is changed', async () => {
|
|
// ARRANGE
|
|
updateJson(appTree, '/workspace.json', (json) => ({
|
|
...json,
|
|
newProjectRoot: 'newProjectRoot',
|
|
}));
|
|
|
|
// ACT
|
|
await generateApp(appTree, 'my-app', {
|
|
e2eTestRunner: E2eTestRunner.Protractor,
|
|
});
|
|
|
|
// ASSERT
|
|
expect(appTree.exists('apps/my-app/src/main.ts')).toEqual(true);
|
|
expect(appTree.exists('apps/my-app-e2e/protractor.conf.js')).toEqual(
|
|
true
|
|
);
|
|
});
|
|
|
|
it('should set projectType to application', async () => {
|
|
await generateApp(appTree, 'app');
|
|
const workspaceJson = readJson(appTree, '/workspace.json');
|
|
expect(workspaceJson.projects['app'].projectType).toEqual('application');
|
|
});
|
|
|
|
it('should extend from tsconfig.base.json', async () => {
|
|
// ACT
|
|
await generateApp(appTree, 'app');
|
|
|
|
// ASSERT
|
|
const appTsConfig = readJson(appTree, 'apps/app/tsconfig.json');
|
|
expect(appTsConfig.extends).toBe('../../tsconfig.base.json');
|
|
});
|
|
|
|
it('should support a root tsconfig.json instead of tsconfig.base.json', async () => {
|
|
// ARRANGE
|
|
appTree.rename('tsconfig.base.json', 'tsconfig.json');
|
|
|
|
// ACT
|
|
await generateApp(appTree, 'app');
|
|
|
|
// ASSERT
|
|
const appTsConfig = readJson(appTree, 'apps/app/tsconfig.json');
|
|
expect(appTsConfig.extends).toBe('../../tsconfig.json');
|
|
});
|
|
|
|
it('should set default project', async () => {
|
|
// ACT
|
|
await generateApp(appTree);
|
|
|
|
// ASSERT
|
|
const { defaultProject } = readWorkspaceConfiguration(appTree);
|
|
expect(defaultProject).toBe('my-app');
|
|
});
|
|
|
|
it('should not overwrite default project if already set', async () => {
|
|
// ARRANGE
|
|
const workspace = readWorkspaceConfiguration(appTree);
|
|
workspace.defaultProject = 'some-awesome-project';
|
|
devkit.updateWorkspaceConfiguration(appTree, workspace);
|
|
|
|
// ACT
|
|
await generateApp(appTree);
|
|
|
|
// ASSERT
|
|
const { defaultProject } = readWorkspaceConfiguration(appTree);
|
|
expect(defaultProject).toBe('some-awesome-project');
|
|
});
|
|
|
|
it('should not set default project when "--skip-default-project=true"', async () => {
|
|
// ACT
|
|
await generateApp(appTree, 'my-app', { skipDefaultProject: true });
|
|
|
|
// ASSERT
|
|
const { defaultProject } = readWorkspaceConfiguration(appTree);
|
|
expect(defaultProject).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('nested', () => {
|
|
it('should update workspace.json', async () => {
|
|
await generateApp(appTree, 'myApp', { directory: 'myDir' });
|
|
const workspaceJson = readJson(appTree, '/workspace.json');
|
|
|
|
expect(workspaceJson.projects['my-dir-my-app']).toMatchSnapshot();
|
|
expect(workspaceJson.projects['my-dir-my-app-e2e']).toMatchSnapshot();
|
|
});
|
|
|
|
it('should update tags + implicit dependencies', async () => {
|
|
await generateApp(appTree, 'myApp', {
|
|
directory: 'myDir',
|
|
tags: 'one,two,my-dir-my-app',
|
|
});
|
|
const projects = devkit.getProjects(appTree);
|
|
expect(projects).toEqual(
|
|
new Map(
|
|
Object.entries({
|
|
'my-dir-my-app': expect.objectContaining({
|
|
tags: ['one', 'two', 'my-dir-my-app'],
|
|
}),
|
|
'my-dir-my-app-e2e': expect.objectContaining({
|
|
implicitDependencies: ['my-dir-my-app'],
|
|
tags: [],
|
|
}),
|
|
})
|
|
)
|
|
);
|
|
});
|
|
|
|
it('should generate files', async () => {
|
|
const hasJsonValue = ({ path, expectedValue, lookupFn }) => {
|
|
const content = readJson(appTree, path);
|
|
|
|
expect(lookupFn(content)).toEqual(expectedValue);
|
|
};
|
|
await generateApp(appTree, 'myApp', { directory: 'myDir' });
|
|
|
|
const appModulePath = 'apps/my-dir/my-app/src/app/app.module.ts';
|
|
expect(appTree.read(appModulePath, 'utf-8')).toContain('class AppModule');
|
|
|
|
// Make sure these exist
|
|
[
|
|
`apps/my-dir/my-app/jest.config.ts`,
|
|
'apps/my-dir/my-app/src/main.ts',
|
|
'apps/my-dir/my-app/src/app/app.module.ts',
|
|
'apps/my-dir/my-app/src/app/app.component.ts',
|
|
'apps/my-dir/my-app-e2e/cypress.config.ts',
|
|
].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: ['jest.config.ts', '**/*.test.ts', '**/*.spec.ts'],
|
|
},
|
|
{
|
|
path: 'apps/my-dir/my-app/.eslintrc.json',
|
|
lookupFn: (json) => json.extends,
|
|
expectedValue: ['../../../.eslintrc.json'],
|
|
},
|
|
].forEach(hasJsonValue);
|
|
});
|
|
|
|
it('should extend from tsconfig.base.json', async () => {
|
|
// ACT
|
|
await generateApp(appTree, 'app', { directory: 'myDir' });
|
|
|
|
// ASSERT
|
|
const appTsConfig = readJson(appTree, 'apps/my-dir/app/tsconfig.json');
|
|
expect(appTsConfig.extends).toBe('../../../tsconfig.base.json');
|
|
});
|
|
|
|
it('should support a root tsconfig.json instead of tsconfig.base.json', async () => {
|
|
// ARRANGE
|
|
appTree.rename('tsconfig.base.json', 'tsconfig.json');
|
|
|
|
// ACT
|
|
await generateApp(appTree, 'app', { directory: 'myDir' });
|
|
|
|
// ASSERT
|
|
const appTsConfig = readJson(appTree, 'apps/my-dir/app/tsconfig.json');
|
|
expect(appTsConfig.extends).toBe('../../../tsconfig.json');
|
|
});
|
|
});
|
|
|
|
describe('at the root', () => {
|
|
beforeEach(() => {
|
|
appTree = createTreeWithEmptyWorkspace();
|
|
updateJson(appTree, 'nx.json', (json) => ({
|
|
...json,
|
|
workspaceLayout: { appsDir: '' },
|
|
}));
|
|
});
|
|
|
|
it('should accept numbers in the path', async () => {
|
|
// ACT
|
|
await generateApp(appTree, 'myApp', { directory: 'src/9-websites' });
|
|
|
|
// ASSERT
|
|
|
|
expect(
|
|
readProjectConfiguration(appTree, 'src-9-websites-my-app').root
|
|
).toMatchSnapshot();
|
|
});
|
|
|
|
it('should generate files', async () => {
|
|
const hasJsonValue = ({ path, expectedValue, lookupFn }) => {
|
|
const content = readJson(appTree, path);
|
|
|
|
expect(lookupFn(content)).toEqual(expectedValue);
|
|
};
|
|
await generateApp(appTree, 'myApp', { directory: 'myDir' });
|
|
|
|
const appModulePath = 'my-dir/my-app/src/app/app.module.ts';
|
|
expect(appTree.read(appModulePath, 'utf-8')).toContain('class AppModule');
|
|
|
|
// Make sure these exist
|
|
[
|
|
'my-dir/my-app/jest.config.ts',
|
|
'my-dir/my-app/src/main.ts',
|
|
'my-dir/my-app/src/app/app.module.ts',
|
|
'my-dir/my-app/src/app/app.component.ts',
|
|
'my-dir/my-app-e2e/cypress.config.ts',
|
|
].forEach((path) => {
|
|
expect(appTree.exists(path)).toBeTruthy();
|
|
});
|
|
|
|
// Make sure these have properties
|
|
[
|
|
{
|
|
path: 'my-dir/my-app/tsconfig.app.json',
|
|
lookupFn: (json) => json.compilerOptions.outDir,
|
|
expectedValue: '../../dist/out-tsc',
|
|
},
|
|
{
|
|
path: 'my-dir/my-app/tsconfig.app.json',
|
|
lookupFn: (json) => json.exclude,
|
|
expectedValue: ['jest.config.ts', '**/*.test.ts', '**/*.spec.ts'],
|
|
},
|
|
{
|
|
path: 'my-dir/my-app/.eslintrc.json',
|
|
lookupFn: (json) => json.extends,
|
|
expectedValue: ['../../.eslintrc.json'],
|
|
},
|
|
].forEach(hasJsonValue);
|
|
});
|
|
});
|
|
|
|
describe('routing', () => {
|
|
it('should include RouterTestingModule', async () => {
|
|
await generateApp(appTree, 'myApp', {
|
|
directory: 'myDir',
|
|
routing: true,
|
|
});
|
|
expect(
|
|
appTree.read('apps/my-dir/my-app/src/app/app.module.ts', 'utf-8')
|
|
).toContain('RouterModule.forRoot');
|
|
expect(
|
|
appTree.read(
|
|
'apps/my-dir/my-app/src/app/app.component.spec.ts',
|
|
'utf-8'
|
|
)
|
|
).toContain('imports: [RouterTestingModule]');
|
|
});
|
|
|
|
it('should not modify tests when --skip-tests is set', async () => {
|
|
await generateApp(appTree, 'myApp', {
|
|
directory: 'myDir',
|
|
routing: true,
|
|
skipTests: true,
|
|
});
|
|
expect(
|
|
appTree.exists('apps/my-dir/my-app/src/app/app.component.spec.ts')
|
|
).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe('template generation mode', () => {
|
|
it('should create Nx specific `app.component.html` template', async () => {
|
|
await generateApp(appTree, 'myApp', { directory: 'myDir' });
|
|
expect(
|
|
appTree.read('apps/my-dir/my-app/src/app/app.component.html', 'utf-8')
|
|
).toContain('<proj-nx-welcome></proj-nx-welcome>');
|
|
});
|
|
|
|
it("should update `template`'s property of AppComponent with Nx content", async () => {
|
|
await generateApp(appTree, 'myApp', {
|
|
directory: 'myDir',
|
|
inlineTemplate: true,
|
|
});
|
|
expect(
|
|
appTree.read('apps/my-dir/my-app/src/app/app.component.ts', 'utf-8')
|
|
).toContain('<proj-nx-welcome></proj-nx-welcome>');
|
|
});
|
|
|
|
it('should create Nx specific `nx-welcome.component.ts` file', async () => {
|
|
await generateApp(appTree, 'myApp', { directory: 'myDir' });
|
|
expect(
|
|
appTree.read(
|
|
'apps/my-dir/my-app/src/app/nx-welcome.component.ts',
|
|
'utf-8'
|
|
)
|
|
).toContain('Hello there');
|
|
});
|
|
|
|
it('should update the AppComponent spec to target Nx content', async () => {
|
|
await generateApp(appTree, 'myApp', {
|
|
directory: 'myDir',
|
|
inlineTemplate: true,
|
|
});
|
|
const testFileContent = appTree.read(
|
|
'apps/my-dir/my-app/src/app/app.component.spec.ts',
|
|
'utf-8'
|
|
);
|
|
|
|
expect(testFileContent).toContain(`querySelector('h1')`);
|
|
expect(testFileContent).toContain('Welcome my-dir-my-app');
|
|
});
|
|
});
|
|
|
|
describe('--style scss', () => {
|
|
it('should generate scss styles', async () => {
|
|
await generateApp(appTree, 'myApp', { style: 'scss' });
|
|
expect(appTree.exists('apps/my-app/src/app/app.component.scss')).toEqual(
|
|
true
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('--style sass', () => {
|
|
it('should generate sass styles', async () => {
|
|
await generateApp(appTree, 'myApp', { style: 'sass' });
|
|
expect(appTree.exists('apps/my-app/src/app/app.component.sass')).toEqual(
|
|
true
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('--style less', () => {
|
|
it('should generate less styles', async () => {
|
|
await generateApp(appTree, 'myApp', { style: 'less' });
|
|
expect(appTree.exists('apps/my-app/src/app/app.component.less')).toEqual(
|
|
true
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('--skipFormat', () => {
|
|
it('should format files by default', async () => {
|
|
const spy = jest.spyOn(devkit, 'formatFiles');
|
|
|
|
await generateApp(appTree);
|
|
|
|
expect(spy).toHaveBeenCalled();
|
|
});
|
|
|
|
// Need a better way of determing if the formatFiles function
|
|
// was called directly from the application generator
|
|
// and not by a different generator that's used withing this
|
|
xit('should skip format when set to true', async () => {
|
|
const spy = jest.spyOn(devkit, 'formatFiles');
|
|
|
|
await generateApp(appTree, 'myApp', { skipFormat: true });
|
|
|
|
expect(spy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('--linter', () => {
|
|
describe('eslint', () => {
|
|
it('should add an architect target for lint', async () => {
|
|
await generateApp(appTree, 'myApp', { linter: Linter.EsLint });
|
|
const workspaceJson = readJson(appTree, 'workspace.json');
|
|
expect(workspaceJson.projects['my-app'].architect.lint)
|
|
.toMatchInlineSnapshot(`
|
|
Object {
|
|
"builder": "@nrwl/linter:eslint",
|
|
"options": Object {
|
|
"lintFilePatterns": Array [
|
|
"apps/my-app/**/*.ts",
|
|
"apps/my-app/**/*.html",
|
|
],
|
|
},
|
|
}
|
|
`);
|
|
expect(workspaceJson.projects['my-app-e2e'].architect.lint)
|
|
.toMatchInlineSnapshot(`
|
|
Object {
|
|
"builder": "@nrwl/linter:eslint",
|
|
"options": Object {
|
|
"lintFilePatterns": Array [
|
|
"apps/my-app-e2e/**/*.{js,ts}",
|
|
],
|
|
},
|
|
"outputs": Array [
|
|
"{options.outputFile}",
|
|
],
|
|
}
|
|
`);
|
|
});
|
|
|
|
it('should add a lint target when e2e test runner is protractor', async () => {
|
|
await generateApp(appTree, 'myApp', {
|
|
linter: Linter.EsLint,
|
|
e2eTestRunner: E2eTestRunner.Protractor,
|
|
});
|
|
const workspaceJson = readJson(appTree, 'workspace.json');
|
|
expect(workspaceJson.projects['my-app'].architect.lint)
|
|
.toMatchInlineSnapshot(`
|
|
Object {
|
|
"builder": "@nrwl/linter:eslint",
|
|
"options": Object {
|
|
"lintFilePatterns": Array [
|
|
"apps/my-app/**/*.ts",
|
|
"apps/my-app/**/*.html",
|
|
],
|
|
},
|
|
}
|
|
`);
|
|
expect(appTree.exists('apps/my-app-e2e/.eslintrc.json')).toBeTruthy();
|
|
expect(workspaceJson.projects['my-app-e2e'].architect.lint)
|
|
.toMatchInlineSnapshot(`
|
|
Object {
|
|
"builder": "@nrwl/linter:eslint",
|
|
"options": Object {
|
|
"lintFilePatterns": Array [
|
|
"apps/my-app-e2e/**/*.ts",
|
|
],
|
|
},
|
|
"outputs": Array [
|
|
"{options.outputFile}",
|
|
],
|
|
}
|
|
`);
|
|
});
|
|
|
|
it('should add valid eslint JSON configuration which extends from Nx presets', async () => {
|
|
await generateApp(appTree, 'myApp', { linter: Linter.EsLint });
|
|
|
|
const eslintConfig = readJson(appTree, 'apps/my-app/.eslintrc.json');
|
|
expect(eslintConfig).toMatchInlineSnapshot(`
|
|
Object {
|
|
"extends": Array [
|
|
"../../.eslintrc.json",
|
|
],
|
|
"ignorePatterns": Array [
|
|
"!**/*",
|
|
],
|
|
"overrides": Array [
|
|
Object {
|
|
"extends": Array [
|
|
"plugin:@nrwl/nx/angular",
|
|
"plugin:@angular-eslint/template/process-inline-templates",
|
|
],
|
|
"files": Array [
|
|
"*.ts",
|
|
],
|
|
"rules": Object {
|
|
"@angular-eslint/component-selector": Array [
|
|
"error",
|
|
Object {
|
|
"prefix": "proj",
|
|
"style": "kebab-case",
|
|
"type": "element",
|
|
},
|
|
],
|
|
"@angular-eslint/directive-selector": Array [
|
|
"error",
|
|
Object {
|
|
"prefix": "proj",
|
|
"style": "camelCase",
|
|
"type": "attribute",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
Object {
|
|
"extends": Array [
|
|
"plugin:@nrwl/nx/angular-template",
|
|
],
|
|
"files": Array [
|
|
"*.html",
|
|
],
|
|
"rules": Object {},
|
|
},
|
|
],
|
|
}
|
|
`);
|
|
});
|
|
});
|
|
|
|
describe('none', () => {
|
|
it('should not add an architect target for lint', async () => {
|
|
await generateApp(appTree, 'myApp', { linter: Linter.None });
|
|
const workspaceJson = readJson(appTree, 'workspace.json');
|
|
expect(workspaceJson.projects['my-app'].architect.lint).toBeUndefined();
|
|
expect(
|
|
workspaceJson.projects['my-app-e2e'].architect.lint
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it('should not add an architect target for lint when e2e test runner is protractor', async () => {
|
|
await generateApp(appTree, 'myApp', {
|
|
linter: Linter.None,
|
|
e2eTestRunner: E2eTestRunner.Protractor,
|
|
});
|
|
const workspaceJson = readJson(appTree, 'workspace.json');
|
|
expect(workspaceJson.projects['my-app'].architect.lint).toBeUndefined();
|
|
expect(
|
|
workspaceJson.projects['my-app-e2e'].architect.lint
|
|
).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('--unit-test-runner', () => {
|
|
describe('default (jest)', () => {
|
|
it('should generate jest.config.ts with serializers', async () => {
|
|
await generateApp(appTree);
|
|
|
|
const jestConfig = appTree.read('apps/my-app/jest.config.ts', 'utf-8');
|
|
|
|
expect(jestConfig).toContain(
|
|
`'jest-preset-angular/build/serializers/no-ng-attributes'`
|
|
);
|
|
expect(jestConfig).toContain(
|
|
`'jest-preset-angular/build/serializers/ng-snapshot'`
|
|
);
|
|
expect(jestConfig).toContain(
|
|
`'jest-preset-angular/build/serializers/html-comment'`
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('karma', () => {
|
|
it('should generate a karma config', async () => {
|
|
await generateApp(appTree, 'myApp', {
|
|
unitTestRunner: UnitTestRunner.Karma,
|
|
});
|
|
|
|
expect(appTree.exists('apps/my-app/tsconfig.spec.json')).toBeTruthy();
|
|
expect(appTree.exists('apps/my-app/karma.conf.js')).toBeTruthy();
|
|
const workspaceJson = readJson(appTree, 'workspace.json');
|
|
expect(workspaceJson.projects['my-app'].architect.test.builder).toEqual(
|
|
'@angular-devkit/build-angular:karma'
|
|
);
|
|
const tsconfigAppJson = readJson(
|
|
appTree,
|
|
'apps/my-app/tsconfig.app.json'
|
|
);
|
|
expect(tsconfigAppJson.compilerOptions.outDir).toEqual(
|
|
'../../dist/out-tsc'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('none', () => {
|
|
it('should not generate test configuration', async () => {
|
|
await generateApp(appTree, 'myApp', {
|
|
unitTestRunner: UnitTestRunner.None,
|
|
});
|
|
expect(appTree.exists('apps/my-app/src/test-setup.ts')).toBeFalsy();
|
|
expect(appTree.exists('apps/my-app/src/test.ts')).toBeFalsy();
|
|
expect(appTree.exists('apps/my-app/tsconfig.spec.json')).toBeFalsy();
|
|
expect(appTree.exists('apps/my-app/jest.config.ts')).toBeFalsy();
|
|
expect(appTree.exists('apps/my-app/karma.config.js')).toBeFalsy();
|
|
expect(
|
|
appTree.exists('apps/my-app/src/app/app.component.spec.ts')
|
|
).toBeFalsy();
|
|
const workspaceJson = readJson(appTree, 'workspace.json');
|
|
expect(workspaceJson.projects['my-app'].architect.test).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('--e2e-test-runner', () => {
|
|
describe(E2eTestRunner.Protractor, () => {
|
|
it('should create the e2e project in v2 workspace', async () => {
|
|
appTree = createTreeWithEmptyWorkspace();
|
|
|
|
expect(
|
|
async () =>
|
|
await generateApp(appTree, 'myApp', {
|
|
e2eTestRunner: E2eTestRunner.Protractor,
|
|
standaloneConfig: true,
|
|
})
|
|
).not.toThrow();
|
|
});
|
|
|
|
it('should update workspace.json', async () => {
|
|
await generateApp(appTree, 'myApp', {
|
|
e2eTestRunner: E2eTestRunner.Protractor,
|
|
});
|
|
const workspaceJson = readJson(appTree, 'workspace.json');
|
|
expect(
|
|
workspaceJson.projects['my-app'].architect.e2e
|
|
).not.toBeDefined();
|
|
expect(workspaceJson.projects['my-app-e2e']).toEqual({
|
|
root: 'apps/my-app-e2e',
|
|
projectType: 'application',
|
|
architect: {
|
|
e2e: {
|
|
builder: '@angular-devkit/build-angular:protractor',
|
|
options: {
|
|
protractorConfig: 'apps/my-app-e2e/protractor.conf.js',
|
|
},
|
|
configurations: {
|
|
development: {
|
|
devServerTarget: 'my-app:serve:development',
|
|
},
|
|
production: {
|
|
devServerTarget: 'my-app:serve:production',
|
|
},
|
|
},
|
|
defaultConfiguration: 'development',
|
|
},
|
|
lint: {
|
|
builder: '@nrwl/linter:eslint',
|
|
outputs: ['{options.outputFile}'],
|
|
options: {
|
|
lintFilePatterns: ['apps/my-app-e2e/**/*.ts'],
|
|
},
|
|
},
|
|
},
|
|
implicitDependencies: ['my-app'],
|
|
tags: [],
|
|
});
|
|
});
|
|
|
|
it('should update E2E spec files to match the app name', async () => {
|
|
await generateApp(appTree, 'myApp', {
|
|
e2eTestRunner: E2eTestRunner.Protractor,
|
|
});
|
|
|
|
expect(
|
|
appTree.read('apps/my-app-e2e/src/app.e2e-spec.ts', 'utf-8')
|
|
).toContain(`'Welcome my-app'`);
|
|
expect(
|
|
appTree.read('apps/my-app-e2e/src/app.po.ts', 'utf-8')
|
|
).toContain(`'proj-root header h1'`);
|
|
});
|
|
|
|
it('should update E2E spec files to match the app name when generating within a directory', async () => {
|
|
await generateApp(appTree, 'myApp', {
|
|
e2eTestRunner: E2eTestRunner.Protractor,
|
|
directory: 'my-directory',
|
|
});
|
|
|
|
expect(
|
|
appTree.read(
|
|
'apps/my-directory/my-app-e2e/src/app.e2e-spec.ts',
|
|
'utf-8'
|
|
)
|
|
).toContain(`'Welcome my-directory-my-app'`);
|
|
expect(
|
|
appTree.read('apps/my-directory/my-app-e2e/src/app.po.ts', 'utf-8')
|
|
).toContain(`'proj-root header h1'`);
|
|
});
|
|
});
|
|
|
|
describe('none', () => {
|
|
it('should not generate test configuration', async () => {
|
|
await generateApp(appTree, 'myApp', {
|
|
e2eTestRunner: E2eTestRunner.None,
|
|
});
|
|
expect(appTree.exists('apps/my-app-e2e')).toBeFalsy();
|
|
const workspaceJson = readJson(appTree, 'workspace.json');
|
|
expect(workspaceJson.projects['my-app-e2e']).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('replaceAppNameWithPath', () => {
|
|
it('should protect `workspace.json` commands and properties', async () => {
|
|
await generateApp(appTree, 'ui');
|
|
const workspaceJson = readJson(appTree, 'workspace.json');
|
|
expect(workspaceJson.projects['ui']).toBeDefined();
|
|
expect(
|
|
workspaceJson.projects['ui']['architect']['build']['builder']
|
|
).toEqual('@angular-devkit/build-angular:browser');
|
|
});
|
|
|
|
it('should protect `workspace.json` sensible properties value to be renamed', async () => {
|
|
await generateApp(appTree, 'ui', { prefix: 'ui' });
|
|
const workspaceJson = readJson(appTree, 'workspace.json');
|
|
expect(workspaceJson.projects['ui'].prefix).toEqual('ui');
|
|
});
|
|
});
|
|
|
|
describe('--backend-project', () => {
|
|
describe('with a backend project', () => {
|
|
it('should add a proxy.conf.json to app', async () => {
|
|
await generateApp(appTree, 'customer-ui', {
|
|
backendProject: 'customer-api',
|
|
});
|
|
|
|
const proxyConfContent = JSON.stringify(
|
|
{
|
|
'/customer-api': {
|
|
target: 'http://localhost:3333',
|
|
secure: false,
|
|
},
|
|
},
|
|
null,
|
|
2
|
|
);
|
|
|
|
expect(appTree.exists('apps/customer-ui/proxy.conf.json')).toBeTruthy();
|
|
expect(
|
|
appTree.read('apps/customer-ui/proxy.conf.json', 'utf-8')
|
|
).toContain(proxyConfContent);
|
|
});
|
|
});
|
|
|
|
describe('with no backend project', () => {
|
|
it('should not generate a proxy.conf.json', async () => {
|
|
await generateApp(appTree, 'customer-ui');
|
|
|
|
expect(appTree.exists('apps/customer-ui/proxy.conf.json')).toBeFalsy();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('--strict', () => {
|
|
it('should enable strict type checking', async () => {
|
|
await generateApp(appTree, 'my-app', { strict: true });
|
|
|
|
// define all the tsconfig files to update
|
|
const configFiles = [
|
|
'apps/my-app/tsconfig.json',
|
|
'apps/my-app-e2e/tsconfig.json',
|
|
];
|
|
|
|
for (const configFile of configFiles) {
|
|
const { compilerOptions, angularCompilerOptions } = parseJson(
|
|
appTree.read(configFile, 'utf-8')
|
|
);
|
|
|
|
// check that the TypeScript compiler options have been updated
|
|
expect(compilerOptions.forceConsistentCasingInFileNames).toBe(true);
|
|
expect(compilerOptions.strict).toBe(true);
|
|
expect(compilerOptions.noImplicitOverride).toBe(true);
|
|
expect(compilerOptions.noPropertyAccessFromIndexSignature).toBe(true);
|
|
expect(compilerOptions.noImplicitReturns).toBe(true);
|
|
expect(compilerOptions.noFallthroughCasesInSwitch).toBe(true);
|
|
|
|
// check that the Angular Template options have been updated
|
|
expect(angularCompilerOptions.strictInjectionParameters).toBe(true);
|
|
expect(angularCompilerOptions.strictTemplates).toBe(true);
|
|
}
|
|
|
|
// should not update workspace configuration since --strict=true is the default
|
|
const nxJson = readJson<NxJsonConfiguration>(appTree, 'nx.json');
|
|
expect(
|
|
nxJson.generators['@nrwl/angular:application'].strict
|
|
).not.toBeDefined();
|
|
});
|
|
|
|
it('should set defaults when --strict=false', async () => {
|
|
await generateApp(appTree, 'my-app', { strict: false });
|
|
|
|
// check to see if the workspace configuration has been updated to turn off
|
|
// strict mode by default in future applications
|
|
const nxJson = readJson<NxJsonConfiguration>(appTree, 'nx.json');
|
|
expect(nxJson.generators['@nrwl/angular:application'].strict).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('--add-tailwind', () => {
|
|
it('should not add a tailwind.config.js and relevant packages when "--add-tailwind" is not specified', async () => {
|
|
// ACT
|
|
await generateApp(appTree, 'app1');
|
|
|
|
// ASSERT
|
|
expect(appTree.exists('apps/app1/tailwind.config.js')).toBeFalsy();
|
|
const { devDependencies } = readJson(appTree, 'package.json');
|
|
expect(devDependencies['tailwindcss']).toBeUndefined();
|
|
expect(devDependencies['postcss']).toBeUndefined();
|
|
expect(devDependencies['autoprefixer']).toBeUndefined();
|
|
});
|
|
|
|
it('should not add a tailwind.config.js and relevant packages when "--add-tailwind=false"', async () => {
|
|
// ACT
|
|
await generateApp(appTree, 'app1', { addTailwind: false });
|
|
|
|
// ASSERT
|
|
expect(appTree.exists('apps/app1/tailwind.config.js')).toBeFalsy();
|
|
const { devDependencies } = readJson(appTree, 'package.json');
|
|
expect(devDependencies['tailwindcss']).toBeUndefined();
|
|
expect(devDependencies['postcss']).toBeUndefined();
|
|
expect(devDependencies['autoprefixer']).toBeUndefined();
|
|
});
|
|
|
|
it('should add a tailwind.config.js and relevant packages when "--add-tailwind=true"', async () => {
|
|
// ACT
|
|
await generateApp(appTree, 'app1', { addTailwind: true });
|
|
|
|
// ASSERT
|
|
expect(appTree.read('apps/app1/tailwind.config.js', 'utf-8'))
|
|
.toMatchInlineSnapshot(`
|
|
"const { createGlobPatternsForDependencies } = require('@nrwl/angular/tailwind');
|
|
const { join } = require('path');
|
|
|
|
/** @type {import('tailwindcss').Config} */
|
|
module.exports = {
|
|
content: [
|
|
join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'),
|
|
...createGlobPatternsForDependencies(__dirname),
|
|
],
|
|
theme: {
|
|
extend: {},
|
|
},
|
|
plugins: [],
|
|
};
|
|
"
|
|
`);
|
|
const { devDependencies } = readJson(appTree, 'package.json');
|
|
expect(devDependencies['tailwindcss']).toBe(tailwindVersion);
|
|
expect(devDependencies['postcss']).toBe(postcssVersion);
|
|
expect(devDependencies['autoprefixer']).toBe(autoprefixerVersion);
|
|
});
|
|
});
|
|
|
|
describe('--standalone', () => {
|
|
it('should generate a standalone app correctly with routing', async () => {
|
|
// ACT
|
|
await generateApp(appTree, 'standalone', {
|
|
standalone: true,
|
|
routing: true,
|
|
});
|
|
|
|
// ASSERT
|
|
expect(
|
|
appTree.read('apps/standalone/src/main.ts', 'utf-8')
|
|
).toMatchSnapshot();
|
|
expect(
|
|
appTree.read('apps/standalone/src/app/app.routes.ts', 'utf-8')
|
|
).toMatchSnapshot();
|
|
expect(
|
|
appTree.read('apps/standalone/src/app/app.component.ts', 'utf-8')
|
|
).toMatchSnapshot();
|
|
expect(
|
|
appTree.read('apps/standalone/src/app/app.component.spec.ts', 'utf-8')
|
|
).toMatchSnapshot();
|
|
expect(
|
|
appTree.exists('apps/standalone/src/app/app.module.ts')
|
|
).toBeFalsy();
|
|
expect(
|
|
appTree.read('apps/standalone/src/app/nx-welcome.component.ts', 'utf-8')
|
|
).toContain('standalone: true');
|
|
});
|
|
|
|
it('should generate a standalone app correctly without routing', async () => {
|
|
// ACT
|
|
await generateApp(appTree, 'standalone', {
|
|
standalone: true,
|
|
routing: false,
|
|
});
|
|
|
|
// ASSERT
|
|
expect(
|
|
appTree.read('apps/standalone/src/main.ts', 'utf-8')
|
|
).toMatchSnapshot();
|
|
expect(
|
|
appTree.read('apps/standalone/src/app/app.component.ts', 'utf-8')
|
|
).toMatchSnapshot();
|
|
expect(
|
|
appTree.read('apps/standalone/src/app/app.component.spec.ts', 'utf-8')
|
|
).toMatchSnapshot();
|
|
expect(
|
|
appTree.exists('apps/standalone/src/app/app.module.ts')
|
|
).toBeFalsy();
|
|
expect(
|
|
appTree.read('apps/standalone/src/app/nx-welcome.component.ts', 'utf-8')
|
|
).toContain('standalone: true');
|
|
});
|
|
});
|
|
|
|
it('should generate correct main.ts', async () => {
|
|
// ACT
|
|
await generateApp(appTree, 'myapp');
|
|
|
|
// ASSERT
|
|
expect(appTree.read('apps/myapp/src/main.ts', 'utf-8'))
|
|
.toMatchInlineSnapshot(`
|
|
"import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
|
|
|
import { AppModule } from './app/app.module';
|
|
|
|
|
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
|
.catch(err => console.error(err));
|
|
"
|
|
`);
|
|
});
|
|
});
|
|
|
|
async function generateApp(
|
|
appTree: Tree,
|
|
name: string = 'myApp',
|
|
options: Partial<Schema> = {}
|
|
) {
|
|
await applicationGenerator(appTree, {
|
|
name,
|
|
skipFormat: false,
|
|
e2eTestRunner: E2eTestRunner.Cypress,
|
|
unitTestRunner: UnitTestRunner.Jest,
|
|
linter: Linter.EsLint,
|
|
...options,
|
|
});
|
|
}
|