less 4.4.1 → 4.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/Gruntfile.js +16 -17
  2. package/bin/lessc +35 -40
  3. package/dist/less.js +125 -186
  4. package/dist/less.min.js +2 -2
  5. package/dist/less.min.js.map +1 -1
  6. package/lib/less/contexts.js +0 -1
  7. package/lib/less/contexts.js.map +1 -1
  8. package/lib/less/default-options.js +22 -3
  9. package/lib/less/default-options.js.map +1 -1
  10. package/lib/less/less-error.js +2 -1
  11. package/lib/less/less-error.js.map +1 -1
  12. package/lib/less/parse-tree.js +58 -1
  13. package/lib/less/parse-tree.js.map +1 -1
  14. package/lib/less/parser/parser-input.js +2 -19
  15. package/lib/less/parser/parser-input.js.map +1 -1
  16. package/lib/less/parser/parser.js +8 -14
  17. package/lib/less/parser/parser.js.map +1 -1
  18. package/lib/less/source-map-output.js +1 -1
  19. package/lib/less/source-map-output.js.map +1 -1
  20. package/lib/less/tree/debug-info.js +29 -0
  21. package/lib/less/tree/debug-info.js.map +1 -1
  22. package/lib/less/tree/nested-at-rule.js +1 -1
  23. package/lib/less/tree/nested-at-rule.js.map +1 -1
  24. package/lib/less-node/lessc-helper.js +8 -7
  25. package/lib/less-node/lessc-helper.js.map +1 -1
  26. package/package.json +10 -5
  27. package/scripts/coverage-lines.js +207 -0
  28. package/scripts/coverage-report.js +158 -0
  29. package/scripts/postinstall.js +61 -0
  30. package/test/browser/common.js +22 -1
  31. package/test/browser/generator/runner.config.js +20 -24
  32. package/test/browser/generator/template.js +14 -3
  33. package/test/browser/runner-browser-options.js +5 -5
  34. package/test/index.js +293 -98
  35. package/test/less-test.js +453 -94
  36. package/test/sourcemaps/comprehensive.json +1 -0
  37. package/test/sourcemaps/sourcemaps-basepath.json +1 -0
  38. package/test/sourcemaps/sourcemaps-include-source.json +1 -0
  39. package/test/sourcemaps/sourcemaps-rootpath.json +1 -0
  40. package/test/sourcemaps/sourcemaps-url.json +1 -0
  41. package/test.less +1 -0
  42. package/tsconfig.json +20 -20
  43. package/lib/less/parser/chunker.js +0 -148
  44. package/lib/less/parser/chunker.js.map +0 -1
  45. package/test/browser/runner-legacy-options.js +0 -6
  46. package/test/browser/runner-legacy-spec.js +0 -3
package/test/less-test.js CHANGED
@@ -1,6 +1,8 @@
1
1
  /* jshint latedef: nofunc */
2
2
  var semver = require('semver');
3
3
  var logger = require('../lib/less/logger').default;
4
+ var { cosmiconfigSync } = require('cosmiconfig');
5
+ var glob = require('glob');
4
6
 
5
7
  var isVerbose = process.env.npm_config_loglevel !== 'concise';
