Skip to content

Commit

Permalink
Update style preprocessor to use new compiler
Browse files Browse the repository at this point in the history
  • Loading branch information
drwpow authored and Nate Moore committed Oct 21, 2021
1 parent f0408b4 commit ab34c9b
Show file tree
Hide file tree
Showing 5 changed files with 29 additions and 151 deletions.
14 changes: 7 additions & 7 deletions docs/src/pages/guides/styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,10 @@ It’s recommended to only use this in scenarios where a `<link>` tag won’t wo

## Autoprefixer

[Autoprefixer][autoprefixer] takes care of cross-browser CSS compatibility for you. Use it in astro by installing it (`npm install --save-dev autoprefixer`) and adding a `postcss.config.js` file to the root of your project:
[Autoprefixer][autoprefixer] takes care of cross-browser CSS compatibility for you. Use it in astro by installing it (`npm install --save-dev autoprefixer`) and adding a `postcss.config.cjs` file to the root of your project:

```js
// postcss.config.js
// postcss.config.cjs
module.exports = {
autoprefixer: {
/* (optional) autoprefixer settings */
Expand All @@ -139,7 +139,7 @@ _Note: Astro v0.21 and later requires this manual setup for autoprefixer. Previo

## PostCSS

You can use any PostCSS plugin by adding a `postcss.config.js` file to the root of your project. Follow the documentation for the plugin you’re trying to install for configuration and setup.
You can use any PostCSS plugin by adding a `postcss.config.cjs` file to the root of your project. Follow the documentation for the plugin you’re trying to install for configuration and setup.

---

Expand Down Expand Up @@ -208,10 +208,10 @@ Astro can be configured to use [Tailwind][tailwind] easily! Install the dependen
npm install --save-dev tailwindcss
```

And create 2 files in your project root: `tailwind.config.js` and `postcss.config.js`:
And create 2 files in your project root: `tailwind.config.cjs` and `postcss.config.cjs`:

```js
// tailwind.config.js
// tailwind.config.cjs
module.exports = {
mode: 'jit',
purge: ['./public/**/*.html', './src/**/*.{astro,js,jsx,svelte,ts,tsx,vue}'],
Expand All @@ -220,7 +220,7 @@ module.exports = {
```

