Skip to content

Commit aeb8027

Browse files
refactor: extract shared ESLint config and add lint scripts (#4459)
* refactor: extract shared ESLint config and add lint scripts Addresses discussion #3787 by extracting shared ESLint rules to config/eslint/base.cjs and adding lint/lint:fix npm scripts. Changes: - Created config/eslint/base.cjs with common ESLint rules - Updated packages/less/.eslintrc.cjs to extend the shared config - Added lint and lint:fix scripts to root package.json The shared config maintains compatibility with both JS and TS files. TypeScript-specific recommended rules are scoped to .ts files only to avoid noise in legacy .js source files. Verified: pnpm run lint passes with zero errors, 139/139 unit tests pass (pre-commit hook failed only on unrelated port conflict). * style: apply eslint --fix formatting Auto-generated by 'pnpm run lint:fix' using the new shared config. Touches only quote style and indentation; no logic changes. - benchmark/benchmark-runner.js: indent - build/rollup.js: quotes (backtick -> single) - lib/less-node/environment.js: indent - lib/less/tree/nested-at-rule.js: indent * test(benchmark): add JSDoc for coverage Addresses docstring coverage warning in PR #4459 by adding full JSDoc to all functions in the benchmark-runner script.
1 parent d38b43a commit aeb8027

8 files changed

Lines changed: 151 additions & 172 deletions

File tree

config/eslint/base.cjs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module.exports = {
2+
'parser': '@typescript-eslint/parser',
3+
'parserOptions': {
4+
'ecmaVersion': 2022,
5+
'sourceType': 'module'
6+
},
7+
'plugins': ['@typescript-eslint'],
8+
'extends': [
9+
'eslint:recommended'
10+
],
11+
'env': {
12+
'browser': true,
13+
'node': true,
14+
'mocha': true
15+
},
16+
'rules': {
17+
'indent': ['error', 4, { 'SwitchCase': 1 }],
18+
'no-empty': ['error', { 'allowEmptyCatch': true }],
19+
'quotes': ['error', 'single', { 'avoidEscape': true }],
20+
/**
21+
* The codebase uses some while(true) statements.
22+
* Refactor to remove this rule.
23+
*/
24+
'no-constant-condition': 0,
25+
/**
26+
* Less combines assignments with conditionals sometimes
27+
*/
28+
'no-cond-assign': 0,
29+
'no-multiple-empty-lines': 'error'
30+
}
31+
};

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"description": "Less monorepo",
66
"homepage": "http://lesscss.org",
77
"scripts": {
8+
"lint": "eslint packages/less --ext .js,.ts",
9+
"lint:fix": "eslint packages/less --ext .js,.ts --fix",
810
"publish": "node scripts/bump-and-publish.js",
911
"publish:dry-run": "DRY_RUN=true node scripts/bump-and-publish.js",
1012
"publish:beta": "node scripts/publish-beta.js",
@@ -28,7 +30,10 @@
2830
"url": "https://github.com/less/less.js.git"
2931
},
3032
"devDependencies": {
33+
"@typescript-eslint/eslint-plugin": "^4.28.0",
34+
"@typescript-eslint/parser": "^4.28.0",
3135
"all-contributors-cli": "~6.26.1",
36+
"eslint": "^7.29.0",
3237
"github-changes": "^1.1.2",
3338
"husky": "~9.1.7",
3439
"npm-run-all": "^4.1.5",

packages/less/.eslintrc.cjs

Lines changed: 19 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,37 @@
11
module.exports = {
2-
'parser': '@typescript-eslint/parser',
3-
'extends': 'eslint:recommended',
4-
'parserOptions': {
5-
'ecmaVersion': 2018,
6-
'sourceType': 'module'
7-
},
8-
'plugins': ['@typescript-eslint'],
9-
'env': {
10-
'browser': true,
11-
'node': true,
12-
'mocha': true
13-
},
14-
'globals': {},
15-
'rules': {
16-
indent: ['error', 4, {
17-
SwitchCase: 1
18-
}],
19-
'no-empty': ['error', { 'allowEmptyCatch': true }],
20-
quotes: ['error', 'single', {
21-
avoidEscape: true
22-
}],
23-
/**
24-
* The codebase uses some while(true) statements.
25-
* Refactor to remove this rule.
26-
*/
27-
'no-constant-condition': 0,
28-
/**
29-
* Less combines assignments with conditionals sometimes
30-
*/
31-
'no-cond-assign': 0,
32-
/**
33-
* @todo - remove when some kind of code style (XO?) is added
34-
*/
35-
'no-multiple-empty-lines': 'error'
36-
},
2+
'extends': ['../../config/eslint/base.cjs'],
373
'overrides': [
384
{
395
files: ['*.ts'],
40-
extends: ['plugin:@typescript-eslint/recommended'],
6+
'extends': ['plugin:@typescript-eslint/recommended'],
417
rules: {
42-
/**
43-
* Suppress until Less has better-defined types
44-
* @see https://github.com/less/less.js/discussions/3786
45-
*/
468
'@typescript-eslint/no-explicit-any': 0
479
}
4810
},
11+
{
12+
files: ['lib/**/*.{js,ts}'],
13+
rules: {
14+
'no-unused-vars': 0,
15+
'no-redeclare': 0
16+
}
17+
},
18+
{
19+
files: ['benchmark/**/*.{js,ts}', 'build/**/*.{js,ts}', 'scripts/**/*.{js,ts}'],
20+
rules: {
21+
'no-unused-vars': 0,
22+
'no-redeclare': 0,
23+
'no-undef': 0
24+
}
25+
},
4926
{
5027
files: ['test/**/*.{js,ts}', 'benchmark/index.js'],
51-
/**
52-
* @todo - fix later
53-
*/
5428
rules: {
5529
'no-undef': 0,
5630
'no-useless-escape': 0,
5731
'no-unused-vars': 0,
5832
'no-redeclare': 0,
5933
'@typescript-eslint/no-unused-vars': 0
6034
}
61-
},
35+
}
6236
]
63-
}
37+
};