6
8
  logger.addListener({
@@ -18,7 +20,7 @@ logger.addListener({
18
20
  });
19
21
 
20
22
 
21
- module.exports = function() {
23
+ module.exports = function(testFilter) {
22
24
  var path = require('path'),
23
25
  fs = require('fs'),
24
26
  clone = require('copy-anything').copy;
@@ -29,11 +31,11 @@ module.exports = function() {
29
31
 
30
32
  var globals = Object.keys(global);
31
33
 
32
- var oneTestOnly = process.argv[2],
34
+ var oneTestOnly = testFilter || process.argv[2],
33
35
  isFinished = false;
34
36
 
35
37
  var testFolder = path.dirname(require.resolve('@less/test-data'));
36
- var lessFolder = path.join(testFolder, 'less');
38
+ var lessFolder = testFolder;
37
39
 
38
40
  // Define String.prototype.endsWith if it doesn't exist (in older versions of node)
39
41
  // This is required by the testSourceMap function below
@@ -83,33 +85,202 @@ module.exports = function() {
83
85
  }
84
86
  });
85
87
 
86
- function testSourcemap(name, err, compiledLess, doReplacements, sourcemap, baseFolder) {
88
+ function validateSourcemapMappings(sourcemap, lessFile, compiledCSS) {
89
+ // Validate sourcemap mappings using SourceMapConsumer
90
+ var SourceMapConsumer = require('source-map').SourceMapConsumer;
91
+ // sourcemap can be either a string or already parsed object
92
+ var sourceMapObj = typeof sourcemap === 'string' ? JSON.parse(sourcemap) : sourcemap;
93
+ var consumer = new SourceMapConsumer(sourceMapObj);
94
+
95
+ // Read the LESS source file
96
+ var lessSource = fs.readFileSync(lessFile, 'utf8');
97
+ var lessLines = lessSource.split('\n');
98
+
99
+ // Use the compiled CSS (remove sourcemap annotation for validation)
100
+ var cssSource = compiledCSS.replace(/\/\*# sourceMappingURL=.*\*\/\s*$/, '').trim();
101
+ var cssLines = cssSource.split('\n');
102
+
103
+ var errors = [];
104
+ var validatedMappings = 0;
105
+
106
+ // Validate mappings for each line in the CSS
107
+ for (var cssLine = 1; cssLine <= cssLines.length; cssLine++) {
108
+ var cssLineContent = cssLines[cssLine - 1];
109
+ // Skip empty lines
110
+ if (!cssLineContent.trim()) {
111
+ continue;
112
+ }
113
+
114
+ // Check mapping for the start of this CSS line
115
+ var mapping = consumer.originalPositionFor({
116
+ line: cssLine,
117
+ column: 0
118
+ });
119
+
120
+ if (mapping.source) {
121
+ validatedMappings++;
122
+
123
+ // Verify the source file exists in the sourcemap
124
+ if (!sourceMapObj.sources || sourceMapObj.sources.indexOf(mapping.source) === -1) {
125
+ errors.push('Line ' + cssLine + ': mapped to source "' + mapping.source + '" which is not in sources array');
126
+ }
127
+
128
+ // Verify the line number is valid
129
+ if (mapping.line && mapping.line > 0) {
130
+ // If we can find the source file, validate the line exists
131
+ var sourceIndex = sourceMapObj.sources.indexOf(mapping.source);
132
+ if (sourceIndex >= 0 && sourceMapObj.sourcesContent && sourceMapObj.sourcesContent[sourceIndex] !== undefined && sourceMapObj.sourcesContent[sourceIndex] !== null) {
133
+ var sourceContent = sourceMapObj.sourcesContent[sourceIndex];
134
+ // Ensure sourceContent is a string (it should be, but be defensive)
135
+ if (typeof sourceContent !== 'string') {
136
+ sourceContent = String(sourceContent);
137
+ }
138
+ // Split by newline - handle both \n and \r\n
139
+ var sourceLines = sourceContent.split(/\r?\n/);
140
+ if (mapping.line > sourceLines.length) {
141
+ errors.push('Line ' + cssLine + ': mapped to line ' + mapping.line + ' in "' + mapping.source + '" but source only has ' + sourceLines.length + ' lines');
142
+ }
143
+ } else if (sourceIndex >= 0) {
144
+ // Source content not embedded, try to validate against the actual file if it matches
145
+ // This is a best-effort validation
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ // Validate that all sources in the sourcemap are valid
152
+ if (sourceMapObj.sources) {
153
+ sourceMapObj.sources.forEach(function(source, index) {
154
+ if (sourceMapObj.sourcesContent && sourceMapObj.sourcesContent[index]) {
155
+ // Source content is embedded, validate it's not empty
156
+ if (!sourceMapObj.sourcesContent[index].trim()) {
157
+ errors.push('Source "' + source + '" has empty content');
158
+ }
159
+ }
160
+ });
161
+ }
162
+
163
+ if (consumer.destroy && typeof consumer.destroy === 'function') {
164
+ consumer.destroy();
165
+ }
166
+
167
+ return {
168
+ valid: errors.length === 0,
169
+ errors: errors,
170
+ mappingsValidated: validatedMappings
171
+ };
172
+ }
173
+
174
+ function testSourcemap(name, err, compiledLess, doReplacements, sourcemap, baseFolder, getFilename) {
87
175
  if (err) {
88
176
  fail('ERROR: ' + (err && err.message));
89
177
  return;
90
178
  }
91
179
  // Check the sourceMappingURL at the bottom of the file
92
- var expectedSourceMapURL = name + '.css.map',
93
- sourceMappingPrefix = '/*# sourceMappingURL=',
94
- sourceMappingSuffix = ' */',
95
- expectedCSSAppendage = sourceMappingPrefix + expectedSourceMapURL + sourceMappingSuffix;
96
- if (!compiledLess.endsWith(expectedCSSAppendage)) {
97
- // To display a better error message, we need to figure out what the actual sourceMappingURL value was, if it was even present
98
- var indexOfSourceMappingPrefix = compiledLess.indexOf(sourceMappingPrefix);
99
- if (indexOfSourceMappingPrefix === -1) {
100
- fail('ERROR: sourceMappingURL was not found in ' + baseFolder + '/' + name + '.css.');
101
- return;
102
- }
103
-
104
- var startOfSourceMappingValue = indexOfSourceMappingPrefix + sourceMappingPrefix.length,
105
- indexOfNextSpace = compiledLess.indexOf(' ', startOfSourceMappingValue),
106
- actualSourceMapURL = compiledLess.substring(startOfSourceMappingValue, indexOfNextSpace === -1 ? compiledLess.length : indexOfNextSpace);
107
- fail('ERROR: sourceMappingURL should be "' + expectedSourceMapURL + '" but is "' + actualSourceMapURL + '".');
180
+ // Default expected URL is name + '.css.map', but can be overridden by sourceMapURL option
181
+ var sourceMappingPrefix = '/*# sourceMappingURL=',
182
+ sourceMappingSuffix = ' */';
183
+ var indexOfSourceMappingPrefix = compiledLess.indexOf(sourceMappingPrefix);
184
+ if (indexOfSourceMappingPrefix === -1) {
185
+ fail('ERROR: sourceMappingURL was not found in ' + baseFolder + '/' + name + '.css.');
186
+ return;
187
+ }
188
+
189
+ var startOfSourceMappingValue = indexOfSourceMappingPrefix + sourceMappingPrefix.length,
190
+ indexOfSuffix = compiledLess.indexOf(sourceMappingSuffix, startOfSourceMappingValue),
191
+ actualSourceMapURL = compiledLess.substring(startOfSourceMappingValue, indexOfSuffix === -1 ? compiledLess.length : indexOfSuffix).trim();
192
+
193
+ // For tests with custom sourceMapURL, we just verify it exists and is non-empty
194
+ // The actual value will be validated by comparing the sourcemap JSON
195
+ if (!actualSourceMapURL) {
196
+ fail('ERROR: sourceMappingURL is empty in ' + baseFolder + '/' + name + '.css.');
197
+ return;
108
198
  }
109
199
 
110
- fs.readFile(path.join('test/', name) + '.json', 'utf8', function (e, expectedSourcemap) {
200
+ // Use getFilename if available (for sourcemap tests with subdirectories)
201
+ var jsonPath;
202
+ if (getFilename && typeof getFilename === 'function') {
203
+ jsonPath = getFilename(name, 'sourcemap', baseFolder);
204
+ } else {
205
+ // Fallback: extract just the filename for sourcemap JSON files
206
+ var jsonFilename = path.basename(name);
207
+ jsonPath = path.join('test/sourcemaps', jsonFilename) + '.json';
208
+ }
209
+ fs.readFile(jsonPath, 'utf8', function (e, expectedSourcemap) {
111
210
  process.stdout.write('- ' + path.join(baseFolder, name) + ': ');
112
- if (sourcemap === expectedSourcemap) {
211
+ if (e) {
212
+ fail('ERROR: Could not read expected sourcemap file: ' + jsonPath + ' - ' + e.message);
213
+ return;
214
+ }
215
+
216
+ // Apply doReplacements to the expected sourcemap to handle {path} placeholders
217
+ // This normalizes absolute paths that differ between environments
218
+ // For sourcemaps, we need to ensure {path} uses forward slashes to avoid breaking JSON
219
+ // (backslashes in JSON strings need escaping, and sourcemaps should use forward slashes anyway)
220
+ var replacementPath = path.join(path.dirname(path.join(baseFolder, name) + '.less'), '/');
221
+ // Normalize to forward slashes for sourcemap JSON (web-compatible)
222
+ replacementPath = replacementPath.replace(/\\/g, '/');
223
+ // Replace {path} with normalized forward-slash path BEFORE calling doReplacements
224
+ // This ensures the JSON is always valid and uses web-compatible paths
225
+ expectedSourcemap = expectedSourcemap.replace(/\{path\}/g, replacementPath);
226
+ // Also handle other placeholders that might be in the sourcemap (but {path} is already done)
227
+ expectedSourcemap = doReplacements(expectedSourcemap, baseFolder, path.join(baseFolder, name) + '.less');
228
+
229
+ // Normalize paths in sourcemap JSON to use forward slashes (web-compatible)
230
+ // We need to parse the JSON, normalize the file property, then stringify for comparison
231
+ // This avoids breaking escape sequences like \n in the JSON string
232
+ function normalizeSourcemapPaths(sm) {
233
+ try {
234
+ var parsed = typeof sm === 'string' ? JSON.parse(sm) : sm;
235
+ if (parsed.file) {
236
+ parsed.file = parsed.file.replace(/\\/g, '/');
237
+ }
238
+ // Also normalize paths in sources array
239
+ if (parsed.sources && Array.isArray(parsed.sources)) {
240
+ parsed.sources = parsed.sources.map(function(src) {
241
+ return src.replace(/\\/g, '/');
242
+ });
243
+ }
244
+ return JSON.stringify(parsed, null, 0);
245
+ } catch (parseErr) {
246
+ // If parsing fails, return original (shouldn't happen)
247
+ return sm;
248
+ }
249
+ }
250
+
251
+ var normalizedSourcemap = normalizeSourcemapPaths(sourcemap);
252
+ var normalizedExpected = normalizeSourcemapPaths(expectedSourcemap);
253
+
254
+ if (normalizedSourcemap === normalizedExpected) {
255
+ // Validate the sourcemap mappings are correct
256
+ // Find the actual LESS file - it might be in a subdirectory
257
+ var nameParts = name.split('/');
258
+ var lessFileName = nameParts[nameParts.length - 1];
259
+ var lessFileDir = nameParts.length > 1 ? nameParts.slice(0, -1).join('/') : '';
260
+ var lessFile = path.join(lessFolder, lessFileDir, lessFileName) + '.less';
261
+
262
+ // Only validate if the LESS file exists
263
+ if (fs.existsSync(lessFile)) {
264
+ try {
265
+ // Parse the sourcemap once for validation (avoid re-parsing)
266
+ // Use the original sourcemap string, not the normalized one
267
+ var sourceMapObjForValidation = typeof sourcemap === 'string' ? JSON.parse(sourcemap) : sourcemap;
268
+ var validation = validateSourcemapMappings(sourceMapObjForValidation, lessFile, compiledLess);
269
+ if (!validation.valid) {
270
+ fail('ERROR: Sourcemap validation failed:\n' + validation.errors.join('\n'));
271
+ return;
272
+ }
273
+ if (isVerbose && validation.mappingsValidated > 0) {
274
+ process.stdout.write(' (validated ' + validation.mappingsValidated + ' mappings)');
275
+ }
276
+ } catch (validationErr) {
277
+ if (isVerbose) {
278
+ process.stdout.write(' (validation error: ' + validationErr.message + ')');
279
+ }
280
+ // Don't fail the test if validation has an error, just log it
281
+ }
282
+ }
283
+
113
284
  ok('OK');
114
285
  } else if (err) {
115
286
  fail('ERROR: ' + (err && err.message));
@@ -118,7 +289,7 @@ module.exports = function() {
118
289
  process.stdout.write(err.stack + '\n');
119
290
  }
120
291
  } else {
121
- difference('FAIL', expectedSourcemap, sourcemap);
292
+ difference('FAIL', normalizedExpected, normalizedSourcemap);
122
293
  }
123
294
  });
124
295
  }
@@ -281,7 +452,7 @@ module.exports = function() {
281
452
  return new less.tree.Anonymous('file');
282
453
  });
283
454
  var expected = '@charset "utf-8";\n';
284
- toCSS({}, path.join(lessFolder, 'root-registry', 'root.less'), function(error, output) {
455
+ toCSS({}, path.join(lessFolder, 'tests-config', 'root-registry', 'root.less'), function(error, output) {
285
456
  if (error) {
286
457
  return fail('ERROR: ' + error);
287
458
  }
@@ -294,9 +465,42 @@ module.exports = function() {
294
465
 
295
466
  function globalReplacements(input, directory, filename) {
296
467
  var path = require('path');
297
- var p = filename ? path.join(path.dirname(filename), '/') : directory,
298
- pathimport = path.join(directory + 'import/'),
299
- pathesc = p.replace(/[.:/\\]/g, function(a) { return '\\' + (a == '\\' ? '\/' : a); }),
468
+ var p = filename ? path.join(path.dirname(filename), '/') : directory;
469
+
470
+ // For debug tests in subdirectories (comments/, mediaquery/, all/),
471
+ // the import/ directory and main linenumbers.less file are at the parent debug/ level, not in the subdirectory
472
+ var isDebugSubdirectory = false;
473
+ var debugParentPath = null;
474
+
475
+ if (directory) {
476
+ // Normalize directory path separators for matching
477
+ var normalizedDir = directory.replace(/\\/g, '/');
478
+ // Check if we're in a debug subdirectory
479
+ if (normalizedDir.includes('/debug/') && (normalizedDir.includes('/comments/') || normalizedDir.includes('/mediaquery/') || normalizedDir.includes('/all/'))) {
480
+ isDebugSubdirectory = true;
481
+ // Extract the debug/ directory path (parent of the subdirectory)
482
+ // Match everything up to and including /debug/ (works with both absolute and relative paths)
483
+ var debugMatch = normalizedDir.match(/(.+\/debug)\//);
484
+ if (debugMatch) {
485
+ debugParentPath = debugMatch[1];
486
+ }
487
+ }
488
+ }
489
+
490
+ if (isDebugSubdirectory && debugParentPath) {
491
+ // For {path} placeholder, use the parent debug/ directory
492
+ // Convert back to native path format
493
+ p = debugParentPath.replace(/\//g, path.sep) + path.sep;
494
+ }
495
+
496
+ var pathimport;
497
+ if (isDebugSubdirectory && debugParentPath) {
498
+ pathimport = path.join(debugParentPath.replace(/\//g, path.sep), 'import') + path.sep;
499
+ } else {
500
+ pathimport = path.join(directory + 'import/');
501
+ }
502
+
503
+ var pathesc = p.replace(/[.:/\\]/g, function(a) { return '\\' + (a == '\\' ? '\/' : a); }),
300
504
  pathimportesc = pathimport.replace(/[.:/\\]/g, function(a) { return '\\' + (a == '\\' ? '\/' : a); });
301
505
 
302
506
  return input.replace(/\{path\}/g, p)
@@ -340,7 +544,18 @@ module.exports = function() {
340
544
  }
341
545
 
342
546
  function runTestSet(options, foldername, verifyFunction, nameModifier, doReplacements, getFilename) {
343
- options = options ? clone(options) : {};
547
+ // Handle case where first parameter is glob patterns (no options object)
548
+ if (Array.isArray(options)) {
549
+ // First parameter is glob patterns, no options object
550
+ foldername = options;
551
+ options = {};
552
+ } else if (typeof options === 'string') {
553
+ // First parameter is foldername (no options object)
554
+ foldername = options;
555
+ options = {};
556
+ } else {
557
+ options = options ? clone(options) : {};
558
+ }
344
559
  runTestSetInternal(lessFolder, options, foldername, verifyFunction, nameModifier, doReplacements, getFilename);
345
560
  }
346
561
 
@@ -357,41 +572,125 @@ module.exports = function() {
357
572
  doReplacements = globalReplacements;
358
573
  }
359
574
 
360
- function getBasename(file) {
361
- return foldername + path.basename(file, '.less');
362
- }
575
+ // Handle glob patterns with exclusions
576
+ if (Array.isArray(foldername)) {
577
+ var patterns = foldername;
578
+ var includePatterns = [];
579
+ var excludePatterns = [];
580
+
581
+
582
+ patterns.forEach(function(pattern) {
583
+ if (pattern.startsWith('!')) {
584
+ excludePatterns.push(pattern.substring(1));
585
+ } else {
586
+ includePatterns.push(pattern);
587
+ }
588
+ });
589
+
590
+ // Use glob to find all matching files, excluding the excluded patterns
591
+ var allFiles = [];
592
+ includePatterns.forEach(function(pattern) {
593
+ var files = glob.sync(pattern, {
594
+ cwd: baseFolder,
595
+ absolute: true,
596
+ ignore: excludePatterns
597
+ });
598
+
599
+ allFiles = allFiles.concat(files);
600
+ });
601
+
602
+ // Note: needle mocking is set up globally in index.js
603
+
604
+ // Process each .less file found
605
+ allFiles.forEach(function(filePath) {
606
+ if (/\.less$/.test(filePath)) {
607
+ var file = path.basename(filePath);
608
+ // For glob patterns, we need to construct the relative path differently
609
+ // The filePath is absolute, so we need to get the path relative to the test-data directory
610
+ var relativePath = path.relative(baseFolder, path.dirname(filePath)) + '/';
611
+
612
+ // Only process files that have corresponding .css files (these are the actual tests)
613
+ var cssPath = path.join(path.dirname(filePath), path.basename(file, '.less') + '.css');
614
+ if (fs.existsSync(cssPath)) {
615
+ // Process this file using the existing logic
616
+ processFileWithInfo({
617
+ file: file,
618
+ fullPath: filePath,
619
+ relativePath: relativePath
620
+ });
621
+ }
622
+ }
623
+ });
624
+
363
625
 
364
- fs.readdirSync(path.join(baseFolder, foldername)).forEach(function (file) {
365
- if (!/\.less$/.test(file)) { return; }
626
+ return;
627
+ }
366
628
 
367
- var options = clone(originalOptions);
629
+ function processFileWithInfo(fileInfo) {
630
+ var file = fileInfo.file;
631
+ var fullPath = fileInfo.fullPath;
632
+ var relativePath = fileInfo.relativePath;
633
+
634
+ // Load config for this specific file using cosmiconfig
635
+ var configResult = cosmiconfigSync('styles').search(path.dirname(fullPath));
636
+
637
+ // Deep clone the original options to prevent Less from modifying shared objects
638
+ var options = JSON.parse(JSON.stringify(originalOptions || {}));
639
+
640
+ if (configResult && configResult.config && configResult.config.language && configResult.config.language.less) {
641
+ // Deep clone and merge the language.less settings with the original options
642
+ var lessConfig = JSON.parse(JSON.stringify(configResult.config.language.less));
643
+ Object.keys(lessConfig).forEach(function(key) {
644
+ options[key] = lessConfig[key];
645
+ });
646
+ }
647
+
648
+ // Merge any lessOptions from the testMap (for dynamic options like getVars functions)
649
+ if (originalOptions && originalOptions.lessOptions) {
650
+ Object.keys(originalOptions.lessOptions).forEach(function(key) {
651
+ var value = originalOptions.lessOptions[key];
652
+ if (typeof value === 'function') {
653
+ // For functions, call them with the file path
654
+ var result = value(fullPath);
655
+ options[key] = result;
656
+ } else {
657
+ // For static values, use them directly
658
+ options[key] = value;
659
+ }
660
+ });
661
+ }
368
662
 
369
- options.stylize = stylize;
663
+ // Don't pass stylize to less.render as it's not a valid option
370
664
 
371
- var name = getBasename(file);
665
+ var name = getBasename(file, relativePath);
666
+
372
667
 
373
- if (oneTestOnly && name !== oneTestOnly) {
668
+ if (oneTestOnly && typeof oneTestOnly === 'string' && !name.includes(oneTestOnly)) {
374
669
  return;
375
670
  }
376
671
 
377
672
  totalTests++;
378
673
 
379
674
  if (options.sourceMap && !options.sourceMap.sourceMapFileInline) {
380
- options.sourceMap = {
381
- sourceMapOutputFilename: name + '.css',
382
- sourceMapBasepath: baseFolder,
383
- sourceMapRootpath: 'testweb/',
384
- disableSourcemapAnnotation: options.sourceMap.disableSourcemapAnnotation
385
- };
386
- // This options is normally set by the bin/lessc script. Setting it causes the sourceMappingURL comment to be appended to the CSS
387
- // output. The value is designed to allow the sourceMapBasepath option to be tested, as it should be removed by less before
388
- // setting the sourceMappingURL value, leaving just the sourceMapOutputFilename and .map extension.
389
- options.sourceMap.sourceMapFilename = options.sourceMap.sourceMapBasepath + '/' + options.sourceMap.sourceMapOutputFilename + '.map';
675
+ // Set test infrastructure defaults only if not already set by styles.config.cjs
676
+ // Less.js core (parse-tree.js) will handle normalization of:
677
+ // - sourceMapBasepath (defaults to input file's directory)
678
+ // - sourceMapInputFilename (defaults to options.filename)
679
+ // - sourceMapFilename (derived from sourceMapOutputFilename or input filename)
680
+ // - sourceMapOutputFilename (derived from input filename if not set)
681
+ if (!options.sourceMap.sourceMapOutputFilename) {
682
+ // Needed for sourcemap file name in JSON output
683
+ options.sourceMap.sourceMapOutputFilename = name + '.css';
684
+ }
685
+ if (!options.sourceMap.sourceMapRootpath) {
686
+ // Test-specific default for consistent test output paths
687
+ options.sourceMap.sourceMapRootpath = 'testweb/';
688
+ }
390
689
  }
391
690
 
392
691
  options.getVars = function(file) {
393
692
  try {
394
- return JSON.parse(fs.readFileSync(getFilename(getBasename(file), 'vars', baseFolder), 'utf8'));
693
+ return JSON.parse(fs.readFileSync(getFilename(getBasename(file, relativePath), 'vars', baseFolder), 'utf8'));
395
694
  }
396
695
  catch (e) {
397
696
  return {};
@@ -400,7 +699,7 @@ module.exports = function() {
400
699
 
401
700
  var doubleCallCheck = false;
402
701
  queue(function() {
403
- toCSS(options, path.join(baseFolder, foldername + file), function (err, result) {
702
+ toCSS(options, fullPath, function (err, result) {
404
703
 
405
704
  if (doubleCallCheck) {
406
705
  totalTests++;
@@ -416,7 +715,7 @@ module.exports = function() {
416
715
  */
417
716
  if (verifyFunction) {
418
717
  var verificationResult = verifyFunction(
419
- name, err, result && result.css, doReplacements, result && result.map, baseFolder, result && result.imports
718
+ name, err, result && result.css, doReplacements, result && result.map, baseFolder, result && result.imports, getFilename
420
719
  );
421
720
  release();
422
721
  return verificationResult;
@@ -439,31 +738,90 @@ module.exports = function() {
439
738
  var css_name = name;
440
739
  if (nameModifier) { css_name = nameModifier(name); }
441
740
 
442
- fs.readFile(path.join(testFolder, 'css', css_name) + '.css', 'utf8', function (e, css) {
443
- process.stdout.write('- ' + path.join(baseFolder, css_name) + ': ');
741
+ // Check if we're using the new co-located structure (tests-unit/ or tests-config/) or the old separated structure
742
+ var cssPath;
743
+ if (relativePath.startsWith('tests-unit/') || relativePath.startsWith('tests-config/')) {
744
+ // New co-located structure: CSS file is in the same directory as LESS file
745
+ cssPath = path.join(path.dirname(fullPath), path.basename(file, '.less') + '.css');
746
+ } else {
747
+ // Old separated structure: CSS file is in separate css/ folder
748
+ // Windows compatibility: css_name may already contain path separators
749
+ // Use path.join with empty string to let path.join handle normalization
750
+ cssPath = path.join(testFolder, css_name) + '.css';
751
+ }
444
752
 
445
- css = css && doReplacements(css, path.join(baseFolder, foldername));
446
- if (result.css === css) { ok('OK'); }
447
- else {
448
- difference('FAIL', css, result.css);
753
+ // For the new structure, we need to handle replacements differently
754
+ var replacementPath;
755
+ if (relativePath.startsWith('tests-unit/') || relativePath.startsWith('tests-config/')) {
756
+ replacementPath = path.dirname(fullPath);
757
+ // Ensure replacementPath ends with a path separator for consistent matching
758
+ if (!replacementPath.endsWith(path.sep)) {
759
+ replacementPath += path.sep;
449
760
  }
450
- release();
451
- });
761
+ } else {
762
+ replacementPath = path.join(baseFolder, relativePath);
763
+ }
764
+
765
+ var testName = fullPath.replace(/\.less$/, '');
766
+ process.stdout.write('- ' + testName + ': ');
767
+
768
+
769
+ var css = fs.readFileSync(cssPath, 'utf8');
770
+ css = css && doReplacements(css, replacementPath);
771
+ if (result.css === css) { ok('OK'); }
772
+ else {
773
+ difference('FAIL', css, result.css);
774
+ }
775
+
776
+ release();
452
777
  });
453
778
  });
779
+ }
780
+
781
+ function getBasename(file, relativePath) {
782
+ var basePath = relativePath || foldername;
783
+ // Ensure basePath ends with a slash for proper path construction
784
+ if (basePath.charAt(basePath.length - 1) !== '/') {
785
+ basePath = basePath + '/';
786
+ }
787
+ return basePath + path.basename(file, '.less');
788
+ }
789
+
790
+
791
+ // This function is only called for non-glob patterns now
792
+ // For glob patterns, we use the glob library in the calling code
793
+ var dirPath = path.join(baseFolder, foldername);
794
+ var items = fs.readdirSync(dirPath);
795
+
796
+ items.forEach(function(item) {
797
+ if (/\.less$/.test(item)) {
798
+ processFileWithInfo({
799
+ file: item,
800
+ fullPath: path.join(dirPath, item),
801
+ relativePath: foldername
802
+ });
803
+ }
454
804
  });
455
805
  }
456
806
 
457
807
  function diff(left, right) {
458
- require('diff').diffLines(left, right).forEach(function(item) {
459
- if (item.added || item.removed) {
460
- var text = item.value && item.value.replace('\n', String.fromCharCode(182) + '\n').replace('\ufeff', '[[BOM]]');
461
- process.stdout.write(stylize(text, item.added ? 'green' : 'red'));
462
- } else {
463
- process.stdout.write(item.value && item.value.replace('\ufeff', '[[BOM]]'));
464
- }
808
+ // Configure chalk to always show colors
809
+ var chalk = require('chalk');
810
+ chalk.level = 3; // Force colors on
811
+
812
+ // Use jest-diff for much clearer output like Vitest
813
+ var diffResult = require('jest-diff').diffStringsUnified(left || '', right || '', {
814
+ expand: false,
815
+ includeChangeCounts: true,
816
+ contextLines: 1,
817
+ aColor: chalk.red,
818
+ bColor: chalk.green,
819
+ changeColor: chalk.inverse,
820
+ commonColor: chalk.dim
465
821
  });
466
- process.stdout.write('\n');
822
+
823
+ // jest-diff returns a string with ANSI colors, so we can output it directly
824
+ process.stdout.write(diffResult + '\n');
467
825
  }
468
826
 
469
827
  function fail(msg) {
@@ -476,6 +834,9 @@ module.exports = function() {
476
834
  process.stdout.write(stylize(msg, 'yellow') + '\n');
477
835
  failedTests++;
478
836
 
837
+ // Only show the diff, not the full text
838
+ process.stdout.write(stylize('Diff:', 'yellow') + '\n');
839
+
479
840
  diff(left || '', right || '');
480
841
  endTest();
481
842
  }
@@ -528,27 +889,41 @@ module.exports = function() {
528
889
  * @param {Function} callback
529
890
  */
530
891
  function toCSS(options, filePath, callback) {
531
- options = options || {};
892
+ // Deep clone options to prevent modifying the original, but preserve functions
893
+ var originalOptions = options || {};
894
+ options = JSON.parse(JSON.stringify(originalOptions));
895
+
896
+ // Restore functions that were lost in JSON serialization
897
+ if (originalOptions.getVars) {
898
+ options.getVars = originalOptions.getVars;
899
+ }
532
900
  var str = fs.readFileSync(filePath, 'utf8'), addPath = path.dirname(filePath);
901
+
902
+ // Initialize paths array if it doesn't exist
533
903
  if (typeof options.paths !== 'string') {
534
904
  options.paths = options.paths || [];
535
- if (!contains(options.paths, addPath)) {
536
- options.paths.push(addPath);
537
- }
538
905
  } else {
539
- options.paths = [options.paths]
906
+ options.paths = [options.paths];
907
+ }
908
+
909
+ // Add the current directory to paths if not already present
910
+ if (!contains(options.paths, addPath)) {
911
+ options.paths.push(addPath);
540
912
  }
913
+
914
+ // Resolve all paths relative to the test file's directory
541
915
  options.paths = options.paths.map(searchPath => {
542
- return path.resolve(lessFolder, searchPath)
916
+ if (path.isAbsolute(searchPath)) {
917
+ return searchPath;
918
+ }
919
+ // Resolve relative to the test file's directory
920
+ return path.resolve(path.dirname(filePath), searchPath);
543
921
  })
922
+
544
923
  options.filename = path.resolve(process.cwd(), filePath);
545
924
  options.optimization = options.optimization || 0;
546
925
 
547
- if (options.globalVars) {
548
- options.globalVars = options.getVars(filePath);
549
- } else if (options.modifyVars) {
550
- options.modifyVars = options.getVars(filePath);
551
- }
926
+ // Note: globalVars and modifyVars are now handled via styles.config.cjs or lessOptions
552
927
  if (options.plugin) {
553
928
  var Plugin = require(path.resolve(process.cwd(), options.plugin));
554
929
  options.plugins = [Plugin];
@@ -571,22 +946,7 @@ module.exports = function() {
571
946
  ok(stylize('OK\n', 'green'));
572
947
  }
573
948
 
574
- function testImportRedirect(nockScope) {
575
- return (name, err, css, doReplacements, sourcemap, baseFolder) => {
576
- process.stdout.write('- ' + path.join(baseFolder, name) + ': ');
577
- if (err) {
578
- fail('FAIL: ' + (err && err.message));
579
- return;
580
- }
581
- const expected = 'h1 {\n color: red;\n}\n';
582
- if (css !== expected) {
583
- difference('FAIL', expected, css);
584
- return;
585
- }
586
- nockScope.done();
587
- ok('OK');
588
- };
589
- }
949
+ // HTTP redirect testing is now handled directly in test/index.js
590
950
 
591
951
  function testDisablePluginRule() {
592
952
  less.render(
@@ -615,7 +975,6 @@ module.exports = function() {
615
975
  testSourcemapWithoutUrlAnnotation: testSourcemapWithoutUrlAnnotation,
616
976
  testSourcemapWithVariableInSelector: testSourcemapWithVariableInSelector,
617
977
  testImports: testImports,
618
- testImportRedirect: testImportRedirect,
619
978
  testEmptySourcemap: testEmptySourcemap,
620
979
  testNoOptions: testNoOptions,
621
980
  testDisablePluginRule: testDisablePluginRule,