├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src ├── cli.js └── index.js ├── test ├── fixtures-json5 │ ├── array │ │ ├── style.scss │ │ └── variables.json5 │ ├── convert-case │ │ ├── style.scss │ │ └── variables.json5 │ ├── empty-string │ │ ├── style.scss │ │ └── variables.json5 │ ├── include-paths │ │ ├── style.scss │ │ └── variables │ │ │ └── variables.json5 │ ├── invalid-variables │ │ ├── style.scss │ │ └── variables.json5 │ ├── lists │ │ ├── style.scss │ │ └── variables.json5 │ ├── maps │ │ ├── style.scss │ │ └── variables.json5 │ ├── non-json │ │ ├── style.scss │ │ └── variables.scss │ └── strings │ │ ├── style.scss │ │ └── variables.json5 ├── fixtures │ ├── array │ │ ├── style.scss │ │ └── variables.json │ ├── convert-case │ │ ├── style.scss │ │ └── variables.json │ ├── empty-string │ │ ├── style.scss │ │ └── variables.json │ ├── include-paths │ │ ├── style.scss │ │ └── variables │ │ │ └── variables.json │ ├── invalid-variables │ │ ├── style.scss │ │ └── variables.json │ ├── lists │ │ ├── style.scss │ │ └── variables.json │ ├── maps │ │ ├── style.scss │ │ └── variables.json │ ├── non-json │ │ ├── style.scss │ │ └── variables.scss │ └── strings │ │ ├── style.scss │ │ └── variables.json └── index.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env" 5 | ] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | rules: 2 | indent: 3 | - 2 4 | - 2 5 | quotes: 6 | - 2 7 | - single 8 | linebreak-style: 9 | - 2 10 | - unix 11 | semi: 12 | - 2 13 | - always 14 | env: 15 | es6: true 16 | node: true 17 | extends: 'eslint:recommended' 18 | parser: 'babel-eslint' 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | *.sublime* 4 | .idea/ 5 | *.iml 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src 2 | /test 3 | .editorconfig 4 | .gitignore 5 | .jshintrc 6 | .travis.yml 7 | *.sublime* 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 13 4 | notifications: 5 | email: false 6 | before_install: 7 | - curl -o- -L https://yarnpkg.com/install.sh | bash 8 | - export PATH="$HOME/.yarn/bin:$PATH" 9 | after_success: 10 | - "[[ $TRAVIS_PULL_REQUEST != 'false' ]] && npx semantic-release-github-pr" 11 | - npx semantic-release --debug 12 | cache: yarn 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## *Deprecated* 4 | For versions above **4.1.0**, please refer to "Releases" in GitHub UI. 5 | 6 | ## 4.1.0 7 | 8 | ## 4.0.1 9 | * Update package.json's main field (#73). 10 | 11 | ## 4.0.0 12 | * Add resolver option (ability to override how json file paths gets resolved) (#71). 13 | 14 | **BBREAKING CHANGE:** 15 | 16 | The importer is now a function, accepting an options object, instead of an object: `jsonImporter -> jsonImporter()` 17 | 18 | ## 3.3.1 19 | * Remove support for one element lists. `3.3.0` broke parsing of empty lists (#67). 20 | 21 | ## 3.3.0 22 | * Add support for one element lists. 23 | 24 | > Comma-separated lists may have a trailing comma. This is especially useful because it allows you to represent a single-element list. For example, (1,) is a list containing 1 and (1 2 3,) is a comma-separated list containing a space-separated list containing 1, 2, and 3. 25 | 26 | https://sass-lang.com/documentation/file.SASS_REFERENCE.html#lists 27 | 28 | ## 3.2.0 29 | * Allow importing JSON as a top-level array. 30 | 31 | ## 3.1.6 32 | * Filter out `#` as value for a variable 33 | 34 | ## 3.1.5 35 | * Reverts `3.1.4`. We aren't able to find a way to support automatic handling of values containing `,` that isn't full of edge cases. The recommendation remains to wrap such values in single quotes if they're meant to be interpreted as strings. 36 | 37 | ## 3.1.4 38 | * Convert values containing commas inside of an object into strings 39 | 40 | ## 3.1.3 41 | * Extend key filtering to nested maps 42 | 43 | ## 3.1.2 44 | * Filter out invalid variable names to prevent Sass compiler from crashing 45 | 46 | ## 3.1.1 47 | * Return empty strings correctly to prevent Sass compiler from crashing 48 | 49 | ## 3.1.0 50 | * Add support for `.json5` files 51 | 52 | ## 3.0.2 53 | ##### Fixes 54 | * Fix `includePaths` option for Windows users by using the environment's delimiter instead of harcoding unix's. 55 | 56 | ## 3.0.1 57 | ##### Fixes 58 | * Update `node-sass` dependency versions from `^3.5.3` to `>=3.5.3` to allow using 4.x and above without triggering npm warnings. 59 | * Add `yarn.lock`. 60 | 61 | ## 3.0.0 62 | ##### (Possibly) Breaking 63 | * If parsing errors out, catch and return a well formed `Error` as per [`node-sass` best-practices for importers](https://github.com/sass/node-sass/blob/dc92c18e1dffd4acbab69e76c4bcda238f52da27/README.md#importer--v200---experimental) (see "Starting from v3.0.0:" section). 64 | 65 | ## 2.1.1 66 | ##### Fixes 67 | * Fix 2.1.0 breaking the default export for CommonJS. 68 | 69 | ## 2.1.0 70 | ##### Features 71 | * Export internal methods that compose the importer. E.g. `transformJSONtoSass` can now be used independently of `node-sass` to transform parsed JSON into Sass. 72 | 73 | ## 2.0.0 74 | ##### Breaking 75 | * Add `node-sass` `^3.5.3` as a `peerDependency`. 76 | 77 | ##### Fixes 78 | * Return plain `null` instead of `sass.NULL` when not handling an import per [updated guidelines](https://github.com/sass/node-sass/blob/master/README.md#importer--v200---experimental). https://github.com/sass/node-sass/issues/1291 79 | 80 | ## 1.0.6 81 | ##### Fixes 82 | * Invalidate `require` cache on each importer run. 83 | 84 | ## 1.0.5 85 | ##### Fixes 86 | * Return `sass.NULL` when not handling an import per [`node-sass` guidelines](https://github.com/sass/node-sass/blob/fe8dbae1ddbbb602bf508d63b558a003f496f9b6/README.md#importer--v200---experimental). 87 | 88 | ## 1.0.4 89 | ##### Fixes 90 | * Revert attempting to wrap strings with spaces/commas (wrap strings in extra quotes instead). 91 | 92 | ## 1.0.3 93 | ##### Fixes 94 | * Fix importing strings with spaces/commas breaking. **Reverted in 1.0.4** 95 | 96 | ## 1.0.2 97 | ##### Fixes 98 | * Fix `includePaths` not working with multiple entries. 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Updater 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-sass-json-importer 2 | 3 | JSON importer for [node-sass](https://github.com/sass/node-sass). Allows `@import`ing `.json` or `.json5` files in Sass files parsed by `node-sass`. 4 | 5 | [![npm](https://img.shields.io/npm/v/node-sass-json-importer.svg)](https://www.npmjs.com/package/node-sass-json-importer) 6 | [![build status](https://travis-ci.org/pmowrer/node-sass-json-importer.svg?branch=master)](https://travis-ci.org/Updater/node-sass-json-importer) 7 | 8 | ## Usage 9 | ### [node-sass](https://github.com/sass/node-sass) 10 | This module hooks into [node-sass's importer api](https://github.com/sass/node-sass#importer--v200---experimental). 11 | 12 | ```javascript 13 | var sass = require('node-sass'); 14 | var jsonImporter = require('node-sass-json-importer'); 15 | 16 | // Example 1 17 | sass.render({ 18 | file: scss_filename, 19 | importer: jsonImporter(), 20 | [, options..] 21 | }, function(err, result) { /*...*/ }); 22 | 23 | // Example 2 24 | var result = sass.renderSync({ 25 | data: scss_content 26 | importer: [jsonImporter(), someOtherImporter] 27 | [, options..] 28 | }); 29 | ``` 30 | 31 | ### [node-sass](https://github.com/sass/node-sass) command-line interface 32 | 33 | To run this using node-sass CLI, point `--importer` to your installed json importer, for example: 34 | 35 | ```sh 36 | ./node_modules/.bin/node-sass --importer node_modules/node-sass-json-importer/dist/cli.js --recursive ./src --output ./dist 37 | ``` 38 | 39 | ### Webpack / [sass-loader](https://github.com/jtangelder/sass-loader) 40 | 41 | #### Webpack v1 42 | 43 | ```javascript 44 | import jsonImporter from 'node-sass-json-importer'; 45 | 46 | // Webpack config 47 | export default { 48 | module: { 49 | loaders: [{ 50 | test: /\.scss$/, 51 | loaders: ["style", "css", "sass"] 52 | }], 53 | }, 54 | // Apply the JSON importer via sass-loader's options. 55 | sassLoader: { 56 | importer: jsonImporter() 57 | } 58 | }; 59 | ``` 60 | 61 | #### Webpack v2 62 | 63 | ```javascript 64 | import jsonImporter from 'node-sass-json-importer'; 65 | 66 | // Webpack config 67 | export default { 68 | module: { 69 | rules: [ 70 | { 71 | test: /\.scss$/, 72 | use: [ 73 | 'style-loader', 74 | { 75 | loader: 'css-loader', 76 | options: { 77 | importLoaders: 1 78 | }, 79 | }, 80 | { 81 | loader: 'sass-loader', 82 | // Apply the JSON importer via sass-loader's options. 83 | options: { 84 | importer: jsonImporter(), 85 | }, 86 | }, 87 | }, 88 | ], 89 | ], 90 | }, 91 | }; 92 | ``` 93 | 94 | ## Importing 95 | 96 | ### Importing strings 97 | Since JSON doesn't map directly to SASS's data types, a common source of confusion is how to handle strings. While [SASS allows strings to be both quoted and unquoted](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#sass-script-strings), strings containing spaces, commas and/or other special characters have to be wrapped in quotes. In terms of JSON, this means the string has to be double quoted: 98 | 99 | ##### Incorrect 100 | ```json 101 | { 102 | "description": "A sentence with spaces." 103 | } 104 | ``` 105 | 106 | ##### Correct 107 | ```json 108 | { 109 | "description": "'A sentence with spaces.'" 110 | } 111 | ``` 112 | 113 | See discussion here for more: 114 | 115 | https://github.com/Updater/node-sass-json-importer/pull/5 116 | 117 | ### Importing *.js Files 118 | 119 | You can also import *.js Files. This way you can use javascript to compose and export json structure for node-sass-json-importer. 120 | ``` 121 | const xl = require('./variables.json') 122 | const md = require('./variables-md.json') 123 | const xs = require('./variables-xs.json') 124 | 125 | module.exports = { 126 | xl, 127 | md, 128 | xs, 129 | } 130 | ``` 131 | 132 | ## Custom resolver 133 | 134 | Should you care to resolve paths, say, starting with `~/` relative to project root or some other arbitrary directory, you can do it as follows: 135 | 136 | `1.sass`: 137 | 138 | ```sass 139 | @import '~/1.json' 140 | body 141 | margin: $body-margin 142 | ``` 143 | 144 | `json/1.json`: 145 | 146 | ```json 147 | {"body-margin": 0} 148 | ``` 149 | 150 | ```js 151 | var path = require('path'); 152 | var sass = require('node-sass'); 153 | var jsonImporter = require('../dist/node-sass-json-importer'); 154 | 155 | sass.render({ 156 | file: './1.sass', 157 | importer: jsonImporter({ 158 | resolver: function(dir, url) { 159 | return url.startsWith('~/') 160 | ? path.resolve(dir, 'json', url.substr(2)) 161 | : path.resolve(dir, url); 162 | }, 163 | }), 164 | }, function(err, result) { console.log(err || result.css.toString()) }); 165 | ``` 166 | 167 | ## camelCase to kebab-case 168 | 169 | If you want to convert standard JavaScript caseCase keys into CSS/SCSS compliant kebab-case keys, for example: 170 | 171 | `variables.json`: 172 | 173 | ```JS 174 | { 175 | "bgBackgroundColor": 'red' 176 | } 177 | ``` 178 | 179 | For usage like this: 180 | 181 | `style.scss`: 182 | 183 | ```SCSS 184 | @import "variables.json"; 185 | 186 | div { 187 | background: $bg-background-color; 188 | } 189 | ``` 190 | 191 | You can pass set the `convertCase` option to true as an argument to `jsonImporter` like so: 192 | 193 | ```js 194 | sass.render({ 195 | file: './1.sass', 196 | importer: jsonImporter({ 197 | convertCase: true, 198 | }), 199 | }, function(err, result) { console.log(err || result.css.toString()) }); 200 | ``` 201 | 202 | ## Thanks to 203 | This module is based on the [sass-json-vars](https://github.com/vigetlabs/sass-json-vars) gem, which unfortunately isn't compatible with `node-sass`. 204 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sass-json-importer", 3 | "version": "0.0.0-development", 4 | "description": "Allows importing json in sass files parsed by node-sass.", 5 | "main": "dist/index.js", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/Updater/node-sass-json-importer.git" 10 | }, 11 | "keywords": [ 12 | "sass", 13 | "node-sass", 14 | "importer", 15 | "json" 16 | ], 17 | "scripts": { 18 | "test": "mocha --require @babel/register", 19 | "compile": "mkdirp dist && babel src -d dist", 20 | "prepublishOnly": "npm run compile" 21 | }, 22 | "peerDependencies": { 23 | "node-sass": ">=3.5.3" 24 | }, 25 | "dependencies": { 26 | "is-there": "^4.4.4", 27 | "json5": "^2.1.1", 28 | "lodash": "^4.17.15" 29 | }, 30 | "devDependencies": { 31 | "@babel/cli": "^7.8.4", 32 | "@babel/core": "^7.8.4", 33 | "@babel/preset-env": "^7.8.4", 34 | "@babel/register": "^7.8.3", 35 | "chai": "^4.2.0", 36 | "mkdirp": "^1.0.3", 37 | "mocha": "^7.0.1", 38 | "node-sass": ">=3.5.3", 39 | "semantic-release": "^17.0.2", 40 | "semantic-release-github-pr": "^6.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | import jsonImporter from './index'; 2 | export default jsonImporter(); 3 | 4 | // Super-hacky: Override Babel's transpiled export to provide both 5 | // a default CommonJS export and named exports. 6 | // Fixes: https://github.com/Updater/node-sass-json-importer/issues/32 7 | // TODO: Remove in 3.0.0. Upgrade to Babel6. 8 | module.exports = exports.default; 9 | Object.keys(exports).forEach(key => module.exports[key] = exports[key]); 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import isThere from 'is-there'; 3 | import path, { resolve, basename, extname, dirname} from 'path'; 4 | 5 | import 'json5/lib/register'; // Enable JSON5 support 6 | 7 | export default function(options = {}) { 8 | return function(url, prev) { 9 | if (!isJSONfile(url)) { 10 | return null; 11 | } 12 | 13 | let includePaths = this.options.includePaths ? this.options.includePaths.split(path.delimiter) : []; 14 | let paths = [] 15 | .concat(dirname(prev)) 16 | .concat(includePaths); 17 | 18 | const resolver = options.resolver || resolve; 19 | let fileName = paths 20 | .map(path => resolver(path, url)) 21 | .filter(isThere) 22 | .pop(); 23 | 24 | if (!fileName) { 25 | return new Error(`Unable to find "${url}" from the following path(s): ${paths.join(', ')}. Check includePaths.`); 26 | } 27 | 28 | // Prevent file from being cached by Node's `require` on continuous builds. 29 | // https://github.com/Updater/node-sass-json-importer/issues/21 30 | delete require.cache[require.resolve(fileName)]; 31 | 32 | try { 33 | const fileContents = require(fileName); 34 | const extensionlessFilename = basename(fileName, extname(fileName)); 35 | const json = Array.isArray(fileContents) ? { [extensionlessFilename]: fileContents } : fileContents; 36 | 37 | return { 38 | contents: transformJSONtoSass(json, options), 39 | }; 40 | } catch(error) { 41 | return new Error(`node-sass-json-importer: Error transforming JSON/JSON5 to SASS. Check if your JSON/JSON5 parses correctly. ${error}`); 42 | } 43 | } 44 | } 45 | 46 | export function isJSONfile(url) { 47 | return /\.js(on5?)?$/.test(url); 48 | } 49 | 50 | export function transformJSONtoSass(json, opts = {}) { 51 | return Object.keys(json) 52 | .filter(key => isValidKey(key)) 53 | .filter(key => json[key] !== '#') 54 | .map(key => `$${opts.convertCase ? toKebabCase(key) : key}: ${parseValue(json[key], opts)};`) 55 | .join('\n'); 56 | } 57 | 58 | export function isValidKey(key) { 59 | return /^[^$@:].*/.test(key) 60 | } 61 | 62 | export function toKebabCase(key) { 63 | return key 64 | .replace(/([a-z0-9])([A-Z])/g, '$1-$2') 65 | .replace(/([A-Z])([A-Z])(?=[a-z])/g, '$1-$2') 66 | .toLowerCase(); 67 | } 68 | 69 | export function parseValue(value, opts = {}) { 70 | if (_.isArray(value)) { 71 | return parseList(value, opts); 72 | } else if (_.isPlainObject(value)) { 73 | return parseMap(value, opts); 74 | } else if (value === '') { 75 | return '""'; // Return explicitly an empty string (Sass would otherwise throw an error as the variable is set to nothing) 76 | } else { 77 | return value; 78 | } 79 | } 80 | 81 | export function parseList(list, opts = {}) { 82 | return `(${list 83 | .map(value => parseValue(value)) 84 | .join(',')})`; 85 | } 86 | 87 | export function parseMap(map, opts = {}) { 88 | return `(${Object.keys(map) 89 | .filter(key => isValidKey(key)) 90 | .map(key => `${opts.convertCase ? toKebabCase(key) : key}: ${parseValue(map[key], opts)}`) 91 | .join(',')})`; 92 | } 93 | 94 | // Super-hacky: Override Babel's transpiled export to provide both 95 | // a default CommonJS export and named exports. 96 | // Fixes: https://github.com/Updater/node-sass-json-importer/issues/32 97 | // TODO: Remove in 3.0.0. Upgrade to Babel6. 98 | module.exports = exports.default; 99 | Object.keys(exports).forEach(key => module.exports[key] = exports[key]); 100 | -------------------------------------------------------------------------------- /test/fixtures-json5/array/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.json5'; 2 | 3 | body { 4 | color: nth($variables, 1); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures-json5/array/variables.json5: -------------------------------------------------------------------------------- 1 | ['#c33', '#33c'] // Array of items 2 | -------------------------------------------------------------------------------- /test/fixtures-json5/convert-case/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.json5'; 2 | 3 | body { 4 | color: $color-red; 5 | color: $color-green; 6 | color: $color-blue; 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures-json5/convert-case/variables.json5: -------------------------------------------------------------------------------- 1 | { 2 | 'colorRed': '#c33', // Red color 3 | 'ColorGreen': '#3c3', // Green color 4 | 'COLORBlue': '#33c', // Blue color 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures-json5/empty-string/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.json5'; 2 | 3 | body { 4 | color: $colors; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures-json5/empty-string/variables.json5: -------------------------------------------------------------------------------- 1 | { 2 | 'colors': '', // Empty string 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures-json5/include-paths/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.json5'; 2 | 3 | body { 4 | color: $color-red; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures-json5/include-paths/variables/variables.json5: -------------------------------------------------------------------------------- 1 | { 2 | 'color-red': '#c33', // Red color 3 | 'color-blue': '#33c', // Blue color 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures-json5/invalid-variables/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.json5'; 2 | 3 | body { 4 | color: $colors; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures-json5/invalid-variables/variables.json5: -------------------------------------------------------------------------------- 1 | { 2 | "colors": "", 3 | "@not-valid": "123", // Not valid because of the leading @ 4 | ":not-valid": "123", // Not valid because of the leading : 5 | "$not-valid": "123", // Not valid because of the leading $ 6 | "invalid-value": "#", // Not valid because of `#` as value 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures-json5/lists/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.json5'; 2 | 3 | body { 4 | color: nth($colors, 1); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures-json5/lists/variables.json5: -------------------------------------------------------------------------------- 1 | { 2 | 'colors': ['#c33', '#33c'], // Array of items 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures-json5/maps/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.json5'; 2 | 3 | body { 4 | color: map-get($colors, red); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures-json5/maps/variables.json5: -------------------------------------------------------------------------------- 1 | { 2 | 'colors': { 3 | 'red': '#c33', // Nested object 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures-json5/non-json/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | body { 4 | color: $color-red; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures-json5/non-json/variables.scss: -------------------------------------------------------------------------------- 1 | $color-red: #c33; 2 | -------------------------------------------------------------------------------- /test/fixtures-json5/strings/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.json5'; 2 | 3 | body { 4 | color: $color-red; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures-json5/strings/variables.json5: -------------------------------------------------------------------------------- 1 | { 2 | 'color-red': '#c33', // Red color 3 | 'color-blue': '#33c', // Blue color 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/array/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.json'; 2 | 3 | body { 4 | color: nth($variables, 1); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/array/variables.json: -------------------------------------------------------------------------------- 1 | ["#c33", "#33c"] 2 | -------------------------------------------------------------------------------- /test/fixtures/convert-case/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.json'; 2 | 3 | body { 4 | color: $color-red; 5 | color: $color-green; 6 | color: $color-blue; 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/convert-case/variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "colorRed": "#c33", 3 | "ColorGreen": "#3c3", 4 | "COLORBlue": "#33c" 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/empty-string/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.json'; 2 | 3 | body { 4 | color: $colors; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/empty-string/variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors": "" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/include-paths/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.json'; 2 | 3 | body { 4 | color: $color-red; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/include-paths/variables/variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "color-red": "#c33", 3 | "color-blue": "#33c" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/invalid-variables/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.json'; 2 | 3 | body { 4 | color: $colors; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/invalid-variables/variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors": "", 3 | "@not-valid": "123", 4 | ":not-valid": "123", 5 | "$not-valid": "123", 6 | "invalid-value": "#" 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/lists/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.json'; 2 | 3 | body { 4 | color: nth($colors, 1); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/lists/variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors": ["#c33", "#33c"] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/maps/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.json'; 2 | 3 | body { 4 | color: map-get($colors, red); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/maps/variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors": { 3 | "red": "#c33" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/non-json/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | body { 4 | color: $color-red; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/non-json/variables.scss: -------------------------------------------------------------------------------- 1 | $color-red: #c33; 2 | -------------------------------------------------------------------------------- /test/fixtures/strings/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.json'; 2 | 3 | body { 4 | color: $color-red; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/strings/variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "color-red": "#c33", 3 | "color-blue": "#33c" 4 | } 5 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import jsonImporter, { 3 | isJSONfile, 4 | isValidKey, 5 | toKebabCase, 6 | parseValue, 7 | } from '../src' 8 | import sass from 'node-sass'; 9 | import {expect} from 'chai'; 10 | import {resolve} from 'path'; 11 | 12 | const requiredImporter = require('../src/index'); 13 | const EXPECTATION = 'body {\n color: #c33; }\n'; 14 | 15 | describe('node-sass-json-importer', function() { 16 | // TODO: Added to verify named exports + CommonJS default export hack (see index.js). 17 | it('provides the default export when using node require to import', function() { 18 | let result = sass.renderSync({ 19 | file: './test/fixtures/strings/style.scss', 20 | importer: requiredImporter(), 21 | }); 22 | 23 | expect(result.css.toString()).to.eql(EXPECTATION); 24 | }); 25 | 26 | // TODO: Added to verify named exports + CommonJS default export hack (see index.js). 27 | it('provides named exports of internal methods', function() { 28 | expect(isJSONfile('import.json')).to.be.true; 29 | }); 30 | }); 31 | 32 | describe('Import type test (JSON)', function() { 33 | it('imports strings', function() { 34 | let result = sass.renderSync({ 35 | file: './test/fixtures/strings/style.scss', 36 | importer: jsonImporter(), 37 | }); 38 | 39 | expect(result.css.toString()).to.eql(EXPECTATION); 40 | }); 41 | 42 | it('imports lists', function() { 43 | let result = sass.renderSync({ 44 | file: './test/fixtures/lists/style.scss', 45 | importer: jsonImporter(), 46 | }); 47 | 48 | expect(result.css.toString()).to.eql(EXPECTATION); 49 | }); 50 | 51 | it('imports maps', function() { 52 | let result = sass.renderSync({ 53 | file: './test/fixtures/maps/style.scss', 54 | importer: jsonImporter(), 55 | }); 56 | 57 | expect(result.css.toString()).to.eql(EXPECTATION); 58 | }); 59 | 60 | it('imports map from json with array as top level', function() { 61 | let result = sass.renderSync({ 62 | file: './test/fixtures/array/style.scss', 63 | importer: jsonImporter(), 64 | }); 65 | 66 | expect(result.css.toString()).to.eql(EXPECTATION); 67 | }); 68 | 69 | it('finds imports via includePaths', function() { 70 | let result = sass.renderSync({ 71 | file: './test/fixtures/include-paths/style.scss', 72 | includePaths: ['./test/fixtures/include-paths/variables'], 73 | importer: jsonImporter(), 74 | }); 75 | 76 | expect(result.css.toString()).to.eql(EXPECTATION); 77 | }); 78 | 79 | it('finds imports via multiple includePaths', function() { 80 | let result = sass.renderSync({ 81 | file: './test/fixtures/include-paths/style.scss', 82 | includePaths: ['./test/fixtures/include-paths/variables', './some/other/path/'], 83 | importer: jsonImporter(), 84 | }); 85 | 86 | expect(result.css.toString()).to.eql(EXPECTATION); 87 | }); 88 | 89 | it(`throws when an import doesn't exist`, function() { 90 | function render() { 91 | sass.renderSync({ 92 | file: './test/fixtures/include-paths/style.scss', 93 | includePaths: ['./test/fixtures/include-paths/foo'], 94 | importer: jsonImporter(), 95 | }); 96 | } 97 | 98 | expect(render).to.throw( 99 | 'Unable to find "variables.json" from the following path(s): ' + 100 | `${resolve(process.cwd(), 'test/fixtures/include-paths')}, ${process.cwd()}, ./test/fixtures/include-paths/foo. ` + 101 | 'Check includePaths.' 102 | ); 103 | }); 104 | 105 | it('allows for a custom resolver', function() { 106 | let result = sass.renderSync({ 107 | file: './test/fixtures/include-paths/style.scss', 108 | importer: jsonImporter({ 109 | resolver: function(dir, url) { 110 | return resolve(dir, 'variables', url); 111 | }, 112 | }), 113 | }); 114 | 115 | expect(result.css.toString()).to.eql(EXPECTATION); 116 | }); 117 | 118 | it('ignores non-json imports', function() { 119 | let result = sass.renderSync({ 120 | file: './test/fixtures/non-json/style.scss', 121 | importer: jsonImporter(), 122 | }); 123 | 124 | expect(result.css.toString()).to.eql(EXPECTATION); 125 | }); 126 | 127 | it('imports empty strings correctly', function() { 128 | let result = sass.renderSync({ 129 | file: './test/fixtures/empty-string/style.scss', 130 | importer: jsonImporter(), 131 | }); 132 | 133 | expect(result.css.toString()).to.eql('body {\n color: ""; }\n'); 134 | }); 135 | 136 | it('ignores variables starting with @, : or $', function() { 137 | let result = sass.renderSync({ 138 | file: './test/fixtures/invalid-variables/style.scss', 139 | importer: jsonImporter(), 140 | }); 141 | 142 | expect(result.css.toString()).to.eql('body {\n color: ""; }\n'); 143 | }); 144 | 145 | it('filters out `#` as variable value', function() { 146 | let result = sass.renderSync({ 147 | file: './test/fixtures-json5/invalid-variables/style.scss', 148 | importer: jsonImporter(), 149 | }); 150 | 151 | expect(result.css.toString()).to.eql('body {\n color: ""; }\n'); 152 | }); 153 | 154 | it('allows case conversion', function() { 155 | let result = sass.renderSync({ 156 | file: './test/fixtures/convert-case/style.scss', 157 | importer: jsonImporter({ 158 | convertCase: true, 159 | }), 160 | }); 161 | 162 | expect(result.css.toString()).to.eql('body {\n color: #c33;\n color: #3c3;\n color: #33c; }\n'); 163 | }); 164 | }); 165 | 166 | describe('Import type test (JSON5)', function() { 167 | it('imports strings', function() { 168 | let result = sass.renderSync({ 169 | file: './test/fixtures-json5/strings/style.scss', 170 | importer: jsonImporter(), 171 | }); 172 | 173 | expect(result.css.toString()).to.eql(EXPECTATION); 174 | }); 175 | 176 | it('imports lists', function() { 177 | let result = sass.renderSync({ 178 | file: './test/fixtures-json5/lists/style.scss', 179 | importer: jsonImporter(), 180 | }); 181 | 182 | expect(result.css.toString()).to.eql(EXPECTATION); 183 | }); 184 | 185 | it('imports maps', function() { 186 | let result = sass.renderSync({ 187 | file: './test/fixtures-json5/maps/style.scss', 188 | importer: jsonImporter(), 189 | }); 190 | 191 | expect(result.css.toString()).to.eql(EXPECTATION); 192 | }); 193 | 194 | it('imports map from json with array as top level', function() { 195 | let result = sass.renderSync({ 196 | file: './test/fixtures-json5/array/style.scss', 197 | importer: jsonImporter(), 198 | }); 199 | 200 | expect(result.css.toString()).to.eql(EXPECTATION); 201 | }); 202 | 203 | it('finds imports via includePaths', function() { 204 | let result = sass.renderSync({ 205 | file: './test/fixtures-json5/include-paths/style.scss', 206 | includePaths: ['./test/fixtures-json5/include-paths/variables'], 207 | importer: jsonImporter(), 208 | }); 209 | 210 | expect(result.css.toString()).to.eql(EXPECTATION); 211 | }); 212 | 213 | it('finds imports via multiple includePaths', function() { 214 | let result = sass.renderSync({ 215 | file: './test/fixtures-json5/include-paths/style.scss', 216 | includePaths: ['./test/fixtures-json5/include-paths/variables', './some/other/path/'], 217 | importer: jsonImporter(), 218 | }); 219 | 220 | expect(result.css.toString()).to.eql(EXPECTATION); 221 | }); 222 | 223 | it(`throws when an import doesn't exist`, function() { 224 | function render() { 225 | sass.renderSync({ 226 | file: './test/fixtures-json5/include-paths/style.scss', 227 | includePaths: ['./test/fixtures-json5/include-paths/foo'], 228 | importer: jsonImporter(), 229 | }); 230 | } 231 | 232 | expect(render).to.throw( 233 | 'Unable to find "variables.json5" from the following path(s): ' + 234 | `${resolve(process.cwd(), 'test/fixtures-json5/include-paths')}, ${process.cwd()}, ./test/fixtures-json5/include-paths/foo. ` + 235 | 'Check includePaths.' 236 | ); 237 | }); 238 | 239 | it('allows for a custom resolver', function() { 240 | let result = sass.renderSync({ 241 | file: './test/fixtures-json5/include-paths/style.scss', 242 | importer: jsonImporter({ 243 | resolver: function(dir, url) { 244 | return resolve(dir, 'variables', url); 245 | }, 246 | }), 247 | }); 248 | 249 | expect(result.css.toString()).to.eql(EXPECTATION); 250 | }); 251 | 252 | it('ignores non-json imports', function() { 253 | let result = sass.renderSync({ 254 | file: './test/fixtures-json5/non-json/style.scss', 255 | importer: jsonImporter(), 256 | }); 257 | 258 | expect(result.css.toString()).to.eql(EXPECTATION); 259 | }); 260 | 261 | it('imports empty strings correctly', function() { 262 | let result = sass.renderSync({ 263 | file: './test/fixtures-json5/empty-string/style.scss', 264 | importer: jsonImporter(), 265 | }); 266 | 267 | expect(result.css.toString()).to.eql('body {\n color: ""; }\n'); 268 | }); 269 | 270 | it('ignores variables starting with @, : or $', function() { 271 | let result = sass.renderSync({ 272 | file: './test/fixtures-json5/invalid-variables/style.scss', 273 | importer: jsonImporter(), 274 | }); 275 | 276 | expect(result.css.toString()).to.eql('body {\n color: ""; }\n'); 277 | }); 278 | 279 | it('filters out `#` as variable value', function() { 280 | let result = sass.renderSync({ 281 | file: './test/fixtures-json5/invalid-variables/style.scss', 282 | importer: jsonImporter(), 283 | }); 284 | 285 | expect(result.css.toString()).to.eql('body {\n color: ""; }\n'); 286 | }); 287 | 288 | it('allows case conversion', function() { 289 | let result = sass.renderSync({ 290 | file: './test/fixtures-json5/convert-case/style.scss', 291 | importer: jsonImporter({ 292 | convertCase: true, 293 | }), 294 | }); 295 | 296 | expect(result.css.toString()).to.eql('body {\n color: #c33;\n color: #3c3;\n color: #33c; }\n'); 297 | }); 298 | }); 299 | 300 | describe('parseValue', function() { 301 | it('returns comma-separated items wrapped in parentheses for an array', function() { 302 | expect(parseValue(['some', 'entries'])).to.eql('(some,entries)'); 303 | }); 304 | 305 | it('calls comma-separated key value pairs wrapped in parentheses for an object', function() { 306 | expect(parseValue({'key1': 'value1', 'key2': 'value2'})).to.eql('(key1: value1,key2: value2)'); 307 | }); 308 | 309 | it('returns an empty string in an empty for empty strings', function() { 310 | expect(parseValue("")).to.eql('""'); 311 | }); 312 | 313 | it('returns the raw value if not an array, object or empty string', function() { 314 | expect(parseValue(123)).to.eql(123); 315 | }); 316 | 317 | it('can parse nested maps with invalid keys', function() { 318 | const nestedWithInvalid = { 319 | inner: { 320 | ':problem1': 'value1', 321 | '$problem2': 'value2', 322 | '@problem3': 'value3', 323 | valid: 'value4', 324 | } 325 | }; 326 | expect(parseValue(nestedWithInvalid)).to.eql('(inner: (valid: value4))'); 327 | }); 328 | }); 329 | 330 | describe('isJSONfile', function() { 331 | it('returns true if the given URL is a JSON file', function() { 332 | expect(isJSONfile('/test/variables.json')).to.be.true; 333 | }); 334 | 335 | it('returns true if the given URL is a JSON5 file', function() { 336 | expect(isJSONfile('/test/variables.json5')).to.be.true; 337 | }); 338 | 339 | it('returns true if the given URL is a JS file', function() { 340 | expect(isJSONfile('/test/composed-variables.js')).to.be.true; 341 | }); 342 | 343 | it('returns false if the given URL is not a JSON or JSON5 file', function() { 344 | expect(isJSONfile('/test/variables.not-json-or-json5')).to.be.false; 345 | }); 346 | }); 347 | 348 | describe('isValidKey', function() { 349 | it('returns false if the given key starts with @', function() { 350 | expect(isValidKey('@invalid')).to.be.false; 351 | }); 352 | 353 | it('returns false if the given key starts with :', function() { 354 | expect(isValidKey(':invalid')).to.be.false; 355 | }); 356 | 357 | it('returns false if the given key starts with $', function() { 358 | expect(isValidKey('$invalid')).to.be.false; 359 | }); 360 | 361 | it('returns true if the given key does not start with @, : or $', function() { 362 | expect(isValidKey('valid')).to.be.true; 363 | }); 364 | }); 365 | 366 | describe('toKebabCase', function() { 367 | it('can handle camelCase', function() { 368 | expect(toKebabCase('camelCase')).to.eql('camel-case'); 369 | }); 370 | 371 | it('can handle PascalCase', function() { 372 | expect(toKebabCase('PascalCase')).to.eql('pascal-case'); 373 | }); 374 | 375 | it('can handle ALLCAPSCase', function() { 376 | expect(toKebabCase('ALLCAPSCase')).to.eql('allcaps-case'); 377 | }); 378 | 379 | it('can even handle EDGECaseWELLCase', function() { 380 | expect(toKebabCase('EDGECaseWELLCase')).to.eql('edge-case-well-case'); 381 | }); 382 | }); 383 | --------------------------------------------------------------------------------