Skip to content

Commit

Permalink
Add Tailwind support to Astro Dev Server (#222)
Browse files Browse the repository at this point in the history
* Improve PostCSS and Tailwind support

* Update styling docs

* Changelog

* Fix test hanging
  • Loading branch information
drwpow committed May 21, 2021
1 parent 69d693b commit 19e20f2
Show file tree
Hide file tree
Showing 18 changed files with 227 additions and 1,839 deletions.
5 changes: 5 additions & 0 deletions .changeset/red-eyes-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': minor
---

Add Tailwind JIT support for Astro
2 changes: 2 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export default {
devOptions: {
/** The port to run the dev server on. */
port: 3000,
/** Path to tailwind.config.js if used, e.g. './tailwind.config.js' */
tailwindConfig: undefined,
},
};
```
46 changes: 32 additions & 14 deletions docs/styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ Styling in Astro is meant to be as flexible as you’d like it to be! The follow

| Framework | Global CSS | Scoped CSS | CSS Modules |
| :--------------- | :--------: | :--------: | :---------: |
| Astro (`.astro`) ||| N/A¹ |
| React / Preact ||||
| Vue ||||
| Svelte ||||
| `.astro` ||| N/A¹ |
| `.jsx` \| `.tsx` ||||
| `.vue` ||||
| `.svelte` ||||

¹ _`.astro` files have no runtime, therefore Scoped CSS takes the place of CSS Modules (styles are still scoped to components, but don’t need dynamic values)_

All styles in Astro are automatically [**autoprefixed**](#-autoprefixer) and optimized, so you can just write CSS and we’ll handle the rest ✨.

## 🖍 Quick Start

##### Astro
Expand Down Expand Up @@ -92,27 +94,43 @@ And also create a `tailwind.config.js` in your project root:

```js
// tailwind.config.js

module.exports = {
mode: 'jit',
purge: ['./public/**/*.html', './src/**/*.{astro,js,jsx,ts,tsx,vue}'],
// more options here
};
```

Then add [Tailwind utilities][tailwind-utilities] to any Astro component that needs it:
Be sure to add the config path to `astro.config.mjs`, so that Astro enables JIT support in the dev server.

```html
<style>
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>
```diff
// astro.config.mjs
export default {
+ devOptions: {
+ tailwindConfig: './tailwindConfig.js',
+ },
};
```

You should see Tailwind styles compile successfully in Astro.
Now you’re ready to write Tailwind! Our recommended approach is to create a `public/global.css` file with [Tailwind utilities][tailwind-utilities] like so:

```css
/* public/global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
```

💁 As an alternative to `public/global.css`, You may also add Tailwind utilities to individual `pages/*.astro` components in `<style>` tags, but be mindful of duplication! If you end up creating multiple Tailwind-managed stylesheets for your site, make sure you’re not sending the same CSS to users over and over again in separate CSS files.

#### 📦 Bundling

All CSS is minified and bundled automatically for you in running `astro build`. The general specifics are:

- If a style only appears on one route, it’s only loaded for that route
- If a style appears on multiple routes, it’s deduplicated into a `common.css` bundle

💁 **Tip**: to reduce duplication, try loading `@tailwind base` from a parent page (`./pages/*.astro`) instead of the component itself.
We’ll be expanding our styling optimization story over time, and would love your feedback! If `astro build` generates unexpected styles, or if you can think of improvements, [please open an issue](https://github.com/snowpackjs/astro/issues).

## 📚 Advanced Styling Architecture in Astro

Expand Down
15 changes: 0 additions & 15 deletions examples/snowpack/.stylelintrc.js

This file was deleted.

7 changes: 1 addition & 6 deletions examples/snowpack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"astro-dev": "nodemon --delay 0.5 -w ../../packages/astro/dist -x '../../packages/astro/astro.mjs dev'",
"test": "jest /__test__/",
"format": "prettier --write \"src/**/*.js\" && yarn format:css",
"format:css": "stylelint 'src/**/*.scss' --fix",
"lint": "prettier --check \"src/**/*.js\""
},
"dependencies": {
Expand All @@ -31,11 +30,7 @@
"luxon": "^1.25.0",
"markdown-it": "^12.0.2",
"markdown-it-anchor": "^6.0.0",
"nodemon": "^2.0.7",
"stylelint": "^13.8.0",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-config-standard": "^20.0.0"
"nodemon": "^2.0.7"
},
"snowpack": {
"workspaceRoot": "../.."
Expand Down
5 changes: 5 additions & 0 deletions examples/tailwindcss/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
devOptions: {
tailwindConfig: './tailwind.config.js',
},
};
2 changes: 1 addition & 1 deletion examples/tailwindcss/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"devDependencies": {
"astro": "^0.10.0",
"tailwindcss": "^2.1.1"
"tailwindcss": "^2.1.2"
},
"snowpack": {
"workspaceRoot": "../.."
Expand Down
3 changes: 3 additions & 0 deletions examples/tailwindcss/public/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
5 changes: 0 additions & 5 deletions examples/tailwindcss/src/components/Button.astro
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
<style>
@tailwind components;
@tailwind utilities;
</style>

<button class="py-2 px-4 bg-green-500 text-white font-semibold rounded-lg shadow-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-opacity-75">
<slot />
</button>
4 changes: 1 addition & 3 deletions examples/tailwindcss/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import Button from '../components/Button.astro';
<head>
<meta charset="UTF-8" />
<title>Astro + TailwindCSS</title>
<style>
@tailwind base;
</style>
<link rel="stylesheet" type="text/css" href="/global.css">
</head>

<body>
Expand Down
9 changes: 5 additions & 4 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@
"@babel/parser": "^7.13.15",
"@babel/traverse": "^7.13.15",
"@silvenon/remark-smartypants": "^1.0.0",
"@snowpack/plugin-postcss": "^1.4.0",
"@snowpack/plugin-sass": "^1.4.0",
"@snowpack/plugin-svelte": "^3.6.1",
"@snowpack/plugin-vue": "^2.4.0",
"@snowpack/plugin-svelte": "^3.7.0",
"@snowpack/plugin-vue": "^2.5.0",
"@vue/server-renderer": "^3.0.10",
"acorn": "^7.4.0",
"astro-parser": "0.1.0",
Expand All @@ -64,7 +65,7 @@
"moize": "^6.0.1",
"node-fetch": "^2.6.1",
"picomatch": "^2.2.3",
"postcss": "^8.2.8",
"postcss": "^8.2.15",
"postcss-icss-keyframes": "^0.2.1",
"preact": "^10.5.13",
"preact-render-to-string": "^5.1.18",
Expand All @@ -83,7 +84,7 @@
"sass": "^1.32.13",
"shorthash": "^0.0.2",
"slash": "^4.0.0",
"snowpack": "^3.3.7",
"snowpack": "^3.5.1",
"source-map-support": "^0.5.19",
"string-width": "^5.0.0",
"svelte": "^3.35.0",
Expand Down
3 changes: 3 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export interface AstroConfig {
/** The port to run the dev server on. */
port: number;
projectRoot?: string;
/** Path to tailwind.config.js, if used */
tailwindConfig?: string;
};
}

Expand All @@ -36,6 +38,7 @@ export type AstroUserConfig = Omit<AstroConfig, 'buildOptions' | 'devOptions'> &
devOptions: {
port?: number;
projectRoot?: string;
tailwindConfig?: string;
};
};

Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/@types/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface CompileOptions {
astroConfig: AstroConfig;
extensions?: Record<string, ValidExtensionPlugins>;
mode: RuntimeMode;
tailwindConfig?: string;
}
23 changes: 4 additions & 19 deletions packages/astro/src/compiler/transform/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { TransformOptions, Transformer } from '../../@types/transformer';
import type { TemplateNode } from 'astro-parser';

