diff --git a/integrations/execute.js b/integrations/execute.js index 09ff45792..fccabcdda 100644 --- a/integrations/execute.js +++ b/integrations/execute.js @@ -58,21 +58,29 @@ module.exports = function $(command, options = {}) { let stdout = '' let stderr = '' + let combined = '' child.stdout.on('data', (data) => { stdoutMessages.push(data.toString()) notifyNextStdoutActor() stdout += data + combined += data }) child.stderr.on('data', (data) => { stderrMessages.push(data.toString()) notifyNextStderrActor() stderr += data + combined += data }) child.on('close', (code, signal) => { - ;(signal === 'SIGTERM' ? resolve : code === 0 ? resolve : reject)({ code, stdout, stderr }) + ;(signal === 'SIGTERM' ? resolve : code === 0 ? resolve : reject)({ + code, + stdout, + stderr, + combined, + }) }) }) diff --git a/integrations/html.js b/integrations/html.js deleted file mode 100644 index be2f6f9da..000000000 --- a/integrations/html.js +++ /dev/null @@ -1,4 +0,0 @@ -// Small helper to allow for html highlighting / formatting in most editors. -module.exports = function html(templates) { - return templates.join('') -} diff --git a/integrations/io.js b/integrations/io.js index 1740715bb..f1a406ec7 100644 --- a/integrations/io.js +++ b/integrations/io.js @@ -30,7 +30,15 @@ module.exports = function ({ // Restore all written files afterEach(async () => { await Promise.all( - Object.entries(fileCache).map(([file, content]) => fs.writeFile(file, content, 'utf8')) + Object.entries(fileCache).map(async ([file, content]) => { + try { + if (content === null) { + return await fs.unlink(file) + } else { + return await fs.writeFile(file, content, 'utf8') + } + } catch {} + }) ) }) @@ -68,6 +76,21 @@ module.exports = function ({ } return { + cleanupFile(file) { + let filePath = path.resolve(toolRoot, file) + fileCache[filePath] = null + }, + async fileExists(file) { + let filePath = path.resolve(toolRoot, file) + return existsSync(filePath) + }, + async removeFile(file) { + let filePath = path.resolve(toolRoot, file) + if (!fileCache[filePath]) { + fileCache[filePath] = await fs.readFile(filePath, 'utf8') + } + await fs.unlink(filePath) + }, async readOutputFile(file) { file = await resolveFile(file, absoluteOutputFolder) return fs.readFile(path.resolve(absoluteOutputFolder, file), 'utf8') @@ -83,7 +106,15 @@ module.exports = function ({ async writeInputFile(file, contents) { let filePath = path.resolve(absoluteInputFolder, file) if (!fileCache[filePath]) { - fileCache[filePath] = await fs.readFile(filePath, 'utf8') + try { + fileCache[filePath] = await fs.readFile(filePath, 'utf8') + } catch (err) { + if (err.code === 'ENOENT') { + fileCache[filePath] = null // Sentinel value to `delete` the file afterwards. This also means that we are writing to a `new` file inside the test. + } else { + throw err + } + } } return fs.writeFile(path.resolve(absoluteInputFolder, file), contents, 'utf8') diff --git a/integrations/tailwindcss-cli/package-lock.json b/integrations/tailwindcss-cli/package-lock.json index 6e05f67f1..7b7a3814b 100644 --- a/integrations/tailwindcss-cli/package-lock.json +++ b/integrations/tailwindcss-cli/package-lock.json @@ -1,11 +1,10 @@ { - "name": "postcss-cli", + "name": "tailwindcss-cli", "version": "0.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "postcss-cli", "version": "0.0.0" } } diff --git a/integrations/tailwindcss-cli/package.json b/integrations/tailwindcss-cli/package.json index da323c33e..882e30b46 100644 --- a/integrations/tailwindcss-cli/package.json +++ b/integrations/tailwindcss-cli/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "scripts": { "build": "NODE_ENV=production node ../../lib/cli.js -i ./src/index.css -o ./dist/main.css", - "test": "jest" + "test": "jest --runInBand" }, "jest": { "displayName": "Tailwind CSS CLI", diff --git a/integrations/tailwindcss-cli/tests/cli.test.js b/integrations/tailwindcss-cli/tests/cli.test.js new file mode 100644 index 000000000..fc065a57d --- /dev/null +++ b/integrations/tailwindcss-cli/tests/cli.test.js @@ -0,0 +1,269 @@ +let path = require('path') +let $ = require('../../execute') +let { css, html } = require('../../syntax') +let resolveToolRoot = require('../../resolve-tool-root') + +let { readOutputFile, writeInputFile, cleanupFile, fileExists, removeFile } = require('../../io')({ + output: 'dist', + input: 'src', +}) + +let EXECUTABLE = 'node ../../lib/cli.js' + +describe('Build command', () => { + test('--output', async () => { + await writeInputFile('index.html', html`
`) + + await $(`${EXECUTABLE} --output ./dist/main.css`) + + let contents = await readOutputFile('main.css') + + // `-i` is omitted, therefore the default `@tailwind base; @tailwind + // components; @tailwind utilities` is used. However `preflight` is + // disabled. I still want to verify that the `base` got included. + expect(contents).toIncludeCss( + css` + *, + ::before, + ::after { + --tw-border-opacity: 1; + border-color: rgba(229, 231, 235, var(--tw-border-opacity)); + } + ` + ) + + // Verify `utilities` output is correct + expect(contents).toIncludeCss( + css` + .font-bold { + font-weight: 700; + } + ` + ) + }) + + test('--input, --output', async () => { + await writeInputFile('index.html', html`
`) + + await $(`${EXECUTABLE} --input ./src/index.css --output ./dist/main.css`) + + expect(await readOutputFile('main.css')).toIncludeCss( + css` + .font-bold { + font-weight: 700; + } + ` + ) + }) + + test('--minify', async () => { + await writeInputFile('index.html', html`
`) + + await $(`${EXECUTABLE} --output ./dist/main.css --minify`) + let withMinify = await readOutputFile('main.css') + + // Verify that we got the expected output. Note: `.toIncludeCss` formats + // `actual` & `expected` + expect(withMinify).toIncludeCss( + css` + .font-bold { + font-weight: 700; + } + ` + ) + + await $(`${EXECUTABLE} --output ./dist/main.css`) + let withoutMinify = await readOutputFile('main.css') + + // Let's verify that the actual minified output is smaller than the not + // minified version. + expect(withoutMinify.length).toBeGreaterThan(withMinify.length) + }) + + test('--no-autoprefixer', async () => { + await writeInputFile('index.html', html`
`) + + await $(`${EXECUTABLE} --output ./dist/main.css`) + let withAutoprefixer = await readOutputFile('main.css') + + expect(withAutoprefixer).toIncludeCss(css` + .select-none { + -webkit-user-select: none; + user-select: none; + } + `) + + await $(`${EXECUTABLE} --output ./dist/main.css --no-autoprefixer`) + let withoutAutoprefixer = await readOutputFile('main.css') + + expect(withoutAutoprefixer).toIncludeCss(css` + .select-none { + user-select: none; + } + `) + }) + + test('--config (non-existing config file)', async () => { + await writeInputFile('index.html', html`
`) + + let { stderr } = await $( + `${EXECUTABLE} --output ./dist/main.css --config ./non-existing.config.js` + ).catch((err) => err) + + let toolRoot = resolveToolRoot() + expect(stderr).toEqual( + `Specified config file ${path.resolve(toolRoot, 'non-existing.config.js')} does not exist.\n` + ) + }) + + test('--config (existing config file)', async () => { + await writeInputFile('index.html', html`
`) + + let customConfig = `module.exports = ${JSON.stringify( + { + purge: ['./src/index.html'], + mode: 'jit', + darkMode: false, // or 'media' or 'class' + theme: { + extend: { + fontWeight: { + bold: 'BOLD', + }, + }, + }, + variants: { + extend: {}, + }, + corePlugins: { + preflight: false, + }, + plugins: [], + }, + + null, + 2 + )}` + + await writeInputFile('../custom.config.js', customConfig) + + await $(`${EXECUTABLE} --output ./dist/main.css --config ./custom.config.js`) + + expect(await readOutputFile('main.css')).toIncludeCss( + css` + .font-bold { + font-weight: BOLD; + } + ` + ) + }) + + test('--help', async () => { + let { combined } = await $(`${EXECUTABLE} --help`) + + expect(combined).toMatchInlineSnapshot(` + " + tailwindcss v2.1.2 + + Usage: + tailwindcss build [options] + + Options: + -i, --input Input file + -o, --output Output file + -w, --watch Watch for changes and rebuild as needed + --jit Build using JIT mode + --files Template files to scan for class names + --postcss Load custom PostCSS configuration + -m, --minify Minify the output + -c, --config Path to a custom config file + --no-autoprefixer Disable autoprefixer + -h, --help Display usage information + + " + `) + }) +}) + +describe('Init command', () => { + test('--full', async () => { + cleanupFile('full.config.js') + + let { combined } = await $(`${EXECUTABLE} init full.config.js --full`) + + expect(combined).toMatchInlineSnapshot(` + " + Created Tailwind CSS config file: full.config.js + " + `) + + // Not a clean way to test this. We could require the file and verify that + // multiple keys in `theme` exists. However it loads `tailwindcss/colors` + // which doesn't exists in this context. + expect((await readOutputFile('../full.config.js')).split('\n').length).toBeGreaterThan(50) + }) + + test('--jit', async () => { + cleanupFile('with-jit.config.js') + + let { combined } = await $(`${EXECUTABLE} init with-jit.config.js --jit`) + + expect(combined).toMatchInlineSnapshot(` + " + Created Tailwind CSS config file: with-jit.config.js + " + `) + + expect(await readOutputFile('../with-jit.config.js')).toContain("mode: 'jit'") + }) + + test('--full, --jit', async () => { + cleanupFile('full-with-jit.config.js') + + let { combined } = await $(`${EXECUTABLE} init full-with-jit.config.js --jit --full`) + + expect(combined).toMatchInlineSnapshot(` + " + Created Tailwind CSS config file: full-with-jit.config.js + " + `) + + expect(await readOutputFile('../full-with-jit.config.js')).toContain("mode: 'jit'") + }) + + test('--postcss', async () => { + expect(await fileExists('postcss.config.js')).toBe(true) + await removeFile('postcss.config.js') + expect(await fileExists('postcss.config.js')).toBe(false) + + let { combined } = await $(`${EXECUTABLE} init --postcss`) + + expect(await fileExists('postcss.config.js')).toBe(true) + + expect(combined).toMatchInlineSnapshot(` + " + tailwind.config.js already exists. + Created PostCSS config file: postcss.config.js + " + `) + }) + + test('--help', async () => { + let { combined } = await $(`${EXECUTABLE} init --help`) + + expect(combined).toMatchInlineSnapshot(` + " + tailwindcss v2.1.2 + + Usage: + tailwindcss init [options] + + Options: + --jit Initialize for JIT mode + -f, --full Initialize a full \`tailwind.config.js\` file + -p, --postcss Initialize a \`postcss.config.js\` file + -h, --help Display usage information + + " + `) + }) +}) diff --git a/jest/customMatchers.js b/jest/customMatchers.js index de6e6a23c..6368ef5f4 100644 --- a/jest/customMatchers.js +++ b/jest/customMatchers.js @@ -1,5 +1,5 @@ const prettier = require('prettier') -const diff = require('jest-diff').default +const { diff } = require('jest-diff') function format(input) { return prettier.format(input, { diff --git a/package-lock.json b/package-lock.json index 38f9ef991..d82020888 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,6 @@ "requires": true, "packages": { "": { - "name": "tailwindcss", "version": "2.1.2", "license": "MIT", "dependencies": { @@ -59,6 +58,7 @@ "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^3.4.0", "jest": "^27.0.2", + "jest-diff": "^27.0.2", "postcss": "^8.3.0", "postcss-cli": "^8.3.1", "prettier": "^2.3.0", @@ -11761,7 +11761,8 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.2.1.tgz", "integrity": "sha512-aDFi80aHQ3JM3symJ5iKU70lm151ugIGFCI0yRZGpyjgQSDS+Fbe93QwypC1tCEllQE8p0S7TUu20ih1b9IKLA==", - "dev": true + "dev": true, + "requires": {} }, "@tootallnate/once": { "version": "1.1.2", @@ -11917,7 +11918,8 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", - "dev": true + "dev": true, + "requires": {} }, "acorn-node": { "version": "1.8.2", @@ -12903,7 +12905,8 @@ "cssnano-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz", - "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==" + "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==", + "requires": {} }, "csso": { "version": "4.2.0", @@ -13369,7 +13372,8 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true + "dev": true, + "requires": {} }, "eslint-plugin-prettier": { "version": "3.4.0", @@ -14998,7 +15002,8 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true + "dev": true, + "requires": {} }, "jest-regex-util": { "version": "27.0.1", @@ -16201,22 +16206,26 @@ "postcss-discard-comments": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz", - "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==" + "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==", + "requires": {} }, "postcss-discard-duplicates": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz", - "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==" + "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==", + "requires": {} }, "postcss-discard-empty": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz", - "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==" + "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==", + "requires": {} }, "postcss-discard-overridden": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz", - "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==" + "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==", + "requires": {} }, "postcss-js": { "version": "3.0.3", @@ -16308,7 +16317,8 @@ "postcss-normalize-charset": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz", - "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==" + "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==", + "requires": {} }, "postcss-normalize-display-values": { "version": "5.0.1", @@ -17867,7 +17877,8 @@ "version": "7.4.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", - "dev": true + "dev": true, + "requires": {} }, "xml-name-validator": { "version": "3.0.0", diff --git a/package.json b/package.json index 0dfe2739e..437337ccd 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^3.4.0", "jest": "^27.0.2", + "jest-diff": "^27.0.2", "postcss": "^8.3.0", "postcss-cli": "^8.3.1", "prettier": "^2.3.0",