Skip to content

Commit 28bc315

Browse files
jeremyzahnereddyerburgh
authored andcommitted
feat: improved support for SCSS and SASS (#82)
1 parent f9516a8 commit 28bc315

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+496
-102
lines changed

README.md

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@ Jest Vue transformer with source map support
44

55
## Usage
66

7-
```
7+
```bash
88
npm install --save-dev vue-jest
99
```
1010

1111
## Setup
1212

1313
To define vue-jest as a transformer for your .vue files, you need to map .vue files to the vue-jest module.
1414

15-
```
16-
"transform": {
17-
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
18-
},
15+
```json
16+
{
17+
"jest": {
18+
"transform": {
19+
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
20+
}
21+
}
1922
```
2023

2124
A full config will look like this.
@@ -47,7 +50,7 @@ Example repositories testing Vue components with jest and vue-jest:
4750

4851
## Supported langs
4952

50-
vue-jest compiles the script and template of SFCs into a JavaScript file that Jest can run. **Currently, SCSS and Stylus are the only style languages that are compiled**.
53+
vue-jest compiles the script and template of SFCs into a JavaScript file that Jest can run. **Currently, SCSS, SASS and Stylus are the only style languages that are compiled**.
5154

5255
### Supported script languages
5356

@@ -59,8 +62,8 @@ vue-jest compiles the script and template of SFCs into a JavaScript file that Je
5962
- **pug** (`lang="pug"`)
6063
- To give options for the Pug compiler, enter them into the Jest configuration.
6164
The options will be passed to pug.compile().
62-
```js
63-
// package.json
65+
66+
```json
6467
{
6568
"jest": {
6669
"globals": {
@@ -72,41 +75,44 @@ vue-jest compiles the script and template of SFCs into a JavaScript file that Je
7275
}
7376
}
7477
}
75-
```
78+
```
79+
7680
- **jade** (`lang="jade"`)
7781
- **haml** (`lang="haml"`)
7882

7983
### Supported style languages
8084

8185
- **stylus** (`lang="stylus"`, `lang="styl"`)
86+
- **sass** (`lang="sass"`)
87+
- The SASS compiler supports jest's [moduleNameMapper](https://facebook.github.io/jest/docs/en/configuration.html#modulenamemapper-object-string-string) which is the suggested way of dealing with Webpack aliases.
8288
- **scss** (`lang="scss"`)
89+
- The SCSS compiler supports jest's [moduleNameMapper](https://facebook.github.io/jest/docs/en/configuration.html#modulenamemapper-object-string-string) which is the suggested way of dealing with Webpack aliases.
8390
- To import globally included files (ie. variables, mixins, etc.), include them in the Jest configuration at `jest.globals['vue-jest'].resources.scss`:
84-
```js
85-
// package.json
86-
{
87-
"jest": {
88-
"globals": {
89-
"vue-jest": {
90-
"resources": {
91-
"scss": [
92-
"./node_modules/package/_mixins.scss",
93-
"./src/assets/css/globals.scss"
94-
]
91+
92+
```json
93+
{
94+
"jest": {
95+
"globals": {
96+
"vue-jest": {
97+
"resources": {
98+
"scss": [
99+
"./node_modules/package/_mixins.scss",
100+
"./src/assets/css/globals.scss"
101+
]
102+
}
95103
}
96104
}
97105
}
98106
}
99-
}
100-
```
107+
```
101108

102-
## CSS options
109+
## CSS options
103110

104111
`experimentalCSSCompile`: `Boolean` Default true. Turn off CSS compilation
105112
`hideStyleWarn`: `Boolean` Default false. Hide warnings about CSS compilation
106113
`resources`:
107114