import crypto from 'crypto';
import fs from 'fs';
import { createRequire } from 'module';
import path from 'path';
import { fileURLToPath } from 'url';
Expand Down Expand Up @@ -55,7 +54,6 @@ export interface StyleTransformResult {

interface StylesMiniCache {
nodeModules: Map<string, string>; // filename: node_modules location
tailwindEnabled?: boolean; // cache once per-run
}

/** Simple cache that only exists in memory per-run. Prevents the same lookups from happening over and over again within the same build or dev server session. */
Expand All @@ -68,6 +66,7 @@ export interface TransformStyleOptions {
type?: string;
filename: string;
scopedClass: string;
tailwindConfig?: string;
}

/** given a class="" string, does it contain a given class? */
Expand All @@ -80,7 +79,7 @@ function hasClass(classList: string, className: string): boolean {
}

/** Convert styles to scoped CSS */
async function transformStyle(code: string, { logging, type, filename, scopedClass }: TransformStyleOptions): Promise<StyleTransformResult> {
async function transformStyle(code: string, { logging, type, filename, scopedClass, tailwindConfig }: TransformStyleOptions): Promise<StyleTransformResult> {
let styleType: StyleType = 'css'; // important: assume CSS as default
if (type) {
styleType = getStyleType.get(type) || styleType;
Expand Down Expand Up @@ -122,7 +121,7 @@ async function transformStyle(code: string, { logging, type, filename, scopedCla
const postcssPlugins: Plugin[] = [];

// 2a. Tailwind (only if project uses Tailwind)
if (miniCache.tailwindEnabled) {
if (tailwindConfig) {
try {
const require = createRequire(import.meta.url);
const tw = require.resolve('tailwindcss', { paths: [import.meta.url, process.cwd()] });
Expand Down Expand Up @@ -192,21 +191,6 @@ export default function transformStyles({ compileOptions, filename, fileID }: Tr
const styleTransformPromises: Promise<StyleTransformResult>[] = []; // async style transform results to be finished in finalize();
const scopedClass = `astro-${hashFromFilename(fileID)}`; // this *should* generate same hash from fileID every time

// find Tailwind config, if first run (cache for subsequent runs)
if (miniCache.tailwindEnabled === undefined) {
const tailwindNames = ['tailwind.config.js', 'tailwind.config.mjs'];
for (const loc of tailwindNames) {
const tailwindLoc = path.join(fileURLToPath(compileOptions.astroConfig.projectRoot), loc);
if (fs.existsSync(tailwindLoc)) {
miniCache.tailwindEnabled = true; // Success! We have a Tailwind config file.
debug(compileOptions.logging, 'tailwind', 'Found config. Enabling.');
break;
}
}
if (miniCache.tailwindEnabled !== true) miniCache.tailwindEnabled = false; // We couldn‘t find one; mark as false
debug(compileOptions.logging, 'tailwind', 'No config found. Skipping.');
}

return {
visitors: {
html: {
Expand All @@ -231,6 +215,7 @@ export default function transformStyles({ compileOptions, filename, fileID }: Tr
type: (langAttr && langAttr.value[0] && langAttr.value[0].data) || undefined,
filename,
scopedClass,
tailwindConfig: compileOptions.tailwindConfig,
})
);
return;
Expand Down
18 changes: 12 additions & 6 deletions packages/astro/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,25 @@ function validateConfig(config: any): void {
}

// buildOptions
if (config.buildOptions && config.buildOptions.site !== undefined) {
if (typeof config.buildOptions.site !== 'string') throw new Error(`[config] buildOptions.site is not a string`);
try {
new URL(config.buildOptions.site);
} catch (err) {
throw new Error('[config] buildOptions.site must be a valid URL');
if (config.buildOptions) {
// buildOptions.site
if (config.buildOptions.site !== undefined) {
if (typeof config.buildOptions.site !== 'string') throw new Error(`[config] buildOptions.site is not a string`);
try {
new URL(config.buildOptions.site);
} catch (err) {
throw new Error('[config] buildOptions.site must be a valid URL');
}
}
}

// devOptions
if (typeof config.devOptions?.port !== 'number') {
throw new Error(`[config] devOptions.port: Expected number, received ${type(config.devOptions?.port)}`);
}
if (config.devOptions?.tailwindConfig !== undefined && typeof config.devOptions?.tailwindConfig !== 'string') {
throw new Error(`[config] devOptions.tailwindConfig: Expected string, received ${type(config.devOptions?.tailwindConfig)}`);
}
}

/** Set default config values */
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default async function dev(astroConfig: AstroConfig) {
break;
}
case 404: {
const fullurl = new URL(req.url || '/', 'https://example.org/');
const fullurl = new URL(req.url || '/', astroConfig.buildOptions.site || `http://localhost${astroConfig.devOptions.port}`);
const reqPath = decodeURI(fullurl.pathname);
error(logging, 'static', 'Not found', reqPath);
res.statusCode = 404;
Expand Down

0 comments on commit 19e20f2

Please sign in to comment.