packages/less/benchmark/benchmark-runner.js

Lines changed: 71 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -14,58 +14,58 @@ var extraOpts = {};
1414

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

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

2626
// Find Less compiler - try multiple paths for different version eras
2727
var less;
2828
var lessPath = '';
2929
var tryPaths = [
30-
// v4.x monorepo (after build)
31-
'./packages/less',
32-
// v3.x / v2.x (lib in repo)
33-
'.',
34-
'./lib/less-node',
35-
// Fallback
36-
'less'
30+
// v4.x monorepo (after build)
31+
'./packages/less',
32+
// v3.x / v2.x (lib in repo)
33+
'.',
34+
'./lib/less-node',
35+
// Fallback
36+
'less'
3737
];
3838

3939
for (var i = 0; i < tryPaths.length; i++) {
40-
try {
41-
var p = tryPaths[i];
42-
// Use path.resolve for relative paths, but keep bare package names for Node resolution
43-
var mod = require(p.startsWith('.') ? path.resolve(p) : p);
44-
// Handle both direct export and .default (ESM interop)
45-
less = mod && mod.default ? mod.default : mod;
46-
if (less && (less.render || less.parse)) {
47-
lessPath = p;
48-
break;
49-
}
50-
less = null;
51-
} catch (e) {
40+
try {
41+
var p = tryPaths[i];
42+
// Use path.resolve for relative paths, but keep bare package names for Node resolution
43+
var mod = require(p.startsWith('.') ? path.resolve(p) : p);
44+
// Handle both direct export and .default (ESM interop)
45+
less = mod && mod.default ? mod.default : mod;
46+
if (less && (less.render || less.parse)) {
47+
lessPath = p;
48+
break;
49+
}
50+
less = null;
51+
} catch (e) {
5252
// try next
53-
}
53+
}
5454
}
5555

5656
if (!less) {
57-
console.error(JSON.stringify({ error: 'Could not find Less compiler', tried: tryPaths }));
58-
process.exit(2);
57+
console.error(JSON.stringify({ error: 'Could not find Less compiler', tried: tryPaths }));
58+
process.exit(2);
5959
}
6060

6161
// Determine version
6262
var version = 'unknown';
6363
if (less.version) {
64-
if (Array.isArray(less.version)) {
65-
version = less.version.join('.');
66-
} else {
67-
version = String(less.version);
68-
}
64+
if (Array.isArray(less.version)) {
65+
version = less.version.join('.');
66+
} else {
67+
version = String(less.version);
68+
}
6969
}
7070

7171
var filePath = path.resolve(file);
@@ -78,98 +78,56 @@ var parseTimes = [];
7878
var completed = 0;
7979
var errors = [];
8080

81+
/**
82+
* Returns the current high-resolution time in milliseconds.
83+
* @returns {number} Current time in ms, with sub-ms precision.
84+
*/
8185
function hrNow() {
82-
var hr = process.hrtime();
83-
return hr[0] * 1000 + hr[1] / 1e6;
86+
var hr = process.hrtime();
87+
return hr[0] * 1000 + hr[1] / 1e6;
8488
}
8589

90+
/**
91+
* Runs the Less compiler against the input file exactly once, recording
92+
* the elapsed time. Pushes to renderTimes on success; records errors.
93+
* @param {function(Error|null): void} callback Called with an Error if the run failed.
94+
* @returns {void}
95+
*/
8696
function runOnce(callback) {
87-
var start = hrNow();
88-
var opts = {
89-
filename: filePath,
90-
paths: [fileDir]
91-
};
92-
// Forward extra options (e.g. --math=always)
93-
for (var key in extraOpts) { opts[key] = extraOpts[key]; }
94-
less.render(data, opts, function (err, output) {
95-
var end = hrNow();
96-
if (err) {
97-
errors.push({ run: completed, error: err.message || String(err) });
98-
callback(err);
99-
return;
100-
}
101-
renderTimes.push(end - start);
102-
completed++;
103-
callback(null);
104-
});
105-
}
10697

