diff --git a/.changeset/rude-deers-turn.md b/.changeset/rude-deers-turn.md new file mode 100644 index 000000000000..ee54f44163ee --- /dev/null +++ b/.changeset/rude-deers-turn.md @@ -0,0 +1,5 @@ +--- +'@astrojs/tailwind': minor +--- + +Adds `nesting` option to enable `tailwindcss/nesting` support diff --git a/packages/integrations/tailwind/package.json b/packages/integrations/tailwind/package.json index 34cda3a4c63e..8ef499dbeb07 100644 --- a/packages/integrations/tailwind/package.json +++ b/packages/integrations/tailwind/package.json @@ -29,7 +29,8 @@ "scripts": { "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", - "dev": "astro-scripts dev \"src/**/*.ts\"" + "dev": "astro-scripts dev \"src/**/*.ts\"", + "test": "mocha --exit --timeout 20000 test/" }, "dependencies": { "autoprefixer": "^10.4.15", @@ -39,6 +40,8 @@ "devDependencies": { "astro": "workspace:*", "astro-scripts": "workspace:*", + "chai": "^4.3.7", + "mocha": "^10.2.0", "tailwindcss": "^3.3.5", "vite": "^5.0.10" }, diff --git a/packages/integrations/tailwind/src/index.ts b/packages/integrations/tailwind/src/index.ts index 5f30ae4b67d6..1da44f5dd45b 100644 --- a/packages/integrations/tailwind/src/index.ts +++ b/packages/integrations/tailwind/src/index.ts @@ -1,3 +1,4 @@ +import { fileURLToPath } from 'node:url'; import type { AstroIntegration } from 'astro'; import autoprefixerPlugin from 'autoprefixer'; import tailwindPlugin from 'tailwindcss'; @@ -23,15 +24,22 @@ async function getPostCssConfig( async function getViteConfiguration( tailwindConfigPath: string | undefined, - viteConfig: UserConfig + nesting: boolean, + root: string, + postcssInlineOptions: CSSOptions['postcss'] ): Promise<Partial<UserConfig>> { // We need to manually load postcss config files because when inlining the tailwind and autoprefixer plugins, // that causes vite to ignore postcss config files - const postcssConfigResult = await getPostCssConfig(viteConfig.root, viteConfig.css?.postcss); + const postcssConfigResult = await getPostCssConfig(root, postcssInlineOptions); const postcssOptions = postcssConfigResult?.options ?? {}; const postcssPlugins = postcssConfigResult?.plugins?.slice() ?? []; + if (nesting) { + const tailwindcssNestingPlugin = (await import('tailwindcss/nesting/index.js')).default; + postcssPlugins.push(tailwindcssNestingPlugin()); + } + postcssPlugins.push(tailwindPlugin(tailwindConfigPath)); postcssPlugins.push(autoprefixerPlugin()); @@ -59,18 +67,30 @@ type TailwindOptions = { * @default true */ applyBaseStyles?: boolean; + /** + * Add CSS nesting support using `tailwindcss/nesting`. See {@link https://tailwindcss.com/docs/using-with-preprocessors#nesting Tailwind's docs} + * for how this works with `postcss-nesting` and `postcss-nested`. + */ + nesting?: boolean; }; export default function tailwindIntegration(options?: TailwindOptions): AstroIntegration { const applyBaseStyles = options?.applyBaseStyles ?? true; const customConfigPath = options?.configFile; + const nesting = options?.nesting ?? false; + return { name: '@astrojs/tailwind', hooks: { 'astro:config:setup': async ({ config, updateConfig, injectScript }) => { // Inject the Tailwind postcss plugin updateConfig({ - vite: await getViteConfiguration(customConfigPath, config.vite), + vite: await getViteConfiguration( + customConfigPath, + nesting, + fileURLToPath(config.root), + config.vite.css?.postcss + ), }); if (applyBaseStyles) { diff --git a/packages/integrations/tailwind/test/basic.test.js b/packages/integrations/tailwind/test/basic.test.js new file mode 100644 index 000000000000..e5275c81e9d6 --- /dev/null +++ b/packages/integrations/tailwind/test/basic.test.js @@ -0,0 +1,33 @@ +import { expect } from 'chai'; +import { loadFixture } from '../../../astro/test/test-utils.js'; + +describe('Basic', () => { + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: new URL('./fixtures/basic/', import.meta.url), + }); + }); + + describe('build', () => { + before(async () => { + await fixture.build(); + }); + + it('works', async () => { + const astroChunkDir = await fixture.readdir('/_astro'); + + let css = ''; + for (const file of astroChunkDir) { + if (file.endsWith('.css')) { + css += await fixture.readFile(`/_astro/${file}`); + } + } + + expect(css).to.include('box-sizing:border-box;'); // base css + expect(css).to.include('text-red-500'); // class css + expect(css).to.match(/\.a\[data-astro-cid-.*?\] \.b\[data-astro-cid-.*?\]/); // nesting + }); + }); +}); diff --git a/packages/integrations/tailwind/test/fixtures/basic/astro.config.js b/packages/integrations/tailwind/test/fixtures/basic/astro.config.js new file mode 100644 index 000000000000..cd684d6eb94c --- /dev/null +++ b/packages/integrations/tailwind/test/fixtures/basic/astro.config.js @@ -0,0 +1,12 @@ +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'astro/config'; +import tailwind from '@astrojs/tailwind'; + +export default defineConfig({ + integrations: [ + tailwind({ + configFile: fileURLToPath(new URL('./tailwind.config.js', import.meta.url)), + nesting: true + }), + ] +}); diff --git a/packages/integrations/tailwind/test/fixtures/basic/package.json b/packages/integrations/tailwind/test/fixtures/basic/package.json new file mode 100644 index 000000000000..33e20daaa380 --- /dev/null +++ b/packages/integrations/tailwind/test/fixtures/basic/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/tailwind-basic", + "version": "0.0.0", + "private": true, + "type": "module", + "dependencies": { + "astro": "workspace:*", + "@astrojs/tailwind": "workspace:*" + } +} diff --git a/packages/integrations/tailwind/test/fixtures/basic/src/pages/index.astro b/packages/integrations/tailwind/test/fixtures/basic/src/pages/index.astro new file mode 100644 index 000000000000..ab4df58e3305 --- /dev/null +++ b/packages/integrations/tailwind/test/fixtures/basic/src/pages/index.astro @@ -0,0 +1,13 @@ +<div class="text-red-500">red</div> + +<div class="a"> + <div class="b">nested blue</div> +</div> + +<style> + .a { + .b { + color: blue; + } + } +</style> diff --git a/packages/integrations/tailwind/test/fixtures/basic/tailwind.config.js b/packages/integrations/tailwind/test/fixtures/basic/tailwind.config.js new file mode 100644 index 000000000000..f1090968169a --- /dev/null +++ b/packages/integrations/tailwind/test/fixtures/basic/tailwind.config.js @@ -0,0 +1,6 @@ +import path from 'node:path'; + +/** @type {import('tailwindcss').Config} */ +export default { + content: [path.join(__dirname, 'src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}')], +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b64ed8dc673..0603b09027d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4566,6 +4566,12 @@ importers: astro-scripts: specifier: workspace:* version: link:../../../scripts + chai: + specifier: ^4.3.7 + version: 4.3.10 + mocha: + specifier: ^10.2.0 + version: 10.2.0 tailwindcss: specifier: ^3.3.5 version: 3.3.5 @@ -4573,6 +4579,15 @@ importers: specifier: ^5.0.10 version: 5.0.10(@types/node@18.18.6)(sass@1.69.5) + packages/integrations/tailwind/test/fixtures/basic: + dependencies: + '@astrojs/tailwind': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + packages/integrations/vercel: dependencies: '@astrojs/internal-helpers':