chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:36 +08:00
commit 8e2a6eb840
10194 changed files with 1593658 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
import { baseConfig, e2eTestOnlyIgnores } from '../../eslint.config.mjs';
export default [...baseConfig, e2eTestOnlyIgnores];
+13
View File
@@ -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',
};
+11
View File
@@ -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:*"
}
}
+15
View File
@@ -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
}
}
}
}
+156
View File
@@ -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);
});
+185
View File
@@ -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);
});
+410
View File
@@ -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}`
);
});
});
+81
View File
@@ -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);
});
+185
View File
@@ -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}`);
});
});
+158
View File
@@ -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>`
);
});
});
+83
View File
@@ -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);
});
+95
View File
@@ -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();
}
+93
View File
@@ -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`);
});
});
+25
View File
@@ -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"
}
]
}
+21
View File
@@ -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"
]
}