import * as devkit from '@nx/devkit'; import { addProjectConfiguration, readJson, readProjectConfiguration, Tree, updateProjectConfiguration, } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { setupTailwindGenerator } from './setup-tailwind'; import { autoprefixerVersion, postcssVersion, tailwindVersion, } from '../../utils/versions'; describe('setupTailwind generator', () => { let tree: Tree; beforeEach(() => { tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); jest.clearAllMocks(); }); it('should fail when the project does not exist', async () => { await expect( setupTailwindGenerator(tree, { project: 'not-found' }) ).rejects.toThrow(); }); describe('libraries', () => { const project = 'lib1'; beforeEach(() => { addProjectConfiguration(tree, project, { name: project, projectType: 'library', root: `libs/${project}`, sourceRoot: `libs/${project}/src`, }); }); it('should throw when tailwind is installed as a dependency with a version lower than 2.0.0', async () => { tree.write( 'package.json', JSON.stringify({ dependencies: { tailwindcss: '^1.99.99' } }) ); await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow( `Tailwind CSS version "^1.99.99" is not supported. Please upgrade to v2.0.0 or higher.` ); }); it('should throw when tailwind is installed as a devDependency with a version lower than 2.0.0', async () => { tree.write( 'package.json', JSON.stringify({ devDependencies: { tailwindcss: '^1.99.99' } }) ); await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow( `Tailwind CSS version "^1.99.99" is not supported. Please upgrade to v2.0.0 or higher.` ); }); it('should throw when the build target is not found', async () => { await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow( expect.objectContaining({ message: expect.stringContaining( `The target "build" was not found for project "${project}".` ), }) ); }); it('should throw when the specified build target is not found', async () => { await expect( setupTailwindGenerator(tree, { project, buildTarget: 'custom-build' }) ).rejects.toThrow( expect.objectContaining({ message: expect.stringContaining( `The target "custom-build" was not found for project "${project}".` ), }) ); }); it('should throw when the build target is using an unsupported executor', async () => { const projectConfig = readProjectConfiguration(tree, project); projectConfig.targets = { build: { executor: '@angular/build-angular:browser', options: {}, }, }; updateProjectConfiguration(tree, project, projectConfig); await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow( expect.objectContaining({ message: expect.stringContaining( `The build target for project "${project}" is using an unsupported executor "@angular/build-angular:browser".` ), }) ); }); it('should throw when the tailwind config is configured in the build target and the file it points to exists', async () => { const tailwindConfig = `libs/${project}/my-tailwind.config.js`; let projectConfig = readProjectConfiguration(tree, project); projectConfig.targets = { build: { executor: '@nx/angular:package', options: { tailwindConfig }, }, }; updateProjectConfiguration(tree, project, projectConfig); tree.write(tailwindConfig, ''); await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow( expect.objectContaining({ message: expect.stringContaining( `The "${tailwindConfig}" file is already configured for the project "${project}". Are you sure this is the right project to set up Tailwind?` ), }) ); }); it('should add the tailwind config path to the "build" target by default when no build target is specified', async () => { let projectConfig = readProjectConfiguration(tree, project); projectConfig.targets = { build: { executor: '@nx/angular:package', options: {} }, }; updateProjectConfiguration(tree, project, projectConfig); await setupTailwindGenerator(tree, { project, skipFormat: true }); projectConfig = readProjectConfiguration(tree, project); expect(projectConfig.targets.build.options.tailwindConfig).toBe( `libs/${project}/tailwind.config.js` ); }); it('should add the tailwind config path to the specified buildTarget', async () => { const buildTarget = 'custom-build'; let projectConfig = readProjectConfiguration(tree, project); projectConfig.targets = { [buildTarget]: { executor: '@nx/angular:package', options: {} }, }; updateProjectConfiguration(tree, project, projectConfig); await setupTailwindGenerator(tree, { project, buildTarget, skipFormat: true, }); projectConfig = readProjectConfiguration(tree, project); expect(projectConfig.targets[buildTarget].options.tailwindConfig).toBe( `libs/${project}/tailwind.config.js` ); }); it.each(['@nx/angular:ng-packagr-lite', '@nx/angular:package'])( 'should add the tailwind config path when using the "%s" executor', async (executor) => { let projectConfig = readProjectConfiguration(tree, project); projectConfig.targets = { build: { executor, options: {} } }; updateProjectConfiguration(tree, project, projectConfig); await setupTailwindGenerator(tree, { project, skipFormat: true }); projectConfig = readProjectConfiguration(tree, project); expect(projectConfig.targets.build.options.tailwindConfig).toBe( `libs/${project}/tailwind.config.js` ); } ); it('should add required packages', async () => { const projectConfig = readProjectConfiguration(tree, project); projectConfig.targets = { build: { executor: '@nx/angular:package', options: {} }, }; updateProjectConfiguration(tree, project, projectConfig); await setupTailwindGenerator(tree, { project, skipFormat: true }); const { devDependencies } = readJson(tree, 'package.json'); expect(devDependencies.tailwindcss).toBe(tailwindVersion); expect(devDependencies.autoprefixer).toBe(autoprefixerVersion); expect(devDependencies.postcss).toBe(postcssVersion); }); it('should generate the tailwind.config.js file in the project root for v3 by default', async () => { const projectConfig = readProjectConfiguration(tree, project); projectConfig.targets = { build: { executor: '@nx/angular:package', options: {} }, }; updateProjectConfiguration(tree, project, projectConfig); await setupTailwindGenerator(tree, { project, skipFormat: true }); expect(tree.read(`libs/${project}/tailwind.config.js`, 'utf-8')) .toMatchInlineSnapshot(` "const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind'); const { join } = require('path'); /** @type {import('tailwindcss').Config} */ module.exports = { content: [ join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'), ...createGlobPatternsForDependencies(__dirname), ], theme: { extend: {}, }, plugins: [], }; " `); }); it('should generate the tailwind.config.js file in the project root with the config for v3 when a version greater than 3 is installed', async () => { const projectConfig = readProjectConfiguration(tree, project); projectConfig.targets = { build: { executor: '@nx/angular:package', options: {} }, }; updateProjectConfiguration(tree, project, projectConfig); tree.write( 'package.json', JSON.stringify({ devDependencies: { tailwindcss: '^3.0.1' } }) ); await setupTailwindGenerator(tree, { project, skipFormat: true }); expect(tree.read(`libs/${project}/tailwind.config.js`, 'utf-8')) .toMatchInlineSnapshot(` "const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind'); const { join } = require('path'); /** @type {import('tailwindcss').Config} */ module.exports = { content: [ join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'), ...createGlobPatternsForDependencies(__dirname), ], theme: { extend: {}, }, plugins: [], }; " `); }); it('should generate the tailwind.config.js file in the project root with the config for v2 when a version greater than 2 and lower than 3 is installed', async () => { const projectConfig = readProjectConfiguration(tree, project); projectConfig.targets = { build: { executor: '@nx/angular:package', options: {} }, }; updateProjectConfiguration(tree, project, projectConfig); tree.write( 'package.json', JSON.stringify({ devDependencies: { tailwindcss: '~2.0.0' } }) ); await setupTailwindGenerator(tree, { project, skipFormat: true }); expect(tree.read(`libs/${project}/tailwind.config.js`, 'utf-8')) .toMatchInlineSnapshot(` "const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind'); const { join } = require('path'); module.exports = { mode: 'jit', purge: [ join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'), ...createGlobPatternsForDependencies(__dirname), ], darkMode: false, // or 'media' or 'class' theme: { extend: {}, }, variants: { extend: {}, }, plugins: [], }; " `); }); it('should format files', async () => { const projectConfig = readProjectConfiguration(tree, project); projectConfig.targets = { build: { executor: '@nx/angular:package', options: {} }, }; updateProjectConfiguration(tree, project, projectConfig); jest.spyOn(devkit, 'formatFiles'); await setupTailwindGenerator(tree, { project }); expect(devkit.formatFiles).toHaveBeenCalled(); }); it('should not format files when "skipFormat: true"', async () => { const projectConfig = readProjectConfiguration(tree, project); projectConfig.targets = { build: { executor: '@nx/angular:package', options: {} }, }; updateProjectConfiguration(tree, project, projectConfig); jest.spyOn(devkit, 'formatFiles'); await setupTailwindGenerator(tree, { project, skipFormat: true }); expect(devkit.formatFiles).not.toHaveBeenCalled(); }); }); });