From 27023225534f10de7cb93f97c81c7bad0b23fad7 Mon Sep 17 00:00:00 2001 From: Peter Goldberg Date: Sun, 2 Apr 2023 17:08:28 -0400 Subject: [PATCH] fix faulty source map generation with variables in selectors (#3761) * fix faulty source map generation * alternative solution * add test --- packages/less/src/less/parser/parser.js | 127 ++++++++---------- packages/less/src/less/tree/ruleset.js | 15 +-- packages/less/src/less/tree/selector.js | 7 +- packages/less/test/index.js | 2 + packages/less/test/less-test.js | 42 ++++-- .../sourcemaps-variable-selector/basic.json | 1 + .../sourcemaps-variable-selector/basic.less | 5 + .../sourcemaps-variable-selector/vars.less | 3 + 8 files changed, 113 insertions(+), 89 deletions(-) create mode 100644 packages/less/test/sourcemaps-variable-selector/basic.json create mode 100644 packages/test-data/less/sourcemaps-variable-selector/basic.less create mode 100644 packages/test-data/less/sourcemaps-variable-selector/vars.less diff --git a/packages/less/src/less/parser/parser.js b/packages/less/src/less/parser/parser.js index 1c05b145b..f7df98a3f 100644 --- a/packages/less/src/less/parser/parser.js +++ b/packages/less/src/less/parser/parser.js @@ -38,7 +38,8 @@ import functionRegistry from '../functions/function-registry'; // It also takes care of moving all the indices forwards. // -const Parser = function Parser(context, imports, fileInfo) { +const Parser = function Parser(context, imports, fileInfo, currentIndex) { + currentIndex = currentIndex || 0; let parsers; const parserInput = getParserInput(); @@ -60,7 +61,7 @@ const Parser = function Parser(context, imports, fileInfo) { if (result) { return result; } - + error(msg || (typeof arg === 'string' ? `expected '${arg}' got '${parserInput.currentChar()}'` : 'unexpected token')); @@ -85,13 +86,13 @@ const Parser = function Parser(context, imports, fileInfo) { /** * Used after initial parsing to create nodes on the fly - * - * @param {String} str - string to parse + * + * @param {String} str - string to parse * @param {Array} parseList - array of parsers to run input through e.g. ["value", "important"] * @param {Number} currentIndex - start number to begin indexing * @param {Object} fileInfo - fileInfo to attach to created nodes */ - function parseNode(str, parseList, currentIndex, fileInfo, callback) { + function parseNode(str, parseList, callback) { let result; const returnNodes = []; const parser = parserInput; @@ -103,19 +104,9 @@ const Parser = function Parser(context, imports, fileInfo) { index: index + currentIndex }); }); - for (let x = 0, p, i; (p = parseList[x]); x++) { - i = parser.i; + for (let x = 0, p; (p = parseList[x]); x++) { result = parsers[p](); - if (result) { - try { - result._index = i + currentIndex; - result._fileInfo = fileInfo; - } catch (e) {} - returnNodes.push(result); - } - else { - returnNodes.push(null); - } + returnNodes.push(result || null); } const endInfo = parser.end(); @@ -207,7 +198,7 @@ const Parser = function Parser(context, imports, fileInfo) { root.root = true; root.firstRoot = true; root.functionRegistry = functionRegistry.inherit(); - + } catch (e) { return callback(new LessError(e, imports, fileInfo.filename)); } @@ -337,7 +328,7 @@ const Parser = function Parser(context, imports, fileInfo) { continue; } - node = mixin.definition() || this.declaration() || mixin.call(false, false) || + node = mixin.definition() || this.declaration() || mixin.call(false, false) || this.ruleset() || this.variableCall() || this.entities.call() || this.atrule(); if (node) { root.push(node); @@ -360,7 +351,7 @@ const Parser = function Parser(context, imports, fileInfo) { comment: function () { if (parserInput.commentStore.length) { const comment = parserInput.commentStore.shift(); - return new(tree.Comment)(comment.text, comment.isLineComment, comment.index, fileInfo); + return new(tree.Comment)(comment.text, comment.isLineComment, comment.index + currentIndex, fileInfo); } }, @@ -396,7 +387,7 @@ const Parser = function Parser(context, imports, fileInfo) { } parserInput.forget(); - return new(tree.Quoted)(str.charAt(0), str.substr(1, str.length - 2), isEscaped, index, fileInfo); + return new(tree.Quoted)(str.charAt(0), str.substr(1, str.length - 2), isEscaped, index + currentIndex, fileInfo); }, // @@ -433,7 +424,7 @@ const Parser = function Parser(context, imports, fileInfo) { name = parserInput.$re(/^([\w-]+|%|~|progid:[\w\.]+)\(/); if (!name) { - parserInput.forget(); + parserInput.forget(); return; } @@ -456,9 +447,9 @@ const Parser = function Parser(context, imports, fileInfo) { parserInput.forget(); - return new(tree.Call)(name, args, index, fileInfo); + return new(tree.Call)(name, args, index + currentIndex, fileInfo); }, - + // // Parsing rules for functions with non-standard args, e.g.: // @@ -481,11 +472,11 @@ const Parser = function Parser(context, imports, fileInfo) { function f(parse, stop) { return { parse, // parsing function - stop // when true - stop after parse() and return its result, + stop // when true - stop after parse() and return its result, // otherwise continue for plain args }; } - + function condition() { return [expect(parsers.condition, 'expected condition')]; } @@ -591,10 +582,10 @@ const Parser = function Parser(context, imports, fileInfo) { expectChar(')'); - return new(tree.URL)((value.value !== undefined || - value instanceof tree.Variable || + return new(tree.URL)((value.value !== undefined || + value instanceof tree.Variable || value instanceof tree.Property) ? - value : new(tree.Anonymous)(value, index), index, fileInfo); + value : new(tree.Anonymous)(value, index), index + currentIndex, fileInfo); }, // @@ -622,7 +613,7 @@ const Parser = function Parser(context, imports, fileInfo) { } } parserInput.forget(); - return new(tree.Variable)(name, index, fileInfo); + return new(tree.Variable)(name, index + currentIndex, fileInfo); } parserInput.restore(); }, @@ -633,7 +624,7 @@ const Parser = function Parser(context, imports, fileInfo) { const index = parserInput.i; if (parserInput.currentChar() === '@' && (curly = parserInput.$re(/^@\{([\w-]+)\}/))) { - return new(tree.Variable)(`@${curly[1]}`, index, fileInfo); + return new(tree.Variable)(`@${curly[1]}`, index + currentIndex, fileInfo); } }, // @@ -646,7 +637,7 @@ const Parser = function Parser(context, imports, fileInfo) { const index = parserInput.i; if (parserInput.currentChar() === '$' && (name = parserInput.$re(/^\$[\w-]+/))) { - return new(tree.Property)(name, index, fileInfo); + return new(tree.Property)(name, index + currentIndex, fileInfo); } }, @@ -656,7 +647,7 @@ const Parser = function Parser(context, imports, fileInfo) { const index = parserInput.i; if (parserInput.currentChar() === '$' && (curly = parserInput.$re(/^\$\{([\w-]+)\}/))) { - return new(tree.Property)(`$${curly[1]}`, index, fileInfo); + return new(tree.Property)(`$${curly[1]}`, index + currentIndex, fileInfo); } }, // @@ -674,7 +665,7 @@ const Parser = function Parser(context, imports, fileInfo) { if (!rgb[2]) { parserInput.forget(); return new(tree.Color)(rgb[1], undefined, rgb[0]); - } + } } parserInput.restore(); }, @@ -749,7 +740,7 @@ const Parser = function Parser(context, imports, fileInfo) { js = parserInput.$re(/^[^`]*`/); if (js) { parserInput.forget(); - return new(tree.JavaScript)(js.substr(0, js.length - 1), Boolean(escape), index, fileInfo); + return new(tree.JavaScript)(js.substr(0, js.length - 1), Boolean(escape), index + currentIndex, fileInfo); } parserInput.restore('invalid javascript definition'); } @@ -844,7 +835,7 @@ const Parser = function Parser(context, imports, fileInfo) { if (!elements) { error('Missing target selector for :extend().'); } - extend = new(tree.Extend)(new(tree.Selector)(elements), option, index, fileInfo); + extend = new(tree.Extend)(new(tree.Selector)(elements), option, index + currentIndex, fileInfo); if (extendList) { extendList.push(extend); } else { @@ -930,7 +921,7 @@ const Parser = function Parser(context, imports, fileInfo) { if (inValue || parsers.end()) { parserInput.forget(); - const mixin = new(tree.mixin.Call)(elements, args, index, fileInfo, !lookups && important); + const mixin = new(tree.mixin.Call)(elements, args, index + currentIndex, fileInfo, !lookups && important); if (lookups) { return new tree.NamespaceValue(mixin, lookups); } @@ -956,11 +947,11 @@ const Parser = function Parser(context, imports, fileInfo) { while (true) { elemIndex = parserInput.i; e = parserInput.$re(re); - + if (!e) { break; } - elem = new(tree.Element)(c, e, false, elemIndex, fileInfo); + elem = new(tree.Element)(c, e, false, elemIndex + currentIndex, fileInfo); if (elements) { elements.push(elem); } else { @@ -1167,13 +1158,13 @@ const Parser = function Parser(context, imports, fileInfo) { parserInput.restore(); } }, - + ruleLookups: function() { let rule; let args; const lookups = []; - if (parserInput.currentChar() !== '[') { + if (parserInput.currentChar() !== '[') { return; } @@ -1192,27 +1183,27 @@ const Parser = function Parser(context, imports, fileInfo) { return lookups; } }, - + lookupValue: function() { parserInput.save(); - - if (!parserInput.$char('[')) { + + if (!parserInput.$char('[')) { parserInput.restore(); return; } - + const name = parserInput.$re(/^(?:[@$]{0,2})[_a-zA-Z0-9-]*/); - + if (!parserInput.$char(']')) { parserInput.restore(); return; - } + } if (name || name === '') { parserInput.forget(); return name; } - + parserInput.restore(); } }, @@ -1296,7 +1287,7 @@ const Parser = function Parser(context, imports, fileInfo) { } } - if (e) { return new(tree.Element)(c, e, e instanceof tree.Variable, index, fileInfo); } + if (e) { return new(tree.Element)(c, e, e instanceof tree.Variable, index + currentIndex, fileInfo); } }, // @@ -1380,7 +1371,7 @@ const Parser = function Parser(context, imports, fileInfo) { } } - if (elements) { return new(tree.Selector)(elements, allExtends, condition, index, fileInfo); } + if (elements) { return new(tree.Selector)(elements, allExtends, condition, index + currentIndex, fileInfo); } if (allExtends) { error('Extend must be used to extend a selector, it cannot be used on its own'); } }, selectors: function () { @@ -1466,7 +1457,7 @@ const Parser = function Parser(context, imports, fileInfo) { parserInput.save(); if (parserInput.$re(/^[.#]\(/)) { /** - * DR args currently only implemented for each() function, and not + * DR args currently only implemented for each() function, and not * yet settable as `@dr: #(@arg) {}` * This should be done when DRs are merged with mixins. * See: https://github.com/less/less-meta/issues/16 @@ -1561,7 +1552,7 @@ const Parser = function Parser(context, imports, fileInfo) { if (value) { parserInput.forget(); // anonymous values absorb the end ';' which is required for them to work - return new(tree.Declaration)(name, value, false, merge, index, fileInfo); + return new(tree.Declaration)(name, value, false, merge, index + currentIndex, fileInfo); } if (!value) { @@ -1578,7 +1569,7 @@ const Parser = function Parser(context, imports, fileInfo) { if (value && (this.end() || hasDR)) { parserInput.forget(); - return new(tree.Declaration)(name, value, important, merge, index, fileInfo); + return new(tree.Declaration)(name, value, important, merge, index + currentIndex, fileInfo); } else { parserInput.restore(); @@ -1591,14 +1582,14 @@ const Parser = function Parser(context, imports, fileInfo) { const index = parserInput.i; const match = parserInput.$re(/^([^.#@\$+\/'"*`(;{}-]*);/); if (match) { - return new(tree.Anonymous)(match[1], index); + return new(tree.Anonymous)(match[1], index + currentIndex); } }, /** * Used for custom properties, at-rules, and variables (as fallback) * Parses almost anything inside of {} [] () "" blocks * until it reaches outer-most tokens. - * + * * First, it will try to parse comments and entities to reach * the end. This is mostly like the Expression parser except no * math is allowed. @@ -1715,7 +1706,7 @@ const Parser = function Parser(context, imports, fileInfo) { error('missing semi-colon or unrecognised media features on import'); } features = features && new(tree.Value)(features); - return new(tree.Import)(path, features, options, index, fileInfo); + return new(tree.Import)(path, features, options, index + currentIndex, fileInfo); } else { parserInput.i = index; @@ -1777,7 +1768,7 @@ const Parser = function Parser(context, imports, fileInfo) { e = this.value(); if (parserInput.$char(')')) { if (p && e) { - nodes.push(new(tree.Paren)(new(tree.Declaration)(p, e, null, null, parserInput.i, fileInfo, true))); + nodes.push(new(tree.Paren)(new(tree.Declaration)(p, e, null, null, parserInput.i + currentIndex, fileInfo, true))); } else if (e) { nodes.push(new(tree.Paren)(e)); } else { @@ -1840,7 +1831,7 @@ const Parser = function Parser(context, imports, fileInfo) { parserInput.forget(); - media = new(tree.Media)(rules, features, index, fileInfo); + media = new(tree.Media)(rules, features, index + currentIndex, fileInfo); if (context.dumpLineNumbers) { media.debugInfo = debugInfo; } @@ -1883,7 +1874,7 @@ const Parser = function Parser(context, imports, fileInfo) { parserInput.i = index; error('missing semi-colon on @plugin'); } - return new(tree.Import)(path, null, options, index, fileInfo); + return new(tree.Import)(path, null, options, index + currentIndex, fileInfo); } else { parserInput.i = index; @@ -1904,7 +1895,7 @@ const Parser = function Parser(context, imports, fileInfo) { parserInput.forget(); return args[1].trim(); } - else { + else { parserInput.restore(); return null; } @@ -1999,7 +1990,7 @@ const Parser = function Parser(context, imports, fileInfo) { if (rules || (!hasBlock && value && parserInput.$char(';'))) { parserInput.forget(); - return new(tree.AtRule)(name, value, rules, index, fileInfo, + return new(tree.AtRule)(name, value, rules, index + currentIndex, fileInfo, context.dumpLineNumbers ? getDebugInfo(index) : null, isRooted ); @@ -2030,7 +2021,7 @@ const Parser = function Parser(context, imports, fileInfo) { } while (e); if (expressions.length > 0) { - return new(tree.Value)(expressions, index); + return new(tree.Value)(expressions, index + currentIndex); } }, important: function () { @@ -2132,7 +2123,7 @@ const Parser = function Parser(context, imports, fileInfo) { if (!b) { break; } - condition = new(tree.Condition)('or', condition || a, b, index); + condition = new(tree.Condition)('or', condition || a, b, index + currentIndex); } return condition || a; } @@ -2282,12 +2273,12 @@ const Parser = function Parser(context, imports, fileInfo) { if (op) { b = cond(); if (b) { - c = new(tree.Condition)(op, a, b, index, false); + c = new(tree.Condition)(op, a, b, index + currentIndex, false); } else { error('expected expression'); } } else { - c = new(tree.Condition)('=', a, new(tree.Keyword)('true'), index, false); + c = new(tree.Condition)('=', a, new(tree.Keyword)('true'), index + currentIndex, false); } return c; } @@ -2350,7 +2341,7 @@ const Parser = function Parser(context, imports, fileInfo) { if (!parserInput.peek(/^\/[\/*]/)) { delim = parserInput.$char('/'); if (delim) { - entities.push(new(tree.Anonymous)(delim, index)); + entities.push(new(tree.Anonymous)(delim, index + currentIndex)); } } } @@ -2410,8 +2401,8 @@ const Parser = function Parser(context, imports, fileInfo) { name[k] = (s.charAt(0) !== '@' && s.charAt(0) !== '$') ? new(tree.Keyword)(s) : (s.charAt(0) === '@' ? - new(tree.Variable)(`@${s.slice(2, -1)}`, index[k], fileInfo) : - new(tree.Property)(`$${s.slice(2, -1)}`, index[k], fileInfo)); + new(tree.Variable)(`@${s.slice(2, -1)}`, index[k] + currentIndex, fileInfo) : + new(tree.Property)(`$${s.slice(2, -1)}`, index[k] + currentIndex, fileInfo)); } return name; } diff --git a/packages/less/src/less/tree/ruleset.js b/packages/less/src/less/tree/ruleset.js index dfd044932..76392e868 100644 --- a/packages/less/src/less/tree/ruleset.js +++ b/packages/less/src/less/tree/ruleset.js @@ -11,6 +11,7 @@ import globalFunctionRegistry from '../functions/function-registry'; import defaultFunc from '../functions/default'; import getDebugInfo from './debug-info'; import * as utils from '../utils'; +import Parser from '../parser/parser'; const Ruleset = function(selectors, rules, strictImports, visibilityInfo) { this.selectors = selectors; @@ -79,11 +80,11 @@ Ruleset.prototype = Object.assign(new Node(), { selector = selectors[i]; toParseSelectors[i] = selector.toCSS(context); } - this.parse.parseNode( + const startingIndex = selectors[0].getIndex(); + const selectorFileInfo = selectors[0].fileInfo(); + new Parser(context, this.parse.importManager, selectorFileInfo, startingIndex).parseNode( toParseSelectors.join(','), - ["selectors"], - selectors[0].getIndex(), - selectors[0].fileInfo(), + ["selectors"], function(err, result) { if (result) { selectors = utils.flattenArray(result); @@ -354,11 +355,9 @@ Ruleset.prototype = Object.assign(new Node(), { function transformDeclaration(decl) { if (decl.value instanceof Anonymous && !decl.parsed) { if (typeof decl.value.value === 'string') { - this.parse.parseNode( + new Parser(this.parse.context, this.parse.importManager, decl.fileInfo(), decl.value.getIndex()).parseNode( decl.value.value, - ['value', 'important'], - decl.value.getIndex(), - decl.fileInfo(), + ['value', 'important'], function(err, result) { if (err) { decl.parsed = true; diff --git a/packages/less/src/less/tree/selector.js b/packages/less/src/less/tree/selector.js index d098cc2bc..003455aac 100644 --- a/packages/less/src/less/tree/selector.js +++ b/packages/less/src/less/tree/selector.js @@ -2,6 +2,7 @@ import Node from './node'; import Element from './element'; import LessError from '../less-error'; import * as utils from '../utils'; +import Parser from '../parser/parser'; const Selector = function(elements, extendList, condition, index, currentFileInfo, visibilityInfo) { this.extendList = extendList; @@ -44,11 +45,9 @@ Selector.prototype = Object.assign(new Node(), { return [new Element('', '&', false, this._index, this._fileInfo)]; } if (typeof els === 'string') { - this.parse.parseNode( - els, + new Parser(this.parse.context, this.parse.importManager, this._fileInfo, this._index).parseNode( + els, ['selector'], - this._index, - this._fileInfo, function(err, result) { if (err) { throw new LessError({ diff --git a/packages/less/test/index.js b/packages/less/test/index.js index 2a5803034..5ace868ed 100644 --- a/packages/less/test/index.js +++ b/packages/less/test/index.js @@ -63,6 +63,8 @@ var testMap = [ 'sourcemaps-empty/', lessTester.testEmptySourcemap], [{math: 'strict', strictUnits: true, sourceMap: {disableSourcemapAnnotation: true}}, 'sourcemaps-disable-annotation/', lessTester.testSourcemapWithoutUrlAnnotation], + [{math: 'strict', strictUnits: true, sourceMap: true}, + 'sourcemaps-variable-selector/', lessTester.testSourcemapWithVariableInSelector], [{globalVars: true, banner: '/**\n * Test\n */\n'}, 'globalVars/', null, null, null, function(name, type, baseFolder) { return path.join(baseFolder, name) + '.json'; }], [{modifyVars: true}, 'modifyVars/', diff --git a/packages/less/test/less-test.js b/packages/less/test/less-test.js index 67627d987..c06f4a5be 100644 --- a/packages/less/test/less-test.js +++ b/packages/less/test/less-test.js @@ -169,6 +169,29 @@ module.exports = function() { } } + function testSourcemapWithVariableInSelector(name, err, compiledLess, doReplacements, sourcemap, baseFolder) { + if (err) { + fail('ERROR: ' + (err && err.message)); + return; + } + + // Even if annotation is not necessary, the map file should be there. + fs.readFile(path.join('test/', name) + '.json', 'utf8', function (e, expectedSourcemap) { + process.stdout.write('- ' + path.join(baseFolder, name) + ': '); + if (sourcemap === expectedSourcemap) { + ok('OK'); + } else if (err) { + fail('ERROR: ' + (err && err.message)); + if (isVerbose) { + process.stdout.write('\n'); + process.stdout.write(err.stack + '\n'); + } + } else { + difference('FAIL', expectedSourcemap, sourcemap); + } + }); + } + function testImports(name, err, compiledLess, doReplacements, sourcemap, baseFolder, imports) { if (err) { fail('ERROR: ' + (err && err.message)); @@ -228,7 +251,7 @@ module.exports = function() { // To fix ci fail about error format change in upstream v8 project // https://github.com/v8/v8/commit/c0fd89c3c089e888c4f4e8582e56db7066fa779b - // Node 16.9.0+ include this change via https://github.com/nodejs/node/pull/39947 + // Node 16.9.0+ include this change via https://github.com/nodejs/node/pull/39947 function testTypeErrors(name, err, compiledLess, doReplacements, sourcemap, baseFolder) { const fileSuffix = semver.gte(process.version, 'v16.9.0') ? '-2.txt' : '.txt'; fs.readFile(path.join(baseFolder, name) + fileSuffix, 'utf8', function (e, expectedErr) { @@ -254,7 +277,7 @@ module.exports = function() { // https://github.com/less/less.js/issues/3112 function testJSImport() { process.stdout.write('- Testing root function registry'); - less.functions.functionRegistry.add('ext', function() { + less.functions.functionRegistry.add('ext', function() { return new less.tree.Anonymous('file'); }); var expected = '@charset "utf-8";\n'; @@ -282,7 +305,7 @@ module.exports = function() { .replace(/\{pathhref\}/g, '') .replace(/\{404status\}/g, '') .replace(/\{nodepath\}/g, path.join(process.cwd(), 'node_modules', '/')) - .replace(/\{pathrel\}/g, path.join(path.relative(lessFolder, p), '/')) + .replace(/\{pathrel\}/g, path.join(path.relative(lessFolder, p), '/')) .replace(/\{pathesc\}/g, pathesc) .replace(/\{pathimport\}/g, pathimport) .replace(/\{pathimportesc\}/g, pathimportesc) @@ -327,7 +350,7 @@ module.exports = function() { function runTestSetInternal(baseFolder, opts, foldername, verifyFunction, nameModifier, doReplacements, getFilename) { foldername = foldername || ''; - + var originalOptions = opts || {}; if (!doReplacements) { @@ -497,10 +520,10 @@ module.exports = function() { } /** - * - * @param {Object} options - * @param {string} filePath - * @param {Function} callback + * + * @param {Object} options + * @param {string} filePath + * @param {Function} callback */ function toCSS(options, filePath, callback) { options = options || {}; @@ -577,7 +600,7 @@ module.exports = function() { } ok(stylize('OK\n', 'green')); } - ); + ); } return { @@ -588,6 +611,7 @@ module.exports = function() { testTypeErrors: testTypeErrors, testSourcemap: testSourcemap, testSourcemapWithoutUrlAnnotation: testSourcemapWithoutUrlAnnotation, + testSourcemapWithVariableInSelector: testSourcemapWithVariableInSelector, testImports: testImports, testImportRedirect: testImportRedirect, testEmptySourcemap: testEmptySourcemap, diff --git a/packages/less/test/sourcemaps-variable-selector/basic.json b/packages/less/test/sourcemaps-variable-selector/basic.json new file mode 100644 index 000000000..9a454320f --- /dev/null +++ b/packages/less/test/sourcemaps-variable-selector/basic.json @@ -0,0 +1 @@ +{"version":3,"sources":["testweb/sourcemaps-variable-selector/basic.less"],"names":[],"mappings":"AAEC;EACG,eAAA","file":"sourcemaps-variable-selector/basic.css"} \ No newline at end of file diff --git a/packages/test-data/less/sourcemaps-variable-selector/basic.less b/packages/test-data/less/sourcemaps-variable-selector/basic.less new file mode 100644 index 000000000..fb8a9a05b --- /dev/null +++ b/packages/test-data/less/sourcemaps-variable-selector/basic.less @@ -0,0 +1,5 @@ +@import (reference) "./vars.less"; + +.@{hello}-class { + font-size: @font-size; +} diff --git a/packages/test-data/less/sourcemaps-variable-selector/vars.less b/packages/test-data/less/sourcemaps-variable-selector/vars.less new file mode 100644 index 000000000..202790d5f --- /dev/null +++ b/packages/test-data/less/sourcemaps-variable-selector/vars.less @@ -0,0 +1,3 @@ +@foo: bar; +@font-size: 12px; +@hello: world;