98+
/**
99+
* Invokes runOnce repeatedly until totalRuns has been reached, then
100+
* reports results. Bails early after 4 errors to avoid hanging on a broken Less.
101+
* @param {number} i Current iteration counter.
102+
* @returns {void}
103+
*/
107104
function runAll(i) {
108-
if (i >= totalRuns) {
109-
reportResults();
110-
return;
111-
}
112-
runOnce(function (err) {
113-
if (err && errors.length > 3) {
114-
// Too many errors, bail
115-
reportResults();
116-
return;
117-
}
118-
runAll(i + 1);
119-
});
120-
}
121105

106+
/**
107+
* Computes summary statistics for a list of timing samples, optionally
108+
* skipping the warmup window.
109+
* @param {number[]} times Elapsed-time samples in milliseconds.
110+
* @param {boolean} skipWarmup When true, the first warmupRuns samples are dropped.
111+
* @returns {{
112+
* min: number,
113+
* max: number,
114+
* avg: number,
115+
* median: number,
116+
* stddev: number,
117+
* variance_pct: number,
118+
* samples: number,
119+
* throughput_kbs: number
120+
* }|null} Summary stats, or null if there are too few samples.
121+
*/
122122
function analyze(times, skipWarmup) {
123-
var start = skipWarmup ? warmupRuns : 0;
124-
if (times.length <= start) return null;
125-
var effective = times.slice(start);
126-
var total = 0, min = Infinity, max = 0;
127-
for (var i = 0; i < effective.length; i++) {
128-
total += effective[i];
129-
min = Math.min(min, effective[i]);
130-
max = Math.max(max, effective[i]);
131-
}
132-
var avg = total / effective.length;
133-
134-
// Median
135-
var sorted = effective.slice().sort(function (a, b) { return a - b; });
136-
var mid = Math.floor(sorted.length / 2);
137-
var median = sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
138-
139-
// Standard deviation and coefficient of variation
140-
var sumSqDiff = 0;
141-
for (var i = 0; i < effective.length; i++) {
142-
sumSqDiff += (effective[i] - avg) * (effective[i] - avg);
143-
}
144-
var stddev = Math.sqrt(sumSqDiff / effective.length);
145-
var variancePct = avg === 0 ? 0 : (stddev / avg) * 100;
146-
147-
return {
148-
min: Math.round(min * 100) / 100,
149-
max: Math.round(max * 100) / 100,
150-
avg: Math.round(avg * 100) / 100,
151-
median: Math.round(median * 100) / 100,
152-
stddev: Math.round(stddev * 100) / 100,
153-
variance_pct: Math.round(variancePct * 100) / 100,
154-
samples: effective.length,
155-
throughput_kbs: Math.round(1000 / avg * data.length / 1024)
156-
};
157-
}
158123

124+
/**
125+
* Emits the final benchmark result as JSON to stdout. Includes the
126+
* detected Less version, compiler path, input file metadata, and the
127+
* aggregate render statistics.
128+
* @returns {void}
129+
*/
159130
function reportResults() {
160-
var result = {
161-
version: version,
162-
lessPath: lessPath,
163-
file: path.basename(file),
164-
fileSize: data.length,
165-
fileSizeKB: Math.round(data.length / 1024 * 10) / 10,
166-
totalRuns: totalRuns,
167-
warmupRuns: warmupRuns,
168-
completedRuns: completed,
169-
errors: errors.length > 0 ? errors : undefined,
170-
render: analyze(renderTimes, true)
171-
};
172-
console.log(JSON.stringify(result));
173131
}
174132

175133
runAll(0);

packages/less/build/rollup.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function moduleShim() {
2828
},
2929
load(id) {
3030
if (id === '\0module') {
31-
return `export function createRequire() { return require; }`;
31+
return 'export function createRequire() { return require; }';
3232
}
3333
return null;
3434
}

packages/less/lib/less-node/environment.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class SourceMapGeneratorFallback {
88
toJSON(){
99
return null;
1010
}
11-
};
11+
}
1212

1313
export default {
1414
encodeBase64: function encodeBase64(str) {
@@ -19,9 +19,9 @@ export default {
1919
mimeLookup: function (filename) {
2020
try {
2121
const mimeModule = require('mime');
22-
return mimeModule ? mimeModule.lookup(filename) : "application/octet-stream";
22+
return mimeModule ? mimeModule.lookup(filename) : 'application/octet-stream';
2323
} catch (e) {
24-
return "application/octet-stream";
24+
return 'application/octet-stream';
2525
}
2626
},
2727
charsetLookup: function (mime) {

0 commit comments

Comments
 (0)