get-sass-vars
Version:
Get Sass variables as JSON object.
259 lines (220 loc) • 7.59 kB
JavaScript
;
var stripOuter = require('strip-outer');
var camelcaseKeys = require('camelcase-keys');
var postcss = require('postcss');
var postcssScss = require('postcss-scss');
var sass = require('sass');
var jsonFns = require('node-sass-json-functions');
var fromEntries = require('@ungap/from-entries');
var replaceAll = require('string-replace-all-ponyfill');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n["default"] = e;
return Object.freeze(n);
}
var stripOuter__default = /*#__PURE__*/_interopDefaultLegacy(stripOuter);
var camelcaseKeys__default = /*#__PURE__*/_interopDefaultLegacy(camelcaseKeys);
var postcss__default = /*#__PURE__*/_interopDefaultLegacy(postcss);
var postcssScss__default = /*#__PURE__*/_interopDefaultLegacy(postcssScss);
var sass__namespace = /*#__PURE__*/_interopNamespace(sass);
var jsonFns__default = /*#__PURE__*/_interopDefaultLegacy(jsonFns);
var fromEntries__default = /*#__PURE__*/_interopDefaultLegacy(fromEntries);
var replaceAll__default = /*#__PURE__*/_interopDefaultLegacy(replaceAll);
/**
* @typedef {JsonPrimitive | JsonObject | JsonArray} JsonValue
* @typedef {JsonValue[]} JsonArray
* @typedef {string | number | boolean | null} JsonPrimitive
* @typedef {{[Key in string]?: JsonValue}} JsonObject
*/
/**
* @typedef {object} Options
* @property {boolean=} camelize Camelize first-level JSON object keys and strip inital `$` (e.g. `$foo-bar` will become `fooBar`).
* @property {import("sass").Options<"async">=} sassOptions Options for Sass renderer.
*/
/**
* @typedef {object} SyncOptions
* @property {boolean=} camelize Camelize first-level JSON object keys and strip inital `$` (e.g. `$foo-bar` will become `fooBar`).
* @property {import("sass").Options<"sync">=} sassOptions Options for Sass renderer.
*/
const sassVariableSelector = '.__sassVars__';
const sassVariableDeclarationRegex = /^\$/;
const noop = () => ({
postcssPlugin: '_noop'
});
/**
* @param {import('postcss').Declaration} decl
*/
function getEncodedValueNode(decl) {
return {
prop: 'content',
/*
* Decl.prop as property is wrapped inside quotes so it doesn’t get transformed with Sass
* decl.prop as value will be transformed with Sass
*/
value: `"${decl.prop}" ":" json-encode(${decl.prop}, $quotes: false)`
};
}
/**
* @param {string} value
*/
function sanitizeValue(value) {
value = stripOuter__default["default"](value, '"');
value = stripOuter__default["default"](value, "'");
value = replaceAll__default["default"](value, '\\\\', '\\');
value = value.replace(/^\\"(.+)\\"$/, '"$1"');
value = replaceAll__default["default"](value, '\\\\', '\\');
return value;
}
/**
* Gets Sass variables from Sass string.
*
* Only top-level variables will be considered, anything inside selector or at-rule is ignored.
*
* @param {string} input Sass input string.
* @param {Options=} options
*/
async function main(input, options) {
const {
camelize = false,
sassOptions = /** @type {import("sass").Options<"async">} */ ({})
} = options || {};
const cssProcessor = postcss__default["default"]([noop()]);
/* eslint-disable no-undefined */
const initialResponse = await cssProcessor.process(input, {
syntax: postcssScss__default["default"],
from: undefined
});
const initialRoot = initialResponse.root;
const node = postcss__default["default"].rule({
selector: sassVariableSelector
});
initialRoot.walkDecls(sassVariableDeclarationRegex, (decl) => {
if (decl.parent === initialRoot) {
node.append(getEncodedValueNode(decl));
}
});
initialRoot.append(node);
const { functions, silenceDeprecations, ...otherSassOptions } = sassOptions;
let sassResponse = await sass__namespace.compileStringAsync(initialRoot.toString(), {
functions: { ...jsonFns__default["default"], ...functions },
...otherSassOptions,
// eslint-disable-next-line unicorn/prefer-spread
silenceDeprecations: Array.from(
new Set([...(silenceDeprecations || []), 'color-4-api'])
)
});
let sassResponseString =
typeof sassResponse === 'undefined' ? '' : sassResponse.css.toString();
const finalResponse = await cssProcessor.process(sassResponseString, {
from: undefined
});
const finalRoot = finalResponse.root;
/** @type {JsonObject} */
const data = {};
finalRoot.walkRules(sassVariableSelector, (rule) => {
rule.walkDecls('content', (decl) => {
let [property, value] = decl.value.split(' ":" ');
property = stripOuter__default["default"](property, '"');
value = sanitizeValue(value);
data[property] = JSON.parse(value);
});
});
if (camelize) {
return /** @type {JsonObject} */ (
camelcaseKeys__default["default"](
fromEntries__default["default"](
Object.entries(data).map(([key, value]) => [
stripOuter__default["default"](key, '$'),
value
])
)
)
);
}
return data;
}
/**
* Gets Sass variables from Sass string. Sync version.
*
* Only top-level variables will be considered, anything inside selector or at-rule is ignored.
*
* @param {string} input Sass input string.
* @param {SyncOptions=} options
*/
function mainSync(input, options) {
const {
camelize = false,
sassOptions = /** @type {import("sass").Options<"sync">} */ ({})
} = options || {};
const cssProcessor = postcss__default["default"]([noop()]);
/* eslint-disable no-undefined */
const initialResponse = cssProcessor.process(input, {
syntax: postcssScss__default["default"],
from: undefined
});
const initialRoot = initialResponse.root;
const node = postcss__default["default"].rule({
selector: sassVariableSelector
});
initialRoot.walkDecls(sassVariableDeclarationRegex, (decl) => {
if (decl.parent === initialRoot) {
node.append(getEncodedValueNode(decl));
}
});
initialRoot.append(node);
const { functions, silenceDeprecations, ...otherSassOptions } = sassOptions;
let sassResponse = sass__namespace.compileString(initialRoot.toString(), {
// @ts-ignore
functions: { ...jsonFns__default["default"], ...functions },
...otherSassOptions,
// eslint-disable-next-line unicorn/prefer-spread
silenceDeprecations: Array.from(
new Set([...(silenceDeprecations || []), 'color-4-api'])
)
});
let sassResponseString =
typeof sassResponse === 'undefined' ? '' : sassResponse.css.toString();
const finalResponse = cssProcessor.process(sassResponseString, {
from: undefined
});
const finalRoot = finalResponse.root;
/** @type {JsonObject} */
const data = {};
finalRoot.walkRules(sassVariableSelector, (rule) => {
rule.walkDecls('content', (decl) => {
let [property, value] = decl.value.split(' ":" ');
property = stripOuter__default["default"](property, '"');
value = sanitizeValue(value);
data[property] = JSON.parse(value);
});
});
if (camelize) {
return /** @type {JsonObject} */ (
camelcaseKeys__default["default"](
fromEntries__default["default"](
Object.entries(data).map(([key, value]) => [
stripOuter__default["default"](key, '$'),
value
])
)
)
);
}
return data;
}
main.sync = mainSync;
module.exports = main;
//# sourceMappingURL=index.js.map