diff --git a/packages/cli/jest.config.js b/packages/cli/jest.config.js index 66a52115a507..379dc90cf051 100644 --- a/packages/cli/jest.config.js +++ b/packages/cli/jest.config.js @@ -1,5 +1,5 @@ module.exports = { - testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/*.test.[jt]s?(x)'], + testMatch: ['**/__tests__/**/*.test.[jt]s?(x)', '**/*.test.[jt]s?(x)'], testPathIgnorePatterns: ['fixtures'], moduleNameMapper: { 'src/(.*)': '/src/$1', diff --git a/packages/cli/src/commands/setup/tailwind/tailwind.js b/packages/cli/src/commands/setup/tailwind/tailwind.js index 7c8849a6f7be..2999e7697382 100644 --- a/packages/cli/src/commands/setup/tailwind/tailwind.js +++ b/packages/cli/src/commands/setup/tailwind/tailwind.js @@ -1,61 +1,33 @@ -import fs from 'fs' -import path from 'path' - import chalk from 'chalk' -import execa from 'execa' import Listr from 'listr' -import { getPaths, writeFile } from 'src/lib' +import { + configurePostCSS, + installPackages, + yarnCheckFiles, + initTailwind, + addCSSImports, +} from './tasks' import c from 'src/lib/colors' export const command = 'tailwind' export const description = 'Setup tailwindcss and PostCSS' export const builder = (yargs) => { - yargs.option('force', { - alias: 'f', - default: false, - description: 'Overwrite existing configuration', - type: 'boolean', - }) -} - -const tailwindImportsAndNotes = [ - '/**', - ' * START --- TAILWIND GENERATOR EDIT', - ' *', - ' * `yarn rw setup tailwind` placed these imports here', - " * to inject Tailwind's styles into your CSS.", - ' * For more information, see: https://tailwindcss.com/docs/installation#add-tailwind-to-your-css', - ' */', - '@import "tailwindcss/base";', - '@import "tailwindcss/components";', - '@import "tailwindcss/utilities";', - '/**', - ' * END --- TAILWIND GENERATOR EDIT', - ' */\n', -] - -const INDEX_CSS_PATH = path.join(getPaths().web.src, 'index.css') - -const tailwindImportsExist = (indexCSS) => { - let content = indexCSS.toString() - - const hasBaseImport = () => /@import "tailwindcss\/base"/.test(content) - - const hasComponentsImport = () => - /@import "tailwindcss\/components"/.test(content) - - const hasUtilitiesImport = () => - /@import "tailwindcss\/utilities"/.test(content) - - return hasBaseImport() && hasComponentsImport() && hasUtilitiesImport() + yargs + .option('force', { + alias: 'f', + default: false, + description: 'Overwrite existing configuration', + type: 'boolean', + }) + .option('ui', { + default: false, + description: 'Include TailwindUI Installation', + type: 'boolean', + }) } -const postCSSConfigExists = () => { - return fs.existsSync(getPaths().web.postcss) -} - -export const handler = async ({ force }) => { +export const handler = async (args) => { const tasks = new Listr([ { title: 'Installing packages...', @@ -63,114 +35,26 @@ export const handler = async ({ force }) => { return new Listr([ { title: 'Install postcss-loader, tailwindcss, and autoprefixer', - task: async () => { - /** - * Install postcss-loader, tailwindcss, and autoprefixer - * RedwoodJS currently uses PostCSS v7; postcss-loader and autoprefixers pinned for compatibility - */ - await execa('yarn', [ - 'workspace', - 'web', - 'add', - '-D', - 'postcss-loader@4.0.2', - 'tailwindcss@npm:@tailwindcss/postcss7-compat', - 'autoprefixer@9.8.6', - ]) - }, + task: installPackages(args), }, { title: 'Sync yarn.lock and node_modules', - task: async () => { - /** - * Sync yarn.lock file and node_modules folder. - * Refer https://github.com/redwoodjs/redwood/issues/1301 for more details. - */ - await execa('yarn', ['install', '--check-files']) - }, + task: yarnCheckFiles(args), }, ]) }, }, { title: 'Configuring PostCSS...', - task: () => { - /** - * Make web/config if it doesn't exist - * and write postcss.config.js there - */ - - /** - * Check if PostCSS config already exists. - * If it exists, throw an error. - */ - if (!force && postCSSConfigExists()) { - throw new Error( - 'PostCSS config already exists.\nUse --force to override existing config.' - ) - } else { - return writeFile( - getPaths().web.postcss, - fs - .readFileSync( - path.resolve( - __dirname, - 'templates', - 'postcss.config.js.template' - ) - ) - .toString(), - { overwriteExisting: force } - ) - } - }, + task: configurePostCSS(args), }, { title: 'Initializing Tailwind CSS...', - task: async () => { - const basePath = getPaths().web.base - const tailwindConfigPath = path.join(basePath, 'tailwind.config.js') - const configExists = fs.existsSync(tailwindConfigPath) - - if (configExists) { - if (force) { - // yarn tailwindcss init will fail if the file already exists - fs.unlinkSync(tailwindConfigPath) - } else { - throw new Error( - 'Tailwindcss config already exists.\nUse --force to override existing config.' - ) - } - } - - await execa('yarn', ['tailwindcss', 'init'], { cwd: basePath }) - - // opt-in to upcoming changes - const config = fs.readFileSync(tailwindConfigPath, 'utf-8') - - const uncommentFlags = (str) => - str.replace(/\/{2} ([\w-]+: true)/g, '$1') - - const newConfig = config.replace(/future.*purge/s, uncommentFlags) - - fs.writeFileSync(tailwindConfigPath, newConfig) - }, + task: initTailwind(args), }, { title: 'Adding imports to index.css...', - task: (_ctx, task) => { - /** - * Add tailwind imports and notes to the top of index.css - */ - let indexCSS = fs.readFileSync(INDEX_CSS_PATH) - - if (tailwindImportsExist(indexCSS)) { - task.skip('Imports already exist in index.css') - } else { - indexCSS = tailwindImportsAndNotes.join('\n') + indexCSS - fs.writeFileSync(INDEX_CSS_PATH, indexCSS) - } - }, + task: addCSSImports(args), }, { title: 'One more thing...', @@ -188,7 +72,7 @@ export const handler = async ({ force }) => { ]) try { - await tasks.run() + await tasks(args).run() } catch (e) { console.log(c.error(e.message)) } diff --git a/packages/cli/src/commands/setup/tailwind/tasks/addCSSImports/__tests__/addCSSImports.test.js b/packages/cli/src/commands/setup/tailwind/tasks/addCSSImports/__tests__/addCSSImports.test.js new file mode 100644 index 000000000000..f40ba5c66391 --- /dev/null +++ b/packages/cli/src/commands/setup/tailwind/tasks/addCSSImports/__tests__/addCSSImports.test.js @@ -0,0 +1,67 @@ +import fs from 'fs' + +import 'src/lib/test' +import path from 'path' +import * as lib from 'src/lib' +import addCSSImports from '..' + +jest.mock('src/lib', () => { + return { + ...jest.requireActual('src/lib'), + getPaths: () => ({ + api: {}, + web: { + src: 'some/path/to/web/src', + }, + }), + writeFile: jest.fn(), + } +}) + +describe('rw setup tailwind - addCSSImports task', () => { + const cssPath = path.join(lib.getPaths().web.src, 'index.css') + const cssImports = fs + .readFileSync(path.join(__dirname, '..', 'css-imports.template.css')) + .toString() + + const task = addCSSImports() + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('skips if main CSS already includes TailwindCSS imports', () => { + jest.spyOn(fs, 'readFileSync').mockImplementation(() => { + // Always return `cssImports`, which means `web/src/index.css` will contain the imports + return cssImports + }) + + const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync') + + const taskSkip = jest.fn() + task(undefined, { skip: taskSkip }) + + expect(taskSkip).toHaveBeenCalledWith(`Imports already exist in ${cssPath}`) + expect(writeFileSyncSpy).not.toHaveBeenCalled() + }) + + it("writes CSS imports to web/src/index.css when they're missing", () => { + const cssContent = '.beautiful-text { font-family: "Comic Sans" }' + + jest + .spyOn(fs, 'readFileSync') + .mockImplementation((path) => + path === cssPath ? cssContent : cssImports + ) + + const taskObj = {} + task(undefined, taskObj) + + expect(lib.writeFile).toHaveBeenCalledWith( + cssPath, + cssImports + cssContent, + { overwriteExisting: true }, + taskObj + ) + }) +}) diff --git a/packages/cli/src/commands/setup/tailwind/tasks/addCSSImports/css-imports.template.css b/packages/cli/src/commands/setup/tailwind/tasks/addCSSImports/css-imports.template.css new file mode 100644 index 000000000000..9a3739d5f6c6 --- /dev/null +++ b/packages/cli/src/commands/setup/tailwind/tasks/addCSSImports/css-imports.template.css @@ -0,0 +1,14 @@ +/** + * START --- TAILWIND GENERATOR EDIT + * + * `yarn rw setup tailwind` placed these imports here + * to inject Tailwind's styles into your CSS. + * For more information, see: https://tailwindcss.com/docs/installation#add-tailwind-to-your-css + */ +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; +/** + * END --- TAILWIND GENERATOR EDIT + * + */ diff --git a/packages/cli/src/commands/setup/tailwind/tasks/addCSSImports/index.js b/packages/cli/src/commands/setup/tailwind/tasks/addCSSImports/index.js new file mode 100644 index 000000000000..9e434b9f5e0c --- /dev/null +++ b/packages/cli/src/commands/setup/tailwind/tasks/addCSSImports/index.js @@ -0,0 +1,37 @@ +import fs from 'fs' +import path from 'path' +import { getPaths, writeFile } from 'src/lib' + +function tailwindImportsExist(content) { + const hasBaseImport = /@import "tailwindcss\/base"/.test(content) + const hasComponentsImport = /@import "tailwindcss\/components"/.test(content) + const hasUtilitiesImport = /@import "tailwindcss\/utilities"/.test(content) + + return hasBaseImport && hasComponentsImport && hasUtilitiesImport +} + +export default () => (_ctx, task) => { + /** + * Add tailwind imports and notes to the top of index.css + */ + + const INDEX_CSS_PATH = path.join(getPaths().web.src, 'index.css') + + const tailwindImportsAndNotes = fs + .readFileSync(path.join(__dirname, 'css-imports.template.css')) + .toString() + + const cssPath = path.join(getPaths().web.src, 'INDEX_CSS_PATH') + const cssContent = fs.readFileSync(cssPath).toString() + + if (tailwindImportsExist(cssContent)) { + task.skip(`Imports already exist in ${cssPath}`) + } else { + writeFile( + cssPath, + tailwindImportsAndNotes + cssContent, + { overwriteExisting: true }, + task + ) + } +} diff --git a/packages/cli/src/commands/setup/tailwind/tasks/configurePostCSS/__tests__/configurePostCSS.test.js b/packages/cli/src/commands/setup/tailwind/tasks/configurePostCSS/__tests__/configurePostCSS.test.js new file mode 100644 index 000000000000..fef890c36e97 --- /dev/null +++ b/packages/cli/src/commands/setup/tailwind/tasks/configurePostCSS/__tests__/configurePostCSS.test.js @@ -0,0 +1,91 @@ +global.__dirname = __dirname + +import fs from 'fs' + +import 'src/lib/test' +import * as path from 'path' +import * as lib from 'src/lib' +import configurePostCSS from '..' + +jest.mock('fs', () => { + return { + ...jest.requireActual('fs'), + // readFileSync: () => '', + existsSync: jest.fn(() => false), + } +}) + +jest.mock('src/lib', () => { + return { + ...jest.requireActual('src/lib'), + getPaths: () => ({ + api: {}, + web: { + postcss: 'some/path/to/postcss.config.js', + }, + }), + writeFile: jest.fn(), + } +}) + +describe('rw setup tailwind - configurePostCSS task', () => { + const configTemplate = fs + .readFileSync(path.join(__dirname, '..', 'postcss.config.template.js')) + .toString() + + describe('without the --force option', () => { + const task = configurePostCSS({ force: false }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('fails if PostCSS config already exists', () => { + fs.existsSync.mockImplementation(() => true) + + const expectedError = + 'PostCSS config already exists.\nUse --force to override existing config.' + expect(task).toThrowError(expectedError) + expect(fs.existsSync).toHaveBeenCalledWith(lib.getPaths().web.postcss) + + fs.existsSync.mockImplementation(() => false) + }) + + it('writes the PostCSS config file when it is not present', () => { + task() + expect(lib.writeFile).toHaveBeenCalledWith( + lib.getPaths().web.postcss, + configTemplate, + { overwriteExisting: false } + ) + }) + }) + + describe('with the --force option', () => { + const task = configurePostCSS({ force: true }) + + describe.each([ + /// testLabel, fileExists + ['exists', true], + ['does not exist', false], + /// + ])('when a PostCSS file %s', (testLabel, fileExists) => { + const testLabelMap = { + true: 'overwrites', + false: 'writes', + } + + it(`${testLabelMap[fileExists]} the config file from template`, () => { + fs.existsSync.mockImplementation(() => fileExists) + + task() + + expect(lib.writeFile).toHaveBeenCalledWith( + lib.getPaths().web.postcss, + configTemplate, + { overwriteExisting: true } + ) + }) + }) + }) +}) diff --git a/packages/cli/src/commands/setup/tailwind/tasks/configurePostCSS/index.js b/packages/cli/src/commands/setup/tailwind/tasks/configurePostCSS/index.js new file mode 100644 index 000000000000..f3a79578ac3f --- /dev/null +++ b/packages/cli/src/commands/setup/tailwind/tasks/configurePostCSS/index.js @@ -0,0 +1,38 @@ +import fs from 'fs' +import path from 'path' +import { getPaths, writeFile } from 'src/lib' + +const postCSSConfigExists = () => { + return fs.existsSync(getPaths().web.postcss) +} + +export default ({ force }) => () => { + /** + * Make web/config if it doesn't exist + * and write postcss.config.js there + */ + + /** + * Check if PostCSS config already exists. + * If it exists, throw an error. + */ + if (!force && postCSSConfigExists()) { + throw new Error( + 'PostCSS config already exists.\nUse --force to override existing config.' + ) + } else { + return writeFile( + getPaths().web.postcss, + fs + .readFileSync( + path.resolve( + __dirname, + 'templates', + 'postcss.config.js.template' + ) + ) + .toString(), + { overwriteExisting: force } + ) + } +} diff --git a/packages/cli/src/commands/setup/tailwind/templates/postcss.config.js.template b/packages/cli/src/commands/setup/tailwind/tasks/configurePostCSS/postcss.config.template.js similarity index 100% rename from packages/cli/src/commands/setup/tailwind/templates/postcss.config.js.template rename to packages/cli/src/commands/setup/tailwind/tasks/configurePostCSS/postcss.config.template.js diff --git a/packages/cli/src/commands/setup/tailwind/tasks/index.js b/packages/cli/src/commands/setup/tailwind/tasks/index.js new file mode 100644 index 000000000000..ed2b40e53096 --- /dev/null +++ b/packages/cli/src/commands/setup/tailwind/tasks/index.js @@ -0,0 +1,5 @@ +export { default as installPackages } from './installPackages' +export { default as yarnCheckFiles } from './yarnCheckFiles' +export { default as configurePostCSS } from './configurePostCSS' +export { default as initTailwind } from './initTailwind' +export { default as addCSSImports } from './addCSSImports' diff --git a/packages/cli/src/commands/setup/tailwind/tasks/initTailwind/__tests__/execa.mock.js b/packages/cli/src/commands/setup/tailwind/tasks/initTailwind/__tests__/execa.mock.js new file mode 100644 index 000000000000..faf50ff0c01e --- /dev/null +++ b/packages/cli/src/commands/setup/tailwind/tasks/initTailwind/__tests__/execa.mock.js @@ -0,0 +1,26 @@ +global.__dirname = __dirname + +import fs from 'fs' +import path from 'path' + +import { isEqual } from 'lodash' +import * as lib from 'src/lib' + +const tailwindConfig = jest + .requireActual('fs') + .readFileSync(path.join(global.__dirname, 'fixtures', 'tailwind.config.js')) + .toString() + +const configPath = path.join(lib.getPaths().web.base, 'tailwind.config.js') + +export default function mockExeca(cmd, args) { + if (cmd === 'yarn' && isEqual(args, ['tailwindcss', 'init'])) { + // Simulate the creation of the tailwind config file in current dir + fs.__setMockFiles({ 'tailwind.config.js': tailwindConfig }) + } else if (cmd === 'mv' && isEqual(args, ['tailwind.config.js', 'web/'])) { + // Simulate file move operation + fs.__setMockFiles({ + [configPath]: fs.readFileSync('tailwind.config.js'), + }) + } +} diff --git a/packages/cli/src/commands/setup/tailwind/tasks/initTailwind/__tests__/fixtures/tailwind.config.js b/packages/cli/src/commands/setup/tailwind/tasks/initTailwind/__tests__/fixtures/tailwind.config.js new file mode 100644 index 000000000000..ca0c140f8b57 --- /dev/null +++ b/packages/cli/src/commands/setup/tailwind/tasks/initTailwind/__tests__/fixtures/tailwind.config.js @@ -0,0 +1,16 @@ +module.exports = { + future: { + // removeDeprecatedGapUtilities: true, + // purgeLayersByDefault: true, + // anotherFakeFlagForTestsPurposes: true + }, + theme: { + // thisMustNotBeUncommented: true, + extend: {}, + }, + purge: [], + variants: { + // thisMustNotBeUncommented2: true, + }, + plugins: [require('another/plugin')], +} diff --git a/packages/cli/src/commands/setup/tailwind/tasks/initTailwind/__tests__/initTailwind.test.js b/packages/cli/src/commands/setup/tailwind/tasks/initTailwind/__tests__/initTailwind.test.js new file mode 100644 index 000000000000..4f3e4b549859 --- /dev/null +++ b/packages/cli/src/commands/setup/tailwind/tasks/initTailwind/__tests__/initTailwind.test.js @@ -0,0 +1,151 @@ +global.__dirname = __dirname + +import fs from 'fs' +import execa from 'execa' + +import 'src/lib/test' +import * as path from 'path' +import * as lib from 'src/lib' +import initTailwind from '..' + +jest.mock('fs') + +const tailwindConfig = jest + .requireActual('fs') + .readFileSync(path.join(__dirname, 'fixtures', 'tailwind.config.js')) + .toString() + +jest.mock('execa', () => + jest.fn((...args) => { + const [cmd, cmdArgs] = args + const mockExeca = require('./execa.mock').default + mockExeca(cmd, cmdArgs) + }) +) + +jest.mock('src/lib', () => { + return { + ...jest.requireActual('src/lib'), + getPaths: () => ({ + api: {}, + web: { + base: 'some/path/to/web', + }, + }), + writeFile: jest.fn(), + } +}) + +describe('rw setup tailwind - initTailwind task', () => { + const configPath = path.join(lib.getPaths().web.base, 'tailwind.config.js') + + beforeEach(() => { + jest.clearAllMocks() + fs.__setMockFiles({}) + }) + + describe('without the --force option', () => { + const task = initTailwind({ force: false, ui: false }) + + it('fails if TailwindCSS config already exists', async () => { + fs.__setMockFiles({ [configPath]: 'anything' }) + + const existsSpy = jest.spyOn(fs, 'existsSync') + const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync') + + // https://jestjs.io/docs/en/tutorial-async#error-handling + expect.assertions(3) + + try { + await task() + } catch (e) { + expect(e.message).toEqual( + 'TailwindCSS config already exists.\nUse --force to override existing config.' + ) + } + + expect(existsSpy).toHaveBeenCalledWith(configPath) + expect(writeFileSyncSpy).not.toHaveBeenCalled() + }) + + describe.each([ + /// ui, configLabel, configPresent + [true], + [false], + /// + ])('when --ui is set to %p and config is not present', (ui) => { + testTailwindInit(false, ui, false) + }) + }) + + describe('with the --force option', () => { + describe.each([ + /// ui, configLabel, configPresent + [false, 'not present', false], + [false, 'present', true], + [true, 'present', true], + [true, 'not present', false], + /// + ])('when --ui is set to %p and config is %s', (ui, _, configPresent) => { + testTailwindInit(true, ui, configPresent) + }) + }) + + function testTailwindInit(force, ui, configPresent) { + const task = initTailwind({ force, ui }) + + if (configPresent) { + fs.__setMockFiles({ [configPath]: tailwindConfig }) + } + + it('calls `yarn tailwindcss init`', async () => { + await task() + expect(execa).toHaveBeenCalledWith('yarn', ['tailwindcss', 'init']) + }) + + it('moves the config file to web/', async () => { + await task() + expect(fs.existsSync(configPath)).toBeTruthy() + }) + + it('un-comments future flags', async () => { + const extractRegex = /future: {[^}]*},/g + const futuresStr = tailwindConfig.match(extractRegex)[0] + expect(futuresStr.length > 0).toBeTruthy() + + const futures = [] + for (let match of futuresStr.matchAll(/\/\/\s*([\w_-]+)\s*:\s*true/g)) { + futures.push(match[1]) + } + + await task() + + const finalConfig = fs.readFileSync(configPath) + const finalFutures = eval( + `global.__futures = { ${finalConfig.match(extractRegex)[0]} }` + ).future + + expect(Object.keys(finalFutures).length).toEqual(futures.length) + expect(Object.keys(finalFutures).sort()).toEqual(futures.sort()) + }) + + it('does not uncomment other flag-resembling lines', async () => { + await task() + + const finalConfig = fs.readFileSync(configPath) + expect(finalConfig).toContain('// thisMustNotBeUncommented: true') + expect(finalConfig).toContain('// thisMustNotBeUncommented2: true') + }) + + if (ui) { + it('adds the TailwindUI plugin to the config', async () => { + await task() + + const finalConfig = fs.readFileSync(configPath) + expect(finalConfig).toMatch( + /plugins\s*:\s*\[[^\]]*require\('@tailwindcss\/ui'\)[^\]]*]/ + ) + }) + } + } +}) diff --git a/packages/cli/src/commands/setup/tailwind/tasks/initTailwind/index.js b/packages/cli/src/commands/setup/tailwind/tasks/initTailwind/index.js new file mode 100644 index 000000000000..238a6127d955 --- /dev/null +++ b/packages/cli/src/commands/setup/tailwind/tasks/initTailwind/index.js @@ -0,0 +1,37 @@ +import execa from 'execa' +import fs from 'fs' +import path from 'path' +import { getPaths } from 'src/lib' + +export default ({ force, ui }) => async () => { + /** + * If it doesn't already exist, + * initialize tailwind and move tailwind.config.js to web/ + */ + const basePath = getPaths().web.base + const tailwindConfigPath = path.join(basePath, 'tailwind.config.js') + const configExists = fs.existsSync(tailwindConfigPath) + + if (configExists) { + if (force) { + // yarn tailwindcss init will fail if the file already exists + fs.unlinkSync(tailwindConfigPath) + } else { + throw new Error( + 'Tailwindcss config already exists.\nUse --force to override existing config.' + ) + } + } + + await execa('yarn', ['tailwindcss', 'init'], { cwd: basePath }) + + // opt-in to upcoming changes + const config = fs.readFileSync(tailwindConfigPath, 'utf-8') + + const uncommentFlags = (str) => + str.replace(/\/{2} ([\w-]+: true)/g, '$1') + + const newConfig = config.replace(/future.*purge/s, uncommentFlags) + + fs.writeFileSync(tailwindConfigPath, newConfig) +} diff --git a/packages/cli/src/commands/setup/tailwind/tasks/installPackages/__tests__/installPackages.test.js b/packages/cli/src/commands/setup/tailwind/tasks/installPackages/__tests__/installPackages.test.js new file mode 100644 index 000000000000..663377ebfacf --- /dev/null +++ b/packages/cli/src/commands/setup/tailwind/tasks/installPackages/__tests__/installPackages.test.js @@ -0,0 +1,37 @@ +import execa from 'execa' + +import 'src/lib/test' +import installPackages from '..' + +jest.mock('execa', () => jest.fn()) + +describe('rw setup tailwind - installPackages task', () => { + const yarnArgs = ['workspace', 'web', 'add', '-D'] + const basePackages = [ + 'postcss-loader@4.0.2', + 'tailwindcss', + 'autoprefixer@9.8.6', + ] + + describe('without the --ui flag', () => { + test('it installs postcss, tailwindcss & autoprefixer', async () => { + const task = installPackages({ ui: false }) + await task() + + expect(execa).toHaveBeenCalledWith('yarn', [...yarnArgs, ...basePackages]) + }) + }) + + describe('with the --ui flag', () => { + test('it installs @tailwindcss/ui along with base packages', async () => { + const task = installPackages({ ui: true }) + await task() + + expect(execa).toHaveBeenCalledWith('yarn', [ + ...yarnArgs, + ...basePackages, + '@tailwindcss/ui', + ]) + }) + }) +}) diff --git a/packages/cli/src/commands/setup/tailwind/tasks/installPackages/index.js b/packages/cli/src/commands/setup/tailwind/tasks/installPackages/index.js new file mode 100644 index 000000000000..219054c539fc --- /dev/null +++ b/packages/cli/src/commands/setup/tailwind/tasks/installPackages/index.js @@ -0,0 +1,15 @@ +import execa from 'execa' + +export default ({ ui }) => async () => { + /** + * Install postcss-loader, tailwindcss, and autoprefixer. Add TailwindUI if requested. + * RedwoodJS currently uses PostCSS v7; postcss-loader and autoprefixers pinned for compatibility + */ + let packages = ['postcss-loader@4.0.2', 'tailwindcss@npm:@tailwindcss/postcss7-compat', 'autoprefixer@9.8.6'] + + if (ui) { + packages.push('@tailwindcss/ui') + } + + await execa('yarn', ['workspace', 'web', 'add', '-D', ...packages]) +} diff --git a/packages/cli/src/commands/setup/tailwind/tasks/yarnCheckFiles/__tests__/yarnCheckFiles.test.js b/packages/cli/src/commands/setup/tailwind/tasks/yarnCheckFiles/__tests__/yarnCheckFiles.test.js new file mode 100644 index 000000000000..c1396674fd2a --- /dev/null +++ b/packages/cli/src/commands/setup/tailwind/tasks/yarnCheckFiles/__tests__/yarnCheckFiles.test.js @@ -0,0 +1,15 @@ +import execa from 'execa' + +import 'src/lib/test' +import yarnCheckFiles from '..' + +jest.mock('execa', () => jest.fn()) + +describe('rw setup tailwind - yarnCheckFiles task', () => { + test('it calls yarn install --check-files', async () => { + const task = yarnCheckFiles() + await task() + + expect(execa).toHaveBeenCalledWith('yarn', ['install', '--check-files']) + }) +}) diff --git a/packages/cli/src/commands/setup/tailwind/tasks/yarnCheckFiles/index.js b/packages/cli/src/commands/setup/tailwind/tasks/yarnCheckFiles/index.js new file mode 100644 index 000000000000..570f8d1c1689 --- /dev/null +++ b/packages/cli/src/commands/setup/tailwind/tasks/yarnCheckFiles/index.js @@ -0,0 +1,9 @@ +import execa from 'execa' + +export default () => async () => { + /** + * Sync yarn.lock file and node_modules folder. + * Refer https://github.com/redwoodjs/redwood/issues/1301 for more details. + */ + await execa('yarn', ['install', '--check-files']) +}