108-
```js
109-
// package.json
115+
```json
110116
{
111117
"jest": {
112118
"globals": {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const path = require('path')
2+
3+
/**
4+
* Resolves the path to the file locally.
5+
*
6+
* @param {String} to - the name of the file to resolve to
7+
* @param {String} localPath - the local path
8+
* @returns {String} path - path to the file to import
9+
*/
10+
module.exports = function localResolve (to, localPath) {
11+
if (localPath.startsWith('/')) {
12+
return localPath
13+
}
14+
return path.join(path.dirname(to), localPath)
15+
}
16+
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const localResolve = require('./local-resolve-helper')
2+
3+
/**
4+
* Applies the moduleNameMapper substitution from the jest config
5+
*
6+
* @param {String} source - the original string
7+
* @param {String} filePath - the path of the current file (where the source originates)
8+
* @param {Object} jestConfig - the jestConfig holding the moduleNameMapper settings
9+
* @returns {String} path - the final path to import (including replacements via moduleNameMapper)
10+
*/
11+
module.exports = function applyModuleNameMapper (source, filePath, jestConfig = {}) {
12+
if (!jestConfig.moduleNameMapper) return source
13+
14+
// Extract the moduleNameMapper settings from the jest config. TODO: In case of development via babel@7, somehow the jestConfig.moduleNameMapper might end up being an Array. After a proper upgrade to babel@7 we should probably fix this.
15+
const module = Array.isArray(jestConfig.moduleNameMapper) ? jestConfig.moduleNameMapper : Object.entries(jestConfig.moduleNameMapper)
16+
17+
const importPath = module
18+
.reduce((acc, [regex, replacement]) => {
19+
const matches = acc.match(regex)
20+
21+
if (matches === null) {
22+
return acc
23+
}
24+
25+
return replacement.replace(
26+
/\$([0-9]+)/g,
27+
(_, index) => matches[parseInt(index, 10)]
28+
)
29+
}, source)
30+
31+
return localResolve(
32+
filePath,
33+
importPath
34+
)
35+
}
36+

lib/compilers/sass-compiler.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const ensureRequire = require('../ensure-require')
2+
const getVueJestConfig = require('../get-vue-jest-config')
3+
const logger = require('../logger')
4+
5+
const applyModuleNameMapper = require('./helpers/module-name-mapper-helper')
6+
7+
/**
8+
* This module is meant to compile sass
9+
*
10+
* @param {String} content - the content of the sass string that should be compiled
11+
* @param {String} filePath - the path of the file holding the sass
12+
* @param {Object} jestConfig - the complete jest config
13+
* @returns {String} styles - the compiled sass
14+
*/
15+
module.exports = (content, filePath, jestConfig = {}) => {
16+
const vueJestConfig = getVueJestConfig(jestConfig)
17+
18+
ensureRequire('sass', ['node-sass'])
19+
const sass = require('node-sass')
20+
21+
try {
22+
return sass.renderSync({
23+
data: content,
24+
outputStyle: 'compressed',
25+
indentedSyntax: true,
26+
importer: (url, prev, done) => ({ file: applyModuleNameMapper(url, prev === 'stdin' ? filePath : prev, jestConfig) })
27+
}).css.toString()
28+
} catch (err) {
29+
if (!vueJestConfig.hideStyleWarn) {
30+
logger.warn(`There was an error rendering the SASS in ${filePath}. SASS is fully supported by vue-jest. Still some features might throw errors. Webpack aliases are a common cause of errors. If you use Webpack aliases, please use jest's suggested way via moduleNameMapper which is supported.`)
31+
logger.warn(`Error while compiling styles: ${err}`)
32+
}
33+
}
34+
35+
return ''
36+
}

lib/compilers/scss-compiler.js

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,46 @@
11
const path = require('path')
22
const fs = require('fs')
33
const ensureRequire = require('../ensure-require')
4-
const cwd = process.cwd()
4+
const getVueJestConfig = require('../get-vue-jest-config')
55
const logger = require('../logger')
66

7-
const rewriteImports = (content, filePath) => content.replace(/@import\s+(?:'([^']+)'|"([^"]+)"|([^\s;]+))/g, (entire, single, double, unquoted) => {
8-
const oldImportPath = single || double || unquoted
9-
const absoluteImportPath = path.join(path.dirname(filePath), oldImportPath)
10-
const lastCharacter = entire[entire.length - 1]
11-
const quote = lastCharacter === "'" || lastCharacter === '"' ? lastCharacter : ''
12-
const importPath = path.relative(cwd, absoluteImportPath)
13-
return '@import ' + quote + importPath + quote
14-
})
7+
const applyModuleNameMapper = require('./helpers/module-name-mapper-helper')
8+
9+
/**
10+
* This module is meant to compile scss
11+
*
12+
* @param {String} content - the content of the scss string that should be compiled
13+
* @param {String} filePath - the path of the file holding the scss
14+
* @param {Object} jestConfig - the complete jest config
15+
* @returns {String} styles - the compiled scss
16+
*/
17+
module.exports = (content, filePath, jestConfig = {}) => {
18+
const vueJestConfig = getVueJestConfig(jestConfig)
1519

16-
module.exports = (content, filePath, config) => {
1720
ensureRequire('scss', ['node-sass'])
1821
const sass = require('node-sass')
1922

2023
let scssResources = ''
21-
if (config && config.resources && config.resources.scss) {
22-
scssResources = config.resources.scss
24+
if (vueJestConfig.resources && vueJestConfig.resources.scss) {
25+
scssResources = vueJestConfig.resources.scss
2326
.map(scssResource => path.resolve(process.cwd(), scssResource))
2427
.filter(scssResourcePath => fs.existsSync(scssResourcePath))
25-
.map(scssResourcePath => rewriteImports(fs.readFileSync(scssResourcePath).toString(), scssResourcePath))
28+
.map(scssResourcePath => fs.readFileSync(scssResourcePath).toString())
2629
.join('\n')
2730
}
28-
let result
31+
2932
try {
30-
result = sass.renderSync({
31-
data: scssResources + rewriteImports(content, filePath),
32-
outputStyle: 'compressed'
33+
return sass.renderSync({
34+
data: scssResources + content,
35+
outputStyle: 'compressed',
36+
importer: (url, prev, done) => ({ file: applyModuleNameMapper(url, prev === 'stdin' ? filePath : prev, jestConfig) })
3337
}).css.toString()
3438
} catch (err) {
35-
config.hideStyleWarn &&
36-
logger.warn(`There was an error rendering the SCSS in ${filePath}. SCSS is not fully supported by vue-jest, so some features will throw errors. Webpack aliases are a common cause of errors.`)
39+
if (!vueJestConfig.hideStyleWarn) {
40+
logger.warn(`There was an error rendering the SCSS in ${filePath}. SCSS is fully supported by vue-jest. Still some features might throw errors. Webpack aliases are a common cause of errors. If you use Webpack aliases, please use jest's suggested way via moduleNameMapper which is supported.`)
41+
logger.warn(`Error while compiling styles: ${err}`)
42+
}
3743
}
38-
return result || ''
44+
45+
return ''
3946
}

lib/compilers/stylus-compiler.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const stylus = require('stylus')
22
const path = require('path')
33

4-
module.exports = (content, filePath, config) => stylus.render(
4+
module.exports = (content, filePath, jestConfig) => stylus.render(
55
content, {
66
paths: [path.dirname(filePath), process.cwd()]
77
}

lib/get-vue-jest-config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* This module extracts vue-jest relevant parts of a jest config
3+
*
4+
* @param {Object} jestConfig - a complete jest config object
5+
* @returns {Object} vueJestConfig - an object holding vue-jest specific configuration
6+
*/
7+
module.exports = function getVueJestConfig (jestConfig) {
8+
return (jestConfig && jestConfig.globals && jestConfig.globals['vue-jest']) || {}
9+
}

lib/logger.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module.exports.info = function info (msg) {
22
console.info('\n[vue-jest]: ' + (msg) + '\n')
33
}
44

5-
module.exports.warn = function info (msg) {
5+
module.exports.warn = function warn (msg) {
66
console.warn('\n[vue-jest]: ' + (msg) + '\n')
77
}
88

lib/process-style.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
const getVueJestConfig = require('./get-vue-jest-config')
12
const cssExtract = require('extract-from-css')
23

3-
module.exports = function processStyle (stylePart, filePath, config = {}) {
4-
if (!stylePart || config.experimentalCSSCompile === false) {
4+
module.exports = function processStyle (stylePart, filePath, jestConfig = {}) {
5+
const vueJestConfig = getVueJestConfig(jestConfig)
6+
7+
if (!stylePart || vueJestConfig.experimentalCSSCompile === false) {
58
return {}
69
}
7-
const processStyleByLang = lang => require('./compilers/' + lang + '-compiler')(stylePart.content, filePath, config)
10+
11+
const processStyleByLang = lang => require('./compilers/' + lang + '-compiler')(stylePart.content, filePath, jestConfig)
812

913
let cssCode = stylePart.content
1014
switch (stylePart.lang) {
@@ -15,6 +19,9 @@ module.exports = function processStyle (stylePart, filePath, config = {}) {
1519
case 'scss':
1620
cssCode = processStyleByLang('scss')
1721
break
22+
case 'sass':
23+
cssCode = processStyleByLang('sass')
24+
break
1825
}
1926

2027
const cssNames = cssExtract.extractClasses(cssCode)

lib/process.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const compileTypescript = require('./compilers/typescript-compiler')
77
const compileCoffeeScript = require('./compilers/coffee-compiler')
88
const extractPropsFromFunctionalTemplate = require('./extract-props')
99
const processStyle = require('./process-style')
10+
const getVueJestConfig = require('./get-vue-jest-config')
1011
const fs = require('fs')
1112
const path = require('path')
1213
const join = path.join
@@ -40,7 +41,7 @@ function changePartsIfFunctional (parts) {
4041
}
4142

4243
module.exports = function (src, filePath, jestConfig) {
43-
const config = (jestConfig && jestConfig.globals && jestConfig.globals['vue-jest']) || {}
44+
const vueJestConfig = getVueJestConfig(jestConfig)
4445

4546
var parts = vueCompiler.parseComponent(src, { pad: true })
4647

@@ -74,7 +75,7 @@ module.exports = function (src, filePath, jestConfig) {
7475
parts.template.content = fs.readFileSync(parts.template.filename, 'utf8')
7576
}
7677

77-
const renderFunctions = compileTemplate(parts.template, config)
78+
const renderFunctions = compileTemplate(parts.template, vueJestConfig)
7879

7980
output += '__vue__options__.render = ' + renderFunctions.render + '\n' +
8081
'__vue__options__.staticRenderFns = ' + renderFunctions.staticRenderFns + '\n'
@@ -86,17 +87,17 @@ module.exports = function (src, filePath, jestConfig) {
8687
}
8788

8889
if (Array.isArray(parts.styles) && parts.styles.length > 0) {
89-
if ((parts.styles.some(ast => /^sass|less|pcss|postcss/.test(ast.lang))) && logger.shouldLogStyleWarn) {
90-
!config.hideStyleWarn && logger.warn('Sass, Less and PostCSS are not currently compiled by vue-jest')
90+
if ((parts.styles.some(ast => /^less|pcss|postcss/.test(ast.lang))) && logger.shouldLogStyleWarn) {
91+
!vueJestConfig.hideStyleWarn && logger.warn('Less and PostCSS are not currently compiled by vue-jest')
9192
logger.shouldLogStyleWarn = false
9293
}
9394

9495
const styleStr = parts.styles
95-
.filter(ast => module && ast.module)
96+
.filter(ast => ast.module)
9697
.map(ast => {
97-
const styleObj = (/^sass|less|pcss|postcss/.test(ast.lang))
98+
const styleObj = (/^less|pcss|postcss/.test(ast.lang))
9899
? {}
99-
: processStyle(ast, filePath, config)
100+
: processStyle(ast, filePath, jestConfig)
100101

101102
const moduleName = ast.module === true ? '$style' : ast.module
102103

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@
7979
"^.+\\.js$": "<rootDir>/node_modules/babel-jest",
8080
".*\\.(vue)$": "<rootDir>/vue-jest.js"
8181
},
82-
"mapCoverage": true
82+
"mapCoverage": true,
83+
"moduleNameMapper": {
84+
"^~?__root/(.*)$": "<rootDir>/$1",
85+
"^~?__test/(.*)$": "<rootDir>/test/$1"
86+
}
8387
},
8488
"repository": {
8589
"type": "git",

test/jade.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ import Jade from './resources/Jade.vue'
44
test('processes .vue file with jade template', () => {
55
const wrapper = shallow(Jade)
66
expect(wrapper.is('div')).toBe(true)
7-
expect(wrapper.hasClass('jade')).toBe(true)
7+
expect(wrapper.classes()).toContain('jade')
88
})

0 commit comments

Comments
 (0)