Skip to content

Commit

Permalink
add plugin-sass with new onChange/markChanged interface (#1225)
Browse files Browse the repository at this point in the history
* add plugin-sass with new onChange/markChanged interface

* add tests

* update docs

* update yarn lockfile

* update ts

* update snapshots

* add back missing snapshot tests

* update add true .sass support
  • Loading branch information
FredKSchott committed Oct 8, 2020
1 parent d235ddb commit 68e8d77
Show file tree
Hide file tree
Showing 18 changed files with 323 additions and 36 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ For starter apps and templates, see [create-snowpack-app](./create-snowpack-app)
- [@snowpack/plugin-babel](./plugins/plugin-babel)
- [@snowpack/plugin-svelte](./plugins/plugin-svelte)
- [@snowpack/plugin-vue](./plugins/plugin-vue)
- [@snowpack/plugin-sass](./plugins/plugin-sass)

### Transform

Expand Down
18 changes: 1 addition & 17 deletions docs/docs/08-guides.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,27 +136,11 @@ Follow the official [Tailwind CSS Docs](https://tailwindcss.com/docs/installatio

```js
// snowpack.config.json
// Example: Build all src/css/*.scss files to public/css/*
"plugins": [
["@snowpack/plugin-run-script", {"cmd": "sass src/css:public/css --no-source-map", "watch": "$1 --watch"}]
]

// You can configure this to match your preferred layout:
//
// import './App.css';
// "run:sass": "sass src:src --no-source-map",
//
// import 'public/css/App.css';
// "run:sass": "sass src/css:public/css --no-source-map",
// (Note: Assumes mounted public/ directory ala Create Snowpack App)
"plugins": ["@snowpack/plugin-sass"]
```

[Sass](https://www.sass-lang.com/) is a stylesheet language that’s compiled to CSS. It allows you to use variables, nested rules, mixins, functions, and more, all with a fully CSS-compatible syntax. Sass helps keep large stylesheets well-organized and makes it easy to share design within and across projects.

[Check out the official Sass CLI documentation](https://sass-lang.com/documentation/cli/dart-sass) for a list of all available arguments. You can also use the [node-sass](https://www.npmjs.com/package/node-sass) CLI if you prefer to install Sass from npm.

**Note:** Sass should be run as a "run:" script (see example above) to take advantage of the Sass CLI's partial handling. A `"build:scss"` script would build each file individually as its served, but couldn't handle Sass partials via `@use` due to the fact that Sass bundles these into the importer file CSS.

To use Sass + PostCSS, check out [this guide](https://zellwk.com/blog/eleventy-snowpack-sass-postcss/).

### ESLint
Expand Down
37 changes: 30 additions & 7 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ This is an (obviously) simplified version of the `@snowpack/plugin-webpack` plug

## Plugin API

Check out our ["SnowpackPlugin" TypeScript definition](b/master/packages/snowpack/src/types/snowpack.ts) for a fully documented and up-to-date summary of the Plugin API and all supported options.
Check out our ["SnowpackPlugin" TypeScript definition](https://github.com/pikapkg/snowpack/tree/master/snowpack/src/types/snowpack.ts) for a fully documented and up-to-date summary of the Plugin API and all supported options.

### knownEntrypoints

Expand All @@ -269,7 +269,7 @@ config(snowpackConfig) {

Use this hook to read or make changes to the completed Snowpack configuration object. This is currently the recommended way to access the Snowpack configuration, since the one passed to the top-level plugin function is not yet finalized and may be incomplete.

- [Full TypeScript definition](b/master/packages/snowpack/src/types/snowpack.ts).
- [Full TypeScript definition](https://github.com/pikapkg/snowpack/tree/master/snowpack/src/types/snowpack.ts).

### resolve

Expand All @@ -285,36 +285,59 @@ If your plugin defines a `load()` method, Snowpack will need to know what files

- `input`: An array of file extensions that this plugin will load.
- `output`: The set of all file extensions that this plugin's `load()` method will output.
- [Full TypeScript definition](b/master/packages/snowpack/src/types/snowpack.ts).
- [Full TypeScript definition](https://github.com/pikapkg/snowpack/tree/master/snowpack/src/types/snowpack.ts).

### load()

Load a file from disk and build it for your application. This is most useful for taking a file type that can't run in the browser (TypeScript, Sass, Vue, Svelte) and returning JS and/or CSS. It can even be used to load JS/CSS files directly from disk with a build step like Babel or PostCSS.

- See above for an example of how to use this method.
- [Full TypeScript definition](b/master/packages/snowpack/src/types/snowpack.ts).
- [Full TypeScript definition](https://github.com/pikapkg/snowpack/tree/master/snowpack/src/types/snowpack.ts).

### transform()

Transform a file's contents. Useful for making changes to all types of output (JS, CSS, etc.) regardless of how they were loaded from disk.

- See above for an example of how to use this method.
- Example: [@snowpack/plugin-postcss](https://github.com/pikapkg/snowpack/tree/master/plugins/plugin-postcss)
- [Full TypeScript definition](b/master/packages/snowpack/src/types/snowpack.ts).
- [Full TypeScript definition](https://github.com/pikapkg/snowpack/tree/master/snowpack/src/types/snowpack.ts).

### run()

Run a CLI command, and connect it's output into the Snowpack console. Useful for connecting tools like TypeScript.

- [Full TypeScript definition](b/master/packages/snowpack/src/types/snowpack.ts).
- [Full TypeScript definition](https://github.com/pikapkg/snowpack/tree/master/snowpack/src/types/snowpack.ts).

### optimize()

Snowpack’s bundler plugin API is still experimental and may change in a future release. See our official bundler plugins for an example of using the current interface:

- Example: [@snowpack/plugin-parcel](https://github.com/pikapkg/snowpack/tree/master/plugins/plugin-parcel)
- Example: [@snowpack/plugin-webpack](https://github.com/pikapkg/snowpack/tree/master/plugins/plugin-webpack)
- [Full TypeScript definition](b/master/packages/snowpack/src/types/snowpack.ts).
- [Full TypeScript definition](https://github.com/pikapkg/snowpack/tree/master/snowpack/src/types/snowpack.ts).

### onChange()

Get notified any time a watched file changes. This can be useful when paired with the `markChanged()` plugin method, to mark multiple files changed at once.

- See [@snowpack/plugin-sass](https://github.com/pikapkg/snowpack/tree/master/plugins/plugin-sass/plugin.js) for an example of how to use this method.
- [Full TypeScript definition](https://github.com/pikapkg/snowpack/tree/master/snowpack/src/types/snowpack.ts).

## Plugin API Methods

### this.markChanged()

```js
// Called inside any plugin hooks
this.markChanged('/some/file/path.scss');
```

Manually mark a file as changed, regardless of whether the file changed on disk or not. This can be useful when paired with the `markChanged()` plugin hook, to mark multiple files changed at once.

- See [@snowpack/plugin-sass](https://github.com/pikapkg/snowpack/tree/master/plugins/plugin-sass/plugin.js) for an example of how to use this method.
- [Full TypeScript definition](https://github.com/pikapkg/snowpack/tree/master/snowpack/src/types/snowpack.ts).



## Publishing a Plugin

Expand Down
35 changes: 35 additions & 0 deletions plugins/plugin-sass/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# @snowpack/plugin-sass

This plugin adds [Sass](https://sass-lang.com/) support to any Snowpack project. With this plugin, you can import any `*.scss` or `*.sass` Sass file from JavaScript and have it compile to CSS.

This plugin also adds support for `.module.scss` Sass Modules. [Learn more.](https://www.snowpack.dev/#import-css-modules)

## A Note on Sass Implementations

Sass is interesting in that multiple compilers are available: [sass](https://www.npmjs.com/package/sass) (written in Dart) & [node-sass](https://www.npmjs.com/package/node-sass) (written in JavaScript). Both packages run on Node.js and both are popular on npm. However, [node-sass is now considered deprecated](https://github.com/sass/node-sass/issues/2952).

**This plugin was designed to work with the `sass` package.** `sass` is automatically installed with this plugin as a direct dependency, so no extra effort is required on your part.

## Usage

```bash
npm i @snowpack/plugin-sass
```

Then add the plugin to your Snowpack config:

```js
// snowpack.config.js

module.exports = {
plugins: [
['@snowpack/plugin-sass', { /* see options below */ }
],
};
```

## Plugin Options

| Name | Type | Description |
| :------- | :-------: | :----------------- |
| `native` | `boolean` | If true, the plugin will ignore the npm version of sass installed locally for the native Sass CLI [installed separately](https://sass-lang.com/install). This involves extra set up, but the result can be [up to 9x faster.](https://stackoverflow.com/a/56422541) Defaults to false. |
20 changes: 20 additions & 0 deletions plugins/plugin-sass/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": "1.0.0",
"name": "@snowpack/plugin-sass",
"main": "plugin.js",
"license": "MIT",
"homepage": "https://github.com/pikapkg/snowpack/tree/master/plugins/plugin-sass#readme",
"repository": {
"type": "git",
"url": "https://github.com/pikapkg/snowpack.git",
"directory": "plugins/plugin-sass"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"execa": "^4.0.3",
"npm-run-path": "^4.0.1",
"sass": "^1.3.0"
}
}
89 changes: 89 additions & 0 deletions plugins/plugin-sass/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
const fs = require('fs');
const path = require('path');
const execa = require('execa');
const npmRunPath = require('npm-run-path');

const IMPORT_REGEX = /\@(use|import)\s*['"](.*?)['"]/g;

function scanSassImports(fileContents, filePath, fileExt) {
// TODO: Replace with matchAll once Node v10 is out of TLS.
// const allMatches = [...result.matchAll(new RegExp(HTML_JS_REGEX))];
const allMatches = [];
let match;
const regex = new RegExp(IMPORT_REGEX);
while ((match = regex.exec(fileContents))) {
allMatches.push(match);
}
// return all imports, resolved to true files on disk.
return allMatches
.map((match) => match[2])
.filter((s) => s.trim())
.map((s) => {
// Inherit the default file extension of the importer. This is a cheap
// but effective shortcut to supporting both ".scss" and ".sass"
// since users rarely mix both, and performing multiple lookups
// would be too expensive.
if (!path.extname(s)) {
s += fileExt;
}
return path.resolve(path.dirname(filePath), s);
});
}

module.exports = function postcssPlugin(_, {native}) {
const importedByMap = new Map();

function addImportsToMap(filePath, sassImports) {
for (const imported of sassImports) {
const importedBy = importedByMap.get(imported);
if (importedBy) {
importedBy.add(filePath);
} else {
importedByMap.set(imported, new Set([filePath]));
}
}
}

return {
name: '@snowpack/plugin-sass',
resolve: {
input: ['.scss', '.sass'],
output: ['.css'],
},
/** when a file changes, also mark it's importers as changed. */
onChange({filePath}) {
const importedBy = importedByMap.get(filePath);
if (!importedBy) {
return;
}
importedByMap.delete(filePath);
for (const importerFilePath of importedBy) {
this.markChanged(importerFilePath);
}
},
/** Load the Sass file and compile it to CSS. */
async load({filePath, isDev}) {
const fileExt = path.extname(filePath);
const contents = fs.readFileSync(filePath, 'utf8');
// During development, we need to track changes to Sass dependencies.
if (isDev) {
const sassImports = scanSassImports(contents, filePath, fileExt);
addImportsToMap(filePath, sassImports);
}
// If file is `.sass`, use YAML-style. Otherwise, use default.
const args = ['--stdin', '--load-path', path.dirname(filePath)];
if (fileExt === '.sass') {
args.push('--indented');
}
// Build the file.
const {stdout, stderr} = await execa('sass', args, {
input: contents,
env: native ? undefined : npmRunPath.env(),
extendEnv: native ? true : false,
});
// Handle the output.
if (stderr) throw new Error(stderr);
if (stdout) return stdout;
},
};
};
23 changes: 23 additions & 0 deletions plugins/plugin-sass/test/__snapshots__/plugin.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`plugin-sass returns the compiled Sass result: App.sass 1`] = `
"body {
font-family: Helvetica, sans-serif;
}
.App {
text-align: center;
background: #333;
}"
`;

exports[`plugin-sass returns the compiled Sass result: App.scss 1`] = `
"body {
font-family: Helvetica, sans-serif;
}
.App {
text-align: center;
background: #333;
}"
`;
1 change: 1 addition & 0 deletions plugins/plugin-sass/test/fixtures/bad/bad.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
awegagwagawingawignTHISISBADCODE
5 changes: 5 additions & 0 deletions plugins/plugin-sass/test/fixtures/sass/App.sass
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@use 'base'

.App
text-align: center
background: base.$primary-color
6 changes: 6 additions & 0 deletions plugins/plugin-sass/test/fixtures/sass/base.sass
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// _base.scss
$font-stack: Helvetica, sans-serif
$primary-color: #333

body
font-family: $font-stack
6 changes: 6 additions & 0 deletions plugins/plugin-sass/test/fixtures/scss/App.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@use 'base';

.App {
text-align: center;
background: base.$primary-color;
}
7 changes: 7 additions & 0 deletions plugins/plugin-sass/test/fixtures/scss/base.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// _base.scss
$font-stack: Helvetica, sans-serif;
$primary-color: #333;

body {
font-family: $font-stack;
}
49 changes: 49 additions & 0 deletions plugins/plugin-sass/test/plugin.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const plugin = require('../plugin.js');
const path = require('path');

const pathToSassApp = path.join(__dirname, 'fixtures/sass/App.sass');
const pathToSassBase = path.join(__dirname, 'fixtures/sass/base.sass');
const pathToScssApp = path.join(__dirname, 'fixtures/scss/App.scss');
const pathToBadCode = path.join(__dirname, 'fixtures/bad/bad.scss');

describe('plugin-sass', () => {

test('returns the compiled Sass result', async () => {
const p = plugin(null, {});
const sassResult = await p.load({filePath: pathToSassApp, isDev: false});
expect(sassResult).toMatchSnapshot('App.sass');
const scssResult = await p.load({filePath: pathToScssApp, isDev: true});
expect(scssResult).toMatchSnapshot('App.scss');
});

test('throws an error when stderr output is returned', async () => {
const p = plugin(null, {});
expect(p.load({filePath: pathToBadCode, isDev: false})).rejects.toThrow('Command failed with exit code');
});

test('marks a dependant as changed when an imported changes and isDev=true', async () => {
const p = plugin(null, {});
p.markChanged = jest.fn();
await p.load({filePath: pathToSassApp, isDev: true});
expect(p.markChanged.mock.calls).toEqual([]);
p.onChange({filePath: pathToSassApp});
expect(p.markChanged.mock.calls).toEqual([]);
p.onChange({filePath: pathToSassBase});
expect(p.markChanged.mock.calls).toEqual([[pathToSassApp]]);
});

test('does not track dependant changes when isDev=false', async () => {
const p = plugin(null, {});
p.markChanged = jest.fn();
await p.load({filePath: pathToSassApp, isDev: false});
p.onChange({filePath: pathToSassApp});
p.onChange({filePath: pathToSassBase});
expect(p.markChanged.mock.calls).toEqual([]);
});

test('uses native sass CLI when native option = true', async () => {
const p = plugin(null, {native: true});
process.env.PATH = '';
expect(p.load({filePath: pathToSassApp, isDev: false})).rejects.toThrow('EPIPE');
});
});

0 comments on commit 68e8d77

Please sign in to comment.