Add Tailwind support to Astro Dev Server (#222)
* Improve PostCSS and Tailwind support * Update styling docs * Changelog * Fix test hanging
This commit is contained in:
parent
69d693b77c
commit
19e20f2c54
18 changed files with 227 additions and 1839 deletions
5
.changeset/red-eyes-stare.md
Normal file
5
.changeset/red-eyes-stare.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': minor
|
||||
---
|
||||
|
||||
Add Tailwind JIT support for Astro
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
@ -92,7 +94,6 @@ 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}'],
|
||||
|
@ -100,19 +101,36 @@ module.exports = {
|
|||
};
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
💁 **Tip**: to reduce duplication, try loading `@tailwind base` from a parent page (`./pages/*.astro`) instead of the component itself.
|
||||
```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
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
module.exports = {
|
||||
extends: [
|
||||
'stylelint-config-standard',
|
||||
'stylelint-config-rational-order',
|
||||
'stylelint-config-prettier',
|
||||
],
|
||||
rules: {
|
||||
'at-rule-no-unknown': [
|
||||
true,
|
||||
{ ignoreAtRules: ['use', 'each', 'for', 'mixin', 'extend', 'include'] },
|
||||
], // allow Sass syntax,
|
||||
'no-descending-specificity': null,
|
||||
},
|
||||
syntax: 'scss',
|
||||
};
|
|
@ -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": {
|
||||
|
@ -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": "../.."
|
||||
|
|
5
examples/tailwindcss/astro.config.mjs
Normal file
5
examples/tailwindcss/astro.config.mjs
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
devOptions: {
|
||||
tailwindConfig: './tailwind.config.js',
|
||||
},
|
||||
};
|
|
@ -9,7 +9,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"astro": "^0.10.0",
|
||||
"tailwindcss": "^2.1.1"
|
||||
"tailwindcss": "^2.1.2"
|
||||
},
|
||||
"snowpack": {
|
||||
"workspaceRoot": "../.."
|
||||
|
|
3
examples/tailwindcss/public/global.css
Normal file
3
examples/tailwindcss/public/global.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -36,6 +38,7 @@ export type AstroUserConfig = Omit<AstroConfig, 'buildOptions' | 'devOptions'> &
|
|||
devOptions: {
|
||||
port?: number;
|
||||
projectRoot?: string;
|
||||
tailwindConfig?: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -7,4 +7,5 @@ export interface CompileOptions {
|
|||
astroConfig: AstroConfig;
|
||||
extensions?: Record<string, ValidExtensionPlugins>;
|
||||
mode: RuntimeMode;
|
||||
tailwindConfig?: string;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
@ -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. */
|
||||
|
@ -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? */
|
||||
|
@ -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;
|
||||
|
@ -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()] });
|
||||
|
@ -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: {
|
||||
|
@ -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;
|
||||
|
|
|
@ -28,12 +28,15 @@ 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,6 +44,9 @@ function validateConfig(config: any): void {
|
|||
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 */
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -266,12 +266,13 @@ interface CreateSnowpackOptions {
|
|||
env: Record<string, any>;
|
||||
mode: RuntimeMode;
|
||||
resolvePackageUrl?: (pkgName: string) => Promise<string>;
|
||||
target: 'frontend' | 'backend';
|
||||
}
|
||||
|
||||
/** Create a new Snowpack instance to power Astro */
|
||||
async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackOptions) {
|
||||
const { projectRoot, astroRoot, extensions } = astroConfig;
|
||||
const { env, mode, resolvePackageUrl } = options;
|
||||
const { env, mode, resolvePackageUrl, target } = options;
|
||||
|
||||
const internalPath = new URL('./frontend/', import.meta.url);
|
||||
|
||||
|
@ -295,20 +296,43 @@ async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackO
|
|||
mountOptions[fileURLToPath(astroConfig.public)] = '/';
|
||||
}
|
||||
|
||||
const plugins: (string | [string, any])[] = [
|
||||
[fileURLToPath(new URL('../snowpack-plugin.cjs', import.meta.url)), astroPlugOptions],
|
||||
[require.resolve('@snowpack/plugin-svelte'), { compilerOptions: { hydratable: true } }],
|
||||
require.resolve('@snowpack/plugin-vue'),
|
||||
];
|
||||
|
||||
// note: styles only need processing once
|
||||
if (target === 'frontend') {
|
||||
plugins.push(require.resolve('@snowpack/plugin-sass'));
|
||||
plugins.push([
|
||||
require.resolve('@snowpack/plugin-postcss'),
|
||||
{
|
||||
config: {
|
||||
plugins: {
|
||||
[require.resolve('autoprefixer')]: {},
|
||||
...(astroConfig.devOptions.tailwindConfig ? { [require.resolve('tailwindcss')]: {} } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// Tailwind: IDK what this does but it makes JIT work 🤷♂️
|
||||
if (astroConfig.devOptions.tailwindConfig) {
|
||||
(process.env as any).TAILWIND_DISABLE_TOUCH = true;
|
||||
}
|
||||
|
||||
const snowpackConfig = await loadConfiguration({
|
||||
root: fileURLToPath(projectRoot),
|
||||
mount: mountOptions,
|
||||
mode,
|
||||
plugins: [
|
||||
[fileURLToPath(new URL('../snowpack-plugin.cjs', import.meta.url)), astroPlugOptions],
|
||||
require.resolve('@snowpack/plugin-sass'),
|
||||
[require.resolve('@snowpack/plugin-svelte'), { compilerOptions: { hydratable: true } }],
|
||||
require.resolve('@snowpack/plugin-vue'),
|
||||
],
|
||||
plugins,
|
||||
devOptions: {
|
||||
open: 'none',
|
||||
output: 'stream',
|
||||
port: 0,
|
||||
tailwindConfig: astroConfig.devOptions.tailwindConfig,
|
||||
},
|
||||
buildOptions: {
|
||||
out: astroConfig.dist,
|
||||
|
@ -343,6 +367,7 @@ export async function createRuntime(astroConfig: AstroConfig, { mode, logging }:
|
|||
},
|
||||
mode,
|
||||
resolvePackageUrl,
|
||||
target: 'backend',
|
||||
});
|
||||
debug(logging, 'core', `backend snowpack created [${stopTimer(timer.backend)}]`);
|
||||
|
||||
|
@ -352,6 +377,7 @@ export async function createRuntime(astroConfig: AstroConfig, { mode, logging }:
|
|||
astro: false,
|
||||
},
|
||||
mode,
|
||||
target: 'frontend',
|
||||
});
|
||||
debug(logging, 'core', `frontend snowpack created [${stopTimer(timer.frontend)}]`);
|
||||
|
||||
|
|
Loading…
Reference in a new issue