diff --git a/docs/migration/11.x-grid.md b/docs/migration/11.x-grid.md new file mode 100644 index 000000000000..e9fcbc1f0dad --- /dev/null +++ b/docs/migration/11.x-grid.md @@ -0,0 +1,22 @@ +# Grid + +**Note: everything in this file is a work-in-progress and will be changed.** + +## Changes + +| Filename | v10 | v11 | +| ------------------- | ------------------------ | ---------------------------------------- | +| `scss/12.scss` | | Removed | +| `scss/_mixins.scss` | | | +| `scss/_mixins.scss` | `$carbon--aspect-ratios` | Removed, use styles package instead | +| `scss/_mixins.scss` | `@mixin carbon--grid` | | +| `scss/_prefix.scss` | | Removed, use `scss/_config.scss` instead | +| `scss/grid.scss` | | | +| `scss/index.scss` | | | + +Notes + +- The grid now uses 16 columns by default, there is no longer a 12 column mode +- Configure Sass Modules is now done through a `_config.scss` file (for things + like `$prefix`) +- Breakpoints are now defined in `@carbon/grid` diff --git a/packages/grid/__tests__/__snapshots__/scss-test.js.snap b/packages/grid/__tests__/__snapshots__/scss-test.js.snap new file mode 100644 index 000000000000..1c73b2539fa1 --- /dev/null +++ b/packages/grid/__tests__/__snapshots__/scss-test.js.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`@carbon/grid Public API 1`] = ` +Array [ + "prefix", + "grid-gutter", + "grid-gutter--condensed", + "grid-breakpoints", +] +`; diff --git a/packages/grid/__tests__/scss-test.js b/packages/grid/__tests__/scss-test.js new file mode 100644 index 000000000000..7ed10f6b15b7 --- /dev/null +++ b/packages/grid/__tests__/scss-test.js @@ -0,0 +1,34 @@ +/** + * Copyright IBM Corp. 2015, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + * + * @jest-environment node + */ + +'use strict'; + +const { SassRenderer } = require('@carbon/test-utils/scss'); + +const { render } = SassRenderer.create(__dirname); + +describe('@carbon/grid', () => { + test('Public API', async () => { + const { get } = await render(` + @use 'sass:meta'; + @use '../index.scss' as grid; + + $_: get('variables', meta.module-variables('grid')); + $_: get('mixins', ( + grid: meta.mixin-exists('grid', 'grid'), + )); + `); + + const variables = get('variables'); + expect(Object.keys(variables.value)).toMatchSnapshot(); + + const mixins = get('mixins'); + expect(mixins.value.grid).toBe(true); + }); +}); diff --git a/packages/grid/index.scss b/packages/grid/index.scss new file mode 100644 index 000000000000..ff884a8a29df --- /dev/null +++ b/packages/grid/index.scss @@ -0,0 +1,16 @@ +// +// Copyright IBM Corp. 2018, 2018 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +@forward 'scss/modules/config' with ( + $prefix: 'bx' !default, +); +@forward 'scss/modules/breakpoint'; +@forward 'scss/modules/mixins'; + +@use 'scss/modules/mixins'; + +@include mixins.grid(); diff --git a/packages/grid/scss/modules/_breakpoint.scss b/packages/grid/scss/modules/_breakpoint.scss new file mode 100644 index 000000000000..b5d7aa897478 --- /dev/null +++ b/packages/grid/scss/modules/_breakpoint.scss @@ -0,0 +1,266 @@ +// +// Copyright IBM Corp. 2018, 2018 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +// https://github.com/twbs/bootstrap/blob/v4-dev/scss/mixins/_breakpoints.scss +@use 'sass:list'; +@use 'sass:map'; +@use 'sass:meta'; +@use '@carbon/layout/scss/modules/convert'; + +/// Map deep get +/// @author Hugo Giraudel +/// @access public +/// @param {Map} $map - Map +/// @param {Arglist} $keys - Key chain +/// @return {*} Desired value +/// @group @carbon/layout +@function map-deep-get($map, $keys...) { + @each $key in $keys { + $map: map-get($map, $key); + } + @return $map; +} + +/// Provide a map and index, and get back the relevant key value +/// @access public +/// @param {Map} $map - Map +/// @param {Integer} $index - Key chain +/// @return {String} Desired value +/// @group @carbon/layout +@function key-by-index($map, $index) { + $keys: map-keys($map); + @return nth($keys, $index); +} + +/// Pass in a map, and get the last one in the list back +/// @access public +/// @param {Map} $map - Map +/// @return {*} Desired value +/// @group @carbon/layout +@function last-map-item($map) { + $total-length: length($map); + @return map-get($map, key-by-index($map, $total-length)); +} + +/// Carbon gutter size in rem +/// @type Number +/// @access public +/// @group @carbon/layout +$grid-gutter: convert.rem(32px); + +/// Carbon condensed gutter size in rem +/// @type Number +/// @access public +/// @group @carbon/layout +$grid-gutter--condensed: convert.rem(1px); + +// Initial map of our breakpoints and their values +/// @type Map +/// @access public +/// @group @carbon/layout +$grid-breakpoints: ( + sm: ( + columns: 4, + margin: 0, + width: convert.rem(320px), + ), + md: ( + columns: 8, + margin: convert.rem(16px), + width: convert.rem(672px), + ), + lg: ( + columns: 16, + margin: convert.rem(16px), + width: convert.rem(1056px), + ), + xlg: ( + columns: 16, + margin: convert.rem(16px), + width: convert.rem(1312px), + ), + max: ( + columns: 16, + margin: convert.rem(24px), + width: convert.rem(1584px), + ), +) !default; + +/// Get the value of the next breakpoint, or null for the last breakpoint +/// @param {String} $name - The name of the brekapoint +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name of the breakpoint and the value is the values for the breakpoint +/// @param {List} $breakpoint-names [map-keys($breakpoints)] - A list of names from the `$breakpoints` map +/// @return {String} +/// @access public +/// @group @carbon/layout +@function breakpoint-next( + $name, + $breakpoints: $grid-breakpoints, + $breakpoint-names: map.keys($breakpoints) +) { + $n: list.index($breakpoint-names, $name); + @if $n != null and $n < list.length($breakpoint-names) { + @return list.nth($breakpoint-names, $n + 1); + } + @return null; +} + +/// Get the value of the previous breakpoint, or null for the first breakpoint +/// @param {String} $name - The name of the brekapoint +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name of the breakpoint and the value is the values for the breakpoint +/// @param {List} $breakpoint-names [map-keys($breakpoints)] - A list of names from the `$breakpoints` map +/// @return {String} +/// @access public +/// @group @carbon/layout +@function breakpoint-prev( + $name, + $breakpoints: $grid-breakpoints, + $breakpoint-names: map.keys($breakpoints) +) { + $n: list.index($breakpoint-names, $name); + @if $n != null and $n > 1 { + @return list.nth($breakpoint-names, $n - 1); + } + @return null; +} + +/// Check to see if the given breakpoint name +/// @param {String} $name - The name of the brekapoint +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name of the breakpoint and the value is the values for the breakpoint +/// @return {Bool} +/// @access public +/// @group @carbon/layout +@function is-smallest-breakpoint($name, $breakpoints: $grid-breakpoints) { + @return list.index(map.keys($breakpoints), $name) == 1; +} + +/// Returns the largest breakpoint name +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name +/// @return {String} +/// @access public +/// @group @carbon/layout +@function largest-breakpoint-name($breakpoints: $grid-breakpoints) { + $total-breakpoints: list.length($breakpoints); + @return key-by-index($breakpoints, $total-breakpoints); +} + +/// Get the infix for a given breakpoint in a list of breakpoints. Usesful for generate the size part in a selector, for example: `.prefix--col-sm-2`. +/// @param {String} $name - The name of the breakpoint +/// @return {String} +/// @access public +/// @group @carbon/layout +@function breakpoint-infix($name) { + @return '-#{$name}'; +} + +/// Generate a media query from the width of the given breakpoint to infinity +/// @param {String | Number} $name +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name +/// @content +/// @access public +/// @group @carbon/layout +@mixin breakpoint-up($name, $breakpoints: $grid-breakpoints) { + @if meta.type-of($name) == 'number' { + @media (min-width: $name) { + @content; + } + } @else if map.has-key($breakpoints, $name) { + $breakpoint: map.get($breakpoints, $name); + $width: map.get($breakpoint, width); + @if is-smallest-breakpoint($name, $breakpoints) { + @content; + } @else { + @media (min-width: $width) { + @content; + } + } + } @else { + @error 'Unable to find a breakpoint with name `#{$name}`. Expected one of: (#{map.keys($breakpoints)})'; + } +} + +/// Generate a media query for the maximum width of the given styles +/// @param {String | Number} $name +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name +/// @content +/// @access public +/// @group @carbon/layout +@mixin breakpoint-down($name, $breakpoints: $grid-breakpoints) { + @if meta.type-of($name) == 'number' { + @media (max-width: $name) { + @content; + } + } @else if map.has-key($breakpoints, $name) { + // We borrow this logic from bootstrap for specifying the value of the + // max-width. The maximum width is calculated by finding the breakpoint and + // subtracting .02 from its value. This value is used instead of .01 to + // avoid rounding issues in Safari + // https://github.com/twbs/bootstrap/blob/c5b1919deaf5393fcca9e9b9d7ce9c338160d99d/scss/mixins/_breakpoints.scss#L34-L46 + $breakpoint: map.get($breakpoints, $name); + $width: map.get($breakpoint, width) - 0.02; + @media (max-width: $width) { + @content; + } + } @else { + @error 'Unable to find a breakpoint with name `#{$name}`. Expected one of: (#{map.keys($breakpoints)})'; + } +} + +/// Generate a media query for the range between the lower and upper breakpoints +/// @param {String | Number} $lower +/// @param {String | Number} $upper +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name +/// @content +/// @access public +/// @group @carbon/layout +@mixin breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) { + $is-number-lower: meta.type-of($lower) == 'number'; + $is-number-upper: meta.type-of($upper) == 'number'; + $min: if($is-number-lower, $lower, map.get($breakpoints, $lower)); + $max: if($is-number-upper, $upper, map.get($breakpoints, $upper)); + + @if $min and $max { + $min-width: if(not $is-number-lower and $min, map.get($min, width), $min); + $max-width: if(not $is-number-upper and $max, map.get($max, width), $max); + @media (min-width: $min-width) and (max-width: $max-width) { + @content; + } + } @else if $min != null and $max == null { + @include breakpoint-up($lower) { + @content; + } + } @else if $min == null and $max != null { + @include breakpoint-down($upper) { + @content; + } + } @else { + @error 'Unable to find a breakpoint to satisfy: (#{$lower},#{$upper}). Expected both to be one of (#{map.keys($breakpoints)}).'; + } +} + +/// Generate media query for the largest breakpoint +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name +/// @content +/// @access public +/// @group @carbon/layout +@mixin largest-breakpoint($breakpoints: $grid-breakpoints) { + @include breakpoint(largest-breakpoint-name()) { + @content; + } +} + +/// Generate a media query for a given breakpoint +/// @param {String | Number} $name +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name +/// @content +/// @access public +/// @group @carbon/layout +@mixin breakpoint($name, $breakpoints: $grid-breakpoints) { + @include breakpoint-up($name, $breakpoints) { + @content; + } +} diff --git a/packages/grid/scss/modules/_config.scss b/packages/grid/scss/modules/_config.scss new file mode 100644 index 000000000000..7598b1e4c065 --- /dev/null +++ b/packages/grid/scss/modules/_config.scss @@ -0,0 +1,12 @@ +// +// Copyright IBM Corp. 2018, 2018 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +/// Namespace prefix +/// @type String +/// @access public +/// @group @carbon/grid +$prefix: 'bx' !default; diff --git a/packages/grid/scss/modules/_mixins.scss b/packages/grid/scss/modules/_mixins.scss new file mode 100644 index 000000000000..2afc8091fb4d --- /dev/null +++ b/packages/grid/scss/modules/_mixins.scss @@ -0,0 +1,323 @@ +// +// Copyright IBM Corp. 2018, 2018 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +@use 'config' as *; +@use 'breakpoint' as *; + +// ----------------------------------------------------------------------------- +// Columns +// ----------------------------------------------------------------------------- + +/// Used to initialize the default properties for a column class, most notably +/// for setting width and default gutters when a column's breakpoint has not been +/// hit yet. +/// @param {Number} $gutter [$grid-gutter] - The gutter for the grid system +/// @param {Number} $collapsed-gutter [$grid-gutter--condensed] - The condensed mode gutter +/// @access private +/// @group @carbon/grid +@mixin -make-col-ready( + $gutter: $grid-gutter, + $condensed-gutter: $grid-gutter--condensed +) { + // Prevent columns from becoming too narrow when at smaller grid tiers by + // always setting `width: 100%;`. This works because we use `flex` values + // later on to override this initial width. + width: 100%; + padding-right: ($gutter / 2); + padding-left: ($gutter / 2); + + // For our condensed use-case, our gutters collapse to 2px solid, 1px on each + // side. + .#{$prefix}--row--condensed &, + .#{$prefix}--grid--condensed & { + padding-right: ($condensed-gutter / 2); + padding-left: ($condensed-gutter / 2); + } + + // For our narrow use-case, our container hangs 16px into the gutter + .#{$prefix}--row--narrow &, + .#{$prefix}--grid--narrow & { + padding-right: ($gutter / 2); + padding-left: 0; + } +} + +/// Define the width of the column for a given span and column count. +/// A width of 0 will hide the column entirely. +/// @param {Number} $span - The number of columns covered +/// @param {Number} $columns - The total number of columns available +/// @access private +/// @group @carbon/grid +@mixin -make-col($span, $columns) { + @if $span == 0 { + display: none; + } @else { + // Explicitly include `display: block` to override + display: block; + flex: 0 0 percentage($span / $columns); + // Add a `max-width` to ensure content within each column does not blow out + // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari + // do not appear to require this. + max-width: percentage($span / $columns); + } +} + +/// Create a column offset for a given span and column count. +/// @param {Number} $span - The number of columns the offset should cover +/// @param {Number} $columns - The total number of columns available +/// @access private +/// @group @carbon/grid +@mixin -make-col-offset($span, $columns) { + $offset: $span / $columns; + @if $offset == 0 { + margin-left: 0; + } @else { + margin-left: percentage($offset); + } +} + +/// Output the CSS required for all the columns in a given grid system. +/// @param {Map} $breakpoints [$grid-breakpoints] - The breakpoints in the grid system +/// @param {Number} $gutter [$grid-gutter] - The gutter for the grid system +/// @access private +/// @group @carbon/grid +@mixin -make-grid-columns( + $breakpoints: $grid-breakpoints, + $gutter: $grid-gutter +) { + .#{$prefix}--col { + @include -make-col-ready($gutter); + } + + @each $breakpoint in map-keys($breakpoints) { + $infix: breakpoint-infix($breakpoint); + $columns: map-get(map-get($breakpoints, $breakpoint), columns); + + // Allow columns to stretch full width below their breakpoints + @for $i from 0 through $columns { + .#{$prefix}--col#{$infix}-#{$i} { + @include -make-col-ready($gutter); + } + } + + .#{$prefix}--col#{$infix}, + .#{$prefix}--col#{$infix}--auto { + @include -make-col-ready($gutter); + } + + @include breakpoint($breakpoint, $breakpoints) { + // Provide basic `.col-{bp}` classes for equal-width flexbox columns + .#{$prefix}--col, + .#{$prefix}--col#{$infix} { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + } + + .#{$prefix}--col--auto, + .#{$prefix}--col#{$infix}--auto { + flex: 1 0 0%; + width: auto; + // Reset earlier grid tiers + max-width: 100%; + } + + @for $i from 0 through $columns { + .#{$prefix}--col#{$infix}-#{$i} { + @include -make-col($i, $columns); + } + } + + @for $i from 0 through ($columns - 1) { + @if not($infix == '') { + .#{$prefix}--offset#{$infix}-#{$i} { + @include -make-col-offset($i, $columns); + } + } + } + } + } +} + +// ----------------------------------------------------------------------------- +// Rows +// ----------------------------------------------------------------------------- + +/// Define the properties for a selector assigned to a row in the grid system. +/// @param {Number} $gutter [$grid-gutter] - The gutter in the grid system +/// @access private +/// @group @carbon/grid +@mixin make-row($gutter: $grid-gutter) { + display: flex; + flex-wrap: wrap; + margin-right: -1 * $gutter / 2; + margin-left: -1 * $gutter / 2; +} + +// ----------------------------------------------------------------------------- +// No gutter +// ----------------------------------------------------------------------------- + +/// Add `no-gutter` and `no-gutter--{start,end}` classes to the output CSS. These +/// classes are useful for dropping the gutter in fluid situations. +/// @access private +/// @group @carbon/grid +@mixin -no-gutter { + .#{$prefix}--no-gutter, + .#{$prefix}--row.#{$prefix}--no-gutter [class*='#{$prefix}--col'] { + padding-right: 0; + padding-left: 0; + } + + .#{$prefix}--no-gutter--start, + .#{$prefix}--row.#{$prefix}--no-gutter--start [class*='#{$prefix}--col'] { + padding-left: 0; + } + + .#{$prefix}--no-gutter--end, + .#{$prefix}--row.#{$prefix}--no-gutter--end [class*='#{$prefix}--col'] { + padding-right: 0; + } + + // Deprecated ☠️ + .#{$prefix}--no-gutter--left, + .#{$prefix}--row.#{$prefix}--no-gutter--left [class*='#{$prefix}--col'] { + padding-left: 0; + } + + .#{$prefix}--no-gutter--right, + .#{$prefix}--row.#{$prefix}--no-gutter--right [class*='#{$prefix}--col'] { + padding-right: 0; + } +} + +// ----------------------------------------------------------------------------- +// Hang +// ----------------------------------------------------------------------------- + +/// Add `hang--start` and `hang--end` classes for a given gutter. These classes are +/// used alongside `no-gutter--start` and `no-gutter--end` to "hang" type. +/// @param {Number} $gutter [$grid-gutter] - The gutter in the grid system +/// @access private +/// @group @carbon/grid +@mixin -hang($gutter: $grid-gutter) { + .#{$prefix}--hang--start { + padding-left: ($gutter / 2); + } + + .#{$prefix}--hang--end { + padding-right: ($gutter / 2); + } + + // Deprecated ☠️ + .#{$prefix}--hang--left { + padding-left: ($gutter / 2); + } + + .#{$prefix}--hang--right { + padding-right: ($gutter / 2); + } +} + +// ----------------------------------------------------------------------------- +// Grid +// ----------------------------------------------------------------------------- + +/// Create the container for a grid. Will cause full-bleed for the grid unless +/// max-width properties are added with `-make-container-max-widths` +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name +/// @access private +/// @group @carbon/grid +@mixin -make-container($breakpoints: $grid-breakpoints) { + margin-right: auto; + margin-left: auto; + + @include -set-largest-breakpoint(); + + @each $name, $value in $breakpoints { + $prev-breakpoint: map-get($breakpoints, breakpoint-prev($name)); + $margin: map-get($value, margin); + + @if $prev-breakpoint { + $prev-margin: map-get($prev-breakpoint, margin); + @if $prev-margin != $margin { + @include breakpoint($name) { + padding-right: #{($grid-gutter / 2) + $margin}; + padding-left: #{($grid-gutter / 2) + $margin}; + } + } + } @else { + @include breakpoint($name) { + padding-right: #{($grid-gutter / 2) + $margin}; + padding-left: #{($grid-gutter / 2) + $margin}; + } + } + } +} + +/// Get the last breakpoint width and set max-width to its value +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name +/// @access private +/// @group @carbon/grid +@mixin -set-largest-breakpoint($breakpoints: $grid-breakpoints) { + $largest-breakpoint: last-map-item($breakpoints); + + max-width: map-get($largest-breakpoint, 'width'); +} + +/// Add in the max-widths for each breakpoint to the container +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name +/// @access private +/// @group @carbon/grid +@mixin -make-container-max-widths($breakpoints: $grid-breakpoints) { + @each $name, $value in $breakpoints { + @include breakpoint($name) { + max-width: map-get($value, width); + } + } +} + +/// Generate the CSS for a grid for the given breakpoints and gutters +/// @param {Map} $breakpoints [$grid-breakpoints] - The default breakpoints +/// @param {Number} $grid-gutter [$grid-gutter] - The default gutters +/// @param {Number} $condensed-gutter [$grid-gutter--condensed] - The condensed mode gutter +/// @access public +/// @group @carbon/grid +@mixin grid( + $breakpoints: $grid-breakpoints, + $grid-gutter: $grid-gutter, + $condensed-gutter: $grid-gutter--condensed +) { + .#{$prefix}--grid { + @include -make-container($breakpoints); + } + + @include largest-breakpoint($breakpoints) { + .#{$prefix}--grid--full-width { + max-width: 100%; + } + } + + .#{$prefix}--row { + @include make-row(); + } + + .#{$prefix}--row-padding [class*='#{$prefix}--col'], + .#{$prefix}--col-padding { + padding-top: $grid-gutter / 2; + padding-bottom: $grid-gutter / 2; + } + + .#{$prefix}--grid--condensed [class*='#{$prefix}--col'] { + padding-top: $condensed-gutter / 2; + padding-bottom: $condensed-gutter / 2; + } + + @include -make-grid-columns($breakpoints, $grid-gutter); + @include -no-gutter(); + @include -hang($grid-gutter); +} diff --git a/packages/test-utils/src/renderer.js b/packages/test-utils/src/renderer.js index 95c2f2c1f3a8..fbfb40c749de 100644 --- a/packages/test-utils/src/renderer.js +++ b/packages/test-utils/src/renderer.js @@ -7,24 +7,33 @@ 'use strict'; +const fs = require('fs'); const sass = require('sass'); -const { Importer } = require('./importer'); +const path = require('path'); const SassRenderer = { create(cwd, initialData = '') { - const importer = Importer.create(cwd); + const nodeModules = getNodeModulesFolders(cwd); async function render(data) { const values = []; + const valuesByKey = new Map(); const result = sass.renderSync({ data: `${initialData}\n${data}`, - importer, functions: { 'get-value($arg)': (arg) => { values.push(arg); return sass.types.Null.NULL; }, + 'get($key, $value)': (key, value) => { + valuesByKey.set(convert(key), { + value: convert(value), + nativeValue: value, + }); + return sass.types.Null.NULL; + }, }, + includePaths: [cwd, ...nodeModules], }); return { @@ -33,6 +42,12 @@ const SassRenderer = { getValue(index) { return convert(values[index]); }, + get(key) { + if (valuesByKey.has(key)) { + return valuesByKey.get(key); + } + throw new Error(`Unabled to find value with key: ${key}`); + }, }; } @@ -100,6 +115,29 @@ function convert(value) { return value; } +/** + * Collect all the node_modules folders that are present in the current path by + * traversing upwards and looking for if a node_modules folder exists. This is + * useful for the `includePaths` option for a sass renderer + * @param {string} cwd + * @returns {Array} + */ +function getNodeModulesFolders(cwd) { + const { root } = path.parse(cwd); + const folders = []; + let directory = cwd; + + while (directory !== root) { + const folder = path.join(directory, 'node_modules'); + if (fs.existsSync(folder)) { + folders.push(folder); + } + directory = path.dirname(directory); + } + + return folders; +} + module.exports = { SassRenderer, };