Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 109 additions & 110 deletions packages/less/benchmark/benchmark-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,58 +14,58 @@ var extraOpts = {};

// Parse --key=value options from remaining args
for (var ai = 5; ai < process.argv.length; ai++) {
var optMatch = process.argv[ai].match(/^--([a-z-]+)=(.*)$/);
if (optMatch) { extraOpts[optMatch[1]] = optMatch[2]; }
var optMatch = process.argv[ai].match(/^--([a-z-]+)=(.*)$/);
if (optMatch) { extraOpts[optMatch[1]] = optMatch[2]; }
}

if (!file) {
console.error('Usage: node benchmark-runner.js <file.less> [runs] [warmup]');
process.exit(1);
console.error('Usage: node benchmark-runner.js <file.less> [runs] [warmup]');
process.exit(1);
}

// Find Less compiler - try multiple paths for different version eras
var less;
var lessPath = '';
var tryPaths = [
// v4.x monorepo (after build)
'./packages/less',
// v3.x / v2.x (lib in repo)
'.',
'./lib/less-node',
// Fallback
'less'
// v4.x monorepo (after build)
'./packages/less',
// v3.x / v2.x (lib in repo)
'.',
'./lib/less-node',
// Fallback
'less'
];

for (var i = 0; i < tryPaths.length; i++) {
try {
var p = tryPaths[i];
// Use path.resolve for relative paths, but keep bare package names for Node resolution
var mod = require(p.startsWith('.') ? path.resolve(p) : p);
// Handle both direct export and .default (ESM interop)
less = mod && mod.default ? mod.default : mod;
if (less && (less.render || less.parse)) {
lessPath = p;
break;
}
less = null;
} catch (e) {
try {
var p = tryPaths[i];
// Use path.resolve for relative paths, but keep bare package names for Node resolution
var mod = require(p.startsWith('.') ? path.resolve(p) : p);
// Handle both direct export and .default (ESM interop)
less = mod && mod.default ? mod.default : mod;
if (less && (less.render || less.parse)) {
lessPath = p;
Comment on lines +46 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require render during compiler discovery to prevent runtime crashes.

At Line 46, discovery currently accepts compilers that only expose parse. But runOnce() always calls less.render (Line 93), so a parse-only module will fail at runtime with TypeError: less.render is not a function.

Proposed fix
-        if (less && (less.render || less.parse)) {
+        if (less && typeof less.render === 'function') {
             lessPath = p;
             break;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (less && (less.render || less.parse)) {
lessPath = p;
if (less && typeof less.render === 'function') {
lessPath = p;
break;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/less/benchmark/benchmark-runner.js` around lines 46 - 47, The
compiler discovery condition at line 46 currently uses OR logic to accept
compilers that have either `render` or `parse`, but the `runOnce()` function
always calls `less.render` at line 93. Change the condition from `(less.render
|| less.parse)` to `(less.render && less.parse)` to require both methods to be
present, preventing runtime crashes when a parse-only compiler is selected.

break;
}
less = null;
} catch (e) {
// try next
}
}
}

if (!less) {
console.error(JSON.stringify({ error: 'Could not find Less compiler', tried: tryPaths }));
process.exit(2);
console.error(JSON.stringify({ error: 'Could not find Less compiler', tried: tryPaths }));
process.exit(2);
}

// Determine version
var version = 'unknown';
if (less.version) {
if (Array.isArray(less.version)) {
version = less.version.join('.');
} else {
version = String(less.version);
}
if (Array.isArray(less.version)) {
version = less.version.join('.');
} else {
version = String(less.version);
}
}

var filePath = path.resolve(file);
Expand All @@ -74,102 +74,101 @@ var fileDir = path.dirname(filePath);

// Use less.render() - stable across all versions
var renderTimes = [];
var parseTimes = [];
var completed = 0;
var errors = [];

function hrNow() {
var hr = process.hrtime();
return hr[0] * 1000 + hr[1] / 1e6;
var hr = process.hrtime();
return hr[0] * 1000 + hr[1] / 1e6;
}

function runOnce(callback) {
var start = hrNow();
var opts = {
filename: filePath,
paths: [fileDir]
};
// Forward extra options (e.g. --math=always)
for (var key in extraOpts) { opts[key] = extraOpts[key]; }
less.render(data, opts, function (err, output) {
var end = hrNow();
if (err) {
errors.push({ run: completed, error: err.message || String(err) });
callback(err);
return;
}
renderTimes.push(end - start);
completed++;
callback(null);
});
var start = hrNow();
var opts = {
filename: filePath,
paths: [fileDir]
};
// Forward extra options (e.g. --math=always)
for (var key in extraOpts) { opts[key] = extraOpts[key]; }
less.render(data, opts, function (err) {
var end = hrNow();
if (err) {
errors.push({ run: completed, error: err.message || String(err) });
callback(err);
return;
}
renderTimes.push(end - start);
completed++;
callback(null);
});
}

function runAll(i) {
if (i >= totalRuns) {
reportResults();
return;
}
runOnce(function (err) {
if (err && errors.length > 3) {
// Too many errors, bail
reportResults();
return;
if (i >= totalRuns) {
reportResults();
return;
}
runAll(i + 1);
});
runOnce(function (err) {
if (err && errors.length > 3) {
// Too many errors, bail
reportResults();
return;
}
runAll(i + 1);
});
}

function analyze(times, skipWarmup) {
var start = skipWarmup ? warmupRuns : 0;
if (times.length <= start) return null;
var effective = times.slice(start);
var total = 0, min = Infinity, max = 0;
for (var i = 0; i < effective.length; i++) {
total += effective[i];
min = Math.min(min, effective[i]);
max = Math.max(max, effective[i]);
}
var avg = total / effective.length;

// Median
var sorted = effective.slice().sort(function (a, b) { return a - b; });
var mid = Math.floor(sorted.length / 2);
var median = sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;

// Standard deviation and coefficient of variation
var sumSqDiff = 0;
for (var i = 0; i < effective.length; i++) {
sumSqDiff += (effective[i] - avg) * (effective[i] - avg);
}
var stddev = Math.sqrt(sumSqDiff / effective.length);
var variancePct = avg === 0 ? 0 : (stddev / avg) * 100;

return {
min: Math.round(min * 100) / 100,
max: Math.round(max * 100) / 100,
avg: Math.round(avg * 100) / 100,
median: Math.round(median * 100) / 100,
stddev: Math.round(stddev * 100) / 100,
variance_pct: Math.round(variancePct * 100) / 100,
samples: effective.length,
throughput_kbs: Math.round(1000 / avg * data.length / 1024)
};
var start = skipWarmup ? warmupRuns : 0;
if (times.length <= start) return null;
var effective = times.slice(start);
var total = 0, min = Infinity, max = 0;
for (var i = 0; i < effective.length; i++) {
total += effective[i];
min = Math.min(min, effective[i]);
max = Math.max(max, effective[i]);
}
var avg = total / effective.length;

// Median
var sorted = effective.slice().sort(function (a, b) { return a - b; });
var mid = Math.floor(sorted.length / 2);
var median = sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;

// Standard deviation and coefficient of variation
var sumSqDiff = 0;
for (i = 0; i < effective.length; i++) {
sumSqDiff += (effective[i] - avg) * (effective[i] - avg);
}
var stddev = Math.sqrt(sumSqDiff / effective.length);
var variancePct = avg === 0 ? 0 : (stddev / avg) * 100;

return {
min: Math.round(min * 100) / 100,
max: Math.round(max * 100) / 100,
avg: Math.round(avg * 100) / 100,
median: Math.round(median * 100) / 100,
stddev: Math.round(stddev * 100) / 100,
variance_pct: Math.round(variancePct * 100) / 100,
samples: effective.length,
throughput_kbs: Math.round(1000 / avg * data.length / 1024)
};
}

function reportResults() {
var result = {
version: version,
lessPath: lessPath,
file: path.basename(file),
fileSize: data.length,
fileSizeKB: Math.round(data.length / 1024 * 10) / 10,
totalRuns: totalRuns,
warmupRuns: warmupRuns,
completedRuns: completed,
errors: errors.length > 0 ? errors : undefined,
render: analyze(renderTimes, true)
};
console.log(JSON.stringify(result));
var result = {
version: version,
lessPath: lessPath,
file: path.basename(file),
fileSize: data.length,
fileSizeKB: Math.round(data.length / 1024 * 10) / 10,
totalRuns: totalRuns,
warmupRuns: warmupRuns,
completedRuns: completed,
errors: errors.length > 0 ? errors : undefined,
render: analyze(renderTimes, true)
};
console.log(JSON.stringify(result));
}

runAll(0);
2 changes: 1 addition & 1 deletion packages/less/build/rollup.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function moduleShim() {
},
load(id) {
if (id === '\0module') {
return `export function createRequire() { return require; }`;
return 'export function createRequire() { return require; }';
}
return null;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/less/lib/less/parser/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -2547,6 +2547,9 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
op = '<';
}
} else
if (parserInput.$str('!=')) {
op = '!=';
} else
if (parserInput.$char('=')) {
if (parserInput.$char('>')) {
op = '=>';
Expand Down
6 changes: 3 additions & 3 deletions packages/less/lib/less/tree/condition.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,16 @@ class Condition extends Node {
default:
switch (Node.compare(a, b)) {
case -1:
result = this.op === '<' || this.op === '=<' || this.op === '<=';
result = this.op === '<' || this.op === '=<' || this.op === '<=' || this.op === '!=';
break;
case 0:
result = this.op === '=' || this.op === '>=' || this.op === '=<' || this.op === '<=';
break;
case 1:
result = this.op === '>' || this.op === '>=';
result = this.op === '>' || this.op === '>=' || this.op === '!=';
break;
default:
result = false;
result = this.op === '!=';
}
}

Expand Down
6 changes: 6 additions & 0 deletions packages/test-data/tests-unit/css-guards/css-guards.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@
sub-prop: 2px;
prop: 1px;
}
.inequality-test-1 {
color: green;
}
.inequality-test-3 {
color: green;
}
13 changes: 13 additions & 0 deletions packages/test-data/tests-unit/css-guards/css-guards.less
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,17 @@
}
a:hover when (2 = true) {5:-}

.inequality-test-1 when (2 != 1) {
color: green;
}
.inequality-test-2 when (2 != 2) {
color: red;
}
.inequality-test-3 when (@b != 1) {
color: green;
}
.inequality-test-4 when (@b != 2) {
color: red;
}


Loading