Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Tailwind support to Astro Dev Server #222

Merged
merged 4 commits into from
May 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,
Copy link
Member Author

Choose a reason for hiding this comment

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

This adds a devOptions.tailwindConfig key to Astro, too (copying from Snowpack).

While adding Tailwind-specific config isn‘t great, I think it’s far preferable than writing a bunch of code for Tailwind to try and be “smart” about Tailwind detection.

},
};
```
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",
Copy link
Member Author

Choose a reason for hiding this comment

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

Very weird thing: stylelint was throwing off our monorepo deps 🤪. It was downgrading PostCSS to 7 which was breaking Tailwind support. Removed, so that Astro ships with PostCSS 8.

"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.
Copy link
Member Author

Choose a reason for hiding this comment

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

As part of devOptions.tailwindConfig, we got to remove a lot of Tailwind-specific code in Astro (almost all of it)!

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