Add Tailwind support (#57)

This commit is contained in:
Drew Powers 2021-04-02 19:23:30 -06:00 committed by GitHub
parent b58b493948
commit aa333c2f29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 96 additions and 18 deletions

View file

@ -71,10 +71,38 @@ Supports:
- `lang="scss"`: load as the `.scss` extension - `lang="scss"`: load as the `.scss` extension
- `lang="sass"`: load as the `.sass` extension (no brackets; indent-style) - `lang="sass"`: load as the `.sass` extension (no brackets; indent-style)
### Autoprefixer #### Autoprefixer
We also automatically add browser prefixes using [Autoprefixer][autoprefixer]. By default, Astro loads the default values, but you may also specify your own by placing a [Browserslist][browserslist] file in your project root. We also automatically add browser prefixes using [Autoprefixer][autoprefixer]. By default, Astro loads the default values, but you may also specify your own by placing a [Browserslist][browserslist] file in your project root.
#### Tailwind
Astro can be configured to use [Tailwind][tailwind] easily! Install the dependencies:
```
npm install @tailwindcss/jit tailwindcss
```
And also create a `tailwind.config.js` in your project root:
```
module.exports = {
// your options here
}
```
_Note: a Tailwind config file is currently required to enable Tailwind in Astro, even if you use the default options._
Then write Tailwind in your project just like youre used to:
```astro
<style>
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>
```
## 🚀 Build & Deployment ## 🚀 Build & Deployment
Add a `build` npm script to your `/package.json` file: Add a `build` npm script to your `/package.json` file:
@ -100,3 +128,4 @@ Now upload the contents of `/_site_` to your favorite static site host.
[browserslist]: https://github.com/browserslist/browserslist [browserslist]: https://github.com/browserslist/browserslist
[sass]: https://sass-lang.com/ [sass]: https://sass-lang.com/
[svelte]: https://svelte.dev [svelte]: https://svelte.dev
[tailwind]: https://tailwindcss.com

View file

@ -1,2 +0,0 @@
// dont need types; just a plugin
declare module 'postcss-modules';

3
src/@types/tailwind.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
// we shouldnt have this as a dependency for Astro, but we may dynamically import it if a user requests it, so let TS know about it
declare module 'tailwindcss';
declare module '@tailwindcss/jit';

View file

@ -1,16 +1,26 @@
import crypto from 'crypto'; import crypto from 'crypto';
import fs from 'fs';
import path from 'path'; import path from 'path';
import autoprefixer from 'autoprefixer'; import autoprefixer from 'autoprefixer';
import postcss from 'postcss'; import esbuild from 'esbuild';
import postcss, { Plugin } from 'postcss';
import findUp from 'find-up'; import findUp from 'find-up';
import sass from 'sass'; import sass from 'sass';
import { RuntimeMode } from '../../@types/astro'; import type { RuntimeMode } from '../../@types/astro';
import { OptimizeOptions, Optimizer } from '../../@types/optimizer'; import type { OptimizeOptions, Optimizer } from '../../@types/optimizer';
import type { TemplateNode } from '../../parser/interfaces'; import type { TemplateNode } from '../../parser/interfaces';
import { debug } from '../../logger.js';
import astroScopedStyles, { NEVER_SCOPED_TAGS } from './postcss-scoped-styles/index.js'; import astroScopedStyles, { NEVER_SCOPED_TAGS } from './postcss-scoped-styles/index.js';
type StyleType = 'css' | 'scss' | 'sass' | 'postcss'; type StyleType = 'css' | 'scss' | 'sass' | 'postcss';
declare global {
interface ImportMeta {
/** https://nodejs.org/api/esm.html#esm_import_meta_resolve_specifier_parent */
resolve(specifier: string, parent?: string): Promise<any>;
}
}
const getStyleType: Map<string, StyleType> = new Map([ const getStyleType: Map<string, StyleType> = new Map([
['.css', 'css'], ['.css', 'css'],
['.pcss', 'postcss'], ['.pcss', 'postcss'],
@ -42,8 +52,15 @@ export interface StyleTransformResult {
type: StyleType; type: StyleType;
} }
// cache node_modules resolutions for each run. saves looking up the same directory over and over again. blown away on exit. interface StylesMiniCache {
const nodeModulesMiniCache = new Map<string, string>(); 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. */
const miniCache: StylesMiniCache = {
nodeModules: new Map<string, string>(),
};
export interface TransformStyleOptions { export interface TransformStyleOptions {
type?: string; type?: string;
@ -72,17 +89,18 @@ async function transformStyle(code: string, { type, filename, scopedClass, mode
let includePaths: string[] = [path.dirname(filename)]; let includePaths: string[] = [path.dirname(filename)];
// include node_modules to includePaths (allows @use-ing node modules, if it can be located) // include node_modules to includePaths (allows @use-ing node modules, if it can be located)
const cachedNodeModulesDir = nodeModulesMiniCache.get(filename); const cachedNodeModulesDir = miniCache.nodeModules.get(filename);
if (cachedNodeModulesDir) { if (cachedNodeModulesDir) {
includePaths.push(cachedNodeModulesDir); includePaths.push(cachedNodeModulesDir);
} else { } else {
const nodeModulesDir = await findUp('node_modules', { type: 'directory', cwd: path.dirname(filename) }); const nodeModulesDir = await findUp('node_modules', { type: 'directory', cwd: path.dirname(filename) });
if (nodeModulesDir) { if (nodeModulesDir) {
nodeModulesMiniCache.set(filename, nodeModulesDir); miniCache.nodeModules.set(filename, nodeModulesDir);
includePaths.push(nodeModulesDir); includePaths.push(nodeModulesDir);
} }
} }
// 1. Preprocess (currently only Sass supported)
let css = ''; let css = '';
switch (styleType) { switch (styleType) {
case 'css': { case 'css': {
@ -91,13 +109,7 @@ async function transformStyle(code: string, { type, filename, scopedClass, mode
} }
case 'sass': case 'sass':
case 'scss': { case 'scss': {
css = sass css = sass.renderSync({ data: code, includePaths }).css.toString('utf8');
.renderSync({
outputStyle: mode === 'production' ? 'compressed' : undefined,
data: code,
includePaths,
})
.css.toString('utf8');
break; break;
} }
default: { default: {
@ -105,7 +117,28 @@ async function transformStyle(code: string, { type, filename, scopedClass, mode
} }
} }
css = await postcss([astroScopedStyles({ className: scopedClass }), autoprefixer()]) // 2. Post-process (PostCSS)
const postcssPlugins: Plugin[] = [];
// 2a. Tailwind (only if project uses Tailwind)
if (miniCache.tailwindEnabled) {
try {
const { default: tailwindcss } = await import('@tailwindcss/jit');
postcssPlugins.push(tailwindcss());
} catch (err) {
console.error(err);
throw new Error(`tailwindcss not installed. Try running \`npm install tailwindcss\` and trying again.`);
}
}
// 2b. Astro scoped styles (always on)
postcssPlugins.push(astroScopedStyles({ className: scopedClass }));
// 2c. Autoprefixer (always on)
postcssPlugins.push(autoprefixer());
// 2e. Run PostCSS
css = await postcss(postcssPlugins)
.process(css, { from: filename, to: undefined }) .process(css, { from: filename, to: undefined })
.then((result) => result.css); .then((result) => result.css);
@ -118,6 +151,21 @@ export default function optimizeStyles({ compileOptions, filename, fileID }: Opt
const styleTransformPromises: Promise<StyleTransformResult>[] = []; // async style transform results to be finished in finalize(); 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 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(compileOptions.astroConfig.projectRoot.pathname, 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 couldnt find one; mark as false
debug(compileOptions.logging, 'tailwind', 'No config found. Skipping.');
}
return { return {
visitors: { visitors: {
html: { html: {