diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e3ff791..0b21e74 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,26 +4,30 @@ on: release: types: [created] +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 with: - node-version: '20.x' + node-version: '24.x' - run: npm install - - run: npm install --prefix jest/legacy + - run: npm run lint + - run: npm run typecheck - run: npm test publish: needs: test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 with: - node-version: '20.x' + node-version: '24.x' registry-url: 'https://registry.npmjs.org' - run: npm version --no-git-tag-version ${{ github.event.release.tag_name }} - run: npm publish diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index da3f2be..8d3bf3a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,19 +2,61 @@ name: Tests on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + jobs: + lint: + name: Linting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: 24.x + - name: Get npm cache directory + id: npm-cache + run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT + - name: Cache npm dependencies + uses: actions/cache@v5 + with: + path: ${{ steps.npm-cache.outputs.dir }} + key: ${{ runner.os }}-node-${{ hashFiles('package.json') }} + restore-keys: ${{ runner.os }}-node- + - run: npm install + - run: npm run lint + - run: npm run typecheck + test: + name: Node ${{ matrix.node-version }} / Tailwind ${{ matrix.tailwind.v3 }} + ${{ matrix.tailwind.v4 }} runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - node-version: [20.x, 22.x] + node-version: [20.x, 22.x, 24.x] + tailwind: + - { v3: '3.1.0', v4: '4.0.7' } # @tailwindcss/postcss < 4.0.7 has a test runner bug + - { v3: '^3', v4: '^4' } steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v5 with: node-version: ${{ matrix.node-version }} + - name: Get npm cache directory + id: npm-cache + run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT + - name: Cache npm dependencies + uses: actions/cache@v5 + with: + path: ${{ steps.npm-cache.outputs.dir }} + key: ${{ runner.os }}-node-${{ hashFiles('package.json') }} + restore-keys: ${{ runner.os }}-node- - run: npm install - - run: npm install --prefix jest/legacy - - run: npm run lint - - run: npm run test + - name: Install Tailwind ${{ matrix.tailwind.v3 }} + ${{ matrix.tailwind.v4 }} + run: npm install --no-save tailwindcss@${{ matrix.tailwind.v4 }} @tailwindcss/postcss@${{ matrix.tailwind.v4 }} @tailwindcss/cli@${{ matrix.tailwind.v4 }} tailwindcss-legacy@npm:tailwindcss@${{ matrix.tailwind.v3 }} + - run: npm test diff --git a/eslint.config.mjs b/eslint.config.mjs index 23767ac..0dc8fd9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,7 +4,10 @@ import eslintPluginJest from 'eslint-plugin-jest' export default [ js.configs.recommended, - eslintPluginJest.configs['flat/recommended'], + { + ...eslintPluginJest.configs['flat/recommended'], + files: ['jest/**/*.js'], + }, { languageOptions: { ecmaVersion: 2022, diff --git a/jest/content/direction.html b/jest/content/direction.html index d026dcf..9636975 100644 --- a/jest/content/direction.html +++ b/jest/content/direction.html @@ -1,2 +1,4 @@
+
+
diff --git a/jest/content/fill-mode.html b/jest/content/fill-mode.html index 5a35293..70b1812 100644 --- a/jest/content/fill-mode.html +++ b/jest/content/fill-mode.html @@ -1,2 +1,4 @@
+
+
diff --git a/jest/content/predefined-animations.html b/jest/content/predefined-animations.html index 3adf0c7..e0098e4 100644 --- a/jest/content/predefined-animations.html +++ b/jest/content/predefined-animations.html @@ -1,2 +1,19 @@ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jest/content/timing-function.html b/jest/content/timing-function.html index 7526fb7..c01e3dd 100644 --- a/jest/content/timing-function.html +++ b/jest/content/timing-function.html @@ -1,3 +1,6 @@
+
+
+
diff --git a/jest/customMatchers.js b/jest/customMatchers.js index ef91523..f66086b 100644 --- a/jest/customMatchers.js +++ b/jest/customMatchers.js @@ -1,24 +1,27 @@ expect.extend({ - toIncludeAll(received, expected) { - function stripped(str) { - return str.replace(/\s/g, '').replace(/;/g, '') + toContainAll(received, expected) { + if (typeof received !== 'string') { + throw new TypeError(`toContainAll: received value must be a string, got ${typeof received}`) } - const receivedStripped = stripped(received) + if (! Array.isArray(expected)) { + throw new TypeError('toContainAll: expected value must be an array of strings') + } - const pass = Array.isArray(expected) && expected.every(value => receivedStripped.includes(stripped(value))) + const normalize = str => str.replace(/[\s;]/g, '') + const pass = expected.every(value => normalize(received).includes(normalize(value))) return { pass, message: () => pass - ? this.utils.matcherHint('.not.toIncludeAll') + - '\n\n' + - `Expected not to have all of: ${this.utils.printExpected(received)}\n` + - `Received: ${this.utils.printReceived(expected)}` - : this.utils.matcherHint('.toIncludeAll') + - '\n\n' + - `Expected to have all of: ${this.utils.printExpected(expected)}\n` + - `Received: ${this.utils.printReceived(received)}`, + ? this.utils.matcherHint('.not.toContainAll') + + '\n\n' + + `Expected not to have all of: ${this.utils.printExpected(expected)}\n` + + `Received: ${this.utils.printReceived(received)}` + : this.utils.matcherHint('.toContainAll') + + '\n\n' + + `Expected to have all of: ${this.utils.printExpected(expected)}\n` + + `Received: ${this.utils.printReceived(received)}`, } }, }) diff --git a/jest/index.test.js b/jest/index.test.js index cc443f2..78e014c 100644 --- a/jest/index.test.js +++ b/jest/index.test.js @@ -24,7 +24,7 @@ async function run(file, options = {}) { describe('modern tailwind', () => { it('generates `composition` utilities', async () => { - expect(await run('content/composition.html', { scope: 'utilities' })).toIncludeAll([ + expect(await run('content/composition.html', { scope: 'utilities' })).toContainAll([ '.animate-add { animation-composition: add; }', '.animate-replace { animation-composition: replace; }', '.animate-accumulate { animation-composition: accumulate; }', @@ -32,7 +32,7 @@ describe('modern tailwind', () => { }) it('generates `delay` utilities', async () => { - expect(await run('content/delay.html', { scope: 'utilities' })).toIncludeAll([ + expect(await run('content/delay.html', { scope: 'utilities' })).toContainAll([ '.animate-delay-75 { animation-delay: 75ms; }', '.animate-delay-333 { animation-delay: 333ms; }', '.animate-delay-\\[666ms\\] { animation-delay: 666ms; }', @@ -40,14 +40,16 @@ describe('modern tailwind', () => { }) it('generates `direction` utilities', async () => { - expect(await run('content/direction.html', { scope: 'utilities' })).toIncludeAll([ + expect(await run('content/direction.html', { scope: 'utilities' })).toContainAll([ '.animate-normal { animation-direction: normal; }', '.animate-reverse { animation-direction: reverse; }', + '.animate-alternate { animation-direction: alternate; }', + '.animate-alternate-reverse { animation-direction: alternate-reverse; }', ]) }) it('generates `duration` utilities', async () => { - expect(await run('content/duration.html', { scope: 'utilities' })).toIncludeAll([ + expect(await run('content/duration.html', { scope: 'utilities' })).toContainAll([ '.animate-duration-75 { animation-duration: 75ms; }', '.animate-duration-333 { animation-duration: 333ms; }', '.animate-duration-\\[666ms\\] { animation-duration: 666ms; }', @@ -55,14 +57,16 @@ describe('modern tailwind', () => { }) it('generates `fill-mode` utilities', async () => { - expect(await run('content/fill-mode.html', { scope: 'utilities' })).toIncludeAll([ - '.animate-fill-both { animation-fill-mode: both; }', + expect(await run('content/fill-mode.html', { scope: 'utilities' })).toContainAll([ '.animate-fill-none { animation-fill-mode: normal; }', + '.animate-fill-forwards { animation-fill-mode: forwards; }', + '.animate-fill-backwards { animation-fill-mode: backwards; }', + '.animate-fill-both { animation-fill-mode: both; }', ]) }) it('generates `iteration-count` utilities', async () => { - expect(await run('content/iteration-count.html', { scope: 'utilities' })).toIncludeAll([ + expect(await run('content/iteration-count.html', { scope: 'utilities' })).toContainAll([ '.animate-infinite { animation-iteration-count: infinite; }', '.animate-once { animation-iteration-count: 1; }', '.animate-iteration-7 { animation-iteration-count: 7; }', @@ -71,37 +75,138 @@ describe('modern tailwind', () => { }) it('generates `play-state` utilities', async () => { - expect(await run('content/play-state.html', { scope: 'utilities' })).toIncludeAll([ + expect(await run('content/play-state.html', { scope: 'utilities' })).toContainAll([ '.animate-play { animation-play-state: running; }', '.animate-stop { animation-play-state: paused; }', ]) }) + it('generates `timing-function` utilities', async () => { + expect(await run('content/timing-function.html', { scope: 'utilities' })).toContainAll([ + '.animate-ease { animation-timing-function: ease; }', + '.animate-ease-linear { animation-timing-function: linear; }', + '.animate-ease-in { animation-timing-function: cubic-bezier(0.4, 0, 1, 1); }', + '.animate-ease-out { animation-timing-function: cubic-bezier(0, 0, 0.2, 1); }', + '.animate-ease-in-out { animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }', + '.animate-ease-\\[cubic-bezier\\(1\\,0\\.66\\,0\\.33\\,0\\)\\] { animation-timing-function: cubic-bezier(1,0.66,0.33,0); }', + ]) + }) + it('generates predefined animations for modern import', async () => { - expect(await run('content/predefined-animations.html', { importType: 'modern' })).toIncludeAll([ - '.animate-fade { animation: var(--animate-fade); }', - '--animate-fade: fade var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both;', - '@keyframes fade { 0% { opacity: 0; } 100% { opacity: 1; }}', + const css = await run('content/predefined-animations.html', { importType: 'modern' }) + + expect(css).toContainAll([ '.animate-spin { animation: var(--animate-spin); }', + '.animate-ping { animation: var(--animate-ping); }', + '.animate-pulse { animation: var(--animate-pulse); }', + '.animate-bounce { animation: var(--animate-bounce); }', + '.animate-wiggle { animation: var(--animate-wiggle); }', + '.animate-wiggle-more { animation: var(--animate-wiggle-more); }', + '.animate-rotate-y { animation: var(--animate-rotate-y); }', + '.animate-rotate-x { animation: var(--animate-rotate-x); }', + '.animate-jump { animation: var(--animate-jump); }', + '.animate-jump-in { animation: var(--animate-jump-in); }', + '.animate-jump-out { animation: var(--animate-jump-out); }', + '.animate-shake { animation: var(--animate-shake); }', + '.animate-fade { animation: var(--animate-fade); }', + '.animate-fade-down { animation: var(--animate-fade-down); }', + '.animate-fade-up { animation: var(--animate-fade-up); }', + '.animate-fade-left { animation: var(--animate-fade-left); }', + '.animate-fade-right { animation: var(--animate-fade-right); }', + '.animate-flip-up { animation: var(--animate-flip-up); }', + '.animate-flip-down { animation: var(--animate-flip-down); }', + ]) + + expect(css).toContainAll([ '--animate-spin: spin var(--default-animation-duration, 1s) var(--default-animation-timing-function, linear) var(--default-animation-delay, 0s) infinite;', + '--animate-ping: ping var(--default-animation-duration, 1s) var(--default-animation-timing-function, cubic-bezier(0, 0, 0.2, 1)) var(--default-animation-delay, 0s) infinite;', + '--animate-pulse: pulse var(--default-animation-duration, 2s) var(--default-animation-timing-function, cubic-bezier(0.4, 0, 0.6, 1)) var(--default-animation-delay, 0s) infinite;', + '--animate-bounce: bounce var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) infinite;', + '--animate-wiggle: wiggle var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both;', + '--animate-wiggle-more: wiggle-more var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both;', + '--animate-rotate-y: rotate-y var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both;', + '--animate-rotate-x: rotate-x var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both;', + '--animate-jump: jump var(--default-animation-duration, 0.5s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both;', + '--animate-jump-in: jump-in var(--default-animation-duration, 0.5s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both;', + '--animate-jump-out: jump-out var(--default-animation-duration, 0.5s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both;', + '--animate-shake: shake var(--default-animation-duration, 0.5s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both;', + '--animate-fade: fade var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both;', + '--animate-fade-down: fade-down var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both;', + '--animate-fade-up: fade-up var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both;', + '--animate-fade-left: fade-left var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both;', + '--animate-fade-right: fade-right var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both;', + '--animate-flip-up: flip-up var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both;', + '--animate-flip-down: flip-down var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both;', + ]) + + expect(css).toContainAll([ '@keyframes spin { to { transform: rotate(360deg); }}', + '@keyframes ping { 75%, 100% { transform: scale(2); opacity: 0; }}', + '@keyframes pulse { 50% { opacity: 0.5; }}', + '@keyframes bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8, 0, 1, 1); } 50% { transform: none; animation-timing-function: cubic-bezier(0, 0, 0.2, 1); }}', + '@keyframes wiggle { 0%, 100% { transform: rotate(-3deg); } 50% { transform: rotate(3deg); }}', + '@keyframes wiggle-more { 0%, 100% { transform: rotate(-12deg); } 50% { transform: rotate(12deg); }}', + '@keyframes rotate-y { 0% { transform: rotateY(360deg); } 100% { transform: rotateY(0); }}', + '@keyframes rotate-x { 0% { transform: rotateX(360deg); } 100% { transform: rotateX(0); }}', + '@keyframes jump { 0%, 100% { transform: scale(1); } 10% { transform: scale(0.8); } 50% { transform: scale(1.2); }}', + '@keyframes jump-in { 0% { transform: scale(0); } 80% { transform: scale(1.2); } 100% { transform: scale(1); }}', + '@keyframes jump-out { 0% { transform: scale(1); } 20% { transform: scale(1.2); } 100% { transform: scale(0); }}', + '@keyframes shake { 0% { transform: translateX(0); } 25% { transform: translateX(-1rem); } 75% { transform: translateX(1rem); } 100% { transform: translateX(0); }}', + '@keyframes fade { 0% { opacity: 0; } 100% { opacity: 1; }}', + '@keyframes fade-down { 0% { opacity: 0; transform: translateY(-2rem); } 100% { opacity: 1; transform: translateY(0); }}', + '@keyframes fade-up { 0% { opacity: 0; transform: translateY(2rem); } 100% { opacity: 1; transform: translateY(0); }}', + '@keyframes fade-left { 0% { opacity: 0; transform: translateX(2rem); } 100% { opacity: 1; transform: translateX(0); }}', + '@keyframes fade-right { 0% { opacity: 0; transform: translateX(-2rem); } 100% { opacity: 1; transform: translateX(0); }}', + '@keyframes flip-up { 0% { transform: rotateX(90deg); transform-origin: bottom; } 100% { transform: rotateX(0); transform-origin: bottom; }}', + '@keyframes flip-down { 0% { transform: rotateX(-90deg); transform-origin: top; } 100% { transform: rotateX(0); transform-origin: top; }}', ]) }) it('generates predefined animations for legacy import', async () => { - expect(await run('content/predefined-animations.html', { importType: 'legacy' })).toIncludeAll([ - '.animate-fade { animation: fade var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', - '@keyframes fade { 0% { opacity: 0; } 100% { opacity: 1; }}', + const css = await run('content/predefined-animations.html', { importType: 'legacy' }) + + expect(css).toContainAll([ '.animate-spin { animation: spin var(--default-animation-duration, 1s) var(--default-animation-timing-function, linear) var(--default-animation-delay, 0s) infinite; }', - '@keyframes spin { to { transform: rotate(360deg); }}', + '.animate-ping { animation: ping var(--default-animation-duration, 1s) var(--default-animation-timing-function, cubic-bezier(0, 0, 0.2, 1)) var(--default-animation-delay, 0s) infinite; }', + '.animate-pulse { animation: pulse var(--default-animation-duration, 2s) var(--default-animation-timing-function, cubic-bezier(0.4, 0, 0.6, 1)) var(--default-animation-delay, 0s) infinite; }', + '.animate-bounce { animation: bounce var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) infinite; }', + '.animate-wiggle { animation: wiggle var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-wiggle-more { animation: wiggle-more var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-rotate-y { animation: rotate-y var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-rotate-x { animation: rotate-x var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-jump { animation: jump var(--default-animation-duration, 0.5s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-jump-in { animation: jump-in var(--default-animation-duration, 0.5s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-jump-out { animation: jump-out var(--default-animation-duration, 0.5s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-shake { animation: shake var(--default-animation-duration, 0.5s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-fade { animation: fade var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-fade-down { animation: fade-down var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-fade-up { animation: fade-up var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-fade-left { animation: fade-left var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-fade-right { animation: fade-right var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-flip-up { animation: flip-up var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-flip-down { animation: flip-down var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', ]) - }) - it('generates `timing-function` utilities', async () => { - expect(await run('content/timing-function.html', { scope: 'utilities' })).toIncludeAll([ - '.animate-ease { animation-timing-function: ease; }', - '.animate-ease-linear { animation-timing-function: linear; }', - '.animate-ease-\\[cubic-bezier\\(1\\,0\\.66\\,0\\.33\\,0\\)\\] { animation-timing-function: cubic-bezier(1,0.66,0.33,0); }', + expect(css).toContainAll([ + '@keyframes spin { to { transform: rotate(360deg); }}', + '@keyframes ping { 75%, 100% { transform: scale(2); opacity: 0; }}', + '@keyframes pulse { 50% { opacity: 0.5; }}', + '@keyframes bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8, 0, 1, 1); } 50% { transform: none; animation-timing-function: cubic-bezier(0, 0, 0.2, 1); }}', + '@keyframes wiggle { 0%, 100% { transform: rotate(-3deg); } 50% { transform: rotate(3deg); }}', + '@keyframes wiggle-more { 0%, 100% { transform: rotate(-12deg); } 50% { transform: rotate(12deg); }}', + '@keyframes rotate-y { 0% { transform: rotateY(360deg); } 100% { transform: rotateY(0); }}', + '@keyframes rotate-x { 0% { transform: rotateX(360deg); } 100% { transform: rotateX(0); }}', + '@keyframes jump { 0%, 100% { transform: scale(1); } 10% { transform: scale(0.8); } 50% { transform: scale(1.2); }}', + '@keyframes jump-in { 0% { transform: scale(0); } 80% { transform: scale(1.2); } 100% { transform: scale(1); }}', + '@keyframes jump-out { 0% { transform: scale(1); } 20% { transform: scale(1.2); } 100% { transform: scale(0); }}', + '@keyframes shake { 0% { transform: translateX(0); } 25% { transform: translateX(-1rem); } 75% { transform: translateX(1rem); } 100% { transform: translateX(0); }}', + '@keyframes fade { 0% { opacity: 0; } 100% { opacity: 1; }}', + '@keyframes fade-down { 0% { opacity: 0; transform: translateY(-2rem); } 100% { opacity: 1; transform: translateY(0); }}', + '@keyframes fade-up { 0% { opacity: 0; transform: translateY(2rem); } 100% { opacity: 1; transform: translateY(0); }}', + '@keyframes fade-left { 0% { opacity: 0; transform: translateX(2rem); } 100% { opacity: 1; transform: translateX(0); }}', + '@keyframes fade-right { 0% { opacity: 0; transform: translateX(-2rem); } 100% { opacity: 1; transform: translateX(0); }}', + '@keyframes flip-up { 0% { transform: rotateX(90deg); transform-origin: bottom; } 100% { transform: rotateX(0); transform-origin: bottom; }}', + '@keyframes flip-down { 0% { transform: rotateX(-90deg); transform-origin: top; } 100% { transform: rotateX(0); transform-origin: top; }}', ]) }) }) diff --git a/jest/legacy.test.js b/jest/legacy.test.js new file mode 100644 index 0000000..38b7a5e --- /dev/null +++ b/jest/legacy.test.js @@ -0,0 +1,135 @@ +const path = require('path') +const postcss = require('postcss') +const tailwindcss = require('tailwindcss-legacy') + +async function run(file) { + const { currentTestName } = expect.getState() + + const config = { + content: [`./jest/${file}`], + corePlugins: { preflight: false }, + plugins: [require('../src')], + } + + const result = await postcss(tailwindcss(config)).process('@tailwind utilities', { + from: `${path.resolve(__filename)}?test=${currentTestName}`, + }) + + return result.css +} + +describe('legacy tailwind', () => { + it('generates `composition` utilities', async () => { + expect(await run('content/composition.html')).toContainAll([ + '.animate-add { animation-composition: add; }', + '.animate-replace { animation-composition: replace; }', + '.animate-accumulate { animation-composition: accumulate; }', + ]) + }) + + it('should add `delay` utilities', async () => { + expect(await run('content/delay.html')).toContainAll([ + '.animate-delay-75 { animation-delay: 75ms; }', + '.animate-delay-\\[666ms\\] { animation-delay: 666ms; }', + ]) + }) + + it('generates `direction` utilities', async () => { + expect(await run('content/direction.html')).toContainAll([ + '.animate-normal { animation-direction: normal; }', + '.animate-reverse { animation-direction: reverse; }', + '.animate-alternate { animation-direction: alternate; }', + '.animate-alternate-reverse { animation-direction: alternate-reverse; }', + ]) + }) + + it('generates `duration` utilities', async () => { + expect(await run('content/duration.html')).toContainAll([ + '.animate-duration-75 { animation-duration: 75ms; }', + '.animate-duration-\\[666ms\\] { animation-duration: 666ms; }', + ]) + }) + + it('generates `fill-mode` utilities', async () => { + expect(await run('content/fill-mode.html')).toContainAll([ + '.animate-fill-none { animation-fill-mode: normal; }', + '.animate-fill-forwards { animation-fill-mode: forwards; }', + '.animate-fill-backwards { animation-fill-mode: backwards; }', + '.animate-fill-both { animation-fill-mode: both; }', + ]) + }) + + it('generates `iteration-count` utilities', async () => { + expect(await run('content/iteration-count.html')).toContainAll([ + '.animate-infinite { animation-iteration-count: infinite; }', + '.animate-once { animation-iteration-count: 1; }', + '.animate-iteration-\\[14\\] { animation-iteration-count: 14; }', + ]) + }) + + it('generates `play-state` utilities', async () => { + expect(await run('content/play-state.html')).toContainAll([ + '.animate-play { animation-play-state: running; }', + '.animate-stop { animation-play-state: paused; }', + ]) + }) + + it('generates `timing-function` utilities', async () => { + expect(await run('content/timing-function.html')).toContainAll([ + '.animate-ease { animation-timing-function: ease; }', + '.animate-ease-linear { animation-timing-function: linear; }', + '.animate-ease-in { animation-timing-function: cubic-bezier(0.4, 0, 1, 1); }', + '.animate-ease-out { animation-timing-function: cubic-bezier(0, 0, 0.2, 1); }', + '.animate-ease-in-out { animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }', + '.animate-ease-\\[cubic-bezier\\(1\\2c 0\\.66\\2c 0\\.33\\2c 0\\)\\] { animation-timing-function: cubic-bezier(1,0.66,0.33,0); }', + ]) + }) + + it('generates predefined animations', async () => { + const css = await run('content/predefined-animations.html') + + expect(css).toContainAll([ + '.animate-spin { animation: spin var(--default-animation-duration, 1s) var(--default-animation-timing-function, linear) var(--default-animation-delay, 0s) infinite; }', + '.animate-ping { animation: ping var(--default-animation-duration, 1s) var(--default-animation-timing-function, cubic-bezier(0, 0, 0.2, 1)) var(--default-animation-delay, 0s) infinite; }', + '.animate-pulse { animation: pulse var(--default-animation-duration, 2s) var(--default-animation-timing-function, cubic-bezier(0.4, 0, 0.6, 1)) var(--default-animation-delay, 0s) infinite; }', + '.animate-bounce { animation: bounce var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) infinite; }', + '.animate-wiggle { animation: wiggle var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-wiggle-more { animation: wiggle-more var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-rotate-y { animation: rotate-y var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-rotate-x { animation: rotate-x var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-jump { animation: jump var(--default-animation-duration, 0.5s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-jump-in { animation: jump-in var(--default-animation-duration, 0.5s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-jump-out { animation: jump-out var(--default-animation-duration, 0.5s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-shake { animation: shake var(--default-animation-duration, 0.5s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-fade { animation: fade var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-fade-down { animation: fade-down var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-fade-up { animation: fade-up var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-fade-left { animation: fade-left var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-fade-right { animation: fade-right var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-flip-up { animation: flip-up var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + '.animate-flip-down { animation: flip-down var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', + ]) + + expect(css).toContainAll([ + '@keyframes spin { to { transform: rotate(360deg); }}', + '@keyframes ping { 75%, 100% { transform: scale(2); opacity: 0; }}', + '@keyframes pulse { 50% { opacity: .5; }}', + '@keyframes bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8, 0, 1, 1); } 50% { transform: none; animation-timing-function: cubic-bezier(0, 0, 0.2, 1); }}', + '@keyframes wiggle { 0%, 100% { transform: rotate(-3deg); } 50% { transform: rotate(3deg); }}', + '@keyframes wiggle-more { 0%, 100% { transform: rotate(-12deg); } 50% { transform: rotate(12deg); }}', + '@keyframes rotate-y { 0% { transform: rotateY(360deg); } 100% { transform: rotateY(0); }}', + '@keyframes rotate-x { 0% { transform: rotateX(360deg); } 100% { transform: rotateX(0); }}', + '@keyframes jump { 0%, 100% { transform: scale(1); } 10% { transform: scale(0.8); } 50% { transform: scale(1.2); }}', + '@keyframes jump-in { 0% { transform: scale(0); } 80% { transform: scale(1.2); } 100% { transform: scale(1); }}', + '@keyframes jump-out { 0% { transform: scale(1); } 20% { transform: scale(1.2); } 100% { transform: scale(0); }}', + '@keyframes shake { 0% { transform: translateX(0); } 25% { transform: translateX(-1rem); } 75% { transform: translateX(1rem); } 100% { transform: translateX(0); }}', + '@keyframes fade { 0% { opacity: 0; } 100% { opacity: 1; }}', + '@keyframes fade-down { 0% { opacity: 0; transform: translateY(-2rem); } 100% { opacity: 1; transform: translateY(0); }}', + '@keyframes fade-up { 0% { opacity: 0; transform: translateY(2rem); } 100% { opacity: 1; transform: translateY(0); }}', + '@keyframes fade-left { 0% { opacity: 0; transform: translateX(2rem); } 100% { opacity: 1; transform: translateX(0); }}', + '@keyframes fade-right { 0% { opacity: 0; transform: translateX(-2rem); } 100% { opacity: 1; transform: translateX(0); }}', + '@keyframes flip-up { 0% { transform: rotateX(90deg); transform-origin: bottom; } 100% { transform: rotateX(0); transform-origin: bottom; }}', + '@keyframes flip-down { 0% { transform: rotateX(-90deg); transform-origin: top; } 100% { transform: rotateX(0); transform-origin: top; }}', + ]) + }) +}) diff --git a/jest/legacy/index.test.js b/jest/legacy/index.test.js deleted file mode 100644 index 124cbfb..0000000 --- a/jest/legacy/index.test.js +++ /dev/null @@ -1,94 +0,0 @@ -const fs = require('fs') -const path = require('path') -const postcss = require('postcss') -const tailwindcss = require('tailwindcss') - -async function run(file) { - const { currentTestName } = expect.getState() - - const config = { - content: [`./jest/${file}`], - corePlugins: { preflight: false }, - plugins: [require('../../src')], - } - - const result = await postcss(tailwindcss(config)).process('@tailwind utilities', { - from: `${path.resolve(__filename)}?test=${currentTestName}`, - }) - - return result.css -} - -describe('legacy tailwind', () => { - if (! fs.existsSync(`${__dirname}/node_modules`)) { - throw new Error(`Dependencies in ${__dirname} are missing`) - } - - it('generates `composition` utilities', async () => { - expect(await run('content/composition.html')).toIncludeAll([ - '.animate-add { animation-composition: add; }', - '.animate-replace { animation-composition: replace; }', - '.animate-accumulate { animation-composition: accumulate; }', - ]) - }) - - it('should add `delay` utilities', async () => { - expect(await run('content/delay.html')).toIncludeAll([ - '.animate-delay-75 { animation-delay: 75ms; }', - '.animate-delay-\\[666ms\\] { animation-delay: 666ms; }', - ]) - }) - - it('generates `direction` utilities', async () => { - expect(await run('content/direction.html')).toIncludeAll([ - '.animate-normal { animation-direction: normal; }', - '.animate-reverse { animation-direction: reverse; }', - ]) - }) - - it('generates `duration` utilities', async () => { - expect(await run('content/duration.html')).toIncludeAll([ - '.animate-duration-75 { animation-duration: 75ms; }', - '.animate-duration-\\[666ms\\] { animation-duration: 666ms; }', - ]) - }) - - it('generates `fill-mode` utilities', async () => { - expect(await run('content/fill-mode.html')).toIncludeAll([ - '.animate-fill-both { animation-fill-mode: both; }', - '.animate-fill-none { animation-fill-mode: normal; }', - ]) - }) - - it('generates `iteration-count` utilities', async () => { - expect(await run('content/iteration-count.html')).toIncludeAll([ - '.animate-infinite { animation-iteration-count: infinite; }', - '.animate-once { animation-iteration-count: 1; }', - '.animate-iteration-\\[14\\] { animation-iteration-count: 14; }', - ]) - }) - - it('generates `play-state` utilities', async () => { - expect(await run('content/play-state.html')).toIncludeAll([ - '.animate-play { animation-play-state: running; }', - '.animate-stop { animation-play-state: paused; }', - ]) - }) - - it('generates predefined animations', async () => { - expect(await run('content/predefined-animations.html')).toIncludeAll([ - '.animate-fade { animation: fade var(--default-animation-duration, 1s) var(--default-animation-timing-function, ease) var(--default-animation-delay, 0s) both; }', - '@keyframes fade { 0% { opacity: 0; } 100% { opacity: 1; }}', - '.animate-spin { animation: spin var(--default-animation-duration, 1s) var(--default-animation-timing-function, linear) var(--default-animation-delay, 0s) infinite; }', - '@keyframes spin { to { transform: rotate(360deg); }}', - ]) - }) - - it('generates `timing-function` utilities', async () => { - expect(await run('content/timing-function.html')).toIncludeAll([ - '.animate-ease { animation-timing-function: ease; }', - '.animate-ease-linear { animation-timing-function: linear; }', - '.animate-ease-\\[cubic-bezier\\(1\\2c 0\\.66\\2c 0\\.33\\2c 0\\)\\] { animation-timing-function: cubic-bezier(1,0.66,0.33,0); }', - ]) - }) -}) diff --git a/jest/legacy/package.json b/jest/legacy/package.json deleted file mode 100644 index 9f442ef..0000000 --- a/jest/legacy/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "private": true, - "devDependencies": { - "autoprefixer": "^10.4.20", - "tailwindcss": "3.1.0", - "postcss": "^8.5.1" - }, - "browserslist": { - "defaults": [ - "> 2%" - ] - } -} diff --git a/package.json b/package.json index bbb40cb..ca67c37 100644 --- a/package.json +++ b/package.json @@ -25,22 +25,28 @@ "scripts": { "watch": "npm run dev -- -w", "dev": "npx @tailwindcss/cli -i resources/app.css -o public/app.css", - "test": "jest --setupFilesAfterEnv '/jest/customMatchers.js'", - "lint": "npx eslint {src,jest}/**", - "lint:fix": "npx eslint {src,jest}/** --fix" + "test": "jest", + "typecheck": "tsc --noEmit", + "lint": "eslint \"{src,jest}/**/*.js\"", + "lint:fix": "eslint \"{src,jest}/**/*.js\" --fix" + }, + "jest": { + "setupFilesAfterEnv": ["/jest/customMatchers.js"] }, "peerDependencies": { "tailwindcss": ">=3.1.0 || >=4.0.0" }, "devDependencies": { - "@eslint/js": "^9.18.0", - "@tailwindcss/cli": "^4.0.0", - "@tailwindcss/postcss": "^4.0.0", - "eslint": "^9.18.0", - "eslint-plugin-jest": "^28.11.0", - "jest": "^29.7.0", - "postcss": "^8.5.1", - "tailwindcss": "^4.0.0", - "typescript": "^5.7.3" + "@eslint/js": "^9.39.4", + "@tailwindcss/cli": "^4.3.0", + "@tailwindcss/postcss": "^4.3.0", + "eslint": "^9.39.4", + "eslint-plugin-jest": "^29.15.2", + "globals": "^17.6.0", + "jest": "^30.4.2", + "postcss": "^8.5.14", + "tailwindcss": "^4.3.0", + "tailwindcss-legacy": "npm:tailwindcss@^3.1.0", + "typescript": "^5.9.3" } } diff --git a/src/compat/theme.js b/src/compat/theme.js index 162f07c..a9b9f0e 100644 --- a/src/compat/theme.js +++ b/src/compat/theme.js @@ -133,7 +133,7 @@ module.exports = { }, 'shake': { '0%': { - transform: 'translateX(0rem)', + transform: 'translateX(0)', }, '25%': { transform: 'translateX(-1rem)', @@ -142,7 +142,7 @@ module.exports = { transform: 'translateX(1rem)', }, '100%': { - transform: 'translateX(0rem)', + transform: 'translateX(0)', }, }, 'fade': { diff --git a/src/index.d.ts b/src/index.d.ts index 8c14ad0..36e8a68 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,3 +1,26 @@ -declare const plugin: { handler: () => void } +interface AnimatedPluginConfig { + theme?: { + extend?: Record + } & Record + [key: string]: unknown +} + +interface AnimatedPlugin { + // any avoids the v3/v4 PluginAPI shape mismatch + handler: (api: any) => void + config?: AnimatedPluginConfig +} + +declare const plugin: AnimatedPlugin export = plugin + +// v3-only theme key autocomplete, no-op on v4 +declare module 'tailwindcss/types/config' { + interface ThemeConfig { + animationDelay?: Record + animationDuration?: Record + animationTimingFunction?: Record + animationIteration?: Record + } +} diff --git a/src/utilities/predefined-animations.css b/src/utilities/predefined-animations.css index ef2be73..919f86a 100644 --- a/src/utilities/predefined-animations.css +++ b/src/utilities/predefined-animations.css @@ -85,10 +85,10 @@ @keyframes jump-out { 0% { - transform: scale(100%); + transform: scale(1); } 20% { - transform: scale(120%); + transform: scale(1.2); } 100% { transform: scale(0); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2393dab --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "preserve", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["src/**/*.d.ts"] +}