diff --git a/README.md b/README.md index b48709f40..a6ac9dda0 100644 --- a/README.md +++ b/README.md @@ -71,10 +71,38 @@ Supports: - `lang="scss"`: load as the `.scss` extension - `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. +#### 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 you‘re used to: + +```astro + +``` + ## 🚀 Build & Deployment 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 [sass]: https://sass-lang.com/ [svelte]: https://svelte.dev +[tailwind]: https://tailwindcss.com diff --git a/src/@types/postcss-modules.d.ts b/src/@types/postcss-modules.d.ts deleted file mode 100644 index 4035404bd..000000000 --- a/src/@types/postcss-modules.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// don’t need types; just a plugin -declare module 'postcss-modules'; diff --git a/src/@types/tailwind.d.ts b/src/@types/tailwind.d.ts new file mode 100644 index 000000000..99ae97419 --- /dev/null +++ b/src/@types/tailwind.d.ts @@ -0,0 +1,3 @@ +// we shouldn‘t 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'; diff --git a/src/compiler/optimize/styles.ts b/src/compiler/optimize/styles.ts index 72781fefe..65b429fef 100644 --- a/src/compiler/optimize/styles.ts +++ b/src/compiler/optimize/styles.ts @@ -1,16 +1,26 @@ import crypto from 'crypto'; +import fs from 'fs'; import path from 'path'; import autoprefixer from 'autoprefixer'; -import postcss from 'postcss'; +import esbuild from 'esbuild'; +import postcss, { Plugin } from 'postcss'; import findUp from 'find-up'; import sass from 'sass'; -import { RuntimeMode } from '../../@types/astro'; -import { OptimizeOptions, Optimizer } from '../../@types/optimizer'; +import type { RuntimeMode } from '../../@types/astro'; +import type { OptimizeOptions, Optimizer } from '../../@types/optimizer'; import type { TemplateNode } from '../../parser/interfaces'; +import { debug } from '../../logger.js'; import astroScopedStyles, { NEVER_SCOPED_TAGS } from './postcss-scoped-styles/index.js'; 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; + } +} + const getStyleType: Map = new Map([ ['.css', 'css'], ['.pcss', 'postcss'], @@ -42,8 +52,15 @@ export interface StyleTransformResult { type: StyleType; } -// cache node_modules resolutions for each run. saves looking up the same directory over and over again. blown away on exit. -const nodeModulesMiniCache = new Map(); +interface StylesMiniCache { + nodeModules: Map; // 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(), +}; export interface TransformStyleOptions { type?: string; @@ -72,17 +89,18 @@ async function transformStyle(code: string, { type, filename, scopedClass, mode let includePaths: string[] = [path.dirname(filename)]; // 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) { includePaths.push(cachedNodeModulesDir); } else { const nodeModulesDir = await findUp('node_modules', { type: 'directory', cwd: path.dirname(filename) }); if (nodeModulesDir) { - nodeModulesMiniCache.set(filename, nodeModulesDir); + miniCache.nodeModules.set(filename, nodeModulesDir); includePaths.push(nodeModulesDir); } } + // 1. Preprocess (currently only Sass supported) let css = ''; switch (styleType) { case 'css': { @@ -91,13 +109,7 @@ async function transformStyle(code: string, { type, filename, scopedClass, mode } case 'sass': case 'scss': { - css = sass - .renderSync({ - outputStyle: mode === 'production' ? 'compressed' : undefined, - data: code, - includePaths, - }) - .css.toString('utf8'); + css = sass.renderSync({ data: code, includePaths }).css.toString('utf8'); break; } 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 }) .then((result) => result.css); @@ -118,6 +151,21 @@ export default function optimizeStyles({ compileOptions, filename, fileID }: Opt const styleTransformPromises: Promise[] = []; // 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(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 couldn‘t find one; mark as false + debug(compileOptions.logging, 'tailwind', 'No config found. Skipping.'); + } + return { visitors: { html: {