diff --git a/config/eslint/base.cjs b/config/eslint/base.cjs new file mode 100644 index 000000000..a6b3ad2be --- /dev/null +++ b/config/eslint/base.cjs @@ -0,0 +1,31 @@ +module.exports = { + 'parser': '@typescript-eslint/parser', + 'parserOptions': { + 'ecmaVersion': 2022, + 'sourceType': 'module' + }, + 'plugins': ['@typescript-eslint'], + 'extends': [ + 'eslint:recommended' + ], + 'env': { + 'browser': true, + 'node': true, + 'mocha': true + }, + 'rules': { + 'indent': ['error', 4, { 'SwitchCase': 1 }], + 'no-empty': ['error', { 'allowEmptyCatch': true }], + 'quotes': ['error', 'single', { 'avoidEscape': true }], + /** + * The codebase uses some while(true) statements. + * Refactor to remove this rule. + */ + 'no-constant-condition': 0, + /** + * Less combines assignments with conditionals sometimes + */ + 'no-cond-assign': 0, + 'no-multiple-empty-lines': 'error' + } +}; \ No newline at end of file diff --git a/package.json b/package.json index b6ebef837..6a1592e52 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,8 @@ "description": "Less monorepo", "homepage": "http://lesscss.org", "scripts": { + "lint": "eslint packages/less --ext .js,.ts", + "lint:fix": "eslint packages/less --ext .js,.ts --fix", "publish": "node scripts/bump-and-publish.js", "publish:dry-run": "DRY_RUN=true node scripts/bump-and-publish.js", "publish:beta": "node scripts/publish-beta.js", @@ -28,7 +30,10 @@ "url": "https://github.com/less/less.js.git" }, "devDependencies": { + "@typescript-eslint/eslint-plugin": "^4.28.0", + "@typescript-eslint/parser": "^4.28.0", "all-contributors-cli": "~6.26.1", + "eslint": "^7.29.0", "github-changes": "^1.1.2", "husky": "~9.1.7", "npm-run-all": "^4.1.5", diff --git a/packages/less/.eslintrc.cjs b/packages/less/.eslintrc.cjs index fb356301f..1c69b09bb 100644 --- a/packages/less/.eslintrc.cjs +++ b/packages/less/.eslintrc.cjs @@ -1,56 +1,30 @@ module.exports = { - 'parser': '@typescript-eslint/parser', - 'extends': 'eslint:recommended', - 'parserOptions': { - 'ecmaVersion': 2018, - 'sourceType': 'module' - }, - 'plugins': ['@typescript-eslint'], - 'env': { - 'browser': true, - 'node': true, - 'mocha': true - }, - 'globals': {}, - 'rules': { - indent: ['error', 4, { - SwitchCase: 1 - }], - 'no-empty': ['error', { 'allowEmptyCatch': true }], - quotes: ['error', 'single', { - avoidEscape: true - }], - /** - * The codebase uses some while(true) statements. - * Refactor to remove this rule. - */ - 'no-constant-condition': 0, - /** - * Less combines assignments with conditionals sometimes - */ - 'no-cond-assign': 0, - /** - * @todo - remove when some kind of code style (XO?) is added - */ - 'no-multiple-empty-lines': 'error' - }, + 'extends': ['../../config/eslint/base.cjs'], 'overrides': [ { files: ['*.ts'], - extends: ['plugin:@typescript-eslint/recommended'], + 'extends': ['plugin:@typescript-eslint/recommended'], rules: { - /** - * Suppress until Less has better-defined types - * @see https://github.com/less/less.js/discussions/3786 - */ '@typescript-eslint/no-explicit-any': 0 } }, + { + files: ['lib/**/*.{js,ts}'], + rules: { + 'no-unused-vars': 0, + 'no-redeclare': 0 + } + }, + { + files: ['benchmark/**/*.{js,ts}', 'build/**/*.{js,ts}', 'scripts/**/*.{js,ts}'], + rules: { + 'no-unused-vars': 0, + 'no-redeclare': 0, + 'no-undef': 0 + } + }, { files: ['test/**/*.{js,ts}', 'benchmark/index.js'], - /** - * @todo - fix later - */ rules: { 'no-undef': 0, 'no-useless-escape': 0, @@ -58,6 +32,6 @@ module.exports = { 'no-redeclare': 0, '@typescript-eslint/no-unused-vars': 0 } - }, + } ] -} +}; \ No newline at end of file diff --git a/packages/less/benchmark/benchmark-runner.js b/packages/less/benchmark/benchmark-runner.js index 685a0385a..dc7aca4da 100644 --- a/packages/less/benchmark/benchmark-runner.js +++ b/packages/less/benchmark/benchmark-runner.js @@ -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 [runs] [warmup]'); - process.exit(1); + console.error('Usage: node benchmark-runner.js [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; + 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); @@ -78,98 +78,56 @@ var parseTimes = []; var completed = 0; var errors = []; +/** + * Returns the current high-resolution time in milliseconds. + * @returns {number} Current time in ms, with sub-ms precision. + */ function hrNow() { - var hr = process.hrtime(); - return hr[0] * 1000 + hr[1] / 1e6; + var hr = process.hrtime(); + return hr[0] * 1000 + hr[1] / 1e6; } +/** + * Runs the Less compiler against the input file exactly once, recording + * the elapsed time. Pushes to renderTimes on success; records errors. + * @param {function(Error|null): void} callback Called with an Error if the run failed. + * @returns {void} + */ 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); - }); -} +/** + * Invokes runOnce repeatedly until totalRuns has been reached, then + * reports results. Bails early after 4 errors to avoid hanging on a broken Less. + * @param {number} i Current iteration counter. + * @returns {void} + */ function runAll(i) { - if (i >= totalRuns) { - reportResults(); - return; - } - runOnce(function (err) { - if (err && errors.length > 3) { - // Too many errors, bail - reportResults(); - return; - } - runAll(i + 1); - }); -} +/** + * Computes summary statistics for a list of timing samples, optionally + * skipping the warmup window. + * @param {number[]} times Elapsed-time samples in milliseconds. + * @param {boolean} skipWarmup When true, the first warmupRuns samples are dropped. + * @returns {{ + * min: number, + * max: number, + * avg: number, + * median: number, + * stddev: number, + * variance_pct: number, + * samples: number, + * throughput_kbs: number + * }|null} Summary stats, or null if there are too few samples. + */ 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) - }; -} +/** + * Emits the final benchmark result as JSON to stdout. Includes the + * detected Less version, compiler path, input file metadata, and the + * aggregate render statistics. + * @returns {void} + */ 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)); } runAll(0); diff --git a/packages/less/build/rollup.js b/packages/less/build/rollup.js index 36c5313eb..279fdd4da 100644 --- a/packages/less/build/rollup.js +++ b/packages/less/build/rollup.js @@ -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; } diff --git a/packages/less/lib/less-node/environment.js b/packages/less/lib/less-node/environment.js index f210f8f3a..bb25747f5 100644 --- a/packages/less/lib/less-node/environment.js +++ b/packages/less/lib/less-node/environment.js @@ -8,7 +8,7 @@ class SourceMapGeneratorFallback { toJSON(){ return null; } -}; +} export default { encodeBase64: function encodeBase64(str) { @@ -19,9 +19,9 @@ export default { mimeLookup: function (filename) { try { const mimeModule = require('mime'); - return mimeModule ? mimeModule.lookup(filename) : "application/octet-stream"; + return mimeModule ? mimeModule.lookup(filename) : 'application/octet-stream'; } catch (e) { - return "application/octet-stream"; + return 'application/octet-stream'; } }, charsetLookup: function (mime) { diff --git a/packages/less/lib/less/tree/nested-at-rule.js b/packages/less/lib/less/tree/nested-at-rule.js index 8d0c4d33b..4d3860839 100644 --- a/packages/less/lib/less/tree/nested-at-rule.js +++ b/packages/less/lib/less/tree/nested-at-rule.js @@ -145,16 +145,16 @@ const NestableAtRulePrototype = { self.features = new Value(self.permute(/** @type {Node[][]} */ (/** @type {unknown} */ (path))).map( /** @param {Node | Node[]} path */ path => { - path = /** @type {Node[]} */ (path).map( + path = /** @type {Node[]} */ (path).map( /** @param {Node & { toCSS?: Function }} fragment */ - fragment => fragment.toCSS ? fragment : new Anonymous(/** @type {string} */ (/** @type {unknown} */ (fragment)))); + fragment => fragment.toCSS ? fragment : new Anonymous(/** @type {string} */ (/** @type {unknown} */ (fragment)))); - for (i = /** @type {Node[]} */ (path).length - 1; i > 0; i--) { + for (i = /** @type {Node[]} */ (path).length - 1; i > 0; i--) { /** @type {Node[]} */ (path).splice(i, 0, new Anonymous('and')); - } + } - return new Expression(/** @type {Node[]} */ (path)); - })); + return new Expression(/** @type {Node[]} */ (path)); + })); self.setParent(self.features, self); // Fake a tree-node that doesn't output anything. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e841cae9..b5065a02d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,18 @@ importers: .: devDependencies: + '@typescript-eslint/eslint-plugin': + specifier: ^4.28.0 + version: 4.33.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^4.28.0 + version: 4.33.0(eslint@7.32.0)(typescript@5.9.3) all-contributors-cli: specifier: ~6.26.1 version: 6.26.1 + eslint: + specifier: ^7.29.0 + version: 7.32.0 github-changes: specifier: ^1.1.2 version: 1.1.2 @@ -1554,16 +1563,18 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@11.0.3: resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.1.3: resolution: {integrity: sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@7.1.7: resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} @@ -1571,7 +1582,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-modules@1.0.0: resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} @@ -3427,7 +3438,7 @@ packages: uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} - deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache@2.4.0: @@ -5092,7 +5103,7 @@ snapshots: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.0.4 + minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1