Skip to content

Commit

Permalink
Incorporate strategies for working alongside preprocessors.
Browse files Browse the repository at this point in the history
Closes ember-cli#34.
  • Loading branch information
dfreeman committed Jul 12, 2016
1 parent abafc1c commit 8413e99
Show file tree
Hide file tree
Showing 21 changed files with 241 additions and 18 deletions.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,51 @@ new EmberApp(defaults, {
});
```

### Other Preprocessors

There are two approaches to integrating CSS modules with other style preprocessors like Sass, Less or Stylus.

#### Modules and preprocessor syntax in isolation

The first approach is to use PostCSS to perform any processing on the modules themselves, and then emit a single vanilla CSS file with those modules that you can then import into your preprocessor of choice. This keeps your modules and other styles in isolation from one another, but provides a nice migration path from another preprocessor to PostCSS + modules.

For example, with Sass you could install ember-cli-sass and then configure ember-css-modules to emit a `_modules` partial:

```js
cssModules: {
intermediateOutputPath: 'app/styles/_modules.scss'
}
```

And then in your `app.scss`, simply import it:

```scss
// other Sass code and imports
@import 'modules';
```

#### Custom syntax directly in modules

The second approach is viable for preprocessors for which there is a PostCSS syntax extension, such as [Sass](https://github.com/postcss/postcss-scss) and (at least partially) [Less](https://github.com/gilt/postcss-less). It allows for using custom preprocessor syntax directly in CSS modules, handing off the concatenated final output directly to the preprocessor.

Again using Sass as an example, you would specify `app.scss` as your intermediate output file so that ember-cli-sass would pick it up directly, and then tell ember-css-modules to look for `.scss` files and pass through custom PostCSS syntax configuration.

```js
cssModules: {
// Emit a combined SCSS file for ember-cli-sass to compile
intermediateOutputPath: 'app/styles/app.scss',

// Use .scss as the extension for CSS modules instead of the default .css
extension: 'scss',

// Pass a custom parser/stringifyer through to PostCSS for processing modules
postcssOptions: {
syntax: require('postcss-scss')
}
}
```


## Ember Support

This addon is tested against and expected to work with Ember 1.13.x, as well as the current 2.x release, beta, and canary builds.
14 changes: 13 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module.exports = {

shouldIncludeChildAddon: function(addon) {
// Don't infinitely recurse – it's the dummy test app that depends on dummy-addon, not this addon itself
return addon.name !== 'dummy-addon';
return addon.name.indexOf('dummy') === -1;
},

init: function() {
Expand Down Expand Up @@ -51,10 +51,22 @@ module.exports = {
return this.modulesPreprocessor.getDependencies();
},

getIntermediateOutputPath: function() {
return this.options.intermediateOutputPath;
},

getPlugins: function() {
return this.options.plugins || [];
},

getFileExtension: function() {
return this.options && this.options.extension || 'css';
},

getPostcssOptions: function() {
return this.options.postcssOptions;
},

belongsToAddon: function() {
return !!this.parent.parent;
}
Expand Down
26 changes: 19 additions & 7 deletions lib/modules-preprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ module.exports = ModulesPreprocessor;

function ModulesPreprocessor(options) {
this.owner = options.owner;
this.ext = this.getExtension();
this.modulesTree = null;
}

Expand All @@ -36,11 +35,12 @@ ModulesPreprocessor.prototype.toTree = function(inputTree, path) {
// Hack: manually exclude stuff in tests/modules because of https://github.com/ember-cli/ember-cli-qunit/pull/96
var modulesSources = new Funnel(inputWithStyles, {
exclude: ['**/tests/modules/**'],
include: ['**/*.css']
include: ['**/*.' + this.owner.getFileExtension()]
});

this.modulesTree = new CSSModules(modulesSources, {
plugins: this.getPlugins(),
postcssOptions: this.owner.getPostcssOptions(),
generateScopedName: this.owner.getScopedNameGenerator(),
resolvePath: this.resolveAndRecordPath.bind(this),
onProcessFile: this.resetFileDependencies.bind(this),
Expand All @@ -50,7 +50,7 @@ ModulesPreprocessor.prototype.toTree = function(inputTree, path) {
var merged = new MergeTrees([inputWithStyles, this.modulesTree], { overwrite: true });

// Exclude the individual CSS files – those will be concatenated into the styles tree later
return new Funnel(merged, { exclude: ['**/*.css'] });
return new Funnel(merged, { exclude: ['**/*.' + this.owner.getFileExtension()] });
};

// This is gross, but we don't have a way to treat stuff in /app/styles uniformly with everything else in /app
Expand All @@ -62,13 +62,24 @@ ModulesPreprocessor.prototype.inputTreeWithStyles = function(inputTree) {
return new MergeTrees([inputTree, appStyles], { overwrite: true });
};

Object.defineProperty(ModulesPreprocessor.prototype, 'ext', {
get: function() {
return this.getFileExtension();
}
});

/*
* When processing an addon, CSS won't be included unless `.css` is specified as the extension. On the other hand,
* we'll get the CSS regardless when processing apps, but registering as a `.css` processor will cause terrible things
* to happen when `app.import`ing a CSS file.
*/
ModulesPreprocessor.prototype.getExtension = function() {
return this.owner.belongsToAddon() ? 'css' : null;
ModulesPreprocessor.prototype.getFileExtension = function() {
var extension = this.owner.getFileExtension();
if (extension === 'css') {
return this.owner.belongsToAddon() ? 'css' : null;
} else {
return extension;
}
};

ModulesPreprocessor.prototype.resetFileDependencies = function(filePath) {
Expand Down Expand Up @@ -107,18 +118,19 @@ ModulesPreprocessor.prototype.recordDependencies = function(fromFile, type, reso

// Records dependencies from `composes` and `@value` imports
ModulesPreprocessor.prototype.resolveAndRecordPath = function(importPath, fromFile) {
var resolved = resolvePath(importPath, fromFile);
var resolved = resolvePath(importPath, fromFile, this.owner.getFileExtension());
this.recordDependencies(fromFile, 'implicit', [resolved]);
return resolved;
};

// Records explicit `@after-module` dependency declarations
ModulesPreprocessor.prototype.dependenciesPlugin = function() {
var recordDependencies = this.recordDependencies.bind(this);
var extension = this.owner.getFileExtension();
return explicitOrdering({
updateDependencies: function(fromFile, relativePaths) {
recordDependencies(fromFile, 'explicit', relativePaths.map(function(relativePath) {
return resolvePath(relativePath, fromFile);
return resolvePath(relativePath, fromFile, extension);
}));
}
});
Expand Down
15 changes: 12 additions & 3 deletions lib/output-styles-preprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
var debug = require('debug')('ember-css-modules:output-styles-preprocessor');
var toposort = require('toposort');
var Concat = require('broccoli-concat');
var MergeTrees = require('broccoli-merge-trees');

module.exports = OutputStylesPreprocessor;

Expand All @@ -13,16 +14,24 @@ function OutputStylesPreprocessor(options) {

OutputStylesPreprocessor.prototype.constructor = OutputStylesPreprocessor;
OutputStylesPreprocessor.prototype.toTree = function(inputNode, inputPath, outputDirectory, options) {
var outputFile = options.outputPaths[this.owner.belongsToAddon() ? 'addon' : 'app'];
var outputFile = this.owner.getIntermediateOutputPath() || options.outputPaths[this.owner.belongsToAddon() ? 'addon' : 'app'];
var concatOptions = {
inputFiles: ['**/*.css'],
inputFiles: ['**/*.' + this.owner.getFileExtension()],
outputFile: outputFile,
allowNone: true
};

debug('concatenating module stylesheets: %o', concatOptions);

return this.dynamicHeaderConcat(concatOptions);
var concat = this.dynamicHeaderConcat(concatOptions);

// If an intermediate output path is specified, we need to pass through the full contents of the styles tree
// and trust that a subsequent preprocessor will appropriately filter out everything else.
if (this.owner.getIntermediateOutputPath()) {
return new MergeTrees([inputNode, concat], { overwrite: true });
} else {
return concat;
}
};

/*
Expand Down
4 changes: 2 additions & 2 deletions lib/resolve-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
var path = require('path');

// This doesn't allow imports from other addons, but it at least gives the ability to specify paths relative to the root
module.exports = function resolvePath(importPath, fromFile) {
var pathWithExtension = path.extname(importPath) ? importPath : (importPath + '.css');
module.exports = function resolvePath(importPath, fromFile, defaultExtension) {
var pathWithExtension = path.extname(importPath) ? importPath : (importPath + '.' + defaultExtension);

if (isRelativePath(pathWithExtension)) {
return path.resolve(path.dirname(fromFile), pathWithExtension);
Expand Down
17 changes: 13 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"scripts": {
"build": "ember build",
"start": "ember server",
"test": "npm run test-lib && npm run test-ember",
"install-all": "for dummy in tests/dummy-*-addon; do cd $dummy && npm install && cd ../..; done",
"test": "npm run install-all && npm run test-lib && npm run test-ember",
"test-ember": "ember try:each",
"test-lib": "tape tests-node/**/*-test.js"
},
Expand All @@ -23,6 +24,7 @@
},
"homepage": "https://github.com/salsify/ember-css-modules#readme",
"devDependencies": {
"broccoli-css-modules": "^0.3.4",
"ember-ajax": "^2.4.1",
"ember-cli": "^2.6.2",
"ember-cli-app-version": "^1.0.0",
Expand All @@ -48,7 +50,7 @@
],
"dependencies": {
"broccoli-concat": "^2.1.0",
"broccoli-css-modules": "^0.3.2",
"broccoli-css-modules": "^0.3.3",
"broccoli-funnel": "^1.0.1",
"broccoli-merge-trees": "^1.1.1",
"debug": "^2.2.0",
Expand All @@ -60,12 +62,19 @@
},
"ember-addon": {
"configPath": "tests/dummy/config",
"before": "ember-cli-babel",
"before": [
"ember-cli-babel",
"ember-cli-less",
"ember-cli-sass",
"ember-cli-stylus"
],
"versionCompatibility": {
"ember": ">=1.13.0 <3.0.0"
},
"paths": [
"tests/dummy-addon"
"tests/dummy-addon",
"tests/dummy-sass-addon",
"tests/dummy-less-addon"
]
}
}
4 changes: 3 additions & 1 deletion tests/acceptance/styles-lookup-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ var componentRoutes = [
'pod-route/nested-pod-template-only-component',
'component-with-explicit-styles-reference',
'component-with-global-class-composition',
'component-with-container-class'
'component-with-container-class',
'sass-addon-component',
'less-addon-component'
].map(name => [name, `/render-component/${name}`]);

var controllerRoutes = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Ember from 'ember';
import layout from '../templates/components/less-addon-component';
import styles from '../styles/components/less-addon-component';

export default Ember.Component.extend({
layout,
styles
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@value --addon-family from '../constants';

/*
TODO use mixin when postcss-modules-local-by-default#60 is available
.local-less-mixin() {
font-family: --addon-family;
}
*/

.component-class {
font-family: --addon-family;
// .local-less-mixin;
}
1 change: 1 addition & 0 deletions tests/dummy-less-addon/addon/styles/constants.less
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@value --addon-family: 'less-addon-component';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="{{styles.component-class}}" data-test-element>less addon component</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'dummy-less-addon/components/less-addon-component';
27 changes: 27 additions & 0 deletions tests/dummy-less-addon/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
var generateScopedName = require('../../lib/generate-scoped-name');

/*jshint node:true*/
module.exports = {
name: 'dummy-less-addon',

options: {
cssModules: {
extension: 'less',
intermediateOutputPath: 'addon.less',
postcssOptions: {
syntax: require('postcss-less')
},
generateScopedName: function(className, modulePath) {
return '_less_addon' + generateScopedName(className, modulePath);
}
}
},

hintingEnabled: function() {
return false;
},

isDevelopingAddon: function() {
return true;
}
};
18 changes: 18 additions & 0 deletions tests/dummy-less-addon/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "dummy-less-addon",
"private": true,
"keywords": [
"ember-addon"
],
"ember-addon": {
"paths": [
"../..",
"../../node_modules/ember-cli-babel",
"../../node_modules/ember-cli-htmlbars"
]
},
"dependencies": {
"ember-cli-less": "^1.5.3",
"postcss-less": "^0.12.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Ember from 'ember';
import layout from '../templates/components/sass-addon-component';
import styles from '../styles/components/sass-addon-component';

export default Ember.Component.extend({
layout,
styles
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@value --addon-family from '../constants';

@mixin sass-addon-component {
font-family: --addon-family;
}

.component-class {
@include sass-addon-component;
}
1 change: 1 addition & 0 deletions tests/dummy-sass-addon/addon/styles/constants.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@value --addon-family: 'sass-addon-component';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="{{styles.component-class}}" data-test-element>sass addon component</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'dummy-sass-addon/components/sass-addon-component';
27 changes: 27 additions & 0 deletions tests/dummy-sass-addon/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
var generateScopedName = require('../../lib/generate-scoped-name');

/*jshint node:true*/
module.exports = {
name: 'dummy-sass-addon',

options: {
cssModules: {
extension: 'scss',
intermediateOutputPath: 'addon.scss',
postcssOptions: {
syntax: require('postcss-scss')
},
generateScopedName: function(className, modulePath) {
return '_sass_addon' + generateScopedName(className, modulePath);
}
}
},

hintingEnabled: function() {
return false;
},

isDevelopingAddon: function() {
return true;
}
};
Loading

0 comments on commit 8413e99

Please sign in to comment.