nx/e2e/angular/src/tailwind.test.ts
Leosvel Pérez Espinosa 752d418f78
feat(angular): support angular cli v20.0.0-rc.3 (#30715)
Add support for the Angular CLI **20.0.0-rc.3** version.
2025-05-26 10:00:47 -04:00

436 lines
13 KiB
TypeScript

import {
cleanupProject,
listFiles,
newProject,
readFile,
removeFile,
runCLI,
uniq,
updateFile,
updateJson,
} from '@nx/e2e/utils';
import { join } from 'path';
describe('Tailwind support', () => {
let project: string;
const defaultButtonBgColor = 'bg-blue-700';
const buildLibWithTailwind = {
name: uniq('build-lib-with-tailwind'),
buttonBgColor: 'bg-green-800',
};
const pubLibWithTailwind = {
name: uniq('pub-lib-with-tailwind'),
buttonBgColor: 'bg-red-900',
};
const spacing = {
root: {
sm: '2px',
md: '4px',
lg: '8px',
},
projectVariant1: {
sm: '1px',
md: '2px',
lg: '4px',
},
projectVariant2: {
sm: '4px',
md: '8px',
lg: '16px',
},
projectVariant3: {
sm: '8px',
md: '16px',
lg: '32px',
},
};
const createWorkspaceTailwindConfigFile = () => {
const tailwindConfigFile = 'tailwind.config.js';
const tailwindConfig = `module.exports = {
content: [
'**/!(*.stories|*.spec).{ts,html}',
'**/!(*.stories|*.spec).{ts,html}',
],
theme: {
spacing: {
sm: '${spacing.root.sm}',
md: '${spacing.root.md}',
lg: '${spacing.root.lg}',
},
},
plugins: [],
};
`;
updateFile(tailwindConfigFile, tailwindConfig);
};
const createTailwindConfigFile = (
tailwindConfigFile = 'tailwind.config.js',
libSpacing: (typeof spacing)['projectVariant1']
) => {
const tailwindConfig = `const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind');
const { join } = require('path');
module.exports = {
content: [
join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'),
...createGlobPatternsForDependencies(__dirname),
],
theme: {
spacing: {
sm: '${libSpacing.sm}',
md: '${libSpacing.md}',
lg: '${libSpacing.lg}',
},
},
plugins: [],
};
`;
updateFile(tailwindConfigFile, tailwindConfig);
};
const updateTailwindConfig = (
tailwindConfigPath: string,
projectSpacing: (typeof spacing)['root']
) => {
const tailwindConfig = readFile(tailwindConfigPath);
const tailwindConfigUpdated = tailwindConfig.replace(
'theme: {',
`theme: {
spacing: {
sm: '${projectSpacing.sm}',
md: '${projectSpacing.md}',
lg: '${projectSpacing.lg}',
},`
);
updateFile(tailwindConfigPath, tailwindConfigUpdated);
};
beforeAll(() => {
project = newProject({ packages: ['@nx/angular'] });
// Create tailwind config in the workspace root
createWorkspaceTailwindConfigFile();
});
afterAll(() => {
cleanupProject();
});
describe('Libraries', () => {
const createLibComponent = (
lib: string,
buttonBgColor: string = defaultButtonBgColor
) => {
updateFile(
`${lib}/src/lib/foo.ts`,
`import { Component } from '@angular/core';
@Component({
selector: '${project}-foo',
standalone: false,
template: '<button class="custom-btn text-white ${buttonBgColor}">Click me!</button>',
styles: [\`
.custom-btn {
@apply m-md p-sm;
}
\`]
})
export class Foo {}
`
);
updateFile(
`${lib}/src/lib/${lib}-module.ts`,
`import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Foo } from './foo';
@NgModule({
imports: [CommonModule],
declarations: [Foo],
exports: [Foo],
})
export class LibModule {}
`
);
updateFile(
`${lib}/src/index.ts`,
`export * from './lib/foo';
export * from './lib/${lib}-module';
`
);
};
const assertLibComponentStyles = (
lib: string,
libSpacing: (typeof spacing)['root'],
isPublishable: boolean = true
) => {
const builtComponentContent = readFile(
isPublishable
? `dist/${lib}/fesm2022/${project}-${lib}.mjs`
: `dist/${lib}/esm2022/lib/foo.js`
);
let expectedStylesRegex = new RegExp(
`styles: \\[\\"\\.custom\\-btn(\\[_ngcontent\\-%COMP%\\])?{margin:${libSpacing.md};padding:${libSpacing.sm}}(\\\\n)?\\"\\]`
);
expect(builtComponentContent).toMatch(expectedStylesRegex);
};
it('should generate a buildable library with tailwind and build correctly', () => {
runCLI(
`generate @nx/angular:lib ${buildLibWithTailwind.name} --buildable --add-tailwind --no-interactive`
);
updateTailwindConfig(
`${buildLibWithTailwind.name}/tailwind.config.js`,
spacing.projectVariant1
);
createLibComponent(
buildLibWithTailwind.name,
buildLibWithTailwind.buttonBgColor
);
runCLI(`build ${buildLibWithTailwind.name}`);
assertLibComponentStyles(
buildLibWithTailwind.name,
spacing.projectVariant1,
false
);
});
it('should set up tailwind in a previously generated buildable library and build correctly', () => {
const buildLibSetupTailwind = uniq('build-lib-setup-tailwind');
runCLI(
`generate @nx/angular:lib ${buildLibSetupTailwind} --buildable --no-interactive`
);
runCLI(
`generate @nx/angular:setup-tailwind ${buildLibSetupTailwind} --no-interactive`
);
updateTailwindConfig(
`${buildLibSetupTailwind}/tailwind.config.js`,
spacing.projectVariant2
);
createLibComponent(buildLibSetupTailwind);
runCLI(`build ${buildLibSetupTailwind}`);
assertLibComponentStyles(
buildLibSetupTailwind,
spacing.projectVariant2,
false
);
});
it('should correctly build a buildable library with a tailwind.config.js file in the project root or workspace root', () => {
const buildLibNoProjectConfig = uniq('build-lib-no-project-config');
runCLI(
`generate @nx/angular:lib ${buildLibNoProjectConfig} --buildable --no-interactive`
);
createTailwindConfigFile(
`${buildLibNoProjectConfig}/tailwind.config.js`,
spacing.projectVariant3
);
createLibComponent(buildLibNoProjectConfig);
runCLI(`build ${buildLibNoProjectConfig}`);
assertLibComponentStyles(
buildLibNoProjectConfig,
spacing.projectVariant3,
false
);
// remove tailwind.config.js file from the project root to test the one in the workspace root
removeFile(`${buildLibNoProjectConfig}/tailwind.config.js`);
runCLI(`build ${buildLibNoProjectConfig}`);
assertLibComponentStyles(buildLibNoProjectConfig, spacing.root, false);
});
it('should generate a publishable library with tailwind and build correctly', () => {
runCLI(
`generate @nx/angular:lib ${pubLibWithTailwind.name} --publishable --add-tailwind --importPath=@${project}/${pubLibWithTailwind.name} --no-interactive`
);
updateTailwindConfig(
`${pubLibWithTailwind.name}/tailwind.config.js`,
spacing.projectVariant1
);
createLibComponent(
pubLibWithTailwind.name,
pubLibWithTailwind.buttonBgColor
);
runCLI(`build ${pubLibWithTailwind.name}`);
assertLibComponentStyles(
pubLibWithTailwind.name,
spacing.projectVariant1
);
});
it('should set up tailwind in a previously generated publishable library and build correctly', () => {
const pubLibSetupTailwind = uniq('pub-lib-setup-tailwind');
runCLI(
`generate @nx/angular:lib ${pubLibSetupTailwind} --publishable --importPath=@${project}/${pubLibSetupTailwind} --no-interactive`
);
runCLI(
`generate @nx/angular:setup-tailwind ${pubLibSetupTailwind} --no-interactive`
);
updateTailwindConfig(
`${pubLibSetupTailwind}/tailwind.config.js`,
spacing.projectVariant2
);
createLibComponent(pubLibSetupTailwind);
runCLI(`build ${pubLibSetupTailwind}`);
assertLibComponentStyles(pubLibSetupTailwind, spacing.projectVariant2);
});
it('should correctly build a publishable library with a tailwind.config.js file in the project root or workspace root', () => {
const pubLibNoProjectConfig = uniq('pub-lib-no-project-config');
runCLI(
`generate @nx/angular:lib ${pubLibNoProjectConfig} --publishable --importPath=@${project}/${pubLibNoProjectConfig} --no-interactive`
);
createTailwindConfigFile(
`${pubLibNoProjectConfig}/tailwind.config.js`,
spacing.projectVariant3
);
createLibComponent(pubLibNoProjectConfig);
runCLI(`build ${pubLibNoProjectConfig}`);
assertLibComponentStyles(pubLibNoProjectConfig, spacing.projectVariant3);
// remove tailwind.config.js file from the project root to test the one in the workspace root
removeFile(`${pubLibNoProjectConfig}/tailwind.config.js`);
runCLI(`build ${pubLibNoProjectConfig}`);
assertLibComponentStyles(pubLibNoProjectConfig, spacing.root);
});
});
describe('Applications', () => {
const readAppStylesBundle = (outputPath: string) => {
const stylesBundlePath = listFiles(outputPath).find((file) =>
/^styles[\.-]/.test(file)
);
const stylesBundle = readFile(`${outputPath}/${stylesBundlePath}`);
return stylesBundle;
};
const assertAppComponentStyles = (
outputPath: string,
appSpacing: (typeof spacing)['root']
) => {
const mainBundlePath = listFiles(outputPath).find((file) =>
/^main[\.-]/.test(file)
);
const mainBundle = readFile(`${outputPath}/${mainBundlePath}`);
let expectedStylesRegex = new RegExp(
`styles:\\[\\"\\.custom\\-btn\\[_ngcontent\\-%COMP%\\]{margin:${appSpacing.md};padding:${appSpacing.sm}}\\"\\]`
);
expect(mainBundle).toMatch(expectedStylesRegex);
};
const setupTailwindAndProjectDependencies = (appName: string) => {
updateTailwindConfig(
`${appName}/tailwind.config.js`,
spacing.projectVariant1
);
updateFile(
`${appName}/src/app/app-module.ts`,
`import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { LibModule as LibModule1 } from '@${project}/${buildLibWithTailwind.name}';
import { LibModule as LibModule2 } from '@${project}/${pubLibWithTailwind.name}';
import { App } from './app';
@NgModule({
declarations: [],
imports: [BrowserModule, App, LibModule1, LibModule2],
providers: [],
bootstrap: [App],
})
export class AppModule {}
`
);
updateFile(
`${appName}/src/app/app.html`,
`<button class="custom-btn text-white">Click me!</button>`
);
updateFile(
`${appName}/src/app/app.css`,
`.custom-btn {
@apply m-md p-sm;
}`
);
};
it('should build correctly and only output the tailwind utilities used', async () => {
const appWithTailwind = uniq('app-with-tailwind');
runCLI(
`generate @nx/angular:app ${appWithTailwind} --add-tailwind --no-interactive`
);
setupTailwindAndProjectDependencies(appWithTailwind);
runCLI(`build ${appWithTailwind}`);
const outputPath = `dist/${appWithTailwind}/browser`;
assertAppComponentStyles(outputPath, spacing.projectVariant1);
let stylesBundle = readAppStylesBundle(outputPath);
expect(stylesBundle).toContain('.text-white');
expect(stylesBundle).not.toContain('.text-black');
expect(stylesBundle).toContain(`.${buildLibWithTailwind.buttonBgColor}`);
expect(stylesBundle).toContain(`.${pubLibWithTailwind.buttonBgColor}`);
expect(stylesBundle).not.toContain(`.${defaultButtonBgColor}`);
});
it('should build correctly and only output the tailwind utilities used when using webpack and incremental builds', async () => {
const appWithTailwind = uniq('app-with-tailwind');
runCLI(
`generate @nx/angular:app ${appWithTailwind} --add-tailwind --bundler=webpack --no-interactive`
);
setupTailwindAndProjectDependencies(appWithTailwind);
updateJson(join(appWithTailwind, 'project.json'), (config) => {
config.targets.build.executor = '@nx/angular:webpack-browser';
config.targets.build.options = {
...config.targets.build.options,
buildLibsFromSource: false,
};
return config;
});
runCLI(`build ${appWithTailwind}`);
const outputPath = `dist/${appWithTailwind}`;
assertAppComponentStyles(outputPath, spacing.projectVariant1);
let stylesBundle = readAppStylesBundle(outputPath);
expect(stylesBundle).toContain('.text-white');
expect(stylesBundle).not.toContain('.text-black');
expect(stylesBundle).toContain(`.${buildLibWithTailwind.buttonBgColor}`);
expect(stylesBundle).toContain(`.${pubLibWithTailwind.buttonBgColor}`);
expect(stylesBundle).not.toContain(`.${defaultButtonBgColor}`);
});
});
});