```js
// postcss.config.js
// postcss.config.cjs
module.exports = {
tailwind: {},
};
Expand Down Expand Up @@ -250,7 +250,7 @@ As of [version 0.20.0](https://github.com/snowpackjs/astro/releases/tag/astro%40

### 🎭 PostCSS

Using PostCSS is as simple as placing a [`postcss.config.js`](https://github.com/postcss/postcss#usage) file in the root of your project.
Using PostCSS is as simple as placing a [`postcss.config.cjs`](https://github.com/postcss/postcss#usage) file in the root of your project.

Be aware that this plugin will run on all CSS in your project, including any files that compiled to CSS (like `.scss` Sass files, for example).

Expand Down
1 change: 0 additions & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
"estree-util-value-to-estree": "^1.2.0",
"fast-xml-parser": "^3.19.0",
"html-entities": "^2.3.2",
"htmlparser2": "^7.1.2",
"kleur": "^4.1.4",
"mime": "^2.5.2",
"morphdom": "^2.6.1",
Expand Down
18 changes: 10 additions & 8 deletions packages/astro/src/vite-plugin-astro/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { fileURLToPath } from 'url';
import { transform } from '@astrojs/compiler';
import { decode } from 'sourcemap-codec';
import { AstroDevServer } from '../core/dev/index.js';
import { preprocessStyle } from './styles.js';
import { getViteTransform, TransformHook, transformWithVite } from './styles.js';

interface AstroPluginOptions {
config: AstroConfig;
Expand All @@ -17,12 +17,12 @@ interface AstroPluginOptions {

/** Transform .astro files for Vite */
export default function astro({ config, devServer }: AstroPluginOptions): vite.Plugin {
let viteConfig: vite.ResolvedConfig;
let viteTransform: TransformHook;
return {
name: '@astrojs/vite-plugin-astro',
enforce: 'pre', // run transforms before other plugins can
configResolved(resolvedConfig) {
viteConfig = resolvedConfig; // gain access to vite:css
viteTransform = getViteTransform(resolvedConfig);
},
// note: don’t claim .astro files with resolveId() — it prevents Vite from transpiling the final JS (import.meta.globEager, etc.)
async load(id) {
Expand All @@ -33,10 +33,6 @@ export default function astro({ config, devServer }: AstroPluginOptions): vite.P
// everything else is treated as a fragment
const isPage = id.startsWith(fileURLToPath(config.pages)) || id.startsWith(fileURLToPath(config.layouts));
let source = await fs.promises.readFile(id, 'utf8');

// preprocess styles before compiler runs
source = await preprocessStyle({ source, filePath: id, config, viteConfig });

let tsResult: TransformResult | undefined;

try {
Expand All @@ -49,6 +45,11 @@ export default function astro({ config, devServer }: AstroPluginOptions): vite.P
sourcefile: id,
sourcemap: 'both',
internalURL: 'astro/internal',
preprocessStyle: async (value: string, attrs: Record<string, string>) => {
if (!attrs || !attrs.lang) return null;
const result = await transformWithVite(value, attrs, id, viteTransform);
return result;
},
});
// Compile `.ts` to `.js`
const { code, map } = await esbuild.transform(tsResult.code, { loader: 'ts', sourcemap: 'external', sourcefile: id });
Expand All @@ -73,7 +74,8 @@ export default function astro({ config, devServer }: AstroPluginOptions): vite.P
return devServer.handleHotUpdate(context);
}
},
transformIndexHtml(html) {
transformIndexHtml() {
// note: this runs only in dev
return [
{
injectTo: 'head-prepend',
Expand Down
128 changes: 10 additions & 118 deletions packages/astro/src/vite-plugin-astro/styles.ts
Original file line number Diff line number Diff line change
@@ -1,130 +1,22 @@
import type vite from '../core/vite';
import type { AstroConfig } from '../@types/astro-core';

import htmlparser2 from 'htmlparser2';

interface StyleProcessOptions {
source: string;
filePath: string;
config: AstroConfig;
viteConfig: vite.ResolvedConfig;
}
export type TransformHook = (code: string, id: string, ssr?: boolean) => Promise<vite.TransformResult>;

// https://vitejs.dev/guide/features.html#css-pre-processors
const SUPPORTED_PREPROCESSORS = new Set(['scss', 'sass', 'styl', 'stylus', 'less']);

/** Given HTML, preprocess (Sass, etc.) */
export async function preprocessStyle({ source, filePath, viteConfig }: StyleProcessOptions): Promise<string> {
// crawl HTML for script tags
const styles = getStyleTags(source);

// if no <style> tags, skip
if (!styles.length) return source;

let html = source;

// load vite:css’ transform() hook
/** Load vite:css’ transform() hook */
export function getViteTransform(viteConfig: vite.ResolvedConfig): TransformHook {
const viteCSSPlugin = viteConfig.plugins.find(({ name }) => name === 'vite:css');
if (!viteCSSPlugin) throw new Error(`vite:css plugin couldn’t be found`);
if (!viteCSSPlugin.transform) throw new Error(`vite:css has no transform() hook`);
const viteCSSTransform = viteCSSPlugin.transform.bind(null as any);

// tranform styles using vite:css’ transform() step
styles.reverse(); // start from back, so "start" and "end" still work
const transformedStyles = await Promise.all(
styles.map(async (style) => {
const { start, end, contents, attrs } = style;
const lang = (attrs.lang || '').toLowerCase(); // don’t be case-sensitive
if (!SUPPORTED_PREPROCESSORS.has(lang)) return undefined; // only preprocess the above
const result = await viteCSSTransform(contents, filePath.replace(/\.astro$/, `.${lang}`));
if (!result) return undefined;
return {
start,
end,
contents: typeof result === 'string' ? result : result.code,
attrs,
};
})
);

// re-insert into HTML
for (const style of transformedStyles) {
if (!style) continue;
const { start, end, contents, attrs } = style;
delete attrs.lang; // remove lang="*" from output
html = html.substring(0, start) + `<style${stringAttrs(attrs)}>` + contents + `</style>` + html.substring(end + 1);
}

return html;
return viteCSSPlugin.transform.bind(null as any) as any;
}

/** Convert attr object to string */
function stringAttrs(attrs: Record<string, string> = {}) {
let output = '';
for (const [k, v] of Object.entries(attrs)) {
if (!v) continue;
if (typeof v === 'string') {
output += ` ${k}="${v}"`;
} else {
output += ` ${k}`;
}
}
return output;
}

interface StyleTag {
attrs: Record<string, string>;
contents: string;
start: number;
end: number;
}

/** Parse HTML with htmlparser2 to return <style> tags within .astro (7x faster than cheerio) */
export function getStyleTags(source: string): StyleTag[] {
let styles: StyleTag[] = [];

// the HTML doc is read top-to-bottom. these are “buffers” that keep track of in-progress reading until we have a complete <style> tag with contents
let styleTagOpen = false; // are we inside <style>?
let styleStart = -1; // char position of <style> open
let styleAttrs: Record<string, string> = {}; // current <style> attributes
let styleContents: string[] = []; // collection of <style> contents

const parser = new htmlparser2.Parser({
// this detects any time tags were opened. we only want <style>
onopentag(tagname, attributes) {
if (tagname === 'style') {
styleAttrs = attributes;
styleStart = parser.startIndex;
styleTagOpen = true;
}
},
// this reads text at all times, but we only want to read contents if a <style> tag has been opened
// note: this may not grab complete <style> contents within one go, hence the array
ontext(text) {
if (styleTagOpen) {
styleContents.push(text);
}
},
// this detects any time tags were closed; here, when </style> is encountered, take everything stored and save it
onclosetag(tagname) {
if (tagname === 'style') {
// skip empty <style> tags
if (styleContents.length) {
styles.push({
start: styleStart,
end: parser.endIndex as number,
attrs: styleAttrs,
contents: styleContents.join(''),
});
}
// make sure to reset the “buffers” and state (styleAttrs and styleStart will simply be overwritten)
styleTagOpen = false;
styleContents = [];
}
},
});
parser.write(source); // start parsing HTML
parser.end();

return styles;
/** Transform style using Vite hook */
export async function transformWithVite(value: string, attrs: Record<string, string>, id: string, transformHook: TransformHook): Promise<vite.TransformResult | null> {
const lang = (attrs.lang || '').toLowerCase(); // don’t be case-sensitive
if (!SUPPORTED_PREPROCESSORS.has(lang)) return null; // only preprocess the above
const result = await transformHook(value, id.replace(/\.astro$/, `.${lang}`));
return result || null;
}
19 changes: 2 additions & 17 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3896,14 +3896,14 @@ domelementtype@^2.0.1, domelementtype@^2.2.0:
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==

domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.2.2:
domhandler@^4.0.0, domhandler@^4.2.0:
version "4.2.2"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f"
integrity sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==
dependencies:
domelementtype "^2.2.0"

domutils@^2.5.2, domutils@^2.6.0, domutils@^2.7.0, domutils@^2.8.0:
domutils@^2.5.2, domutils@^2.6.0, domutils@^2.7.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
Expand Down Expand Up @@ -4026,11 +4026,6 @@ entities@^2.0.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==

entities@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==

env-paths@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
Expand Down Expand Up @@ -5634,16 +5629,6 @@ htmlparser2@^6.1.0:
domutils "^2.5.2"
entities "^2.0.0"

htmlparser2@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-7.1.2.tgz#587923d38f03bc89e03076e00cba2c7473f37f7c"
integrity sha512-d6cqsbJba2nRdg8WW2okyD4ceonFHn9jLFxhwlNcLhQWcFPdxXeJulgOLjLKtAK9T6ahd+GQNZwG9fjmGW7lyg==
dependencies:
domelementtype "^2.0.1"
domhandler "^4.2.2"
domutils "^2.8.0"
entities "^3.0.1"

http-assert@^1.3.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.5.0.tgz#c389ccd87ac16ed2dfa6246fd73b926aa00e6b8f"
Expand Down

0 comments on commit ab34c9b

Please sign in to comment.