diff --git a/README.md b/README.md
index 98287d00e..b48709f40 100644
--- a/README.md
+++ b/README.md
@@ -10,14 +10,6 @@ npm install astro
TODO: astro boilerplate
-### 💧 Partial Hydration
-
-By default, Astro outputs zero client-side JS. If you'd like to include an interactive component in the client output, you may use any of the following techniques.
-
-- `MyComponent:load` will render `MyComponent` on page load
-- `MyComponent:idle` will use `requestIdleCallback` to render `MyComponent` as soon as main thread is free
-- `MyComponent:visible` will use an `IntersectionObserver` to render `MyComponent` when the element enters the viewport
-
## 🧞 Development
Add a `dev` npm script to your `/package.json` file:
@@ -36,6 +28,53 @@ Then run:
npm run dev
```
+### 💧 Partial Hydration
+
+By default, Astro outputs zero client-side JS. If you'd like to include an interactive component in the client output, you may use any of the following techniques.
+
+- `MyComponent:load` will render `MyComponent` on page load
+- `MyComponent:idle` will use `requestIdleCallback` to render `MyComponent` as soon as main thread is free
+- `MyComponent:visible` will use an `IntersectionObserver` to render `MyComponent` when the element enters the viewport
+
+### 💅 Styling
+
+If you‘ve used [Svelte][svelte]’s styles before, Astro works almost the same way. In any `.astro` file, start writing styles in a `
+
+
I’m a scoped style
+```
+
+#### Sass
+
+Astro also supports [Sass][sass] out-of-the-box; no configuration needed:
+
+```astro
+
+
+Title
+```
+
+Supports:
+
+- `lang="scss"`: load as the `.scss` extension
+- `lang="sass"`: load as the `.sass` extension (no brackets; indent-style)
+
+### 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.
+
## 🚀 Build & Deployment
Add a `build` npm script to your `/package.json` file:
@@ -56,3 +95,8 @@ npm run build
```
Now upload the contents of `/_site_` to your favorite static site host.
+
+[autoprefixer]: https://github.com/postcss/autoprefixer
+[browserslist]: https://github.com/browserslist/browserslist
+[sass]: https://sass-lang.com/
+[svelte]: https://svelte.dev
diff --git a/examples/snowpack/astro/components/Nav.astro b/examples/snowpack/astro/components/Nav.astro
index 3582bd0a0..a5f14d656 100644
--- a/examples/snowpack/astro/components/Nav.astro
+++ b/examples/snowpack/astro/components/Nav.astro
@@ -27,7 +27,7 @@ export let version: string = '3.1.2';
color: $white;
background-color: $dark-blue;
- body.is-nav-open & {
+ :global(body.is-nav-open) & {
height: $nav-height * 2;
}
diff --git a/examples/snowpack/package-lock.json b/examples/snowpack/package-lock.json
index d9b20cf23..a52da1c88 100644
--- a/examples/snowpack/package-lock.json
+++ b/examples/snowpack/package-lock.json
@@ -996,7 +996,7 @@
"deepmerge": "^4.2.2",
"domhandler": "^4.0.0",
"es-module-lexer": "^0.4.1",
- "esbuild": "^0.9.6",
+ "esbuild": "^0.10.1",
"find-up": "^5.0.0",
"github-slugger": "^1.3.0",
"gray-matter": "^4.0.2",
@@ -1008,7 +1008,6 @@
"micromark-extension-gfm": "^0.3.3",
"node-fetch": "^2.6.1",
"postcss": "^8.2.8",
- "postcss-modules": "^4.0.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"sass": "^1.32.8",
@@ -2371,9 +2370,9 @@
"dev": true
},
"esbuild": {
- "version": "0.9.6",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.9.6.tgz",
- "integrity": "sha512-F6vASxU0wT/Davt9aj2qtDwDNSkQxh9VbyO56M7PDWD+D/Vgq/rmUDGDQo7te76W5auauVojjnQr/wTu3vpaUA==",
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.10.2.tgz",
+ "integrity": "sha512-/5vsZD7wTJJHC3yNXLUjXNvUDwqwNoIMvFvLd9tcDQ9el5l13pspYm3yufavjIeYvNtAbo+6N/6uoWx9dGA6ug==",
"dev": true
},
"escalade": {
@@ -4253,6 +4252,14 @@
"picomatch": "^2.2.2",
"resolve": "^1.20.0",
"rollup": "^2.34.0"
+ },
+ "dependencies": {
+ "esbuild": {
+ "version": "0.9.7",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.9.7.tgz",
+ "integrity": "sha512-VtUf6aQ89VTmMLKrWHYG50uByMF4JQlVysb8dmg6cOgW8JnFCipmz7p+HNBl+RR3LLCuBxFGVauAe2wfnF9bLg==",
+ "dev": true
+ }
}
},
"source-map": {
diff --git a/package.json b/package.json
index e5d2b2785..1c44c9ab9 100644
--- a/package.json
+++ b/package.json
@@ -56,7 +56,6 @@
"micromark-extension-gfm": "^0.3.3",
"node-fetch": "^2.6.1",
"postcss": "^8.2.8",
- "postcss-modules": "^4.0.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"rollup": "^2.43.1",
diff --git a/src/compiler/optimize/postcss-scoped-styles/index.ts b/src/compiler/optimize/postcss-scoped-styles/index.ts
new file mode 100644
index 000000000..7949f63b5
--- /dev/null
+++ b/src/compiler/optimize/postcss-scoped-styles/index.ts
@@ -0,0 +1,79 @@
+import { Plugin } from 'postcss';
+
+interface AstroScopedOptions {
+ className: string;
+}
+
+interface Selector {
+ start: number;
+ end: number;
+ value: string;
+}
+
+const CSS_SEPARATORS = new Set([' ', ',', '+', '>', '~']);
+
+/**
+ * Scope Selectors
+ * Given a selector string (`.btn>span,.nav>span`), add an additional CSS class to every selector (`.btn.myClass>span.myClass,.nav.myClass>span.myClass`)
+ * @param {string} selector The minified selector string to parse. Cannot contain arbitrary whitespace (other than child selector syntax).
+ * @param {string} className The CSS class to apply.
+ */
+export function scopeSelectors(selector: string, className: string) {
+ const c = className.replace(/^\.?/, '.'); // make sure class always has leading '.'
+ const selectors: Selector[] = [];
+ let ss = selector; // final output
+
+ // Pass 1: parse selector string; extract top-level selectors
+ let start = 0;
+ let lastValue = '';
+ for (let n = 0; n < ss.length; n++) {
+ const isEnd = n === selector.length - 1;
+ if (isEnd || CSS_SEPARATORS.has(selector[n])) {
+ lastValue = selector.substring(start, isEnd ? undefined : n);
+ if (!lastValue) continue;
+ selectors.push({ start, end: isEnd ? n + 1 : n, value: lastValue });
+ start = n + 1;
+ }
+ }
+
+ // Pass 2: starting from end, transform selectors w/ scoped class
+ for (let i = selectors.length - 1; i >= 0; i--) {
+ const { start, end, value } = selectors[i];
+ const head = ss.substring(0, start);
+ const tail = ss.substring(end);
+
+ // replace '*' with className
+ if (value === '*') {
+ ss = head + c + tail;
+ continue;
+ }
+
+ // leave :global() alone!
+ if (value.startsWith(':global(')) {
+ ss = head + ss.substring(start, end).replace(':global(', '').replace(')', '') + tail;
+ continue;
+ }
+
+ // scope everything else
+ let newSelector = ss.substring(start, end);
+ const pseudoIndex = newSelector.indexOf(':');
+ if (pseudoIndex > 0) {
+ // if there‘s a pseudoclass (:focus)
+ ss = head + newSelector.substring(start, pseudoIndex) + c + newSelector.substr(pseudoIndex) + tail;
+ } else {
+ ss = head + newSelector + c + tail;
+ }
+ }
+
+ return ss;
+}
+
+/** PostCSS Scope plugin */
+export default function astroScopedStyles(options: AstroScopedOptions): Plugin {
+ return {
+ postcssPlugin: '@astro/postcss-scoped-styles',
+ Rule(rule) {
+ rule.selector = scopeSelectors(rule.selector, options.className);
+ },
+ };
+}
diff --git a/src/compiler/optimize/styles.ts b/src/compiler/optimize/styles.ts
index 563e9b355..6f0cd9361 100644
--- a/src/compiler/optimize/styles.ts
+++ b/src/compiler/optimize/styles.ts
@@ -2,11 +2,11 @@ import crypto from 'crypto';
import path from 'path';
import autoprefixer from 'autoprefixer';
import postcss from 'postcss';
-import postcssModules from 'postcss-modules';
import findUp from 'find-up';
import sass from 'sass';
import { Optimizer } from '../../@types/optimizer';
import type { TemplateNode } from '../../parser/interfaces';
+import astroScopedStyles from './postcss-scoped-styles/index.js';
type StyleType = 'css' | 'scss' | 'sass' | 'postcss';
@@ -28,6 +28,8 @@ const getStyleType: Map = new Map([
const SASS_OPTIONS: Partial = {
outputStyle: 'compressed',
};
+/** HTML tags that should never get scoped classes */
+const NEVER_SCOPED_TAGS = new Set(['html', 'head', 'body', 'script', 'style', 'link', 'meta']);
/** Should be deterministic, given a unique filename */
function hashFromFilename(filename: string): string {
@@ -42,14 +44,14 @@ function hashFromFilename(filename: string): string {
export interface StyleTransformResult {
css: string;
- cssModules: Map;
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();
-async function transformStyle(code: string, { type, filename, fileID }: { type?: string; filename: string; fileID: string }): Promise {
+/** Convert styles to scoped CSS */
+async function transformStyle(code: string, { type, filename, scopedClass }: { type?: string; filename: string; scopedClass: string }): Promise {
let styleType: StyleType = 'css'; // important: assume CSS as default
if (type) {
styleType = getStyleType.get(type) || styleType;
@@ -81,51 +83,31 @@ async function transformStyle(code: string, { type, filename, fileID }: { type?:
css = sass.renderSync({ ...SASS_OPTIONS, data: code, includePaths }).css.toString('utf8');
break;
}
- case 'postcss': {
- css = code; // TODO
- break;
- }
default: {
- throw new Error(`Unsupported:
-