Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Handle quote escapes in LESS when sorting `@apply` ([#392](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/392))
- Improved monorepo support by loading Tailwind CSS relative to the input file instead of prettier config file ([#386](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/386))
- Improved monorepo support by loading v3 configs relative to the input file instead of prettier config file ([#386](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/386))
- Fallback to Tailwind CSS v4 instead of v3 by default ([#390](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/390))

## [0.6.14] - 2025-07-09

Expand Down
4 changes: 2 additions & 2 deletions build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ function patchCjsInterop() {
let code = [
`import {createRequire as __global__createRequire__} from 'module'`,
`import {dirname as __global__dirname__} from 'path'`,
`import {fileURLToPath} from 'url'`,
`import {fileURLToPath as __global__fileURLToPath__} from 'url'`,

// CJS interop fixes
`const require=__global__createRequire__(import.meta.url)`,
`const __filename=fileURLToPath(import.meta.url)`,
`const __filename=__global__fileURLToPath__(import.meta.url)`,
`const __dirname=__global__dirname__(__filename)`,
]

Expand Down
8 changes: 0 additions & 8 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,6 @@ export async function getTailwindConfig(options: ParserOptions): Promise<any> {
stylesheet ??= `${pkgDir}/theme.css`
}

// No stylesheet was given or otherwise found in a local v4 installation
// nor was a tailwind config given or found.
//
// Fallback to v3
if (!stylesheet) {
return pathToApiMap.remember(null, () => loadV3(null, null))
}

return pathToApiMap.remember(`${pkgDir}:${stylesheet}`, () => loadV4(mod, stylesheet))
}

Expand Down
23 changes: 23 additions & 0 deletions src/versions/assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// @ts-ignore
import index from 'tailwindcss-v4/index.css'
// @ts-ignore
import preflight from 'tailwindcss-v4/preflight.css'
// @ts-ignore
import theme from 'tailwindcss-v4/theme.css'
// @ts-ignore
import utilities from 'tailwindcss-v4/utilities.css'

export const assets: Record<string, string> = {
tailwindcss: index,
'tailwindcss/index': index,
'tailwindcss/index.css': index,

'tailwindcss/preflight': preflight,
'tailwindcss/preflight.css': preflight,

'tailwindcss/theme': theme,
'tailwindcss/theme.css': theme,

'tailwindcss/utilities': utilities,
'tailwindcss/utilities.css': utilities,
}
30 changes: 18 additions & 12 deletions src/versions/v4.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// @ts-check
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { pathToFileURL } from 'node:url'
import { createJiti, type Jiti } from 'jiti'
import * as v4 from 'tailwindcss-v4'
import { resolveCssFrom, resolveJsFrom } from '../resolve'
import type { UnifiedApi } from '../types'
import { assets } from './assets'

interface DesignSystem {
getClassOrder(classList: string[]): [string, bigint | null][]
Expand Down Expand Up @@ -40,11 +41,10 @@ interface ApiV4 {

export async function loadV4(mod: ApiV4 | null, stylesheet: string | null): Promise<UnifiedApi> {
// This is not Tailwind v4
let isFallback = false
if (!mod || !mod.__unstable__loadDesignSystem) {
throw new Error('Unable to load Tailwind CSS v4: Your installation of Tailwind CSS is not v4')

// TODO
// mod = (await import('tailwindcss-v4')) as ApiV4
mod = v4 as ApiV4
isFallback = true
}

// Create a Jiti instance that can be used to load plugins and config files
Expand All @@ -63,9 +63,7 @@ export async function loadV4(mod: ApiV4 | null, stylesheet: string | null): Prom
} else {
importBasePath = process.cwd()
stylesheet = path.join(importBasePath, 'fake.css')

// TODO: bundled theme.css file?
css = ''
css = assets['tailwindcss/theme.css']
}

// Load the design system and set up a compatible context object that is
Expand All @@ -90,11 +88,19 @@ export async function loadV4(mod: ApiV4 | null, stylesheet: string | null): Prom
}),

loadStylesheet: async (id: string, base: string) => {
let resolved = resolveCssFrom(base, id)
try {
let resolved = resolveCssFrom(base, id)

return {
base: path.dirname(resolved),
content: await fs.readFile(resolved, 'utf-8'),
}
} catch (err) {
if (isFallback && id in assets) {
return { base, content: assets[id] }
}

return {
base: path.dirname(resolved),
content: await fs.readFile(resolved, 'utf-8'),
throw err
}
},

Expand Down
10 changes: 10 additions & 0 deletions tests/fixtures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ let fixtures = [
dir: 'no-prettier-config',
ext: 'html',
},
{
name: 'no local install of Tailwind CSS (uses v4)',
dir: 'no-local-version',
ext: 'html',
},
{
name: 'no stylesheet given (uses v4)',
dir: 'no-stylesheet-given',
ext: 'html',
},
{
name: 'inferred config path',
dir: 'basic',
Expand Down
5 changes: 5 additions & 0 deletions tests/fixtures/no-local-version/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import "tailwindcss";

@theme {
--color-tomato: tomato;
}
1 change: 1 addition & 0 deletions tests/fixtures/no-local-version/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="sm:bg-tomato bg-red-500"></div>
1 change: 1 addition & 0 deletions tests/fixtures/no-local-version/output.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="bg-red-500 sm:bg-tomato"></div>
6 changes: 6 additions & 0 deletions tests/fixtures/no-local-version/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions tests/fixtures/no-local-version/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"prettier": {
"tailwindStylesheet": "./app.css"
}
}
1 change: 1 addition & 0 deletions tests/fixtures/no-stylesheet-given/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="sm:bg-red-500 bg-red-500"></div>
1 change: 1 addition & 0 deletions tests/fixtures/no-stylesheet-given/output.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="bg-red-500 sm:bg-red-500"></div>
18 changes: 16 additions & 2 deletions tests/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ describe('other', () => {

expect(result).toEqual('<div class="unknown-class group peer container p-0"></div>')
})

test('parasite utilities (v4)', async ({ expect }) => {
let result = await format('<div class="group peer unknown-class p-0 container"></div>', {
tailwindPackageName: 'tailwindcss-v4',
})

expect(result).toEqual('<div class="group peer unknown-class container p-0"></div>')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't unknown-class exist before group and peer? 🤔

Because while group and peer don't exist, they are still more known than unknown classes.

I don't know if we test this already, but named group and peers should be in a similar position.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In v4 we don't sort group and peer though

})

test('parasite utilities (no install == v4)', async ({ expect }) => {
let result = await format('<div class="group peer unknown-class p-0 container"></div>')

expect(result).toEqual('<div class="group peer unknown-class container p-0"></div>')
})
})

describe('whitespace', () => {
Expand Down Expand Up @@ -61,7 +75,7 @@ describe('whitespace', () => {
test('duplicate classes are dropped', async ({ expect }) => {
let result = await format('<div class="underline line-through underline flex"></div>')

expect(result).toEqual('<div class="flex underline line-through"></div>')
expect(result).toEqual('<div class="flex line-through underline"></div>')
})
})

Expand Down Expand Up @@ -90,6 +104,6 @@ describe('errors', () => {
tailwindPackageName: 'tailwindcss-v3',
})

await expect(result).rejects.toThrowError(/Unable to load Tailwind CSS v4/)
await expect(result).rejects.toThrowError(/no such file or directory/)
})
})