chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
import { baseConfig, e2eTestOnlyIgnores } from '../../eslint.config.mjs';
|
||||
|
||||
export default [...baseConfig, e2eTestOnlyIgnores];
|
||||
@@ -0,0 +1,13 @@
|
||||
/* eslint-disable */
|
||||
module.exports = {
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
|
||||
maxWorkers: 1,
|
||||
globals: {},
|
||||
globalSetup: '../utils/global-setup.ts',
|
||||
globalTeardown: '../utils/global-teardown.ts',
|
||||
displayName: 'e2e-angular',
|
||||
preset: '../jest.preset.e2e.js',
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@nx/e2e-angular",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"nx": "workspace:*",
|
||||
"@nx/devkit": "workspace:*",
|
||||
"@nx/e2e-utils": "workspace:*",
|
||||
"@nx/angular": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "e2e-angular",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "e2e/angular",
|
||||
"projectType": "application",
|
||||
"implicitDependencies": ["angular", "angular-rspack"],
|
||||
"// targets": "to see all targets run: nx show project e2e-angular --web",
|
||||
"targets": {
|
||||
"lint": {
|
||||
"options": {
|
||||
"max-warnings": 9
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import {
|
||||
checkFilesExist,
|
||||
cleanupProject,
|
||||
newProject,
|
||||
removeFile,
|
||||
runCLI,
|
||||
uniq,
|
||||
updateFile,
|
||||
} from '@nx/e2e-utils';
|
||||
|
||||
describe('angular.json v1 config', () => {
|
||||
const app1 = uniq('app1');
|
||||
|
||||
beforeAll(() => {
|
||||
newProject({
|
||||
packages: ['@nx/angular', '@nx/webpack', '@nx/jest', '@nx/playwright'],
|
||||
});
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${app1} --bundler=webpack --unit-test-runner=jest --no-interactive`
|
||||
);
|
||||
// reset workspace to use v1 config
|
||||
updateFile(`angular.json`, angularV1Json(app1));
|
||||
removeFile(`${app1}/project.json`);
|
||||
removeFile(`${app1}-e2e/project.json`);
|
||||
});
|
||||
afterAll(() => cleanupProject());
|
||||
|
||||
it('should support projects in angular.json v1 config', async () => {
|
||||
expect(runCLI(`build ${app1}`)).toContain('Successfully ran target build');
|
||||
expect(runCLI(`test ${app1} --no-watch`)).toContain(
|
||||
'Successfully ran target test'
|
||||
);
|
||||
}, 1000000);
|
||||
|
||||
it('should generate new app with project.json and keep the existing in angular.json', async () => {
|
||||
// create new app
|
||||
const app2 = uniq('app2');
|
||||
runCLI(`generate @nx/angular:app ${app2} --no-interactive`);
|
||||
|
||||
// should generate project.json for new projects
|
||||
checkFilesExist(`${app2}/project.json`);
|
||||
// check it works correctly
|
||||
expect(runCLI(`build ${app2}`)).toContain('Successfully ran target build');
|
||||
expect(runCLI(`test ${app2} --no-watch`)).toContain(
|
||||
'Successfully ran target test'
|
||||
);
|
||||
// check existing app in angular.json still works
|
||||
expect(runCLI(`build ${app1}`)).toContain('Successfully ran target build');
|
||||
expect(runCLI(`test ${app1} --no-watch`)).toContain(
|
||||
'Successfully ran target test'
|
||||
);
|
||||
}, 1000000);
|
||||
});
|
||||
|
||||
const angularV1Json = (appName: string) => `{
|
||||
"version": 1,
|
||||
"projects": {
|
||||
"${appName}": {
|
||||
"projectType": "application",
|
||||
"root": "${appName}",
|
||||
"sourceRoot": "${appName}/src",
|
||||
"prefix": "v1angular",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist${appName}",
|
||||
"index": "${appName}/src/index.html",
|
||||
"main": "${appName}/src/main.ts",
|
||||
"tsConfig": "${appName}/tsconfig.app.json",
|
||||
"assets": ["${appName}/src/favicon.ico", "${appName}/src/assets"],
|
||||
"styles": ["${appName}/src/styles.css"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kb",
|
||||
"maximumError": "8kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "${appName}:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "${appName}:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "${appName}:build"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@nx/eslint:lint"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage${appName}"],
|
||||
"options": {
|
||||
"jestConfig": "${appName}/jest.config.cts",
|
||||
"passWithNoTests": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
"${appName}-e2e": {
|
||||
"root": "${appName}-e2e",
|
||||
"sourceRoot": "${appName}-e2e/src",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"e2e": {
|
||||
"builder": "@nx/playwright:playwright",
|
||||
"options": {
|
||||
"config": "${appName}-e2e/playwright.config.js"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@nx/eslint:lint",
|
||||
"outputs": ["{options.outputFile}"]
|
||||
}
|
||||
},
|
||||
"tags": [],
|
||||
"implicitDependencies": ["${appName}"]
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { runCLI, runE2ETests } from '@nx/e2e-utils';
|
||||
import {
|
||||
setupCypressComponentTests,
|
||||
cleanupCypressComponentTests,
|
||||
CypressComponentTestsSetup,
|
||||
} from './cypress-component-tests-setup';
|
||||
|
||||
describe('Angular Cypress Component Tests - App', () => {
|
||||
let setup: CypressComponentTestsSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
setup = setupCypressComponentTests();
|
||||
});
|
||||
|
||||
afterAll(() => cleanupCypressComponentTests());
|
||||
|
||||
// TODO(jack): re-enable when lodash@4.18.0 assignWith bug is resolved
|
||||
it.skip('should test app', () => {
|
||||
const { appName } = setup;
|
||||
|
||||
runCLI(
|
||||
`generate @nx/angular:cypress-component-configuration --project=${appName} --generate-tests --no-interactive`
|
||||
);
|
||||
if (runE2ETests('cypress')) {
|
||||
expect(runCLI(`component-test ${appName}`)).toContain(
|
||||
'All specs passed!'
|
||||
);
|
||||
}
|
||||
}, 300_000);
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { runCLI, runE2ETests } from '@nx/e2e-utils';
|
||||
import {
|
||||
setupCypressComponentTests,
|
||||
cleanupCypressComponentTests,
|
||||
updateTestToAssertTailwindIsNotApplied,
|
||||
CypressComponentTestsSetup,
|
||||
} from './cypress-component-tests-setup';
|
||||
|
||||
describe('Angular Cypress Component Tests - Buildable Lib', () => {
|
||||
let setup: CypressComponentTestsSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
setup = setupCypressComponentTests();
|
||||
});
|
||||
|
||||
afterAll(() => cleanupCypressComponentTests());
|
||||
|
||||
// TODO(jack): re-enable when lodash@4.18.0 assignWith bug is resolved
|
||||
it.skip('should test buildable lib not being used in app', () => {
|
||||
const { appName, buildableLibName } = setup;
|
||||
|
||||
expect(() => {
|
||||
// should error since no edge in graph between lib and app
|
||||
runCLI(
|
||||
`generate @nx/angular:cypress-component-configuration --project=${buildableLibName} --generate-tests --no-interactive`
|
||||
);
|
||||
}).toThrow();
|
||||
|
||||
updateTestToAssertTailwindIsNotApplied(buildableLibName);
|
||||
|
||||
runCLI(
|
||||
`generate @nx/angular:cypress-component-configuration --project=${buildableLibName} --generate-tests --build-target=${appName}:build --no-interactive`
|
||||
);
|
||||
if (runE2ETests('cypress')) {
|
||||
expect(runCLI(`component-test ${buildableLibName}`)).toContain(
|
||||
'All specs passed!'
|
||||
);
|
||||
}
|
||||
}, 300_000);
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { runCLI, runE2ETests } from '@nx/e2e-utils';
|
||||
import {
|
||||
setupCypressComponentTests,
|
||||
cleanupCypressComponentTests,
|
||||
updateTestToAssertTailwindIsNotApplied,
|
||||
useBuildableLibInLib,
|
||||
updateBuilableLibTestsToAssertAppStyles,
|
||||
CypressComponentTestsSetup,
|
||||
} from './cypress-component-tests-setup';
|
||||
|
||||
describe('Angular Cypress Component Tests - Implicit Dep', () => {
|
||||
let setup: CypressComponentTestsSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
setup = setupCypressComponentTests();
|
||||
|
||||
// Setup cypress component testing for the buildable lib
|
||||
// This is needed for the tests in this file to work
|
||||
const { appName, buildableLibName } = setup;
|
||||
updateTestToAssertTailwindIsNotApplied(buildableLibName);
|
||||
runCLI(
|
||||
`generate @nx/angular:cypress-component-configuration --project=${buildableLibName} --generate-tests --build-target=${appName}:build --no-interactive`
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => cleanupCypressComponentTests());
|
||||
|
||||
// TODO(jack): re-enable when lodash@4.18.0 assignWith bug is resolved
|
||||
it.skip('should test lib with implicit dep on buildTarget', () => {
|
||||
const { projectName, appName, buildableLibName, usedInAppLibName } = setup;
|
||||
|
||||
// creates graph like buildableLib -> lib -> app
|
||||
// updates the apps styles and they should apply to the buildableLib
|
||||
// even though app is not directly connected to buildableLib
|
||||
useBuildableLibInLib(projectName, buildableLibName, usedInAppLibName);
|
||||
|
||||
updateBuilableLibTestsToAssertAppStyles(appName, buildableLibName);
|
||||
|
||||
if (runE2ETests('cypress')) {
|
||||
expect(runCLI(`component-test ${buildableLibName}`)).toContain(
|
||||
'All specs passed!'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { runCLI, runE2ETests } from '@nx/e2e-utils';
|
||||
import {
|
||||
setupCypressComponentTests,
|
||||
cleanupCypressComponentTests,
|
||||
CypressComponentTestsSetup,
|
||||
} from './cypress-component-tests-setup';
|
||||
|
||||
describe('Angular Cypress Component Tests - Lib', () => {
|
||||
let setup: CypressComponentTestsSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
setup = setupCypressComponentTests();
|
||||
});
|
||||
|
||||
afterAll(() => cleanupCypressComponentTests());
|
||||
|
||||
// TODO(jack): re-enable when lodash@4.18.0 assignWith bug is resolved
|
||||
it.skip('should successfully component test lib being used in app', () => {
|
||||
const { usedInAppLibName } = setup;
|
||||
|
||||
runCLI(
|
||||
`generate @nx/angular:cypress-component-configuration --project=${usedInAppLibName} --generate-tests --no-interactive`
|
||||
);
|
||||
if (runE2ETests('cypress')) {
|
||||
expect(runCLI(`component-test ${usedInAppLibName}`)).toContain(
|
||||
'All specs passed!'
|
||||
);
|
||||
}
|
||||
}, 300_000);
|
||||
});
|
||||
@@ -0,0 +1,328 @@
|
||||
import {
|
||||
cleanupProject,
|
||||
createFile,
|
||||
newProject,
|
||||
removeFile,
|
||||
runCLI,
|
||||
uniq,
|
||||
updateFile,
|
||||
updateJson,
|
||||
} from '@nx/e2e-utils';
|
||||
import { names } from '@nx/devkit';
|
||||
import { join } from 'path';
|
||||
|
||||
export interface CypressComponentTestsSetup {
|
||||
projectName: string;
|
||||
appName: string;
|
||||
usedInAppLibName: string;
|
||||
buildableLibName: string;
|
||||
}
|
||||
|
||||
export function setupCypressComponentTests(
|
||||
zoneless: boolean = true
|
||||
): CypressComponentTestsSetup {
|
||||
const projectName = newProject({
|
||||
name: uniq('cy-ng'),
|
||||
packages: [
|
||||
'@nx/angular',
|
||||
'@nx/webpack',
|
||||
'@nx/playwright',
|
||||
'@nx/vitest',
|
||||
'@nx/cypress',
|
||||
],
|
||||
});
|
||||
|
||||
const appName = uniq('cy-angular-app');
|
||||
const usedInAppLibName = uniq('cy-angular-lib');
|
||||
const buildableLibName = uniq('cy-angular-buildable-lib');
|
||||
|
||||
createApp(appName, zoneless ? [] : ['--no-zoneless']);
|
||||
createLib(projectName, appName, usedInAppLibName);
|
||||
useLibInApp(projectName, appName, usedInAppLibName);
|
||||
createBuildableLib(projectName, buildableLibName);
|
||||
useWorkspaceAssetsInApp(appName);
|
||||
|
||||
return {
|
||||
projectName,
|
||||
appName,
|
||||
usedInAppLibName,
|
||||
buildableLibName,
|
||||
};
|
||||
}
|
||||
|
||||
export function cleanupCypressComponentTests(): void {
|
||||
cleanupProject();
|
||||
}
|
||||
|
||||
function createApp(appName: string, extraArgs: string[] = []) {
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${appName} --bundler=webpack --no-interactive ${extraArgs.join(
|
||||
' '
|
||||
)}`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/angular:component ${appName}/src/lib/fancy-component/fancy-component --no-interactive`
|
||||
);
|
||||
}
|
||||
|
||||
function createLib(projectName: string, appName: string, libName: string) {
|
||||
runCLI(`generate @nx/angular:lib ${libName} --no-interactive`);
|
||||
runCLI(
|
||||
`generate @nx/angular:component ${libName}/src/lib/btn/btn --inlineTemplate --inlineStyle --export --no-interactive`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/angular:component ${libName}/src/lib/btn-standalone/btn-standalone --inlineTemplate --inlineStyle --export --standalone --no-interactive`
|
||||
);
|
||||
updateFile(
|
||||
`${libName}/src/lib/btn/btn.ts`,
|
||||
`
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: '${projectName}-btn',
|
||||
template: '<button class="text-green-500">{{text}}</button>',
|
||||
styles: []
|
||||
})
|
||||
export class Btn {
|
||||
@Input() text = 'something';
|
||||
}
|
||||
`
|
||||
);
|
||||
updateFile(
|
||||
`${libName}/src/lib/btn-standalone/btn-standalone.ts`,
|
||||
`
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
@Component({
|
||||
selector: '${projectName}-btn-standalone',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: '<button class="text-green-500">standlone-{{text}}</button>',
|
||||
styles: [],
|
||||
})
|
||||
export class BtnStandalone {
|
||||
@Input() text = 'something';
|
||||
}
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
function createBuildableLib(projectName: string, libName: string) {
|
||||
// create lib
|
||||
runCLI(`generate @nx/angular:lib ${libName} --buildable --no-interactive`);
|
||||
// create cmp for lib
|
||||
runCLI(
|
||||
`generate @nx/angular:component ${libName}/src/lib/input/input.component --inlineTemplate --inlineStyle --export --no-interactive`
|
||||
);
|
||||
// create standlone cmp for lib
|
||||
runCLI(
|
||||
`generate @nx/angular:component ${libName}/src/lib/input-standalone/input-standalone --inlineTemplate --inlineStyle --export --standalone --no-interactive`
|
||||
);
|
||||
// update cmp implmentation to use tailwind clasasserting in tests
|
||||
updateFile(
|
||||
`${libName}/src/lib/input/input.component.ts`,
|
||||
`
|
||||
import {Component, Input} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: '${projectName}-input',
|
||||
template: \`<label class="text-green-500">Email: <input class="border-blue-500" type="email" [readOnly]="readOnly"></label>\`,
|
||||
styles : []
|
||||
})
|
||||
export class InputComponent{
|
||||
@Input() readOnly = false;
|
||||
}
|
||||
`
|
||||
);
|
||||
updateFile(
|
||||
`${libName}/src/lib/input-standalone/input-standalone.ts`,
|
||||
`
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
@Component({
|
||||
selector: '${projectName}-input-standalone',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: \`<label class="text-green-500">Email: <input class="border-blue-500" type="email" [readOnly]="readOnly"></label>\`,
|
||||
styles : []
|
||||
})
|
||||
export class InputStandalone{
|
||||
@Input() readOnly = false;
|
||||
}
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
function useLibInApp(projectName: string, appName: string, libName: string) {
|
||||
createFile(
|
||||
`${appName}/src/app/app.html`,
|
||||
`
|
||||
<${projectName}-btn></${projectName}-btn>
|
||||
<${projectName}-btn-standalone></${projectName}-btn-standalone>
|
||||
<${projectName}-nx-welcome></${projectName}-nx-welcome>
|
||||
`
|
||||
);
|
||||
const btnModuleName = names(libName).className;
|
||||
updateFile(
|
||||
`${appName}/src/app/app.scss`,
|
||||
`
|
||||
@use 'styleguide' as *;
|
||||
|
||||
h1 {
|
||||
@include headline;
|
||||
}`
|
||||
);
|
||||
updateFile(
|
||||
`${appName}/src/app/app-module.ts`,
|
||||
`
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import {${btnModuleName}Module} from "@${projectName}/${libName}";
|
||||
|
||||
import { App } from './app';
|
||||
import { NxWelcome } from './nx-welcome';
|
||||
|
||||
@NgModule({
|
||||
declarations: [App, NxWelcome],
|
||||
imports: [BrowserModule, ${btnModuleName}Module],
|
||||
providers: [],
|
||||
bootstrap: [App],
|
||||
})
|
||||
export class AppModule {}
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
function useWorkspaceAssetsInApp(appName: string) {
|
||||
// make sure assets from the workspace root work.
|
||||
createFile('libs/assets/data.json', JSON.stringify({ data: 'data' }));
|
||||
createFile(
|
||||
'assets/styles/styleguide.scss',
|
||||
`
|
||||
@mixin headline {
|
||||
font-weight: bold;
|
||||
color: darkkhaki;
|
||||
background: lightcoral;
|
||||
font-weight: 24px;
|
||||
}
|
||||
`
|
||||
);
|
||||
updateJson(join(appName, 'project.json'), (config) => {
|
||||
config.targets['build'].options.stylePreprocessorOptions = {
|
||||
includePaths: ['assets/styles'],
|
||||
};
|
||||
config.targets['build'].options.assets.push({
|
||||
glob: '**/*',
|
||||
input: 'libs/assets',
|
||||
output: 'assets',
|
||||
});
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
||||
export function updateTestToAssertTailwindIsNotApplied(libName: string) {
|
||||
createFile(
|
||||
`${libName}/src/lib/input/input.component.cy.ts`,
|
||||
`
|
||||
import { MountConfig } from 'cypress/angular';
|
||||
import { InputComponent } from './input.component';
|
||||
|
||||
describe(InputComponent.name, () => {
|
||||
const config: MountConfig<InputComponent> = {
|
||||
declarations: [],
|
||||
imports: [],
|
||||
providers: [],
|
||||
};
|
||||
|
||||
it('renders', () => {
|
||||
cy.mount(InputComponent, config);
|
||||
// make sure tailwind isn't getting applied
|
||||
cy.get('label').should('have.css', 'color', 'rgb(0, 0, 0)');
|
||||
});
|
||||
it('should be readonly', () => {
|
||||
cy.mount(InputComponent, {
|
||||
...config,
|
||||
componentProperties: {
|
||||
readOnly: true,
|
||||
},
|
||||
});
|
||||
cy.get('input').should('have.attr', 'readonly');
|
||||
});
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
createFile(
|
||||
`${libName}/src/lib/input-standalone/input-standalone.cy.ts`,
|
||||
`
|
||||
import { MountConfig } from 'cypress/angular';
|
||||
import { InputStandalone } from './input-standalone';
|
||||
|
||||
describe(InputStandalone.name, () => {
|
||||
const config: MountConfig<InputStandalone> = {
|
||||
declarations: [],
|
||||
imports: [],
|
||||
providers: [],
|
||||
};
|
||||
|
||||
it('renders', () => {
|
||||
cy.mount(InputStandalone, config);
|
||||
// make sure tailwind isn't getting applied
|
||||
cy.get('label').should('have.css', 'color', 'rgb(0, 0, 0)');
|
||||
});
|
||||
it('should be readonly', () => {
|
||||
cy.mount(InputStandalone, {
|
||||
...config,
|
||||
componentProperties: {
|
||||
readOnly: true,
|
||||
},
|
||||
});
|
||||
cy.get('input').should('have.attr', 'readonly');
|
||||
});
|
||||
});
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
export function useBuildableLibInLib(
|
||||
projectName: string,
|
||||
buildableLibName: string,
|
||||
libName: string
|
||||
) {
|
||||
const buildLibNames = names(buildableLibName);
|
||||
// use the buildable lib in lib so now buildableLib has an indirect dep on app
|
||||
updateFile(
|
||||
`${libName}/src/lib/btn-standalone/btn-standalone.ts`,
|
||||
`
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { InputStandalone } from '@${projectName}/${buildLibNames.fileName}';
|
||||
@Component({
|
||||
selector: '${projectName}-btn-standalone',
|
||||
standalone: true,
|
||||
imports: [CommonModule, InputStandalone],
|
||||
template: '<button class="text-green-500">standlone-{{text}}</button>${projectName} <${projectName}-input-standalone></${projectName}-input-standalone>',
|
||||
styles: [],
|
||||
})
|
||||
export class BtnStandalone {
|
||||
@Input() text = 'something';
|
||||
}
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
export function updateBuilableLibTestsToAssertAppStyles(
|
||||
appName: string,
|
||||
buildableLibName: string
|
||||
) {
|
||||
updateFile(`${appName}/src/styles.css`, `label {color: pink !important;}`);
|
||||
|
||||
removeFile(`${buildableLibName}/src/lib/input/input.component.cy.ts`);
|
||||
updateFile(
|
||||
`${buildableLibName}/src/lib/input-standalone/input-standalone.cy.ts`,
|
||||
(content) => {
|
||||
// app styles should now apply
|
||||
return content.replace('rgb(0, 0, 0)', 'rgb(255, 192, 203)');
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { runCLI, runE2ETests } from '@nx/e2e-utils';
|
||||
import {
|
||||
setupCypressComponentTests,
|
||||
cleanupCypressComponentTests,
|
||||
CypressComponentTestsSetup,
|
||||
} from './cypress-component-tests-setup';
|
||||
|
||||
describe('Angular Cypress Component Tests - Zone.js projects', () => {
|
||||
let setup: CypressComponentTestsSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
setup = setupCypressComponentTests(false);
|
||||
});
|
||||
|
||||
afterAll(() => cleanupCypressComponentTests());
|
||||
|
||||
// TODO(jack): re-enable when lodash@4.18.0 assignWith bug is resolved
|
||||
it.skip('should successfully run for an app', () => {
|
||||
const { appName } = setup;
|
||||
|
||||
runCLI(
|
||||
`generate @nx/angular:cypress-component-configuration --project=${appName} --generate-tests --no-interactive`
|
||||
);
|
||||
if (runE2ETests('cypress')) {
|
||||
expect(runCLI(`component-test ${appName}`)).toContain(
|
||||
'All specs passed!'
|
||||
);
|
||||
}
|
||||
}, 300_000);
|
||||
|
||||
// TODO(jack): re-enable when lodash@4.18.0 assignWith bug is resolved
|
||||
it.skip('should successfully run for a lib', () => {
|
||||
const { usedInAppLibName } = setup;
|
||||
|
||||
runCLI(
|
||||
`generate @nx/angular:cypress-component-configuration --project=${usedInAppLibName} --generate-tests --no-interactive`
|
||||
);
|
||||
if (runE2ETests('cypress')) {
|
||||
expect(runCLI(`component-test ${usedInAppLibName}`)).toContain(
|
||||
'All specs passed!'
|
||||
);
|
||||
}
|
||||
}, 300_000);
|
||||
});
|
||||
@@ -0,0 +1,185 @@
|
||||
import {
|
||||
checkFilesExist,
|
||||
cleanupProject,
|
||||
newProject,
|
||||
readFile,
|
||||
runCLI,
|
||||
uniq,
|
||||
updateFile,
|
||||
} from '@nx/e2e-utils';
|
||||
import { names } from '@nx/devkit';
|
||||
const classify = (s: string) => names(s).className;
|
||||
|
||||
describe('Move Angular Project', () => {
|
||||
let proj: string;
|
||||
let app1: string;
|
||||
let app2: string;
|
||||
let newPath: string;
|
||||
|
||||
beforeAll(() => {
|
||||
proj = newProject({
|
||||
packages: ['@nx/angular', '@nx/workspace', '@nx/jest', '@nx/playwright'],
|
||||
});
|
||||
app1 = uniq('app1');
|
||||
app2 = uniq('app2');
|
||||
newPath = `subfolder/${app2}`;
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${app1} --unit-test-runner=jest --no-interactive`
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => cleanupProject());
|
||||
|
||||
/**
|
||||
* Tries moving an app from ${app1} -> subfolder/${app2}
|
||||
*/
|
||||
it('should work for apps', () => {
|
||||
const moveOutput = runCLI(
|
||||
`generate @nx/workspace:move --project ${app1} ${newPath} `
|
||||
);
|
||||
|
||||
// just check the output
|
||||
expect(moveOutput).toContain(`DELETE ${app1}`);
|
||||
expect(moveOutput).toContain(`CREATE ${newPath}/jest.config.cts`);
|
||||
expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.app.json`);
|
||||
expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.json`);
|
||||
expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.spec.json`);
|
||||
expect(moveOutput).toContain(`CREATE ${newPath}/eslint.config.mjs`);
|
||||
expect(moveOutput).toContain(`CREATE ${newPath}/public/favicon.ico`);
|
||||
expect(moveOutput).toContain(`CREATE ${newPath}/src/index.html`);
|
||||
expect(moveOutput).toContain(`CREATE ${newPath}/src/main.ts`);
|
||||
expect(moveOutput).toContain(`CREATE ${newPath}/src/styles.css`);
|
||||
expect(moveOutput).toContain(`CREATE ${newPath}/src/test-setup.ts`);
|
||||
expect(moveOutput).toContain(`CREATE ${newPath}/src/app/app.html`);
|
||||
expect(moveOutput).toContain(`CREATE ${newPath}/src/app/app.ts`);
|
||||
expect(moveOutput).toContain(`CREATE ${newPath}/src/app/app.config.ts`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Tries moving an e2e project from ${app1} -> ${newPath}
|
||||
*/
|
||||
it('should work for e2e projects w/custom cypress config', () => {
|
||||
// by default the cypress config doesn't contain any app specific paths
|
||||
// create a custom config with some app specific paths
|
||||
updateFile(
|
||||
`${app1}-e2e/cypress.config.ts`,
|
||||
`
|
||||
import { defineConfig } from 'cypress';
|
||||
import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
...nxE2EPreset(__dirname),
|
||||
videosFolder: '../dist/cypress/${app1}-e2e/videos',
|
||||
screenshotsFolder: '../dist/cypress/${app1}-e2e/screenshots',
|
||||
},
|
||||
});
|
||||
`
|
||||
);
|
||||
const moveOutput = runCLI(
|
||||
`generate @nx/workspace:move --projectName=${app1}-e2e --destination=${newPath}-e2e`
|
||||
);
|
||||
|
||||
// just check that the cypress.config.ts is updated correctly
|
||||
const cypressConfigPath = `${newPath}-e2e/cypress.config.ts`;
|
||||
expect(moveOutput).toContain(`CREATE ${cypressConfigPath}`);
|
||||
checkFilesExist(cypressConfigPath);
|
||||
const cypressConfig = readFile(cypressConfigPath);
|
||||
|
||||
expect(cypressConfig).toContain(`../../dist/cypress/${newPath}-e2e/videos`);
|
||||
expect(cypressConfig).toContain(
|
||||
`../../dist/cypress/${newPath}-e2e/screenshots`
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Tries moving a library from ${lib} -> shared/${lib}
|
||||
*/
|
||||
it('should work for libraries', () => {
|
||||
const lib1 = uniq('mylib');
|
||||
const lib2 = uniq('mylib');
|
||||
runCLI(
|
||||
`generate @nx/angular:lib ${lib1} --unit-test-runner=jest --no-standalone --no-interactive`
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a library which imports the module from the other lib
|
||||
*/
|
||||
|
||||
runCLI(
|
||||
`generate @nx/angular:lib ${lib2} --unit-test-runner=jest --no-standalone --no-interactive`
|
||||
);
|
||||
|
||||
updateFile(
|
||||
`${lib2}/src/lib/${lib2}-module.ts`,
|
||||
`import { ${classify(lib1)}Module } from '@${proj}/${lib1}';
|
||||
|
||||
export class ExtendedModule extends ${classify(lib1)}Module { }`
|
||||
);
|
||||
|
||||
const moveOutput = runCLI(
|
||||
`generate @nx/workspace:move --projectName=${lib1} --destination=shared/${lib1} --newProjectName=shared-${lib1}`
|
||||
);
|
||||
|
||||
const newPath = `shared/${lib1}`;
|
||||
const newModule = `Shared${classify(lib1)}Module`;
|
||||
|
||||
const testSetupPath = `${newPath}/src/test-setup.ts`;
|
||||
expect(moveOutput).toContain(`CREATE ${testSetupPath}`);
|
||||
checkFilesExist(testSetupPath);
|
||||
|
||||
const modulePath = `${newPath}/src/lib/shared-${lib1}-module.ts`;
|
||||
expect(moveOutput).toContain(`CREATE ${modulePath}`);
|
||||
checkFilesExist(modulePath);
|
||||
const moduleFile = readFile(modulePath);
|
||||
expect(moduleFile).toContain(`export class ${newModule}`);
|
||||
|
||||
const indexPath = `${newPath}/src/index.ts`;
|
||||
expect(moveOutput).toContain(`CREATE ${indexPath}`);
|
||||
checkFilesExist(indexPath);
|
||||
const index = readFile(indexPath);
|
||||
expect(index).toContain(`export * from './lib/shared-${lib1}-module'`);
|
||||
|
||||
/**
|
||||
* Check that the import in lib2 has been updated
|
||||
*/
|
||||
const lib2FilePath = `${lib2}/src/lib/${lib2}-module.ts`;
|
||||
const lib2File = readFile(lib2FilePath);
|
||||
expect(lib2File).toContain(
|
||||
`import { ${newModule} } from '@${proj}/shared-${lib1}';`
|
||||
);
|
||||
expect(lib2File).toContain(`extends ${newModule}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Convert Angular Webpack Project to Rspack', () => {
|
||||
let proj: string;
|
||||
let app1: string;
|
||||
|
||||
beforeAll(() => {
|
||||
proj = newProject({
|
||||
packages: [
|
||||
'@nx/angular',
|
||||
'@nx/webpack',
|
||||
'@nx/vitest',
|
||||
'@nx/playwright',
|
||||
'@nx/rspack',
|
||||
],
|
||||
});
|
||||
app1 = uniq('app1');
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${app1} --bundler=webpack --no-interactive`
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => cleanupProject());
|
||||
|
||||
it('should convert an Angular Webpack project to Rspack', async () => {
|
||||
runCLI(`generate @nx/angular:convert-to-rspack --project=${app1}`);
|
||||
const buildOutput = runCLI(`build ${app1}`, {
|
||||
env: { NODE_ENV: 'production' },
|
||||
});
|
||||
expect(buildOutput).toContain('rspack build');
|
||||
expect(buildOutput).toContain('Successfully ran target build for project');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,212 @@
|
||||
import { names } from '@nx/devkit';
|
||||
import {
|
||||
checkFilesExist,
|
||||
killProcessAndPorts,
|
||||
reservePort,
|
||||
runCLI,
|
||||
runCommandUntil,
|
||||
uniq,
|
||||
updateFile,
|
||||
updateJson,
|
||||
} from '@nx/e2e-utils';
|
||||
import {
|
||||
setupModuleFederationTest,
|
||||
cleanupModuleFederationTest,
|
||||
ModuleFederationTestSetup,
|
||||
} from './module-federation-setup';
|
||||
|
||||
describe('Angular Module Federation - Host and Remote', () => {
|
||||
let setup: ModuleFederationTestSetup;
|
||||
|
||||
beforeAll(() => {
|
||||
setup = setupModuleFederationTest();
|
||||
});
|
||||
|
||||
afterAll(() => cleanupModuleFederationTest(setup));
|
||||
|
||||
it('should generate valid host and remote apps', async () => {
|
||||
const { proj } = setup;
|
||||
const hostApp = uniq('app');
|
||||
const remoteApp1 = uniq('remote');
|
||||
const sharedLib = uniq('shared-lib');
|
||||
const wildcardLib = uniq('wildcard-lib');
|
||||
const secondaryEntry = uniq('secondary');
|
||||
const hostPort = await reservePort();
|
||||
const remotePort = await reservePort();
|
||||
|
||||
// generate host app
|
||||
runCLI(
|
||||
`generate @nx/angular:host ${hostApp} --port=${hostPort} --style=css --no-standalone --unitTestRunner=jest --no-interactive`
|
||||
);
|
||||
// generate remote app
|
||||
runCLI(
|
||||
`generate @nx/angular:remote ${remoteApp1} --host=${hostApp} --port=${remotePort} --style=css --no-standalone --unitTestRunner=jest --no-interactive`
|
||||
);
|
||||
|
||||
// check files are generated without the layout directory ("apps/")
|
||||
checkFilesExist(
|
||||
`${hostApp}/src/app/app-module.ts`,
|
||||
`${remoteApp1}/src/app/app-module.ts`
|
||||
);
|
||||
|
||||
// check default generated host is built successfully
|
||||
const buildOutput = runCLI(`build ${hostApp}`);
|
||||
expect(buildOutput).toContain('Successfully ran target build');
|
||||
|
||||
// generate a shared lib with a seconary entry point
|
||||
runCLI(
|
||||
`generate @nx/angular:library ${sharedLib} --buildable --no-standalone --no-interactive`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/angular:library-secondary-entry-point --library=${sharedLib} --name=${secondaryEntry} --no-interactive`
|
||||
);
|
||||
|
||||
// Add a library that will be accessed via a wildcard in tspath mappings
|
||||
runCLI(
|
||||
`generate @nx/angular:library ${wildcardLib} --buildable --no-standalone --no-interactive`
|
||||
);
|
||||
|
||||
updateJson('tsconfig.base.json', (json) => {
|
||||
delete json.compilerOptions.paths[`@${proj}/${wildcardLib}`];
|
||||
json.compilerOptions.paths[`@${proj}/${wildcardLib}/*`] = [
|
||||
`./${wildcardLib}/src/lib/*`,
|
||||
];
|
||||
return json;
|
||||
});
|
||||
|
||||
// update host & remote files to use shared library
|
||||
updateFile(
|
||||
`${hostApp}/src/app/app-module.ts`,
|
||||
`import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { ${
|
||||
names(wildcardLib).className
|
||||
}Module } from '@${proj}/${wildcardLib}/${
|
||||
names(wildcardLib).fileName
|
||||
}-module';
|
||||
import { ${
|
||||
names(sharedLib).className
|
||||
}Module } from '@${proj}/${sharedLib}';
|
||||
import { ${
|
||||
names(secondaryEntry).className
|
||||
}Module } from '@${proj}/${sharedLib}/${secondaryEntry}';
|
||||
import { App } from './app';
|
||||
import { NxWelcome } from './nx-welcome';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@NgModule({
|
||||
declarations: [App, NxWelcome],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
${names(sharedLib).className}Module,
|
||||
${names(wildcardLib).className}Module,
|
||||
RouterModule.forRoot(
|
||||
[
|
||||
{
|
||||
path: '${remoteApp1}',
|
||||
loadChildren: () =>
|
||||
import('${remoteApp1}/Module').then(
|
||||
(m) => m.RemoteEntryModule
|
||||
),
|
||||
},
|
||||
],
|
||||
{ initialNavigation: 'enabledBlocking' }
|
||||
),
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [App],
|
||||
})
|
||||
export class AppModule {}
|
||||
`
|
||||
);
|
||||
updateFile(
|
||||
`${remoteApp1}/src/app/remote-entry/entry-module.ts`,
|
||||
`import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { ${names(sharedLib).className}Module } from '@${proj}/${sharedLib}';
|
||||
import { ${
|
||||
names(secondaryEntry).className
|
||||
}Module } from '@${proj}/${sharedLib}/${secondaryEntry}';
|
||||
import { RemoteEntry } from './entry';
|
||||
import { NxWelcome } from './nx-welcome';
|
||||
|
||||
@NgModule({
|
||||
declarations: [RemoteEntry, NxWelcome],
|
||||
imports: [
|
||||
CommonModule,
|
||||
${names(sharedLib).className}Module,
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: '',
|
||||
component: RemoteEntry,
|
||||
},
|
||||
]),
|
||||
],
|
||||
providers: [],
|
||||
})
|
||||
export class RemoteEntryModule {}
|
||||
`
|
||||
);
|
||||
|
||||
const processSwc = await runCommandUntil(
|
||||
`serve ${hostApp} --port=${hostPort} --dev-remotes=${remoteApp1}`,
|
||||
(output) =>
|
||||
!output.includes(`Remote '${remoteApp1}' failed to serve correctly`) &&
|
||||
output.includes(`server ready at http://localhost:${hostPort}`)
|
||||
);
|
||||
await killProcessAndPorts(processSwc.pid, hostPort, remotePort);
|
||||
|
||||
const processTsNode = await runCommandUntil(
|
||||
`serve ${hostApp} --port=${hostPort} --dev-remotes=${remoteApp1}`,
|
||||
(output) =>
|
||||
!output.includes(`Remote '${remoteApp1}' failed to serve correctly`) &&
|
||||
output.includes(`server ready at http://localhost:${hostPort}`),
|
||||
{ env: { NX_PREFER_TS_NODE: 'true' } }
|
||||
);
|
||||
|
||||
await killProcessAndPorts(processTsNode.pid, hostPort, remotePort);
|
||||
}, 20_000_000);
|
||||
|
||||
it('should convert apps to MF successfully', async () => {
|
||||
const app1 = uniq('app1');
|
||||
const app2 = uniq('app2');
|
||||
const app1Port = await reservePort();
|
||||
const app2Port = await reservePort();
|
||||
|
||||
// generate apps
|
||||
runCLI(
|
||||
`generate @nx/angular:application ${app1} --routing --bundler=webpack --unitTestRunner=jest --no-interactive`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/angular:application ${app2} --bundler=webpack --unitTestRunner=jest --no-interactive`
|
||||
);
|
||||
|
||||
// convert apps
|
||||
runCLI(
|
||||
`generate @nx/angular:setup-mf ${app1} --mfType=host --port=${app1Port} --no-interactive`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/angular:setup-mf ${app2} --mfType=remote --host=${app1} --port=${app2Port} --no-interactive`
|
||||
);
|
||||
|
||||
const processSwc = await runCommandUntil(
|
||||
`serve ${app1} --dev-remotes=${app2}`,
|
||||
(output) =>
|
||||
!output.includes(`Remote '${app2}' failed to serve correctly`) &&
|
||||
output.includes(`server ready at http://localhost:${app1Port}`)
|
||||
);
|
||||
|
||||
await killProcessAndPorts(processSwc.pid, app1Port, app2Port);
|
||||
|
||||
const processTsNode = await runCommandUntil(
|
||||
`serve ${app1} --dev-remotes=${app2}`,
|
||||
(output) =>
|
||||
!output.includes(`Remote '${app2}' failed to serve correctly`) &&
|
||||
output.includes(`server ready at http://localhost:${app1Port}`),
|
||||
{ env: { NX_PREFER_TS_NODE: 'true' } }
|
||||
);
|
||||
|
||||
await killProcessAndPorts(processTsNode.pid, app1Port, app2Port);
|
||||
}, 20_000_000);
|
||||
});
|
||||
@@ -0,0 +1,189 @@
|
||||
import {
|
||||
killProcessAndPorts,
|
||||
reservePort,
|
||||
runCLI,
|
||||
runCommandUntil,
|
||||
runE2ETests,
|
||||
uniq,
|
||||
updateFile,
|
||||
} from '@nx/e2e-utils';
|
||||
import {
|
||||
setupModuleFederationTest,
|
||||
cleanupModuleFederationTest,
|
||||
ModuleFederationTestSetup,
|
||||
} from './module-federation-setup';
|
||||
|
||||
describe('Angular Module Federation - Federated Libraries', () => {
|
||||
let setup: ModuleFederationTestSetup;
|
||||
|
||||
beforeAll(() => {
|
||||
setup = setupModuleFederationTest();
|
||||
});
|
||||
|
||||
afterAll(() => cleanupModuleFederationTest(setup));
|
||||
|
||||
it('should federate a module from a library and update an existing remote', async () => {
|
||||
const lib = uniq('lib');
|
||||
const remote = uniq('remote');
|
||||
const module = uniq('module');
|
||||
const host = uniq('host');
|
||||
|
||||
const hostPort = await reservePort();
|
||||
|
||||
runCLI(
|
||||
`generate @nx/angular:host ${host} --port=${hostPort} --remotes=${remote} --e2eTestRunner=cypress --no-interactive`
|
||||
);
|
||||
|
||||
runCLI(`generate @nx/js:lib ${lib} --no-interactive`);
|
||||
|
||||
// Federate Module
|
||||
runCLI(
|
||||
`generate @nx/angular:federate-module ${lib}/src/index.ts --name=${module} --remote=${remote} --no-interactive`
|
||||
);
|
||||
|
||||
updateFile(`${lib}/src/index.ts`, `export { isEven } from './lib/${lib}';`);
|
||||
updateFile(
|
||||
`${lib}/src/lib/${lib}.ts`,
|
||||
`export function isEven(num: number) { return num % 2 === 0; }`
|
||||
);
|
||||
|
||||
// Update Host to use the module
|
||||
updateFile(
|
||||
`${host}/src/app/app.ts`,
|
||||
`
|
||||
import { Component } from '@angular/core';
|
||||
import { isEven } from '${remote}/${module}';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: \`<div class="host">{{title}}</div>\`,
|
||||
standalone: true
|
||||
})
|
||||
export class App {
|
||||
title = \`shell is \${isEven(2) ? 'even' : 'odd'}\`;
|
||||
}`
|
||||
);
|
||||
|
||||
// Update e2e test to check the module
|
||||
updateFile(
|
||||
`${host}-e2e/src/e2e/app.cy.ts`,
|
||||
`
|
||||
describe('${host}', () => {
|
||||
beforeEach(() => cy.visit('/'));
|
||||
|
||||
it('should display contain the remote library', () => {
|
||||
expect(cy.get('div.host')).to.exist;
|
||||
expect(cy.get('div.host').contains('shell is even'));
|
||||
});
|
||||
});
|
||||
|
||||
`
|
||||
);
|
||||
|
||||
// Build host and remote
|
||||
const buildHostOutput = runCLI(`build ${host}`);
|
||||
expect(buildHostOutput).toContain('Successfully ran target build');
|
||||
const buildRemoteOutput = runCLI(`build ${remote}`);
|
||||
expect(buildRemoteOutput).toContain('Successfully ran target build');
|
||||
|
||||
if (runE2ETests('cypress')) {
|
||||
const e2eProcess = await runCommandUntil(
|
||||
`e2e ${host}-e2e`,
|
||||
(output) => output.includes('All specs passed!'),
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
await killProcessAndPorts(e2eProcess.pid, hostPort, hostPort + 1);
|
||||
}
|
||||
}, 500_000);
|
||||
|
||||
it('should federate a module from a library and create a remote that is served recursively', async () => {
|
||||
const lib = uniq('lib');
|
||||
const remote = uniq('remote');
|
||||
const childRemote = uniq('childremote');
|
||||
const module = uniq('module');
|
||||
const host = uniq('host');
|
||||
const hostPort = await reservePort();
|
||||
|
||||
runCLI(
|
||||
`generate @nx/angular:host ${host} --port=${hostPort} --remotes=${remote} --e2eTestRunner=cypress --no-interactive`
|
||||
);
|
||||
|
||||
runCLI(`generate @nx/js:lib ${lib} --no-interactive`);
|
||||
|
||||
// Federate Module
|
||||
runCLI(
|
||||
`generate @nx/angular:federate-module ${lib}/src/index.ts --name=${module} --remote=${childRemote} --remoteDirectory=${childRemote} --no-interactive`
|
||||
);
|
||||
|
||||
updateFile(`${lib}/src/index.ts`, `export { isEven } from './lib/${lib}';`);
|
||||
updateFile(
|
||||
`${lib}/src/lib/${lib}.ts`,
|
||||
`export function isEven(num: number) { return num % 2 === 0; }`
|
||||
);
|
||||
|
||||
// Update Host to use the module
|
||||
updateFile(
|
||||
`${remote}/src/app/remote-entry/entry.ts`,
|
||||
`
|
||||
import { Component } from '@angular/core';
|
||||
import { isEven } from '${childRemote}/${module}';
|
||||
|
||||
@Component({
|
||||
selector: 'app-${remote}-entry',
|
||||
template: \`<div class="childremote">{{title}}</div>\`,
|
||||
standalone: true
|
||||
})
|
||||
export class RemoteEntry {
|
||||
title = \`shell is \${isEven(2) ? 'even' : 'odd'}\`;
|
||||
}`
|
||||
);
|
||||
|
||||
updateFile(
|
||||
`${remote}/module-federation.config.ts`,
|
||||
`
|
||||
import { ModuleFederationConfig } from '@nx/webpack';
|
||||
|
||||
const config: ModuleFederationConfig = {
|
||||
name: '${remote}',
|
||||
remotes: ['${childRemote}'],
|
||||
exposes: {
|
||||
'./Routes': '${remote}/src/app/remote-entry/entry.routes.ts',
|
||||
'./Module': '${remote}/src/app/remote-entry/entry.ts',
|
||||
},
|
||||
};
|
||||
|
||||
export default config;`
|
||||
);
|
||||
|
||||
// Update e2e test to check the module
|
||||
updateFile(
|
||||
`${host}-e2e/src/e2e/app.cy.ts`,
|
||||
`
|
||||
describe('${host}', () => {
|
||||
beforeEach(() => cy.visit('/${remote}'));
|
||||
|
||||
it('should display contain the remote library', () => {
|
||||
expect(cy.get('div.childremote')).to.exist;
|
||||
expect(cy.get('div.childremote').contains('shell is even'));
|
||||
});
|
||||
});
|
||||
|
||||
`
|
||||
);
|
||||
|
||||
// Build host and remote
|
||||
const buildHostOutput = runCLI(`build ${host}`);
|
||||
expect(buildHostOutput).toContain('Successfully ran target build');
|
||||
const buildRemoteOutput = runCLI(`build ${remote}`);
|
||||
expect(buildRemoteOutput).toContain('Successfully ran target build');
|
||||
|
||||
if (runE2ETests('cypress')) {
|
||||
const e2eProcess = await runCommandUntil(
|
||||
`e2e ${host}-e2e`,
|
||||
(output) => output.includes('All specs passed!'),
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
await killProcessAndPorts(e2eProcess.pid, hostPort, hostPort + 1);
|
||||
}
|
||||
}, 500_000);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { cleanupProject, newProject } from '@nx/e2e-utils';
|
||||
|
||||
export interface ModuleFederationTestSetup {
|
||||
proj: string;
|
||||
oldVerboseLoggingValue: string;
|
||||
}
|
||||
|
||||
export function setupModuleFederationTest(): ModuleFederationTestSetup {
|
||||
const proj = newProject({
|
||||
packages: [
|
||||
'@nx/angular',
|
||||
'@nx/jest',
|
||||
'@nx/vitest',
|
||||
'@nx/playwright',
|
||||
'@nx/cypress',
|
||||
],
|
||||
});
|
||||
const oldVerboseLoggingValue = process.env.NX_E2E_VERBOSE_LOGGING;
|
||||
process.env.NX_E2E_VERBOSE_LOGGING = 'true';
|
||||
|
||||
return {
|
||||
proj,
|
||||
oldVerboseLoggingValue,
|
||||
};
|
||||
}
|
||||
|
||||
export function cleanupModuleFederationTest(
|
||||
setup: ModuleFederationTestSetup
|
||||
): void {
|
||||
cleanupProject();
|
||||
process.env.NX_E2E_VERBOSE_LOGGING = setup.oldVerboseLoggingValue;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
checkFilesExist,
|
||||
killPorts,
|
||||
killProcessAndPorts,
|
||||
readJson,
|
||||
reservePort,
|
||||
runCLI,
|
||||
runCommandUntil,
|
||||
runE2ETests,
|
||||
uniq,
|
||||
updateFile,
|
||||
} from '@nx/e2e-utils';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
setupModuleFederationTest,
|
||||
cleanupModuleFederationTest,
|
||||
ModuleFederationTestSetup,
|
||||
} from './module-federation-setup';
|
||||
|
||||
function readPort(appName: string): number {
|
||||
let config;
|
||||
try {
|
||||
config = readJson(join('apps', appName, 'project.json'));
|
||||
} catch {
|
||||
config = readJson(join(appName, 'project.json'));
|
||||
}
|
||||
return config.targets.serve.options.port;
|
||||
}
|
||||
|
||||
describe('Angular Module Federation - SSR', () => {
|
||||
let setup: ModuleFederationTestSetup;
|
||||
|
||||
beforeAll(() => {
|
||||
setup = setupModuleFederationTest();
|
||||
});
|
||||
|
||||
afterAll(() => cleanupModuleFederationTest(setup));
|
||||
|
||||
it('should scaffold MF + SSR setup successfully', async () => {
|
||||
const host = uniq('host');
|
||||
const remote1 = uniq('remote1');
|
||||
const remote2 = uniq('remote2');
|
||||
|
||||
// ports
|
||||
const hostPort = await reservePort();
|
||||
|
||||
// generate remote apps
|
||||
runCLI(
|
||||
`generate @nx/angular:host ${host} --port=${hostPort} --ssr --remotes=${remote1},${remote2} --no-interactive`
|
||||
);
|
||||
|
||||
const remote1Port = readJson(join(remote1, 'project.json')).targets.serve
|
||||
.options.port;
|
||||
const remote2Port = readJson(join(remote2, 'project.json')).targets.serve
|
||||
.options.port;
|
||||
|
||||
[host, remote1, remote2].forEach((app) => {
|
||||
checkFilesExist(
|
||||
`${app}/module-federation.config.ts`,
|
||||
`${app}/webpack.server.config.ts`
|
||||
);
|
||||
|
||||
['build', 'server'].forEach((target) => {
|
||||
['development', 'production'].forEach(async (configuration) => {
|
||||
const cliOutput = runCLI(`run ${app}:${target}:${configuration}`);
|
||||
expect(cliOutput).toContain('Successfully ran target');
|
||||
|
||||
await killPorts(readPort(app));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
||||
|
||||
updateFile(
|
||||
`${host}-e2e/src/example.spec.ts`,
|
||||
(_) => `import { test, expect } from '@playwright/test';
|
||||
test('renders remotes', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Expect the page to contain a specific text.
|
||||
// get ul li text
|
||||
const items = page.locator('ul li');
|
||||
|
||||
await items.nth(2).waitFor()
|
||||
expect(await items.count()).toEqual(3);
|
||||
expect(await items.nth(0).innerText()).toContain('Home');
|
||||
expect(await items.nth(1).innerText()).toContain('${capitalize(remote1)}');
|
||||
expect(await items.nth(2).innerText()).toContain('${capitalize(remote2)}');
|
||||
});`
|
||||
);
|
||||
if (runE2ETests()) {
|
||||
const e2eProcess = await runCommandUntil(
|
||||
`e2e ${host}-e2e`,
|
||||
(output) =>
|
||||
output.includes(
|
||||
`Successfully ran target e2e for project ${host}-e2e`
|
||||
),
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
await killProcessAndPorts(e2eProcess.pid);
|
||||
}
|
||||
}, 20_000_000);
|
||||
});
|
||||
@@ -0,0 +1,227 @@
|
||||
import { names, stripIndents } from '@nx/devkit';
|
||||
import {
|
||||
checkFilesExist,
|
||||
cleanupProject,
|
||||
killProcessAndPorts,
|
||||
newProject,
|
||||
readFile,
|
||||
reservePort,
|
||||
runCLI,
|
||||
runCommandUntil,
|
||||
runE2ETests,
|
||||
uniq,
|
||||
updateFile,
|
||||
updateJson,
|
||||
} from '@nx/e2e-utils';
|
||||
import { join } from 'path';
|
||||
|
||||
describe('Angular Module Federation', () => {
|
||||
let proj: string;
|
||||
let oldVerboseLoggingValue: string;
|
||||
|
||||
beforeAll(() => {
|
||||
proj = newProject({
|
||||
packages: ['@nx/angular', '@nx/rspack', '@nx/vitest', '@nx/playwright'],
|
||||
});
|
||||
oldVerboseLoggingValue = process.env.NX_E2E_VERBOSE_LOGGING;
|
||||
process.env.NX_E2E_VERBOSE_LOGGING = 'true';
|
||||
});
|
||||
afterAll(() => {
|
||||
cleanupProject();
|
||||
process.env.NX_E2E_VERBOSE_LOGGING = oldVerboseLoggingValue;
|
||||
});
|
||||
|
||||
it('should generate valid host and remote apps', async () => {
|
||||
const hostApp = uniq('app');
|
||||
const remoteApp1 = uniq('remote');
|
||||
const sharedLib = uniq('shared-lib');
|
||||
const wildcardLib = uniq('wildcard-lib');
|
||||
const secondaryEntry = uniq('secondary');
|
||||
const hostPort = await reservePort();
|
||||
const remotePort = await reservePort();
|
||||
|
||||
// generate host app
|
||||
runCLI(
|
||||
`generate @nx/angular:host ${hostApp} --port=${hostPort} --style=css --bundler=rspack --no-standalone --no-interactive`
|
||||
);
|
||||
let rspackConfigFileContents = readFile(join(hostApp, 'rspack.config.ts'));
|
||||
let updatedConfigFileContents = rspackConfigFileContents.replace(
|
||||
`maximumError: '1mb'`,
|
||||
`maximumError: '3mb'`
|
||||
);
|
||||
updateFile(join(hostApp, 'rspack.config.ts'), updatedConfigFileContents);
|
||||
|
||||
// generate remote app
|
||||
runCLI(
|
||||
`generate @nx/angular:remote ${remoteApp1} --host=${hostApp} --bundler=rspack --port=${remotePort} --style=css --no-standalone --no-interactive`
|
||||
);
|
||||
rspackConfigFileContents = readFile(join(remoteApp1, 'rspack.config.ts'));
|
||||
updatedConfigFileContents = rspackConfigFileContents.replace(
|
||||
`maximumError: '1mb'`,
|
||||
`maximumError: '3mb'`
|
||||
);
|
||||
updateFile(join(remoteApp1, 'rspack.config.ts'), updatedConfigFileContents);
|
||||
|
||||
// check files are generated without the layout directory ("apps/")
|
||||
checkFilesExist(
|
||||
`${hostApp}/src/app/app-module.ts`,
|
||||
`${remoteApp1}/src/app/app-module.ts`
|
||||
);
|
||||
|
||||
// check default generated host is built successfully
|
||||
const buildOutput = runCLI(`build ${hostApp}`, {
|
||||
env: { NODE_ENV: 'production' },
|
||||
});
|
||||
expect(buildOutput).toContain('Successfully ran target build');
|
||||
|
||||
// generate a shared lib with a seconary entry point
|
||||
runCLI(
|
||||
`generate @nx/angular:library ${sharedLib} --buildable --no-standalone --no-interactive`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/angular:library-secondary-entry-point --library=${sharedLib} --name=${secondaryEntry} --no-interactive`
|
||||
);
|
||||
|
||||
// Add a library that will be accessed via a wildcard in tspath mappings
|
||||
runCLI(
|
||||
`generate @nx/angular:library ${wildcardLib} --buildable --no-standalone --no-interactive`
|
||||
);
|
||||
|
||||
updateJson('tsconfig.base.json', (json) => {
|
||||
delete json.compilerOptions.paths[`@${proj}/${wildcardLib}`];
|
||||
json.compilerOptions.paths[`@${proj}/${wildcardLib}/*`] = [
|
||||
`./${wildcardLib}/src/lib/*`,
|
||||
];
|
||||
return json;
|
||||
});
|
||||
|
||||
// update host & remote files to use shared library
|
||||
updateFile(
|
||||
`${hostApp}/src/app/app-module.ts`,
|
||||
`import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { ${
|
||||
names(wildcardLib).className
|
||||
}Module } from '@${proj}/${wildcardLib}/${
|
||||
names(secondaryEntry).fileName
|
||||
}-module';
|
||||
import { ${
|
||||
names(sharedLib).className
|
||||
}Module } from '@${proj}/${sharedLib}';
|
||||
import { ${
|
||||
names(secondaryEntry).className
|
||||
}Module } from '@${proj}/${sharedLib}/${secondaryEntry}';
|
||||
import { App } from './app';
|
||||
import { NxWelcome } from './nx-welcome';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@NgModule({
|
||||
declarations: [App, NxWelcome],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
${names(sharedLib).className}Module,
|
||||
${names(wildcardLib).className}Module,
|
||||
RouterModule.forRoot(
|
||||
[
|
||||
{
|
||||
path: '${remoteApp1}',
|
||||
loadChildren: () =>
|
||||
import('${remoteApp1}/Module').then(
|
||||
(m) => m.RemoteEntryModule
|
||||
),
|
||||
},
|
||||
],
|
||||
{ initialNavigation: 'enabledBlocking' }
|
||||
),
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [App],
|
||||
})
|
||||
export class AppModule {}
|
||||
`
|
||||
);
|
||||
updateFile(
|
||||
`${remoteApp1}/src/app/remote-entry/entry-module.ts`,
|
||||
`import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { ${names(sharedLib).className}Module } from '@${proj}/${sharedLib}';
|
||||
import { ${
|
||||
names(secondaryEntry).className
|
||||
}Module } from '@${proj}/${sharedLib}/${secondaryEntry}';
|
||||
import { RemoteEntry } from './entry';
|
||||
import { NxWelcome } from './nx-welcome';
|
||||
|
||||
@NgModule({
|
||||
declarations: [RemoteEntry, NxWelcome],
|
||||
imports: [
|
||||
CommonModule,
|
||||
${names(sharedLib).className}Module,
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: '',
|
||||
component: RemoteEntry,
|
||||
},
|
||||
]),
|
||||
],
|
||||
providers: [],
|
||||
})
|
||||
export class RemoteEntryModule {}
|
||||
`
|
||||
);
|
||||
|
||||
const processSwc = await runCommandUntil(
|
||||
`serve ${remoteApp1}`,
|
||||
(output) =>
|
||||
!output.includes(`Remote '${remoteApp1}' failed to serve correctly`) &&
|
||||
output.includes(`Build at:`)
|
||||
);
|
||||
await killProcessAndPorts(processSwc.pid, remotePort);
|
||||
}, 20_000_000);
|
||||
|
||||
it('should load remote app in the browser via ESM module federation', async () => {
|
||||
const hostApp = uniq('host');
|
||||
const remoteApp = uniq('remote');
|
||||
const hostPort = await reservePort();
|
||||
const remotePort = await reservePort();
|
||||
|
||||
// generate host with playwright e2e runner
|
||||
runCLI(
|
||||
`generate @nx/angular:host ${hostApp} --port=${hostPort} --style=css --bundler=rspack --e2eTestRunner=playwright --no-interactive`
|
||||
);
|
||||
|
||||
// generate remote
|
||||
runCLI(
|
||||
`generate @nx/angular:remote ${remoteApp} --host=${hostApp} --bundler=rspack --port=${remotePort} --style=css --no-interactive`
|
||||
);
|
||||
|
||||
// Write playwright e2e test that navigates to the remote route.
|
||||
// This catches ESM-related runtime errors like "Unexpected token 'export'"
|
||||
// which only surface in the browser when loading remoteEntry.js.
|
||||
updateFile(
|
||||
`${hostApp}-e2e/src/example.spec.ts`,
|
||||
stripIndents`
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('should load the host app', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
expect(await page.locator('h1').innerText()).toContain('Welcome');
|
||||
});
|
||||
|
||||
test('should load the remote app via module federation', async ({ page }) => {
|
||||
await page.goto('/${remoteApp}');
|
||||
expect(await page.locator('app-nx-welcome').count()).toBeGreaterThan(0);
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
if (runE2ETests('playwright')) {
|
||||
const e2eProcess = await runCommandUntil(
|
||||
`e2e ${hostApp}-e2e`,
|
||||
(output) => output.includes('Successfully ran target e2e for project'),
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
await killProcessAndPorts(e2eProcess.pid, hostPort, remotePort);
|
||||
}
|
||||
}, 20_000_000);
|
||||
});
|
||||
@@ -0,0 +1,410 @@
|
||||
import {
|
||||
checkFilesDoNotExist,
|
||||
checkFilesExist,
|
||||
cleanupProject,
|
||||
getSelectedPackageManager,
|
||||
packageInstall,
|
||||
readJson,
|
||||
runCLI,
|
||||
runCommand,
|
||||
runNgAdd,
|
||||
runNgNew,
|
||||
uniq,
|
||||
updateFile,
|
||||
} from '@nx/e2e-utils';
|
||||
import { PackageManager } from 'nx/src/utils/package-manager';
|
||||
|
||||
describe('convert Angular CLI workspace to an Nx workspace', () => {
|
||||
let project: string;
|
||||
let packageManager: PackageManager;
|
||||
|
||||
// utility to manually add protractor since it's not generated
|
||||
// in the latest Angular CLI versions, but older projects updated
|
||||
// to latest versions might still have it
|
||||
function addProtractor() {
|
||||
updateFile('e2e/protractor.conf.js', 'exports.config = {};');
|
||||
updateFile(
|
||||
'e2e/tsconfig.json',
|
||||
JSON.stringify({ extends: '../tsconfig.json' }, null, 2)
|
||||
);
|
||||
updateFile(
|
||||
'e2e/src/app.e2e-spec.ts',
|
||||
`describe('app', () => {
|
||||
it('should pass', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});`
|
||||
);
|
||||
|
||||
const angularJson = readJson('angular.json');
|
||||
angularJson.projects[project].architect.e2e = {
|
||||
builder: '@angular-devkit/build-angular:protractor',
|
||||
options: {
|
||||
protractorConfig: 'e2e/protractor.conf.js',
|
||||
devServerTarget: `${project}:serve`,
|
||||
},
|
||||
configurations: {
|
||||
production: { devServerTarget: `${project}:serve:production` },
|
||||
},
|
||||
};
|
||||
updateFile('angular.json', JSON.stringify(angularJson, null, 2));
|
||||
}
|
||||
|
||||
function addCypress() {
|
||||
runNgAdd('@cypress/schematic', '--e2e', 'latest');
|
||||
// pin latest version of Cypress that's supported by Nx to avoid flakiness
|
||||
// when a new major version is released
|
||||
packageInstall('cypress', null, '^15.6.0');
|
||||
}
|
||||
|
||||
function addEsLint() {
|
||||
runNgAdd('@angular-eslint/schematics', undefined, 'latest');
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
packageManager = getSelectedPackageManager();
|
||||
// TODO: solve issues with pnpm and remove this fallback
|
||||
packageManager = packageManager === 'pnpm' ? 'yarn' : packageManager;
|
||||
project = runNgNew(packageManager);
|
||||
packageInstall('nx', null, 'latest');
|
||||
packageInstall('@nx/angular', null, 'latest');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanupProject();
|
||||
});
|
||||
|
||||
it('should generate a workspace', () => {
|
||||
addProtractor();
|
||||
|
||||
// update package.json
|
||||
const packageJson = readJson('package.json');
|
||||
packageJson.description = 'some description';
|
||||
updateFile('package.json', JSON.stringify(packageJson, null, 2));
|
||||
|
||||
// update tsconfig.json
|
||||
const tsConfig = readJson('tsconfig.json');
|
||||
tsConfig.compilerOptions.paths = { a: ['./b'] };
|
||||
updateFile('tsconfig.json', JSON.stringify(tsConfig, null, 2));
|
||||
|
||||
// add an extra script file
|
||||
updateFile('src/scripts.js', 'const x = 1;');
|
||||
|
||||
// update angular.json
|
||||
const angularJson = readJson('angular.json');
|
||||
angularJson.projects[project].architect.build.options.scripts = [
|
||||
'src/scripts.js',
|
||||
];
|
||||
angularJson.projects[project].architect.test.options ??= {};
|
||||
angularJson.projects[project].architect.test.options.tsconfig =
|
||||
'tsconfig.spec.json';
|
||||
updateFile('angular.json', JSON.stringify(angularJson, null, 2));
|
||||
|
||||
// confirm that @nx dependencies do not exist yet
|
||||
expect(packageJson.devDependencies['@nx/workspace']).not.toBeDefined();
|
||||
|
||||
// run ng add
|
||||
runCLI('g @nx/angular:ng-add --default-base main');
|
||||
|
||||
// check that prettier config exits and that files have been moved
|
||||
checkFilesExist(
|
||||
'.vscode/extensions.json',
|
||||
'.prettierrc',
|
||||
`apps/${project}/src/main.ts`,
|
||||
`apps/${project}/src/app/app.config.ts`,
|
||||
`apps/${project}/src/app/app.ts`,
|
||||
`apps/${project}/src/app/app.routes.ts`
|
||||
);
|
||||
|
||||
// check the right VSCode extensions are recommended
|
||||
expect(readJson('.vscode/extensions.json').recommendations).toEqual([
|
||||
'angular.ng-template',
|
||||
'nrwl.angular-console',
|
||||
'dbaeumer.vscode-eslint',
|
||||
'esbenp.prettier-vscode',
|
||||
]);
|
||||
|
||||
// check package.json
|
||||
const updatedPackageJson = readJson('package.json');
|
||||
expect(updatedPackageJson.description).toEqual('some description');
|
||||
expect(updatedPackageJson.scripts).toEqual({
|
||||
ng: 'ng',
|
||||
start: 'nx serve',
|
||||
build: 'nx build',
|
||||
watch: 'nx build --watch --configuration development',
|
||||
test: 'nx test',
|
||||
});
|
||||
expect(updatedPackageJson.devDependencies['@nx/workspace']).toBeDefined();
|
||||
expect(updatedPackageJson.devDependencies['@angular/cli']).toBeDefined();
|
||||
|
||||
// check nx.json
|
||||
const nxJson = readJson('nx.json');
|
||||
expect(nxJson).toEqual({
|
||||
defaultBase: 'main',
|
||||
namedInputs: {
|
||||
default: ['{projectRoot}/**/*', 'sharedGlobals'],
|
||||
production: [
|
||||
'default',
|
||||
'!{projectRoot}/tsconfig.spec.json',
|
||||
'!{projectRoot}/**/*.spec.[jt]s',
|
||||
'!{projectRoot}/karma.conf.js',
|
||||
],
|
||||
sharedGlobals: [],
|
||||
},
|
||||
targetDefaults: {
|
||||
build: {
|
||||
dependsOn: ['^build'],
|
||||
inputs: ['production', '^production'],
|
||||
cache: true,
|
||||
},
|
||||
test: {
|
||||
inputs: ['default', '^production', '{workspaceRoot}/karma.conf.js'],
|
||||
cache: true,
|
||||
},
|
||||
e2e: {
|
||||
inputs: ['default', '^production'],
|
||||
cache: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// check angular.json does not exist
|
||||
checkFilesDoNotExist('angular.json');
|
||||
|
||||
// check project configuration
|
||||
const projectConfig = readJson(`apps/${project}/project.json`);
|
||||
expect(projectConfig.sourceRoot).toEqual(`apps/${project}/src`);
|
||||
expect(projectConfig.targets.build).toStrictEqual({
|
||||
executor: '@angular/build:application',
|
||||
outputs: ['{options.outputPath}'],
|
||||
options: {
|
||||
outputPath: `dist/${project}`,
|
||||
browser: `apps/${project}/src/main.ts`,
|
||||
tsConfig: `apps/${project}/tsconfig.app.json`,
|
||||
assets: [{ glob: '**/*', input: `apps/${project}/public` }],
|
||||
styles: [`apps/${project}/src/styles.css`],
|
||||
scripts: [`apps/${project}/src/scripts.js`],
|
||||
},
|
||||
configurations: {
|
||||
production: {
|
||||
budgets: [
|
||||
{
|
||||
type: 'initial',
|
||||
maximumWarning: '500kB',
|
||||
maximumError: '1MB',
|
||||
},
|
||||
{
|
||||
type: 'anyComponentStyle',
|
||||
maximumWarning: '4kB',
|
||||
maximumError: '8kB',
|
||||
},
|
||||
],
|
||||
outputHashing: 'all',
|
||||
},
|
||||
development: {
|
||||
optimization: false,
|
||||
extractLicenses: false,
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
defaultConfiguration: 'production',
|
||||
});
|
||||
expect(projectConfig.targets.serve).toEqual({
|
||||
executor: '@angular/build:dev-server',
|
||||
configurations: {
|
||||
production: { buildTarget: `${project}:build:production` },
|
||||
development: { buildTarget: `${project}:build:development` },
|
||||
},
|
||||
defaultConfiguration: 'development',
|
||||
});
|
||||
expect(projectConfig.targets.test).toStrictEqual({
|
||||
executor: '@angular/build:unit-test',
|
||||
options: {
|
||||
tsconfig: `apps/${project}/tsconfig.spec.json`,
|
||||
},
|
||||
});
|
||||
expect(projectConfig.targets.e2e).toBeUndefined();
|
||||
|
||||
// check e2e project config
|
||||
const e2eProjectConfig = readJson(`apps/${project}-e2e/project.json`);
|
||||
expect(e2eProjectConfig.targets.e2e).toEqual({
|
||||
executor: '@angular-devkit/build-angular:protractor',
|
||||
options: {
|
||||
protractorConfig: `apps/${project}-e2e/protractor.conf.js`,
|
||||
devServerTarget: `${project}:serve`,
|
||||
},
|
||||
configurations: {
|
||||
production: {
|
||||
devServerTarget: `${project}:serve:production`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
runCLI(`build ${project} --configuration production --outputHashing none`);
|
||||
checkFilesExist(`dist/${project}/browser/main.js`);
|
||||
});
|
||||
|
||||
it('should handle a workspace with cypress', () => {
|
||||
addCypress();
|
||||
|
||||
runCLI('g @nx/angular:ng-add --skip-install');
|
||||
|
||||
const e2eProject = `${project}-e2e`;
|
||||
//check e2e project files
|
||||
checkFilesDoNotExist(
|
||||
'cypress.config.ts',
|
||||
'cypress/tsconfig.json',
|
||||
'cypress/e2e/spec.cy.ts',
|
||||
'cypress/fixtures/example.json',
|
||||
'cypress/support/commands.ts',
|
||||
'cypress/support/e2e.ts'
|
||||
);
|
||||
checkFilesExist(
|
||||
`apps/${e2eProject}/cypress.config.ts`,
|
||||
`apps/${e2eProject}/tsconfig.json`,
|
||||
`apps/${e2eProject}/src/e2e/spec.cy.ts`,
|
||||
`apps/${e2eProject}/src/fixtures/example.json`,
|
||||
`apps/${e2eProject}/src/support/commands.ts`,
|
||||
`apps/${e2eProject}/src/support/e2e.ts`
|
||||
);
|
||||
|
||||
const projectConfig = readJson(`apps/${project}/project.json`);
|
||||
expect(projectConfig.targets['cypress-run']).toBeUndefined();
|
||||
expect(projectConfig.targets['cypress-open']).toBeUndefined();
|
||||
expect(projectConfig.targets.e2e).toBeUndefined();
|
||||
|
||||
// check e2e project config
|
||||
const e2eProjectConfig = readJson(`apps/${project}-e2e/project.json`);
|
||||
expect(e2eProjectConfig.targets['cypress-run']).toEqual({
|
||||
executor: '@nx/cypress:cypress',
|
||||
options: {
|
||||
devServerTarget: `${project}:serve`,
|
||||
cypressConfig: `apps/${e2eProject}/cypress.config.ts`,
|
||||
},
|
||||
configurations: {
|
||||
production: {
|
||||
devServerTarget: `${project}:serve:production`,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(e2eProjectConfig.targets['cypress-open']).toEqual({
|
||||
executor: '@nx/cypress:cypress',
|
||||
options: {
|
||||
watch: true,
|
||||
headless: false,
|
||||
cypressConfig: `apps/${e2eProject}/cypress.config.ts`,
|
||||
},
|
||||
});
|
||||
expect(e2eProjectConfig.targets.e2e).toEqual({
|
||||
executor: '@nx/cypress:cypress',
|
||||
options: {
|
||||
devServerTarget: `${project}:serve`,
|
||||
watch: true,
|
||||
headless: false,
|
||||
cypressConfig: `apps/${e2eProject}/cypress.config.ts`,
|
||||
},
|
||||
configurations: {
|
||||
production: {
|
||||
devServerTarget: `${project}:serve:production`,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// TODO(leo): The current Verdaccio setup fails to resolve older versions
|
||||
// of @nx/* packages, the @angular-eslint/builder package depends on an
|
||||
// older version of @nx/devkit so we skip this test for now.
|
||||
it.skip('should handle a workspace with ESLint', () => {
|
||||
addEsLint();
|
||||
|
||||
runCLI('g @nx/angular:ng-add');
|
||||
|
||||
checkFilesExist(`apps/${project}/.eslintrc.json`, `.eslintrc.json`);
|
||||
|
||||
const projectConfig = readJson(`apps/${project}/project.json`);
|
||||
expect(projectConfig.targets.lint).toStrictEqual({
|
||||
executor: '@nx/eslint:lint',
|
||||
});
|
||||
|
||||
let output = runCLI(`lint ${project}`);
|
||||
expect(output).toContain(`> nx run ${project}:lint`);
|
||||
expect(output).toContain('All files pass linting');
|
||||
expect(output).toContain(
|
||||
`Successfully ran target lint for project ${project}`
|
||||
);
|
||||
|
||||
output = runCLI(`lint ${project}`);
|
||||
expect(output).toContain(
|
||||
`> nx run ${project}:lint [existing outputs match the cache, left as is]`
|
||||
);
|
||||
expect(output).toContain('All files pass linting');
|
||||
expect(output).toContain(
|
||||
`Successfully ran target lint for project ${project}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should support a workspace with multiple projects', () => {
|
||||
// add other projects
|
||||
const app1 = uniq('app1');
|
||||
const lib1 = uniq('lib1');
|
||||
runCommand(`ng g @schematics/angular:application ${app1} --no-interactive`);
|
||||
runCommand(`ng g @schematics/angular:library ${lib1} --no-interactive`);
|
||||
|
||||
runCLI('g @nx/angular:ng-add');
|
||||
|
||||
// check angular.json does not exist
|
||||
checkFilesDoNotExist('angular.json');
|
||||
|
||||
// check building project
|
||||
let output = runCLI(`build ${project} --outputHashing none`);
|
||||
expect(output).toContain(
|
||||
`> nx run ${project}:build:production --outputHashing none`
|
||||
);
|
||||
expect(output).toContain(
|
||||
`Successfully ran target build for project ${project}`
|
||||
);
|
||||
checkFilesExist(`dist/${project}/browser/main.js`);
|
||||
|
||||
output = runCLI(`build ${project} --outputHashing none`);
|
||||
expect(output).toContain(
|
||||
`> nx run ${project}:build:production --outputHashing none [existing outputs match the cache, left as is]`
|
||||
);
|
||||
expect(output).toContain(
|
||||
`Successfully ran target build for project ${project}`
|
||||
);
|
||||
|
||||
// check building app1
|
||||
output = runCLI(`build ${app1} --outputHashing none`);
|
||||
expect(output).toContain(
|
||||
`> nx run ${app1}:build:production --outputHashing none`
|
||||
);
|
||||
expect(output).toContain(
|
||||
`Successfully ran target build for project ${app1}`
|
||||
);
|
||||
checkFilesExist(`dist/${app1}/browser/main.js`);
|
||||
|
||||
output = runCLI(`build ${app1} --outputHashing none`);
|
||||
expect(output).toContain(
|
||||
`> nx run ${app1}:build:production --outputHashing none [existing outputs match the cache, left as is]`
|
||||
);
|
||||
expect(output).toContain(
|
||||
`Successfully ran target build for project ${app1}`
|
||||
);
|
||||
|
||||
// check building lib1
|
||||
output = runCLI(`build ${lib1}`);
|
||||
expect(output).toContain(`> nx run ${lib1}:build:production`);
|
||||
expect(output).toContain(
|
||||
`Successfully ran target build for project ${lib1}`
|
||||
);
|
||||
checkFilesExist(`dist/${lib1}/package.json`);
|
||||
|
||||
output = runCLI(`build ${lib1}`);
|
||||
expect(output).toContain(
|
||||
`> nx run ${lib1}:build:production [existing outputs match the cache, left as is]`
|
||||
);
|
||||
expect(output).toContain(
|
||||
`Successfully ran target build for project ${lib1}`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
cleanupProject,
|
||||
newProject,
|
||||
readJson,
|
||||
runCLI,
|
||||
runCLIAsync,
|
||||
uniq,
|
||||
} from '@nx/e2e-utils';
|
||||
|
||||
describe('NgRx', () => {
|
||||
beforeAll(() => {
|
||||
newProject({ packages: ['@nx/angular', '@nx/playwright', '@nx/vitest'] });
|
||||
});
|
||||
afterAll(() => {
|
||||
cleanupProject();
|
||||
});
|
||||
|
||||
it('should work', async () => {
|
||||
const myapp = uniq('myapp');
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${myapp} --no-standalone --no-interactive`
|
||||
);
|
||||
|
||||
// Generate root ngrx state management with a global feature state
|
||||
runCLI(
|
||||
`generate @nx/angular:ngrx-root-store ${myapp} --name=users --minimal=false --addDevTools`
|
||||
);
|
||||
const packageJson = readJson('package.json');
|
||||
expect(packageJson.dependencies['@ngrx/store']).toBeDefined();
|
||||
expect(packageJson.dependencies['@ngrx/effects']).toBeDefined();
|
||||
expect(packageJson.dependencies['@ngrx/router-store']).toBeDefined();
|
||||
expect(packageJson.devDependencies['@ngrx/store-devtools']).toBeDefined();
|
||||
|
||||
const mylib = uniq('mylib');
|
||||
// Generate feature library and ngrx state within that library
|
||||
runCLI(`g @nx/angular:lib ${mylib} --prefix=fl --no-standalone`);
|
||||
runCLI(
|
||||
`generate @nx/angular:ngrx-feature-store flights --parent=${mylib}/src/lib/${mylib}-module.ts --facade`
|
||||
);
|
||||
|
||||
expect(runCLI(`build ${myapp}`)).toMatch(/main-[a-zA-Z0-9]+\.js/);
|
||||
expect(
|
||||
(await runCLIAsync(`test ${myapp} --no-watch`)).combinedOutput
|
||||
).toContain(`Successfully ran target test for project ${myapp}`);
|
||||
expect(
|
||||
(await runCLIAsync(`test ${mylib} --no-watch`)).combinedOutput
|
||||
).toContain(`Successfully ran target test for project ${mylib}`);
|
||||
}, 1000000);
|
||||
|
||||
it('should work with barrels', async () => {
|
||||
const myapp = uniq('myapp');
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${myapp} --routing --no-standalone --no-interactive`
|
||||
);
|
||||
|
||||
// Generate root ngrx state management
|
||||
runCLI(`generate @nx/angular:ngrx-root-store ${myapp} --addDevTools`);
|
||||
const packageJson = readJson('package.json');
|
||||
expect(packageJson.dependencies['@ngrx/entity']).toBeDefined();
|
||||
expect(packageJson.dependencies['@ngrx/store']).toBeDefined();
|
||||
expect(packageJson.dependencies['@ngrx/effects']).toBeDefined();
|
||||
expect(packageJson.dependencies['@ngrx/router-store']).toBeDefined();
|
||||
expect(packageJson.devDependencies['@ngrx/schematics']).toBeDefined();
|
||||
expect(packageJson.devDependencies['@ngrx/store-devtools']).toBeDefined();
|
||||
|
||||
const mylib = uniq('mylib');
|
||||
// Generate feature library and ngrx state within that library, using barrels
|
||||
runCLI(`g @nx/angular:lib ${mylib} --prefix=fl --no-standalone`);
|
||||
runCLI(
|
||||
`generate @nx/angular:ngrx-feature-store flights --parent=${mylib}/src/lib/${mylib}-module.ts --facade --barrels`
|
||||
);
|
||||
|
||||
expect(runCLI(`build ${myapp}`)).toMatch(/main-[a-zA-Z0-9]+\.js/);
|
||||
expect(
|
||||
(await runCLIAsync(`test ${myapp} --no-watch`)).combinedOutput
|
||||
).toContain(`Successfully ran target test for project ${myapp}`);
|
||||
expect(
|
||||
(await runCLIAsync(`test ${mylib} --no-watch`)).combinedOutput
|
||||
).toContain(`Successfully ran target test for project ${mylib}`);
|
||||
}, 1000000);
|
||||
});
|
||||
@@ -0,0 +1,185 @@
|
||||
import {
|
||||
checkFilesExist,
|
||||
cleanupProject,
|
||||
getPackageManagerCommand,
|
||||
getSelectedPackageManager,
|
||||
isVerbose,
|
||||
isVerboseE2ERun,
|
||||
logInfo,
|
||||
newProject,
|
||||
runCLI,
|
||||
runCommand,
|
||||
tmpProjPath,
|
||||
uniq,
|
||||
updateFile,
|
||||
updateJson,
|
||||
} from '@nx/e2e-utils';
|
||||
import { angularDevkitVersion } from '@nx/angular/internal';
|
||||
import { ensureDirSync } from 'fs-extra';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { join } from 'node:path';
|
||||
|
||||
describe('Angular Crystal Plugin', () => {
|
||||
let proj: string;
|
||||
|
||||
beforeAll(() => {
|
||||
proj = newProject({
|
||||
packages: ['@nx/angular'],
|
||||
});
|
||||
|
||||
if (getSelectedPackageManager() === 'pnpm') {
|
||||
updateFile(
|
||||
'pnpm-workspace.yaml',
|
||||
`packages:
|
||||
- 'projects/*'
|
||||
`
|
||||
);
|
||||
} else {
|
||||
updateJson('package.json', (json) => {
|
||||
json.workspaces = ['projects/*'];
|
||||
return json;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => cleanupProject());
|
||||
|
||||
it('should infer tasks from multiple angular.json files', () => {
|
||||
const ngOrg1App1 = uniq('ng-org1-app1');
|
||||
const ngOrg1Lib1 = uniq('ng-org1-lib1');
|
||||
const org1Root = join(tmpProjPath(), 'projects', ngOrg1App1);
|
||||
const ngOrg2App1 = uniq('ng-org2-app1');
|
||||
const ngOrg2Lib1 = uniq('ng-org2-lib1');
|
||||
const org2Root = join(tmpProjPath(), 'projects', ngOrg2App1);
|
||||
const pmc = getPackageManagerCommand();
|
||||
|
||||
// first angular inner repo (e.g. imported with nx import)
|
||||
runNgNew(ngOrg1App1, 'projects');
|
||||
// exclude scripts from nx, to prevent them to override the inferred tasks
|
||||
updateJson(`projects/${ngOrg1App1}/package.json`, (json) => {
|
||||
json.nx = { includedScripts: [] };
|
||||
return json;
|
||||
});
|
||||
runCommand(pmc.run(`ng g @schematics/angular:library ${ngOrg1Lib1}`, ''), {
|
||||
cwd: org1Root,
|
||||
});
|
||||
|
||||
// second angular inner repo
|
||||
runNgNew(ngOrg2App1, 'projects');
|
||||
// exclude scripts from nx
|
||||
updateJson(`projects/${ngOrg2App1}/package.json`, (json) => {
|
||||
json.nx = { includedScripts: [] };
|
||||
return json;
|
||||
});
|
||||
runCommand(pmc.run(`ng g @schematics/angular:library ${ngOrg2Lib1}`, ''), {
|
||||
cwd: org2Root,
|
||||
});
|
||||
|
||||
// add Angular Crystal plugin
|
||||
updateJson('nx.json', (json) => {
|
||||
json.plugins ??= [];
|
||||
json.plugins.push('@nx/angular/plugin');
|
||||
return json;
|
||||
});
|
||||
|
||||
// check org1 tasks
|
||||
|
||||
// build
|
||||
runCLI(`build ${ngOrg1App1} --output-hashing none`);
|
||||
checkFilesExist(
|
||||
`projects/${ngOrg1App1}/dist/${ngOrg1App1}/browser/main.js`
|
||||
);
|
||||
expect(runCLI(`build ${ngOrg1App1} --output-hashing none`)).toContain(
|
||||
'Nx read the output from the cache instead of running the command for 1 out of 1 tasks'
|
||||
);
|
||||
runCLI(`build ${ngOrg1Lib1}`);
|
||||
checkFilesExist(
|
||||
`projects/${ngOrg1App1}/dist/${ngOrg1Lib1}/fesm2022/${ngOrg1Lib1}.mjs`
|
||||
);
|
||||
expect(runCLI(`build ${ngOrg1Lib1}`)).toContain(
|
||||
'Nx read the output from the cache instead of running the command for 1 out of 1 tasks'
|
||||
);
|
||||
|
||||
// test
|
||||
expect(
|
||||
runCLI(`run-many -t test -p ${ngOrg1App1},${ngOrg1Lib1} --no-watch`)
|
||||
).toContain('Successfully ran target test for 2 projects');
|
||||
expect(
|
||||
runCLI(`run-many -t test -p ${ngOrg1App1},${ngOrg1Lib1} --no-watch`)
|
||||
).toContain(
|
||||
'Nx read the output from the cache instead of running the command for 2 out of 2 tasks'
|
||||
);
|
||||
|
||||
// check org2 tasks
|
||||
|
||||
// build
|
||||
runCLI(`build ${ngOrg2App1} --output-hashing none`);
|
||||
checkFilesExist(
|
||||
`projects/${ngOrg2App1}/dist/${ngOrg2App1}/browser/main.js`
|
||||
);
|
||||
expect(runCLI(`build ${ngOrg2App1} --output-hashing none`)).toContain(
|
||||
'Nx read the output from the cache instead of running the command for 1 out of 1 tasks'
|
||||
);
|
||||
runCLI(`build ${ngOrg2Lib1}`);
|
||||
checkFilesExist(
|
||||
`projects/${ngOrg2App1}/dist/${ngOrg2Lib1}/fesm2022/${ngOrg2Lib1}.mjs`
|
||||
);
|
||||
expect(runCLI(`build ${ngOrg2Lib1}`)).toContain(
|
||||
'Nx read the output from the cache instead of running the command for 1 out of 1 tasks'
|
||||
);
|
||||
|
||||
// test
|
||||
expect(
|
||||
runCLI(`run-many -t test -p ${ngOrg2App1},${ngOrg2Lib1} --no-watch`)
|
||||
).toContain('Successfully ran target test for 2 projects');
|
||||
expect(
|
||||
runCLI(`run-many -t test -p ${ngOrg2App1},${ngOrg2Lib1} --no-watch`)
|
||||
).toContain(
|
||||
'Nx read the output from the cache instead of running the command for 2 out of 2 tasks'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function runNgNew(projectName: string, cwd: string): void {
|
||||
const packageManager = getSelectedPackageManager();
|
||||
const pmc = getPackageManagerCommand({ packageManager });
|
||||
|
||||
const command = `${pmc.runUninstalledPackage} @angular/cli@${angularDevkitVersion} new ${projectName} --package-manager=${packageManager}`;
|
||||
const fullCwd = join(tmpProjPath(), cwd);
|
||||
ensureDirSync(fullCwd);
|
||||
execSync(command, {
|
||||
cwd: fullCwd,
|
||||
stdio: isVerbose() ? 'inherit' : 'pipe',
|
||||
env: process.env,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
// ensure angular packages are installed with ~ instead of ^ to prevent
|
||||
// potential failures when new minor versions are released
|
||||
function updateAngularDependencies(dependencies: any): void {
|
||||
Object.keys(dependencies).forEach((key) => {
|
||||
if (key.startsWith('@angular/') || key.startsWith('@angular-devkit/')) {
|
||||
dependencies[key] = dependencies[key].replace(/^\^/, '~');
|
||||
}
|
||||
});
|
||||
}
|
||||
updateJson(join(cwd, projectName, 'package.json'), (json) => {
|
||||
updateAngularDependencies(json.dependencies ?? {});
|
||||
updateAngularDependencies(json.devDependencies ?? {});
|
||||
return json;
|
||||
});
|
||||
|
||||
execSync(pmc.install, {
|
||||
cwd: join(fullCwd, projectName),
|
||||
stdio: isVerbose() ? 'inherit' : 'pipe',
|
||||
env: process.env,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
if (isVerboseE2ERun()) {
|
||||
logInfo(
|
||||
`NX`,
|
||||
`E2E created an Angular CLI project at ${join(cwd, projectName)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { names } from '@nx/devkit';
|
||||
import {
|
||||
checkFilesExist,
|
||||
getSize,
|
||||
killPort,
|
||||
killProcessAndPorts,
|
||||
readFile,
|
||||
reservePort,
|
||||
runCLI,
|
||||
runCommandUntil,
|
||||
runE2ETests,
|
||||
tmpProjPath,
|
||||
uniq,
|
||||
updateFile,
|
||||
} from '@nx/e2e-utils';
|
||||
import {
|
||||
setupProjectsTest,
|
||||
resetProjectsTest,
|
||||
cleanupProjectsTest,
|
||||
ProjectsTestSetup,
|
||||
} from './projects-setup';
|
||||
|
||||
describe('Angular Projects - Build and Test', () => {
|
||||
let setup: ProjectsTestSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
setup = await setupProjectsTest();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetProjectsTest(setup);
|
||||
});
|
||||
|
||||
afterAll(() => cleanupProjectsTest());
|
||||
|
||||
it('should successfully generate apps and libs and work correctly', async () => {
|
||||
const { proj, app1, esbuildApp, lib1, app1Port } = setup;
|
||||
const standaloneApp = uniq('standalone-app');
|
||||
runCLI(
|
||||
`generate @nx/angular:app my-dir/${standaloneApp} --bundler=webpack --no-interactive`
|
||||
);
|
||||
|
||||
const esbuildStandaloneApp = uniq('esbuild-app');
|
||||
runCLI(
|
||||
`generate @nx/angular:app my-dir/${esbuildStandaloneApp} --bundler=esbuild --no-interactive`
|
||||
);
|
||||
|
||||
updateFile(
|
||||
`${app1}/src/app/app-module.ts`,
|
||||
`
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { App } from './app';
|
||||
import { appRoutes } from './app.routes';
|
||||
import { NxWelcome } from './nx-welcome';
|
||||
import { ${names(lib1).className} } from '@${proj}/${lib1}';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule.forRoot(appRoutes, { initialNavigation: 'enabledBlocking' }),
|
||||
${names(lib1).className}
|
||||
],
|
||||
declarations: [App, NxWelcome],
|
||||
bootstrap: [App]
|
||||
})
|
||||
export class AppModule {}
|
||||
`
|
||||
);
|
||||
|
||||
// check build
|
||||
runCLI(
|
||||
`run-many --target build --projects=${app1},${esbuildApp},${standaloneApp},${esbuildStandaloneApp} --parallel --prod --output-hashing none`
|
||||
);
|
||||
checkFilesExist(`dist/${app1}/main.js`);
|
||||
checkFilesExist(`dist/${esbuildApp}/browser/main.js`);
|
||||
checkFilesExist(`dist/my-dir/${standaloneApp}/main.js`);
|
||||
checkFilesExist(`dist/my-dir/${esbuildStandaloneApp}/browser/main.js`);
|
||||
// This is a loose requirement because there are a lot of
|
||||
// influences external from this project that affect this.
|
||||
const es2015BundleSize = getSize(tmpProjPath(`dist/${app1}/main.js`));
|
||||
console.log(
|
||||
`The current es2015 bundle size is ${es2015BundleSize / 1000} KB`
|
||||
);
|
||||
expect(es2015BundleSize).toBeLessThanOrEqual(226000);
|
||||
|
||||
// check unit tests
|
||||
runCLI(
|
||||
`run-many --target test --projects=${app1},${standaloneApp},${esbuildStandaloneApp},${lib1} --parallel`
|
||||
);
|
||||
|
||||
// check e2e tests
|
||||
if (runE2ETests('playwright')) {
|
||||
// app1 was generated with --port=app1Port, so its e2e serves there
|
||||
expect(() => runCLI(`e2e ${app1}-e2e`)).not.toThrow();
|
||||
expect(await killPort(app1Port)).toBeTruthy();
|
||||
}
|
||||
|
||||
const appPort = await reservePort();
|
||||
const process = await runCommandUntil(
|
||||
`serve ${app1} -- --port=${appPort}`,
|
||||
(output) => output.includes(`listening on localhost:${appPort}`),
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
|
||||
// port and process cleanup
|
||||
await killProcessAndPorts(process.pid, appPort);
|
||||
|
||||
const esbProcess = await runCommandUntil(
|
||||
`serve ${esbuildStandaloneApp} -- --port=${appPort}`,
|
||||
(output) =>
|
||||
output.includes(`Application bundle generation complete`) &&
|
||||
output.includes(`localhost:${appPort}`),
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
|
||||
// port and process cleanup
|
||||
await killProcessAndPorts(esbProcess.pid, appPort);
|
||||
}, 1000000);
|
||||
|
||||
it('should successfully work with rspack for build', async () => {
|
||||
const app = uniq('app');
|
||||
const port = await reservePort();
|
||||
runCLI(
|
||||
`generate @nx/angular:app my-dir/${app} --port=${port} --bundler=rspack --no-interactive`
|
||||
);
|
||||
runCLI(`build ${app}`, {
|
||||
env: { NODE_ENV: 'production' },
|
||||
});
|
||||
|
||||
if (runE2ETests()) {
|
||||
expect(() => runCLI(`e2e ${app}-e2e`)).not.toThrow();
|
||||
expect(await killPort(port)).toBeTruthy();
|
||||
}
|
||||
}, 1000000);
|
||||
|
||||
it('should successfully generate and run tests for vitest-angular', async () => {
|
||||
// Workspace default unitTestRunner is vitest-analog (set when app1
|
||||
// was generated with --bundler=webpack via setGeneratorDefaults
|
||||
// during projects-setup), so opt into vitest-angular explicitly.
|
||||
// - App: --bundler=esbuild required (uses @angular/build:unit-test).
|
||||
// - Lib: --buildable required (uses @nx/angular:unit-test against
|
||||
// the built output).
|
||||
const app = uniq('vitest-angular-app');
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${app} --bundler=esbuild --unitTestRunner=vitest-angular --no-interactive`
|
||||
);
|
||||
|
||||
const lib = uniq('vitest-angular-lib');
|
||||
runCLI(
|
||||
`generate @nx/angular:lib ${lib} --buildable --unitTestRunner=vitest-angular --no-interactive`
|
||||
);
|
||||
|
||||
runCLI(`run-many --target test --projects=${app},${lib}`);
|
||||
}, 1000000);
|
||||
|
||||
it('should successfully work with playwright for e2e tests', async () => {
|
||||
const app = uniq('app');
|
||||
const port = await reservePort();
|
||||
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${app} --port=${port} --e2eTestRunner=playwright --no-interactive`
|
||||
);
|
||||
|
||||
if (runE2ETests('playwright')) {
|
||||
expect(() => runCLI(`e2e ${app}-e2e`)).not.toThrow();
|
||||
expect(await killPort(port)).toBeTruthy();
|
||||
}
|
||||
}, 1000000);
|
||||
});
|
||||
@@ -0,0 +1,174 @@
|
||||
import { names } from '@nx/devkit';
|
||||
import { readFile, runCLI, uniq, updateFile, updateJson } from '@nx/e2e-utils';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
setupProjectsTest,
|
||||
resetProjectsTest,
|
||||
cleanupProjectsTest,
|
||||
ProjectsTestSetup,
|
||||
} from './projects-setup';
|
||||
|
||||
describe('Angular Projects - Buildable Libraries', () => {
|
||||
let setup: ProjectsTestSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
setup = await setupProjectsTest();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetProjectsTest(setup);
|
||||
});
|
||||
|
||||
afterAll(() => cleanupProjectsTest());
|
||||
|
||||
it('should build the dependent buildable lib and its child lib, as well as the app', async () => {
|
||||
const { proj, app1, esbuildApp } = setup;
|
||||
|
||||
// ARRANGE
|
||||
const buildableLib = uniq('buildlib1');
|
||||
const buildableChildLib = uniq('buildlib2');
|
||||
|
||||
runCLI(
|
||||
`generate @nx/angular:library ${buildableLib} --buildable=true --no-standalone --no-interactive`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/angular:library ${buildableChildLib} --buildable=true --no-standalone --no-interactive`
|
||||
);
|
||||
|
||||
// update the app module to include a ref to the buildable lib
|
||||
updateFile(
|
||||
`${app1}/src/app/app-module.ts`,
|
||||
`
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { App } from './app';
|
||||
import { appRoutes } from './app.routes';
|
||||
import { NxWelcome } from './nx-welcome';
|
||||
import {${
|
||||
names(buildableLib).className
|
||||
}Module} from '@${proj}/${buildableLib}';
|
||||
|
||||
@NgModule({
|
||||
declarations: [App, NxWelcome],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule.forRoot(appRoutes, { initialNavigation: 'enabledBlocking' }),
|
||||
${names(buildableLib).className}Module
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [App],
|
||||
})
|
||||
export class AppModule {}
|
||||
`
|
||||
);
|
||||
updateFile(
|
||||
`${esbuildApp}/src/app/app-module.ts`,
|
||||
`
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { App } from './app';
|
||||
import { appRoutes } from './app.routes';
|
||||
import { NxWelcome } from './nx-welcome';
|
||||
import {${
|
||||
names(buildableLib).className
|
||||
}Module} from '@${proj}/${buildableLib}';
|
||||
|
||||
@NgModule({
|
||||
declarations: [App, NxWelcome],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
RouterModule.forRoot(appRoutes, { initialNavigation: 'enabledBlocking' }),
|
||||
${names(buildableLib).className}Module
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [App],
|
||||
})
|
||||
export class AppModule {}
|
||||
`
|
||||
);
|
||||
|
||||
// update the buildable lib module to include a ref to the buildable child lib
|
||||
updateFile(
|
||||
`${buildableLib}/src/lib/${names(buildableLib).fileName}-module.ts`,
|
||||
`
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ${
|
||||
names(buildableChildLib).className
|
||||
}Module } from '@${proj}/${buildableChildLib}';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, ${names(buildableChildLib).className}Module],
|
||||
})
|
||||
export class ${names(buildableLib).className}Module {}
|
||||
|
||||
`
|
||||
);
|
||||
|
||||
// update the project.json
|
||||
updateJson(join(app1, 'project.json'), (config) => {
|
||||
config.targets.build.executor = '@nx/angular:webpack-browser';
|
||||
config.targets.build.options = {
|
||||
...config.targets.build.options,
|
||||
buildLibsFromSource: false,
|
||||
};
|
||||
return config;
|
||||
});
|
||||
updateJson(join(esbuildApp, 'project.json'), (config) => {
|
||||
config.targets.build.executor = '@nx/angular:browser-esbuild';
|
||||
config.targets.build.options = {
|
||||
...config.targets.build.options,
|
||||
outputPath: `dist/${esbuildApp}`,
|
||||
index: `${esbuildApp}/src/index.html`,
|
||||
main: config.targets.build.options.browser,
|
||||
browser: undefined,
|
||||
buildLibsFromSource: false,
|
||||
};
|
||||
return config;
|
||||
});
|
||||
|
||||
// update the nx.json
|
||||
updateJson('nx.json', (config) => {
|
||||
const inputs =
|
||||
config.namedInputs && 'production' in config.namedInputs
|
||||
? ['production', '^production']
|
||||
: ['default', '^default'];
|
||||
const defaults: Record<string, unknown> = {
|
||||
cache: true,
|
||||
dependsOn: ['^build'],
|
||||
inputs,
|
||||
};
|
||||
const targets = [
|
||||
'@nx/angular:webpack-browser',
|
||||
'@nx/angular:browser-esbuild',
|
||||
];
|
||||
config.targetDefaults ??= {};
|
||||
for (const target of targets) {
|
||||
config.targetDefaults[target] ??= defaults;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// ACT
|
||||
const libOutput = runCLI(`build ${app1} --configuration=development`);
|
||||
const esbuildLibOutput = runCLI(
|
||||
`build ${esbuildApp} --configuration=development`
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
expect(libOutput).toContain(
|
||||
`Building entry point '@${proj}/${buildableLib}'`
|
||||
);
|
||||
expect(libOutput).toContain(`nx run ${app1}:build:development`);
|
||||
|
||||
// to proof it has been built from source the "main.js" should actually contain
|
||||
// the path to dist
|
||||
const mainBundle = readFile(`dist/${app1}/main.js`);
|
||||
expect(mainBundle).toContain(`dist/${buildableLib}`);
|
||||
|
||||
const mainEsBuildBundle = readFile(`dist/${esbuildApp}/main.js`);
|
||||
expect(mainEsBuildBundle).toContain(`dist/${buildableLib}`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
import { readFile, runCLI, updateFile, updateJson } from '@nx/e2e-utils';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
setupProjectsTest,
|
||||
resetProjectsTest,
|
||||
cleanupProjectsTest,
|
||||
ProjectsTestSetup,
|
||||
} from './projects-setup';
|
||||
|
||||
describe('Angular Projects - Esbuild', () => {
|
||||
let setup: ProjectsTestSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
setup = await setupProjectsTest();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetProjectsTest(setup);
|
||||
});
|
||||
|
||||
afterAll(() => cleanupProjectsTest());
|
||||
|
||||
it('should support esbuild plugins', async () => {
|
||||
const { esbuildApp } = setup;
|
||||
|
||||
updateFile(
|
||||
`${esbuildApp}/replace-text.plugin.mjs`,
|
||||
`const replaceTextPlugin = {
|
||||
name: 'replace-text',
|
||||
setup(build) {
|
||||
const options = build.initialOptions;
|
||||
options.define.BUILD_DEFINED = '"Value was provided at build time"';
|
||||
},
|
||||
};
|
||||
|
||||
export default replaceTextPlugin;`
|
||||
);
|
||||
updateFile(
|
||||
`${esbuildApp}/src/app/app.ts`,
|
||||
`import { Component } from '@angular/core';
|
||||
|
||||
declare const BUILD_DEFINED: string;
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: false,
|
||||
templateUrl: './app.html',
|
||||
})
|
||||
export class App {
|
||||
title = 'esbuild-app';
|
||||
buildDefined = BUILD_DEFINED;
|
||||
}`
|
||||
);
|
||||
|
||||
// check @nx/angular:application
|
||||
updateJson(join(esbuildApp, 'project.json'), (config) => {
|
||||
config.targets.build.executor = '@nx/angular:application';
|
||||
config.targets.build.options = {
|
||||
...config.targets.build.options,
|
||||
plugins: [`${esbuildApp}/replace-text.plugin.mjs`],
|
||||
};
|
||||
return config;
|
||||
});
|
||||
|
||||
runCLI(`build ${esbuildApp} --configuration=development`);
|
||||
|
||||
let mainBundle = readFile(`dist/${esbuildApp}/browser/main.js`);
|
||||
expect(mainBundle).toContain(
|
||||
'buildDefined = "Value was provided at build time";'
|
||||
);
|
||||
|
||||
// check @nx/angular:browser-esbuild
|
||||
updateJson(join(esbuildApp, 'project.json'), (config) => {
|
||||
config.targets.build.executor = '@nx/angular:browser-esbuild';
|
||||
config.targets.build.options = {
|
||||
...config.targets.build.options,
|
||||
main: config.targets.build.options.browser,
|
||||
browser: undefined,
|
||||
index: `${esbuildApp}/src/index.html`,
|
||||
};
|
||||
return config;
|
||||
});
|
||||
|
||||
runCLI(`build ${esbuildApp} --configuration=development`);
|
||||
|
||||
mainBundle = readFile(`dist/${esbuildApp}/main.js`);
|
||||
expect(mainBundle).toContain(
|
||||
'buildDefined = "Value was provided at build time";'
|
||||
);
|
||||
});
|
||||
|
||||
it('should support providing a transformer function for the "index.html" file with the application executor', async () => {
|
||||
const { esbuildApp } = setup;
|
||||
|
||||
updateFile(
|
||||
`${esbuildApp}/index.transformer.mjs`,
|
||||
`const indexHtmlTransformer = (indexContent) => {
|
||||
return indexContent.replace(
|
||||
'<title>${esbuildApp}</title>',
|
||||
'<title>${esbuildApp} (transformed)</title>'
|
||||
);
|
||||
};
|
||||
|
||||
export default indexHtmlTransformer;`
|
||||
);
|
||||
|
||||
updateJson(join(esbuildApp, 'project.json'), (config) => {
|
||||
config.targets.build.executor = '@nx/angular:application';
|
||||
config.targets.build.options = {
|
||||
...config.targets.build.options,
|
||||
indexHtmlTransformer: `${esbuildApp}/index.transformer.mjs`,
|
||||
};
|
||||
return config;
|
||||
});
|
||||
|
||||
runCLI(`build ${esbuildApp}`);
|
||||
|
||||
let indexHtmlContent = readFile(`dist/${esbuildApp}/browser/index.html`);
|
||||
expect(indexHtmlContent).toContain(
|
||||
`<title>${esbuildApp} (transformed)</title>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should support a TypeScript "index.html" transformer located in a library referenced with {workspaceRoot}', async () => {
|
||||
const { esbuildApp, lib1 } = setup;
|
||||
|
||||
// A .ts transformer is loaded via require(), unlike .mjs which Node
|
||||
// resolves against cwd. Placing it in a library and referencing it with
|
||||
// {workspaceRoot} exercises the workspace-relative path resolution.
|
||||
updateFile(
|
||||
`${lib1}/src/index.transformer.ts`,
|
||||
`const indexHtmlTransformer = (indexContent: string): string => {
|
||||
return indexContent.replace(
|
||||
'<title>${esbuildApp}</title>',
|
||||
'<title>${esbuildApp} (transformed from lib)</title>'
|
||||
);
|
||||
};
|
||||
|
||||
export default indexHtmlTransformer;`
|
||||
);
|
||||
|
||||
updateJson(join(esbuildApp, 'project.json'), (config) => {
|
||||
config.targets.build.executor = '@nx/angular:application';
|
||||
config.targets.build.options = {
|
||||
...config.targets.build.options,
|
||||
indexHtmlTransformer: `{workspaceRoot}/${lib1}/src/index.transformer.ts`,
|
||||
};
|
||||
return config;
|
||||
});
|
||||
|
||||
runCLI(`build ${esbuildApp}`);
|
||||
|
||||
const indexHtmlContent = readFile(`dist/${esbuildApp}/browser/index.html`);
|
||||
expect(indexHtmlContent).toContain(
|
||||
`<title>${esbuildApp} (transformed from lib)</title>`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { readFile, removeFile, runCLI, updateFile } from '@nx/e2e-utils';
|
||||
import { normalize } from 'path';
|
||||
import {
|
||||
setupProjectsTest,
|
||||
resetProjectsTest,
|
||||
cleanupProjectsTest,
|
||||
ProjectsTestSetup,
|
||||
} from './projects-setup';
|
||||
|
||||
describe('Angular Projects - Linting', () => {
|
||||
let setup: ProjectsTestSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
setup = await setupProjectsTest();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetProjectsTest(setup);
|
||||
});
|
||||
|
||||
afterAll(() => cleanupProjectsTest());
|
||||
|
||||
it('should lint correctly with eslint and handle external HTML files and inline templates', async () => {
|
||||
const { app1, lib1 } = setup;
|
||||
|
||||
// disable the prefer-standalone rule for app1 and lib1 which are not standalone.
|
||||
// Use a regex so we match regardless of whether the generated config uses
|
||||
// single or double quotes around the rule name.
|
||||
for (const project of [app1, lib1]) {
|
||||
let eslintConfig = readFile(`${project}/eslint.config.mjs`);
|
||||
eslintConfig = eslintConfig.replace(
|
||||
/(['"])@angular-eslint\/directive-selector\1:\s*\[/,
|
||||
`"@angular-eslint/prefer-standalone": "off",\n "@angular-eslint/directive-selector": [`
|
||||
);
|
||||
updateFile(`${project}/eslint.config.mjs`, eslintConfig);
|
||||
}
|
||||
|
||||
// check apps and lib pass linting for initial generated code
|
||||
runCLI(`run-many --target lint --projects=${app1},${lib1} --parallel`);
|
||||
|
||||
// External HTML template file
|
||||
const templateWhichFailsBananaInBoxLintCheck = `<div ([foo])="bar"></div>`;
|
||||
updateFile(
|
||||
`${app1}/src/app/app.html`,
|
||||
templateWhichFailsBananaInBoxLintCheck
|
||||
);
|
||||
// Inline template within component.ts file
|
||||
const wrappedAsInlineTemplate = `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'inline-template-component',
|
||||
template: \`
|
||||
${templateWhichFailsBananaInBoxLintCheck}
|
||||
\`,
|
||||
})
|
||||
export class InlineTemplateComponent {}
|
||||
`;
|
||||
updateFile(
|
||||
`${app1}/src/app/inline-template.component.ts`,
|
||||
wrappedAsInlineTemplate
|
||||
);
|
||||
|
||||
const appLintStdOut = runCLI(`lint ${app1}`, {
|
||||
silenceError: true,
|
||||
});
|
||||
expect(appLintStdOut).toContain(normalize(`${app1}/src/app/app.html`));
|
||||
expect(appLintStdOut).toContain(`1:6`);
|
||||
expect(appLintStdOut).toContain(`Invalid binding syntax`);
|
||||
expect(appLintStdOut).toContain(
|
||||
normalize(`${app1}/src/app/inline-template.component.ts`)
|
||||
);
|
||||
expect(appLintStdOut).toContain(`5:19`);
|
||||
expect(appLintStdOut).toContain(
|
||||
`The selector should start with one of these prefixes`
|
||||
);
|
||||
expect(appLintStdOut).toContain(`7:16`);
|
||||
expect(appLintStdOut).toContain(`Invalid binding syntax`);
|
||||
|
||||
// cleanup added component
|
||||
removeFile(`${app1}/src/app/inline-template.component.ts`);
|
||||
}, 1000000);
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { names } from '@nx/devkit';
|
||||
import { checkFilesExist, runCLI, uniq, updateFile } from '@nx/e2e-utils';
|
||||
import {
|
||||
setupProjectsTest,
|
||||
resetProjectsTest,
|
||||
cleanupProjectsTest,
|
||||
ProjectsTestSetup,
|
||||
} from './projects-setup';
|
||||
|
||||
describe('Angular Projects - Publishable Libraries', () => {
|
||||
let setup: ProjectsTestSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
setup = await setupProjectsTest();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetProjectsTest(setup);
|
||||
});
|
||||
|
||||
afterAll(() => cleanupProjectsTest());
|
||||
|
||||
it('should build publishable libs successfully', () => {
|
||||
const { proj } = setup;
|
||||
|
||||
// ARRANGE
|
||||
const lib = uniq('lib');
|
||||
const childLib = uniq('child');
|
||||
const entryPoint = uniq('entrypoint');
|
||||
|
||||
runCLI(
|
||||
`generate @nx/angular:lib ${lib} --publishable --importPath=@${proj}/${lib} --no-standalone --no-interactive`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/angular:secondary-entry-point --name=${entryPoint} --library=${lib} --no-interactive`
|
||||
);
|
||||
|
||||
runCLI(
|
||||
`generate @nx/angular:library ${childLib} --publishable=true --importPath=@${proj}/${childLib} --no-standalone --no-interactive`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/angular:secondary-entry-point --name=sub --library=${childLib} --no-interactive`
|
||||
);
|
||||
|
||||
const moduleContent = `
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ${
|
||||
names(childLib).className
|
||||
}Module } from '@${proj}/${childLib}';
|
||||
import { SubModule } from '@${proj}/${childLib}/sub';
|
||||
@NgModule({
|
||||
imports: [CommonModule, ${names(childLib).className}Module, SubModule]
|
||||
})
|
||||
export class ${names(lib).className}Module {}`;
|
||||
|
||||
updateFile(`${lib}/src/lib/${lib}-module.ts`, moduleContent);
|
||||
|
||||
// ACT
|
||||
const buildOutput = runCLI(`build ${lib}`, { env: { CI: 'false' } });
|
||||
|
||||
// ASSERT
|
||||
expect(buildOutput).toContain(`Building entry point '@${proj}/${lib}'`);
|
||||
expect(buildOutput).toContain(
|
||||
`Building entry point '@${proj}/${lib}/${entryPoint}'`
|
||||
);
|
||||
expect(buildOutput).toContain('Successfully ran target build');
|
||||
|
||||
expect(() => runCLI(`lint ${lib} --fix`)).not.toThrow();
|
||||
expect(() => runCLI(`lint ${childLib} --fix`)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should support generating libraries with a scoped name when', () => {
|
||||
const libName = uniq('@my-org/lib1');
|
||||
|
||||
runCLI(`generate @nx/angular:lib ${libName} --buildable --standalone`);
|
||||
|
||||
// check files are generated without the layout directory ("libs/") and
|
||||
// using the project name as the directory when no directory is provided
|
||||
checkFilesExist(
|
||||
`${libName}/src/index.ts`,
|
||||
`${libName}/src/lib/${libName.split('/')[1]}/${libName.split('/')[1]}.ts`
|
||||
);
|
||||
// check build works
|
||||
expect(() => runCLI(`build ${libName}`)).not.toThrow();
|
||||
// check tests pass
|
||||
expect(() => runCLI(`test ${libName}`)).not.toThrow();
|
||||
}, 500_000);
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
cleanupProject,
|
||||
newProject,
|
||||
readFile,
|
||||
reservePort,
|
||||
runCLI,
|
||||
uniq,
|
||||
updateFile,
|
||||
} from '@nx/e2e-utils';
|
||||
|
||||
export interface ProjectsTestSetup {
|
||||
proj: string;
|
||||
app1: string;
|
||||
app1Port: number;
|
||||
esbuildApp: string;
|
||||
lib1: string;
|
||||
app1DefaultModule: string;
|
||||
app1DefaultComponentTemplate: string;
|
||||
esbuildAppDefaultModule: string;
|
||||
esbuildAppDefaultComponentTemplate: string;
|
||||
esbuildAppDefaultProjectConfig: string;
|
||||
}
|
||||
|
||||
export async function setupProjectsTest(): Promise<ProjectsTestSetup> {
|
||||
const proj = newProject({
|
||||
packages: [
|
||||
'@nx/angular',
|
||||
'@nx/webpack',
|
||||
'@nx/vitest',
|
||||
'@nx/playwright',
|
||||
'@nx/rspack',
|
||||
],
|
||||
});
|
||||
const app1 = uniq('app1');
|
||||
// app1 gets an e2e run; pin its dev-server/e2e port to a reserved one so it
|
||||
// does not collide on the shared default 4200.
|
||||
const app1Port = await reservePort();
|
||||
const esbuildApp = uniq('esbuild-app');
|
||||
const lib1 = uniq('lib1');
|
||||
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${app1} --port=${app1Port} --no-standalone --bundler=webpack --no-interactive`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${esbuildApp} --bundler=esbuild --no-standalone --no-interactive`
|
||||
);
|
||||
runCLI(`generate @nx/angular:lib ${lib1} --no-interactive`);
|
||||
|
||||
const app1DefaultModule = readFile(`${app1}/src/app/app-module.ts`);
|
||||
const app1DefaultComponentTemplate = readFile(`${app1}/src/app/app.html`);
|
||||
const esbuildAppDefaultModule = readFile(
|
||||
`${esbuildApp}/src/app/app-module.ts`
|
||||
);
|
||||
const esbuildAppDefaultComponentTemplate = readFile(
|
||||
`${esbuildApp}/src/app/app.html`
|
||||
);
|
||||
const esbuildAppDefaultProjectConfig = readFile(`${esbuildApp}/project.json`);
|
||||
|
||||
return {
|
||||
proj,
|
||||
app1,
|
||||
app1Port,
|
||||
esbuildApp,
|
||||
lib1,
|
||||
app1DefaultModule,
|
||||
app1DefaultComponentTemplate,
|
||||
esbuildAppDefaultModule,
|
||||
esbuildAppDefaultComponentTemplate,
|
||||
esbuildAppDefaultProjectConfig,
|
||||
};
|
||||
}
|
||||
|
||||
export function resetProjectsTest(setup: ProjectsTestSetup): void {
|
||||
updateFile(`${setup.app1}/src/app/app-module.ts`, setup.app1DefaultModule);
|
||||
updateFile(
|
||||
`${setup.app1}/src/app/app.html`,
|
||||
setup.app1DefaultComponentTemplate
|
||||
);
|
||||
updateFile(
|
||||
`${setup.esbuildApp}/src/app/app-module.ts`,
|
||||
setup.esbuildAppDefaultModule
|
||||
);
|
||||
updateFile(
|
||||
`${setup.esbuildApp}/src/app/app.html`,
|
||||
setup.esbuildAppDefaultComponentTemplate
|
||||
);
|
||||
updateFile(
|
||||
`${setup.esbuildApp}/project.json`,
|
||||
setup.esbuildAppDefaultProjectConfig
|
||||
);
|
||||
}
|
||||
|
||||
export function cleanupProjectsTest(): void {
|
||||
cleanupProject();
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
checkFilesDoNotExist,
|
||||
checkFilesExist,
|
||||
rmDist,
|
||||
runCLI,
|
||||
uniq,
|
||||
} from '@nx/e2e-utils';
|
||||
import {
|
||||
setupProjectsTest,
|
||||
resetProjectsTest,
|
||||
cleanupProjectsTest,
|
||||
ProjectsTestSetup,
|
||||
} from './projects-setup';
|
||||
|
||||
describe('Angular Projects - SSR', () => {
|
||||
let setup: ProjectsTestSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
setup = await setupProjectsTest();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetProjectsTest(setup);
|
||||
});
|
||||
|
||||
afterAll(() => cleanupProjectsTest());
|
||||
|
||||
it('should support generating applications with SSR and converting targets with webpack-based executors to use the application executor', async () => {
|
||||
const esbuildApp = uniq('esbuild-app');
|
||||
const webpackApp = uniq('webpack-app');
|
||||
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${esbuildApp} --bundler=esbuild --ssr --no-interactive`
|
||||
);
|
||||
|
||||
// check build produces both the browser and server bundles
|
||||
runCLI(`build ${esbuildApp} --output-hashing none`);
|
||||
checkFilesExist(
|
||||
`dist/${esbuildApp}/browser/main.js`,
|
||||
`dist/${esbuildApp}/server/server.mjs`
|
||||
);
|
||||
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${webpackApp} --bundler=webpack --ssr --no-interactive`
|
||||
);
|
||||
|
||||
// check build only produces the browser bundle
|
||||
runCLI(`build ${webpackApp} --output-hashing none`);
|
||||
checkFilesExist(`dist/${webpackApp}/browser/main.js`);
|
||||
checkFilesDoNotExist(`dist/${webpackApp}/server/main.js`);
|
||||
|
||||
// check server produces the server bundle
|
||||
runCLI(`server ${webpackApp} --output-hashing none`);
|
||||
checkFilesExist(`dist/${webpackApp}/server/main.js`);
|
||||
|
||||
rmDist();
|
||||
|
||||
// convert target with webpack-based executors to use the application executor
|
||||
runCLI(
|
||||
`generate @nx/angular:convert-to-application-executor ${webpackApp}`
|
||||
);
|
||||
|
||||
// check build now produces both the browser and server bundles
|
||||
runCLI(`build ${webpackApp} --output-hashing none`);
|
||||
checkFilesExist(
|
||||
`dist/${webpackApp}/browser/main.js`,
|
||||
`dist/${webpackApp}/server/server.mjs`
|
||||
);
|
||||
|
||||
// check server target is no longer available
|
||||
expect(() =>
|
||||
runCLI(`server ${webpackApp} --output-hashing none`)
|
||||
).toThrow();
|
||||
}, 500_000);
|
||||
|
||||
// TODO: enable this test once vitest issue is resolved
|
||||
it.skip('should generate apps and libs with vitest', async () => {
|
||||
const app = uniq('app');
|
||||
const lib = uniq('lib');
|
||||
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${app} --unit-test-runner=vitest --no-interactive`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/angular:lib ${lib} --unit-test-runner=vitest --no-interactive`
|
||||
);
|
||||
|
||||
// Make sure we are using vitest
|
||||
checkFilesExist(`${app}/vite.config.mts`, `${lib}/vite.config.mts`);
|
||||
|
||||
runCLI(`run-many --target test --projects=${app},${lib} --parallel`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest"]
|
||||
},
|
||||
"include": [],
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/angular"
|
||||
},
|
||||
{
|
||||
"path": "../utils"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/devkit"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/nx"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "out-tsc/spec",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"exclude": ["out-tsc"],
|
||||
"include": [
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.js",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.test.jsx",
|
||||
"src/**/*.ts",
|
||||
"**/*.d.ts",
|
||||
"jest.config.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user