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: '', 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`, `` ); 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}`); }); }); });