| #!/usr/bin/env node | ||
| /** | ||
| * Generates a line-by-line coverage report showing uncovered lines | ||
| * Reads from LCOV format and displays in terminal | ||
| * Also outputs JSON file with uncovered lines for programmatic access | ||
| */ | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const lcovPath = path.join(__dirname, '..', 'coverage', 'lcov.info'); | ||
| const jsonOutputPath = path.join(__dirname, '..', 'coverage', 'uncovered-lines.json'); | ||
| if (!fs.existsSync(lcovPath)) { | ||
| console.error('LCOV coverage file not found. Run pnpm test:coverage first.'); | ||
| process.exit(1); | ||
| } | ||
| const lcovContent = fs.readFileSync(lcovPath, 'utf8'); | ||
| // Parse LCOV format | ||
| const files = []; | ||
| let currentFile = null; | ||
| const lines = lcovContent.split('\n'); | ||
| for (let i = 0; i < lines.length; i++) { | ||
| const line = lines[i]; | ||
| // SF: source file | ||
| if (line.startsWith('SF:')) { | ||
| if (currentFile) { | ||
| files.push(currentFile); | ||
| } | ||
| const filePath = line.substring(3); | ||
| // Only include src/ files (not less-browser) and bin/ | ||
| // Exclude abstract base classes (they're meant to be overridden) | ||
| const normalized = filePath.replace(/\\/g, '/'); | ||
| const abstractClasses = ['abstract-file-manager', 'abstract-plugin-loader']; | ||
| const isAbstract = abstractClasses.some(abstract => normalized.includes(abstract)); | ||
| if (!isAbstract && | ||
| ((normalized.includes('src/less/') && !normalized.includes('src/less-browser/')) || | ||
| normalized.includes('src/less-node/') || | ||
| normalized.includes('bin/'))) { | ||
| // Extract relative path - match src/less/... or src/less-node/... or bin/... | ||
| // Path format: src/less/tree/debug-info.js or src/less-node/file-manager.js | ||
| // Match from src/ or bin/ to end of path | ||
| const match = normalized.match(/(src\/[^/]+\/.+|bin\/.+)$/); | ||
| const relativePath = match ? match[1] : (normalized.includes('/src/') || normalized.includes('/bin/') ? normalized.split('/').slice(-3).join('/') : path.basename(filePath)); | ||
| currentFile = { | ||
| path: relativePath, | ||
| fullPath: filePath, | ||
| uncoveredLines: [], | ||
| uncoveredLineCode: {}, // line number -> source code | ||
| totalLines: 0, | ||
| coveredLines: 0 | ||
| }; | ||
| } else { | ||
| currentFile = null; | ||
| } | ||
| } | ||
| // DA: line data (line number, execution count) | ||
| if (currentFile && line.startsWith('DA:')) { | ||
| const match = line.match(/^DA:(\d+),(\d+)$/); | ||
| if (match) { | ||
| const lineNum = parseInt(match[1], 10); | ||
| const count = parseInt(match[2], 10); | ||
| currentFile.totalLines++; | ||
| if (count > 0) { | ||
| currentFile.coveredLines++; | ||
| } else { | ||
| currentFile.uncoveredLines.push(lineNum); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (currentFile) { | ||
| files.push(currentFile); | ||
| } | ||
| // Read source code for uncovered lines | ||
| files.forEach(file => { | ||
| if (file.uncoveredLines.length > 0 && fs.existsSync(file.fullPath)) { | ||
| try { | ||
| const sourceCode = fs.readFileSync(file.fullPath, 'utf8'); | ||
| const sourceLines = sourceCode.split('\n'); | ||
| file.uncoveredLines.forEach(lineNum => { | ||
| // LCOV uses 1-based line numbers | ||
| if (lineNum > 0 && lineNum <= sourceLines.length) { | ||
| file.uncoveredLineCode[lineNum] = sourceLines[lineNum - 1].trim(); | ||
| } | ||
| }); | ||
| } catch (err) { | ||
| // If we can't read the source (e.g., it's in lib/ but we want src/), that's ok | ||
| // We'll just skip the source code | ||
| } | ||
| } | ||
| }); | ||
| // Filter to only files with uncovered lines and sort by coverage | ||
| const filesWithGaps = files | ||
| .filter(f => f.uncoveredLines.length > 0) | ||
| .sort((a, b) => { | ||
| const aPct = a.totalLines > 0 ? a.coveredLines / a.totalLines : 1; | ||
| const bPct = b.totalLines > 0 ? b.coveredLines / b.totalLines : 1; | ||
| return aPct - bPct; | ||
| }); | ||
| if (filesWithGaps.length === 0) { | ||
| if (files.length === 0) { | ||
| console.log('\n⚠️ No source files found in coverage data. This may indicate an issue with the coverage report.\n'); | ||
| } else { | ||
| console.log('\n✅ All analyzed files have 100% line coverage!\n'); | ||
| console.log(`(Analyzed ${files.length} files from src/less/, src/less-node/, and bin/)\n`); | ||
| } | ||
| process.exit(0); | ||
| } | ||
| console.log('\n' + '='.repeat(100)); | ||
| console.log('Uncovered Lines Report'); | ||
| console.log('='.repeat(100) + '\n'); | ||
| filesWithGaps.forEach(file => { | ||
| const coveragePct = file.totalLines > 0 | ||
| ? ((file.coveredLines / file.totalLines) * 100).toFixed(1) | ||
| : '0.0'; | ||
| console.log(`\n${file.path} (${coveragePct}% coverage)`); | ||
| console.log('-'.repeat(100)); | ||
| // Group consecutive lines into ranges | ||
| const ranges = []; | ||
| let start = file.uncoveredLines[0]; | ||
| let end = file.uncoveredLines[0]; | ||
| for (let i = 1; i < file.uncoveredLines.length; i++) { | ||
| if (file.uncoveredLines[i] === end + 1) { | ||
| end = file.uncoveredLines[i]; | ||
| } else { | ||
| ranges.push(start === end ? `${start}` : `${start}..${end}`); | ||
| start = file.uncoveredLines[i]; | ||
| end = file.uncoveredLines[i]; | ||
| } | ||
| } | ||
| ranges.push(start === end ? `${start}` : `${start}..${end}`); | ||
| // Display ranges (max 5 per line for readability) | ||
| const linesPerRow = 5; | ||
| for (let i = 0; i < ranges.length; i += linesPerRow) { | ||
| const row = ranges.slice(i, i + linesPerRow); | ||
| console.log(` Lines: ${row.join(', ')}`); | ||
| } | ||
| console.log(` Total uncovered: ${file.uncoveredLines.length} of ${file.totalLines} lines`); | ||
| }); | ||
| console.log('\n' + '='.repeat(100) + '\n'); | ||
| // Write JSON output for programmatic access | ||
| const jsonOutput = { | ||
| generated: new Date().toISOString(), | ||
| files: filesWithGaps.map(file => ({ | ||
| path: file.path, | ||
| fullPath: file.fullPath, | ||
| sourcePath: (() => { | ||
| // Try to map lib/ path to src/ path | ||
| const normalized = file.fullPath.replace(/\\/g, '/'); | ||
| if (normalized.includes('/lib/')) { | ||
| return normalized.replace('/lib/', '/src/').replace(/\.js$/, '.ts'); | ||
| } | ||
| return file.fullPath; | ||
| })(), | ||
| coveragePercent: file.totalLines > 0 | ||
| ? parseFloat(((file.coveredLines / file.totalLines) * 100).toFixed(1)) | ||
| : 0, | ||
| totalLines: file.totalLines, | ||
| coveredLines: file.coveredLines, | ||
| uncoveredLines: file.uncoveredLines, | ||
| uncoveredLineCode: file.uncoveredLineCode || {}, | ||
| uncoveredRanges: (() => { | ||
| const ranges = []; | ||
| if (file.uncoveredLines.length === 0) return ranges; | ||
| let start = file.uncoveredLines[0]; | ||
| let end = file.uncoveredLines[0]; | ||
| for (let i = 1; i < file.uncoveredLines.length; i++) { | ||
| if (file.uncoveredLines[i] === end + 1) { | ||
| end = file.uncoveredLines[i]; | ||
| } else { | ||
| ranges.push({ start, end }); | ||
| start = file.uncoveredLines[i]; | ||
| end = file.uncoveredLines[i]; | ||
| } | ||
| } | ||
| ranges.push({ start, end }); | ||
| return ranges; | ||
| })() | ||
| })) | ||
| }; | ||
| fs.writeFileSync(jsonOutputPath, JSON.stringify(jsonOutput, null, 2), 'utf8'); | ||
| console.log('\n📄 Uncovered lines data written to: coverage/uncovered-lines.json\n'); | ||
| #!/usr/bin/env node | ||
| /** | ||
| * Generates a per-file coverage report table for src/ directories | ||
| */ | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const coverageSummaryPath = path.join(__dirname, '..', 'coverage', 'coverage-summary.json'); | ||
| if (!fs.existsSync(coverageSummaryPath)) { | ||
| console.error('Coverage summary not found. Run pnpm test:coverage first.'); | ||
| process.exit(1); | ||
| } | ||
| const coverage = JSON.parse(fs.readFileSync(coverageSummaryPath, 'utf8')); | ||
| // Filter to only src/ files (less, less-node) and bin/ files | ||
| // Note: src/less-browser/ is excluded because browser tests aren't included in coverage | ||
| // Abstract base classes are excluded as they're meant to be overridden by implementations | ||
| const abstractClasses = [ | ||
| 'abstract-file-manager', | ||
| 'abstract-plugin-loader' | ||
| ]; | ||
| const srcFiles = Object.entries(coverage) | ||
| .filter(([filePath]) => { | ||
| const normalized = filePath.replace(/\\/g, '/'); | ||
| // Exclude abstract classes | ||
| if (abstractClasses.some(abstract => normalized.includes(abstract))) { | ||
| return false; | ||
| } | ||
| return (normalized.includes('/src/less/') && !normalized.includes('/src/less-browser/')) || | ||
| normalized.includes('/src/less-node/') || | ||
| normalized.includes('/bin/'); | ||
| }) | ||
| .map(([filePath, data]) => { | ||
| // Extract relative path from absolute path | ||
| const normalized = filePath.replace(/\\/g, '/'); | ||
| // Match src/ paths or bin/ paths | ||
| const match = normalized.match(/((?:src\/[^/]+\/[^/]+\/|bin\/).+)$/); | ||
| const relativePath = match ? match[1] : path.basename(filePath); | ||
| return { | ||
| path: relativePath, | ||
| statements: data.statements, | ||
| branches: data.branches, | ||
| functions: data.functions, | ||
| lines: data.lines | ||
| }; | ||
| }) | ||
| .sort((a, b) => { | ||
| // Sort by directory first, then by coverage percentage | ||
| const pathCompare = a.path.localeCompare(b.path); | ||
| if (pathCompare !== 0) return pathCompare; | ||
| return a.statements.pct - b.statements.pct; | ||
| }); | ||
| if (srcFiles.length === 0) { | ||
| console.log('No src/ files found in coverage report.'); | ||
| process.exit(0); | ||
| } | ||
| // Group by directory | ||
| const grouped = { | ||
| 'src/less/': [], | ||
| 'src/less-node/': [], | ||
| 'bin/': [] | ||
| }; | ||
| srcFiles.forEach(file => { | ||
| if (file.path.startsWith('src/less/')) { | ||
| grouped['src/less/'].push(file); | ||
| } else if (file.path.startsWith('src/less-node/')) { | ||
| grouped['src/less-node/'].push(file); | ||
| } else if (file.path.startsWith('bin/')) { | ||
| grouped['bin/'].push(file); | ||
| } | ||
| }); | ||
| // Print table | ||
| console.log('\n' + '='.repeat(100)); | ||
| console.log('Per-File Coverage Report (src/less/, src/less-node/, and bin/)'); | ||
| console.log('='.repeat(100)); | ||
| console.log('For line-by-line coverage details, open coverage/index.html in your browser.'); | ||
| console.log('='.repeat(100) + '\n'); | ||
| Object.entries(grouped).forEach(([dir, files]) => { | ||
| if (files.length === 0) return; | ||
| console.log(`\n${dir.toUpperCase()}`); | ||
| console.log('-'.repeat(100)); | ||
| console.log( | ||
| 'File'.padEnd(50) + | ||
| 'Statements'.padStart(12) + | ||
| 'Branches'.padStart(12) + | ||
| 'Functions'.padStart(12) + | ||
| 'Lines'.padStart(12) | ||
| ); | ||
| console.log('-'.repeat(100)); | ||
| files.forEach(file => { | ||
| const filename = file.path.replace(dir, ''); | ||
| const truncated = filename.length > 48 ? '...' + filename.slice(-45) : filename; | ||
| console.log( | ||
| truncated.padEnd(50) + | ||
| `${file.statements.pct.toFixed(1)}%`.padStart(12) + | ||
| `${file.branches.pct.toFixed(1)}%`.padStart(12) + | ||
| `${file.functions.pct.toFixed(1)}%`.padStart(12) + | ||
| `${file.lines.pct.toFixed(1)}%`.padStart(12) | ||
| ); | ||
| }); | ||
| // Summary for this directory | ||
| const totals = files.reduce((acc, file) => { | ||
| acc.statements.total += file.statements.total; | ||
| acc.statements.covered += file.statements.covered; | ||
| acc.branches.total += file.branches.total; | ||
| acc.branches.covered += file.branches.covered; | ||
| acc.functions.total += file.functions.total; | ||
| acc.functions.covered += file.functions.covered; | ||
| acc.lines.total += file.lines.total; | ||
| acc.lines.covered += file.lines.covered; | ||
| return acc; | ||
| }, { | ||
| statements: { total: 0, covered: 0 }, | ||
| branches: { total: 0, covered: 0 }, | ||
| functions: { total: 0, covered: 0 }, | ||
| lines: { total: 0, covered: 0 } | ||
| }); | ||
| const stmtPct = totals.statements.total > 0 | ||
| ? (totals.statements.covered / totals.statements.total * 100).toFixed(1) | ||
| : '0.0'; | ||
| const branchPct = totals.branches.total > 0 | ||
| ? (totals.branches.covered / totals.branches.total * 100).toFixed(1) | ||
| : '0.0'; | ||
| const funcPct = totals.functions.total > 0 | ||
| ? (totals.functions.covered / totals.functions.total * 100).toFixed(1) | ||
| : '0.0'; | ||
| const linePct = totals.lines.total > 0 | ||
| ? (totals.lines.covered / totals.lines.total * 100).toFixed(1) | ||
| : '0.0'; | ||
| console.log('-'.repeat(100)); | ||
| console.log( | ||
| 'TOTAL'.padEnd(50) + | ||
| `${stmtPct}%`.padStart(12) + | ||
| `${branchPct}%`.padStart(12) + | ||
| `${funcPct}%`.padStart(12) + | ||
| `${linePct}%`.padStart(12) | ||
| ); | ||
| }); | ||
| console.log('\n' + '='.repeat(100) + '\n'); | ||
| #!/usr/bin/env node | ||
| /** | ||
| * Post-install script for Less.js package | ||
| * | ||
| * This script installs Playwright browsers only when: | ||
| * 1. This is a development environment (not when installed as a dependency) | ||
| * 2. We're in a monorepo context (parent package.json exists) | ||
| * 3. Not running in CI or other automated environments | ||
| */ | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const { execSync } = require('child_process'); | ||
| // Check if we're in a development environment | ||
| function isDevelopmentEnvironment() { | ||
| // Skip if this is a global install or user config | ||
| if (process.env.npm_config_user_config || process.env.npm_config_global) { | ||
| return false; | ||
| } | ||
| // Skip in CI environments | ||
| if (process.env.CI || process.env.GITHUB_ACTIONS || process.env.TRAVIS) { | ||
| return false; | ||
| } | ||
| // Check if we're in a monorepo (parent package.json exists) | ||
| const parentPackageJson = path.join(__dirname, '../../../package.json'); | ||
| if (!fs.existsSync(parentPackageJson)) { | ||
| return false; | ||
| } | ||
| // Check if this is the root of the monorepo | ||
| const currentPackageJson = path.join(__dirname, '../package.json'); | ||
| if (!fs.existsSync(currentPackageJson)) { | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
| // Install Playwright browsers | ||
| function installPlaywrightBrowsers() { | ||
| try { | ||
| console.log('🎭 Installing Playwright browsers for development...'); | ||
| execSync('pnpm exec playwright install', { | ||
| stdio: 'inherit', | ||
| cwd: path.join(__dirname, '..') | ||
| }); | ||
| console.log('✅ Playwright browsers installed successfully'); | ||
| } catch (error) { | ||
| console.warn('⚠️ Failed to install Playwright browsers:', error.message); | ||
| console.warn(' You can install them manually with: pnpm exec playwright install'); | ||
| } | ||
| } | ||
| // Main execution | ||
| if (isDevelopmentEnvironment()) { | ||
| installPlaywrightBrowsers(); | ||
| } |
| .test { color: red; } |
| {"version":3,"sources":["comprehensive.less"],"names":[],"mappings":"AAoBA;EACE,aAAA;EACA,mBAAA;;AAFF,UAIE;EACE,YAAA;EACA,eAAA;;AANJ,UAIE,QAIE;EACE,iBAAA;EACA,mBAAA;;AAVN,UAcE;EACE,mBAAA;EACA,aAAA;;AAhBJ,UAcE,SAIE;EACE,SAAA;EACA,gBAAA;;AAMN;EACE,OAAO,qBAAP;EACA,QAAQ,iBAAR;EACA,YAAA;;AAIF;EACE,cAAA;EACA,mBAAA;EACA,yCAAA;;AAIF;EAlDE,mBAAA;EACA,2BAAA;EACA,wBAAA;EAIA,yCAAA;EA+CA,aAAA;EACA,iBAAA;;AAIF,QAA0B;EACxB;IACE,aAAA;;EADF,UAGE;IACE,eAAA;;;AAKN;EACE;IACE,aAAA;IACA,uBAAuB,cAAvB;IACA,SAAA;;;AAKJ;AAMA;EALE,kBAAA;EACA,YAAA;EACA,eAAA;;AAGF;EAEE,mBAAA;EACA,YAAA;;AAIF,WACE;EACE,gBAAA;;AAFJ,WACE,GAGE;EACE,qBAAA;;AALN,WACE,GAGE,GAGE;EACE,qBAAA;;AAEA,WATN,GAGE,GAGE,EAGG;EACC,cAAA;;AAGF,WAbN,GAGE,GAGE,EAOG;EACC,cAAA;;AAYT;EACC,cAAA;;AAIF,OACE,QACE,QACE;EACE,cAAA","file":"{path}comprehensive.css"} |
| {"version":3,"sources":["testweb/sourcemaps-basepath.less"],"names":[],"mappings":"AAEA;EACE,eAAA;EACA,gBAAA;;AAGF;EACE,iBAAA","file":"tests-config/sourcemaps-basepath/sourcemaps-basepath.css"} |
| {"version":3,"sources":["testweb/sourcemaps-include-source.less"],"names":[],"mappings":"AAGA;EACE,mBAAA;EACA,YAAA;EACA,kBAAA;;AAGF;EACE,mBAAA","file":"tests-config/sourcemaps-include-source/sourcemaps-include-source.css","sourcesContent":["@primary: #007bff;\n@secondary: #6c757d;\n\n.button {\n background: @primary;\n color: white;\n padding: 10px 20px;\n}\n\n.secondary {\n background: @secondary;\n}\n\n"]} |
| {"version":3,"sources":["https://example.com/less/sourcemaps-rootpath.less"],"names":[],"mappings":"AAEA;EACE,aAAA;EACA,YAAA;;AAGF;EACE,WAAA","file":"tests-config/sourcemaps-rootpath/sourcemaps-rootpath.css"} |
| {"version":3,"sources":["testweb/sourcemaps-url.less"],"names":[],"mappings":"AAEA;EACE,UAAA;EACA,gBAAA;;AAGF;EACE,YAAA","file":"tests-config/sourcemaps-url/sourcemaps-url.css"} |
+16
-17
@@ -7,3 +7,3 @@ "use strict"; | ||
| var testFolder = path.relative(process.cwd(), path.dirname(resolve.sync('@less/test-data'))); | ||
| var lessFolder = path.join(testFolder, 'less'); | ||
| var lessFolder = testFolder; | ||
@@ -89,4 +89,3 @@ module.exports = function(grunt) { | ||
| "browser", | ||
| "no-js-errors", | ||
| "legacy" | ||
| "no-js-errors" | ||
| ]; | ||
@@ -219,3 +218,3 @@ | ||
| test: { | ||
| command: 'ts-node test/test-es6.ts && node test/index.js' | ||
| command: 'npx ts-node test/test-es6.ts && node test/index.js' | ||
| }, | ||
@@ -236,13 +235,13 @@ generatebrowser: { | ||
| // CURRENT OPTIONS | ||
| `node bin/lessc --ie-compat ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`, | ||
| `node bin/lessc --ie-compat ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css`, | ||
| // --math | ||
| `node bin/lessc --math=always ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`, | ||
| `node bin/lessc --math=parens-division ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`, | ||
| `node bin/lessc --math=parens ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`, | ||
| `node bin/lessc --math=strict ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`, | ||
| `node bin/lessc --math=strict-legacy ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`, | ||
| `node bin/lessc --math=always ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css`, | ||
| `node bin/lessc --math=parens-division ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css`, | ||
| `node bin/lessc --math=parens ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css`, | ||
| `node bin/lessc --math=strict ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css`, | ||
| `node bin/lessc --math=strict-legacy ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css`, | ||
| // DEPRECATED OPTIONS | ||
| // --strict-math | ||
| `node bin/lessc --strict-math=on ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css` | ||
| `node bin/lessc --strict-math=on ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css` | ||
| ].join(" && ") | ||
@@ -252,9 +251,9 @@ }, | ||
| command: [ | ||
| `node bin/lessc --clean-css="--s1 --advanced" ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`, | ||
| `node bin/lessc --clean-css="--s1 --advanced" ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css`, | ||
| "cd lib", | ||
| `node ../bin/lessc --clean-css="--s1 --advanced" ../${lessFolder}/_main/lazy-eval.less ../tmp/lazy-eval.css`, | ||
| `node ../bin/lessc --source-map=lazy-eval.css.map --autoprefix ../${lessFolder}/_main/lazy-eval.less ../tmp/lazy-eval.css`, | ||
| `node ../bin/lessc --clean-css="--s1 --advanced" ../${lessFolder}/tests-unit/lazy-eval/lazy-eval.less ../tmp/lazy-eval.css`, | ||
| `node ../bin/lessc --source-map=lazy-eval.css.map --autoprefix ../${lessFolder}/tests-unit/lazy-eval/lazy-eval.less ../tmp/lazy-eval.css`, | ||
| "cd ..", | ||
| // Test multiple plugins | ||
| `node bin/lessc --plugin=clean-css="--s1 --advanced" --plugin=autoprefix="ie 11,Edge >= 13,Chrome >= 47,Firefox >= 45,iOS >= 9.2,Safari >= 9" ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css` | ||
| `node bin/lessc --plugin=clean-css="--s1 --advanced" --plugin=autoprefix="ie 11,Edge >= 13,Chrome >= 47,Firefox >= 45,iOS >= 9.2,Safari >= 9" ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css` | ||
| ].join(" && ") | ||
@@ -265,4 +264,4 @@ }, | ||
| command: [ | ||
| `node bin/lessc --source-map=test/sourcemaps/maps/import-map.map ${lessFolder}/_main/import.less test/sourcemaps/import.css`, | ||
| `node bin/lessc --source-map ${lessFolder}/sourcemaps/basic.less test/sourcemaps/basic.css` | ||
| `node bin/lessc --source-map=test/sourcemaps/maps/import-map.map ${lessFolder}/tests-unit/import/import.less test/sourcemaps/import.css`, | ||
| `node bin/lessc --source-map ${lessFolder}/tests-config/sourcemaps/basic.less test/sourcemaps/basic.css` | ||
| ].join(" && ") | ||
@@ -269,0 +268,0 @@ } |
@@ -35,3 +35,3 @@ // lessc_helper.js | ||
| console.log(' --quiet Suppresses output of warnings.'); | ||
| console.log(' --strict-imports Forces evaluation of imports.'); | ||
| console.log(' --strict-imports (DEPRECATED) Ignores .less imports inside selector blocks. Has confusing behavior.'); | ||
| console.log(' --insecure Allows imports from insecure https hosts.'); | ||
@@ -76,8 +76,9 @@ console.log(' -v, --version Prints version number and exit.'); | ||
| console.log(''); | ||
| console.log(' --line-numbers=TYPE Outputs filename and line numbers.'); | ||
| console.log(' TYPE can be either \'comments\', which will output'); | ||
| console.log(' the debug info within comments, \'mediaquery\''); | ||
| console.log(' that will output the information within a fake'); | ||
| console.log(' media query which is compatible with the SASS'); | ||
| console.log(' format, and \'all\' which will do both.'); | ||
| console.log(' --line-numbers=TYPE (DEPRECATED) Outputs filename and line numbers.'); | ||
| console.log(' TYPE can be either \'comments\', \'mediaquery\', or \'all\'.'); | ||
| console.log(' The entire dumpLineNumbers option is deprecated.'); | ||
| console.log(' Use sourcemaps (--source-map) instead.'); | ||
| console.log(' All modes will be removed in a future version.'); | ||
| console.log(' Note: \'mediaquery\' and \'all\' modes generate @media -sass-debug-info'); | ||
| console.log(' which had short-lived usage and is no longer recommended.'); | ||
| console.log(' -x, --compress Compresses output by removing some whitespaces.'); | ||
@@ -84,0 +85,0 @@ console.log(' We recommend you use a dedicated minifer like less-plugin-clean-css'); |
@@ -30,3 +30,2 @@ "use strict"; | ||
| 'syncImport', | ||
| 'chunkInput', | ||
| 'mime', | ||
@@ -33,0 +32,0 @@ 'useFileCache', |
@@ -24,5 +24,24 @@ "use strict"; | ||
| color: true, | ||
| /* The strictImports controls whether the compiler will allow an @import inside of either | ||
| * @media blocks or (a later addition) other selector blocks. | ||
| * See: https://github.com/less/less.js/issues/656 */ | ||
| /** | ||
| * @deprecated This option has confusing behavior and may be removed in a future version. | ||
| * | ||
| * When true, prevents @import statements for .less files from being evaluated inside | ||
| * selector blocks (rulesets). The imports are silently ignored and not output. | ||
| * | ||
| * Behavior: | ||
| * - @import at root level: Always processed | ||
| * - @import inside @-rules (@media, @supports, etc.): Processed (these are not selector blocks) | ||
| * - @import inside selector blocks (.class, #id, etc.): NOT processed (silently ignored) | ||
| * | ||
| * When false (default): All @import statements are processed regardless of context. | ||
| * | ||
| * Note: Despite the name "strict", this option does NOT throw an error when imports | ||
| * are used in selector blocks - it silently ignores them. This is confusing | ||
| * behavior that may catch users off guard. | ||
| * | ||
| * Note: Only affects .less file imports. CSS imports (url(...) or .css files) are | ||
| * always output as CSS @import statements regardless of this setting. | ||
| * | ||
| * @see https://github.com/less/less.js/issues/656 | ||
| */ | ||
| strictImports: false, | ||
@@ -29,0 +48,0 @@ /* Allow Imports from Insecure HTTPS Hosts */ |
@@ -33,2 +33,4 @@ "use strict"; | ||
| this.stack = e.stack; | ||
| // Set type early so it's always available, even if fileContentMap is missing | ||
| this.type = e.type || 'Syntax'; | ||
| if (fileContentMap && filename) { | ||
@@ -41,3 +43,2 @@ var input = fileContentMap.contents[filename]; | ||
| var lines = input ? input.split('\n') : ''; | ||
| this.type = e.type || 'Syntax'; | ||
| this.filename = filename; | ||
@@ -44,0 +45,0 @@ this.index = e.index; |
@@ -31,2 +31,3 @@ "use strict"; | ||
| compress: compress, | ||
| // @deprecated The dumpLineNumbers option is deprecated. Use sourcemaps instead. All modes will be removed in a future version. | ||
| dumpLineNumbers: options.dumpLineNumbers, | ||
@@ -37,3 +38,59 @@ strictUnits: Boolean(options.strictUnits), | ||
| if (options.sourceMap) { | ||
| sourceMapBuilder = new SourceMapBuilder(options.sourceMap); | ||
| // Normalize sourceMap option: if it's just true, convert to object | ||
| if (options.sourceMap === true) { | ||
| options.sourceMap = {}; | ||
| } | ||
| var sourceMapOpts = options.sourceMap; | ||
| // Set sourceMapInputFilename if not set and filename is available | ||
| if (!sourceMapOpts.sourceMapInputFilename && options.filename) { | ||
| sourceMapOpts.sourceMapInputFilename = options.filename; | ||
| } | ||
| // Default sourceMapBasepath to the input file's directory if not set | ||
| // This matches the behavior documented and implemented in bin/lessc | ||
| if (sourceMapOpts.sourceMapBasepath === undefined && options.filename) { | ||
| // Get directory from filename using string manipulation (works cross-platform) | ||
| var lastSlash = Math.max(options.filename.lastIndexOf('/'), options.filename.lastIndexOf('\\')); | ||
| if (lastSlash >= 0) { | ||
| sourceMapOpts.sourceMapBasepath = options.filename.substring(0, lastSlash); | ||
| } | ||
| else { | ||
| // No directory separator found, use current directory | ||
| sourceMapOpts.sourceMapBasepath = '.'; | ||
| } | ||
| } | ||
| // Handle sourceMapFullFilename (CLI-specific: --source-map=filename) | ||
| // This is converted to sourceMapFilename and sourceMapOutputFilename | ||
| if (sourceMapOpts.sourceMapFullFilename && !sourceMapOpts.sourceMapFileInline) { | ||
| // This case is handled by lessc before calling render | ||
| // We just need to ensure sourceMapFilename is set if sourceMapFullFilename is provided | ||
| if (!sourceMapOpts.sourceMapFilename && !sourceMapOpts.sourceMapURL) { | ||
| // Extract just the basename for the sourceMappingURL comment | ||
| var mapBase = sourceMapOpts.sourceMapFullFilename.split(/[/\\]/).pop(); | ||
| sourceMapOpts.sourceMapFilename = mapBase; | ||
| } | ||
| } | ||
| else if (!sourceMapOpts.sourceMapFilename && !sourceMapOpts.sourceMapURL) { | ||
| // If sourceMapFilename is not set and sourceMapURL is not set, | ||
| // derive it from the output filename (if available) or input filename | ||
| if (sourceMapOpts.sourceMapOutputFilename) { | ||
| // Use output filename + .map | ||
| sourceMapOpts.sourceMapFilename = sourceMapOpts.sourceMapOutputFilename + '.map'; | ||
| } | ||
| else if (options.filename) { | ||
| // Fallback to input filename + .css.map | ||
| var inputBase = options.filename.replace(/\.[^/.]+$/, ''); | ||
| sourceMapOpts.sourceMapFilename = inputBase + '.css.map'; | ||
| } | ||
| } | ||
| // Default sourceMapOutputFilename if not set | ||
| if (!sourceMapOpts.sourceMapOutputFilename) { | ||
| if (options.filename) { | ||
| var inputBase = options.filename.replace(/\.[^/.]+$/, ''); | ||
| sourceMapOpts.sourceMapOutputFilename = inputBase + '.css'; | ||
| } | ||
| else { | ||
| sourceMapOpts.sourceMapOutputFilename = 'output.css'; | ||
| } | ||
| } | ||
| sourceMapBuilder = new SourceMapBuilder(sourceMapOpts); | ||
| result.css = sourceMapBuilder.toCSS(evaldRoot, toCSSOptions, this.imports); | ||
@@ -40,0 +97,0 @@ } |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| var tslib_1 = require("tslib"); | ||
| var chunker_1 = tslib_1.__importDefault(require("./chunker")); | ||
| exports.default = (function () { | ||
@@ -320,21 +318,6 @@ var // Less input string | ||
| }; | ||
| parserInput.start = function (str, chunkInput, failFunction) { | ||
| parserInput.start = function (str) { | ||
| input = str; | ||
| parserInput.i = j = currentPos = furthest = 0; | ||
| // chunking apparently makes things quicker (but my tests indicate | ||
| // it might actually make things slower in node at least) | ||
| // and it is a non-perfect parse - it can't recognise | ||
| // unquoted urls, meaning it can't distinguish comments | ||
| // meaning comments with quotes or {}() in them get 'counted' | ||
| // and then lead to parse errors. | ||
| // In addition if the chunking chunks in the wrong place we might | ||
| // not be able to parse a parser statement in one go | ||
| // this is officially deprecated but can be switched on via an option | ||
| // in the case it causes too much performance issues. | ||
| if (chunkInput) { | ||
| chunks = (0, chunker_1.default)(str, failFunction); | ||
| } | ||
| else { | ||
| chunks = [str]; | ||
| } | ||
| chunks = [str]; | ||
| current = chunks[0]; | ||
@@ -341,0 +324,0 @@ skipWhitespace(0); |
@@ -13,3 +13,3 @@ "use strict"; | ||
| } | ||
| this._outputFilename = options.outputFilename; | ||
| this._outputFilename = options.outputFilename ? options.outputFilename.replace(/\\/g, '/') : options.outputFilename; | ||
| this.sourceMapURL = options.sourceMapURL; | ||
@@ -16,0 +16,0 @@ if (options.sourceMapBasepath) { |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| /** | ||
| * @deprecated The dumpLineNumbers option is deprecated. Use sourcemaps instead. | ||
| * This will be removed in a future version. | ||
| * | ||
| * @param {Object} ctx - Context object with debugInfo | ||
| * @returns {string} Debug info as CSS comment | ||
| */ | ||
| function asComment(ctx) { | ||
| return "/* line ".concat(ctx.debugInfo.lineNumber, ", ").concat(ctx.debugInfo.fileName, " */\n"); | ||
| } | ||
| /** | ||
| * @deprecated The dumpLineNumbers option is deprecated. Use sourcemaps instead. | ||
| * This function generates Sass-compatible debug info using @media -sass-debug-info syntax. | ||
| * This format had short-lived usage and is no longer recommended. | ||
| * This will be removed in a future version. | ||
| * | ||
| * @param {Object} ctx - Context object with debugInfo | ||
| * @returns {string} Sass-compatible debug info as @media query | ||
| */ | ||
| function asMediaQuery(ctx) { | ||
@@ -18,2 +34,15 @@ var filenameWithProtocol = ctx.debugInfo.fileName; | ||
| } | ||
| /** | ||
| * Generates debug information (line numbers) for CSS output. | ||
| * | ||
| * @param {Object} context - Context object with dumpLineNumbers option | ||
| * @param {Object} ctx - Context object with debugInfo | ||
| * @param {string} [lineSeparator] - Separator between comment and media query (for 'all' mode) | ||
| * @returns {string} Debug info string | ||
| * | ||
| * @deprecated The dumpLineNumbers option is deprecated. Use sourcemaps instead. | ||
| * All modes ('comments', 'mediaquery', 'all') are deprecated and will be removed in a future version. | ||
| * The 'mediaquery' and 'all' modes generate Sass-compatible @media -sass-debug-info output | ||
| * which had short-lived usage and is no longer recommended. | ||
| */ | ||
| function debugInfo(context, ctx, lineSeparator) { | ||
@@ -20,0 +49,0 @@ var result = ''; |
+10
-5
| { | ||
| "name": "less", | ||
| "version": "4.4.2", | ||
| "version": "4.5.1", | ||
| "description": "Leaner CSS", | ||
@@ -40,2 +40,3 @@ "homepage": "http://lesscss.org", | ||
| "test": "grunt test", | ||
| "test:coverage": "c8 -r lcov -r json-summary -r text-summary -r html --include=\"lib/**/*.js\" --include=\"bin/**/*.js\" --exclude=\"dist/**\" --exclude=\"**/*.test.js\" --exclude=\"**/*.spec.js\" --exclude=\"test/**\" --exclude=\"tmp/**\" --exclude=\"**/abstract-file-manager.js\" --exclude=\"**/abstract-plugin-loader.js\" grunt shell:test && node scripts/coverage-report.js && node scripts/coverage-lines.js", | ||
| "grunt": "grunt", | ||
@@ -48,3 +49,4 @@ "lint": "eslint '**/*.{ts,js}'", | ||
| "dev": "tsc -p tsconfig.build.json -w", | ||
| "prepublishOnly": "grunt dist" | ||
| "prepublishOnly": "grunt dist", | ||
| "postinstall": "node scripts/postinstall.js" | ||
| }, | ||
@@ -71,7 +73,10 @@ "optionalDependencies": { | ||
| "chai": "^4.2.0", | ||
| "c8": "^10.1.3", | ||
| "chalk": "^4.1.2", | ||
| "cosmiconfig": "~9.0.0", | ||
| "cross-env": "^7.0.3", | ||
| "diff": "^3.2.0", | ||
| "eslint": "^7.29.0", | ||
| "fs-extra": "^8.1.0", | ||
| "git-rev": "^0.2.1", | ||
| "glob": "~11.0.3", | ||
| "globby": "^10.0.1", | ||
@@ -86,2 +91,3 @@ "grunt": "^1.0.4", | ||
| "html-template-tag": "^3.2.0", | ||
| "jest-diff": "~30.1.2", | ||
| "jit-grunt": "^0.10.0", | ||
@@ -92,8 +98,7 @@ "less-plugin-autoprefix": "^1.5.1", | ||
| "mocha": "^6.2.1", | ||
| "playwright": "1.50.1", | ||
| "mocha-teamcity-reporter": "^3.0.0", | ||
| "nock": "^11.8.2", | ||
| "npm-run-all": "^4.1.5", | ||
| "performance-now": "^0.2.0", | ||
| "phin": "^2.2.3", | ||
| "playwright": "1.50.1", | ||
| "promise": "^7.1.1", | ||
@@ -100,0 +105,0 @@ "read-glob": "^3.0.0", |
@@ -102,2 +102,14 @@ var logMessages = []; | ||
| } | ||
| // Normalize URLs: convert absolute URLs back to relative for comparison | ||
| // The browser resolves relative URLs when reading from DOM, but we want to compare against the original relative URLs | ||
| lessOutput = lessOutput.replace(/url\("http:\/\/localhost:8081\/packages\/less\/node_modules\/@less\/test-data\/tests-unit\/([^"]+)"\)/g, 'url("$1")'); | ||
| // Also normalize directory-prefixed relative URLs (e.g., "at-rules/myfont.woff2" -> "myfont.woff2") | ||
| // This happens because the browser resolves URLs relative to the HTML document location | ||
| lessOutput = lessOutput.replace(/url\("([a-z-]+\/)([^"]+)"\)/g, 'url("$2")'); | ||
| // Also normalize @import statements that get resolved to absolute URLs | ||
| lessOutput = lessOutput.replace(/@import "http:\/\/localhost:8081\/packages\/less\/node_modules\/@less\/test-data\/tests-unit\/([^"]+)"(.*);/g, '@import "$1"$2;'); | ||
| // Also normalize @import with directory prefix (e.g., "at-rules-keyword-comments/test.css" -> "test.css") | ||
| lessOutput = lessOutput.replace(/@import "([a-z-]+\/)([^"]+)"(.*);/g, '@import "$2"$3;'); | ||
| expect(lessOutput).to.equal(text); | ||
@@ -168,2 +180,11 @@ done(); | ||
| .trim(); | ||
| actualErrorMsg = actualErrorMsg | ||
| .replace(/ in [\w\-]+\.less( on line \d+, column \d+)?:?$/, '') // Remove filename and optional line/column from end of error message | ||
| .replace(/\{path\}/g, '') | ||
| .replace(/\{pathrel\}/g, '') | ||
| .replace(/\{pathhref\}/g, 'http://localhost:8081/packages/less/node_modules/@less/test-data/tests-error/eval/') | ||
| .replace(/\{404status\}/g, ' (404)') | ||
| .replace(/\{node\}[\s\S]*\{\/node\}/g, '') | ||
| .replace(/\n$/, '') | ||
| .trim(); | ||
| errorFile | ||
@@ -174,3 +195,3 @@ .then(function (errorTxt) { | ||
| .replace(/\{pathrel\}/g, '') | ||
| .replace(/\{pathhref\}/g, 'http://localhost:8081/test/less/errors/') | ||
| .replace(/\{pathhref\}/g, 'http://localhost:8081/packages/less/node_modules/@less/test-data/tests-error/eval/') | ||
| .replace(/\{404status\}/g, ' (404)') | ||
@@ -177,0 +198,0 @@ .replace(/\{node\}[\s\S]*\{\/node\}/g, '') |
@@ -7,3 +7,4 @@ var path = require('path'); | ||
| var testFolder = forceCovertToBrowserPath(path.dirname(resolve.sync('@less/test-data'))); | ||
| var lessFolder = forceCovertToBrowserPath(path.join(testFolder, 'less')); | ||
| var testsUnitFolder = forceCovertToBrowserPath(path.join(testFolder, 'tests-unit')); | ||
| var testsConfigFolder = forceCovertToBrowserPath(path.join(testFolder, 'tests-config')); | ||
| var localTests = forceCovertToBrowserPath(path.resolve(__dirname, '..')); | ||
@@ -15,10 +16,13 @@ | ||
| src: [ | ||
| `${lessFolder}/_main/*.less`, | ||
| `!${lessFolder}/_main/plugin-preeval.less`, // uses ES6 syntax | ||
| `${testsUnitFolder}/*/*.less`, | ||
| `!${testsUnitFolder}/plugin-preeval/plugin-preeval.less`, // uses ES6 syntax | ||
| // Don't test NPM import, obviously | ||
| `!${lessFolder}/_main/plugin-module.less`, | ||
| `!${lessFolder}/_main/import-module.less`, | ||
| `!${lessFolder}/_main/javascript.less`, | ||
| `!${lessFolder}/_main/urls.less`, | ||
| `!${lessFolder}/_main/empty.less` | ||
| `!${testsUnitFolder}/plugin-module/plugin-module.less`, | ||
| `!${testsUnitFolder}/import/import-module.less`, | ||
| `!${testsUnitFolder}/javascript/javascript.less`, | ||
| `!${testsUnitFolder}/urls/urls.less`, | ||
| `!${testsUnitFolder}/empty/empty.less`, | ||
| `!${testsUnitFolder}/color-functions/operations.less`, // conflicts with operations/operations.less | ||
| // Exclude debug line numbers tests - these are Node.js only (dumpLineNumbers is deprecated) | ||
| `!${testsConfigFolder}/debug/**/*.less` | ||
| ], | ||
@@ -31,12 +35,4 @@ options: { | ||
| }, | ||
| legacy: { | ||
| src: [`${lessFolder}/legacy/*.less`], | ||
| options: { | ||
| helpers: 'test/browser/runner-legacy-options.js', | ||
| specs: 'test/browser/runner-legacy-spec.js', | ||
| outfile: 'tmp/browser/test-runner-legacy.html' | ||
| } | ||
| }, | ||
| strictUnits: { | ||
| src: [`${lessFolder}/units/strict/*.less`], | ||
| src: [`${testsConfigFolder}/units/strict/*.less`], | ||
| options: { | ||
@@ -50,4 +46,4 @@ helpers: 'test/browser/runner-strict-units-options.js', | ||
| src: [ | ||
| `${lessFolder}/errors/*.less`, | ||
| `${testFolder}/errors/javascript-error.less`, | ||
| `${testFolder}/tests-error/eval/*.less`, | ||
| `${testFolder}/tests-error/parse/*.less`, | ||
| `${localTests}/less/errors/*.less` | ||
@@ -63,3 +59,3 @@ ], | ||
| noJsErrors: { | ||
| src: [`${lessFolder}/no-js-errors/*.less`], | ||
| src: [`${testsConfigFolder}/no-js-errors/*.less`], | ||
| options: { | ||
@@ -149,3 +145,3 @@ helpers: 'test/browser/runner-no-js-errors-options.js', | ||
| postProcessorPlugin: { | ||
| src: [`${lessFolder}/postProcessorPlugin/*.less`], | ||
| src: [`${testsConfigFolder}/postProcessorPlugin/*.less`], | ||
| options: { | ||
@@ -162,3 +158,3 @@ helpers: [ | ||
| preProcessorPlugin: { | ||
| src: [`${lessFolder}/preProcessorPlugin/*.less`], | ||
| src: [`${testsConfigFolder}/preProcessorPlugin/*.less`], | ||
| options: { | ||
@@ -174,3 +170,3 @@ helpers: [ | ||
| visitorPlugin: { | ||
| src: [`${lessFolder}/visitorPlugin/*.less`], | ||
| src: [`${testsConfigFolder}/visitorPlugin/*.less`], | ||
| options: { | ||
@@ -186,3 +182,3 @@ helpers: [ | ||
| filemanagerPlugin: { | ||
| src: [`${lessFolder}/filemanagerPlugin/*.less`], | ||
| src: [`${testsConfigFolder}/filemanagerPlugin/*.less`], | ||
| options: { | ||
@@ -189,0 +185,0 @@ helpers: [ |
@@ -28,5 +28,16 @@ const html = require('html-template-tag') | ||
| var pathParts = fullLessName.split('/'); | ||
| var fullCssName = fullLessName | ||
| .replace(/\/(browser|test-data)\/less\//g, '/$1/css/') | ||
| .replace(/less$/, 'css') | ||
| var fullCssName = fullLessName.replace(/less$/, 'css'); | ||
| // Check if the CSS file exists in the same directory as the LESS file | ||
| var fs = require('fs'); | ||
| var cssExists = fs.existsSync(fullCssName); | ||
| // If not, try the css/ directory for local browser tests | ||
| if (!cssExists && fullLessName.includes('/test/browser/less/')) { | ||
| var cssInCssDir = fullLessName.replace('/test/browser/less/', '/test/browser/css/').replace(/less$/, 'css'); | ||
| if (fs.existsSync(cssInCssDir)) { | ||
| fullCssName = cssInCssDir; | ||
| } | ||
| } | ||
| var lessName = pathParts[pathParts.length - 1]; | ||
@@ -33,0 +44,0 @@ var name = lessName.split('.')[0]; |
@@ -9,3 +9,3 @@ var less = { | ||
| // test inline less in style tags by grabbing an assortment of less files and doing `@import`s | ||
| var testFiles = ['charsets', 'colors', 'comments', 'css-3', 'strings', 'media', 'mixins'], | ||
| var testFiles = ['charsets/charsets', 'color-functions/basic', 'comments/comments', 'css-3/css-3', 'strings/strings', 'media/media', 'mixins/mixins'], | ||
| testSheets = []; | ||
@@ -18,9 +18,9 @@ | ||
| */ | ||
| var lessFolder = '../../node_modules/@less/test-data/less' | ||
| var cssFolder = '../../node_modules/@less/test-data/css' | ||
| var lessFolder = '../../node_modules/@less/test-data/tests-unit' | ||
| var cssFolder = '../../node_modules/@less/test-data/tests-unit' | ||
| for (var i = 0; i < testFiles.length; i++) { | ||
| var file = testFiles[i], | ||
| lessPath = lessFolder + '/_main/' + file + '.less', | ||
| cssPath = cssFolder + '/_main/' + file + '.css', | ||
| lessPath = lessFolder + '/' + file + '.less', | ||
| cssPath = cssFolder + '/' + file + '.css', | ||
| lessStyle = document.createElement('style'), | ||
@@ -27,0 +27,0 @@ cssLink = document.createElement('link'), |
+290
-95
@@ -1,92 +0,293 @@ | ||
| var lessTest = require('./less-test'), | ||
| lessTester = lessTest(), | ||
| path = require('path'), | ||
| stylize = require('../lib/less-node/lessc-helper').stylize, | ||
| nock = require('nock'); | ||
| // Mock needle for HTTP requests BEFORE any other requires | ||
| const Module = require('module'); | ||
| const originalRequire = Module.prototype.require; | ||
| Module.prototype.require = function(id) { | ||
| if (id === 'needle') { | ||
| return { | ||
| get: function(url, options, callback) { | ||
| // Handle CDN requests | ||
| if (url.includes('cdn.jsdelivr.net')) { | ||
| if (url.includes('selectors.less')) { | ||
| setTimeout(() => { | ||
| callback(null, { statusCode: 200 }, fs.readFileSync(path.join(__dirname, '../../test-data/tests-unit/selectors/selectors.less'), 'utf8')); | ||
| }, 10); | ||
| return; | ||
| } | ||
| if (url.includes('media.less')) { | ||
| setTimeout(() => { | ||
| callback(null, { statusCode: 200 }, fs.readFileSync(path.join(__dirname, '../../test-data/tests-unit/media/media.less'), 'utf8')); | ||
| }, 10); | ||
| return; | ||
| } | ||
| if (url.includes('empty.less')) { | ||
| setTimeout(() => { | ||
| callback(null, { statusCode: 200 }, fs.readFileSync(path.join(__dirname, '../../test-data/tests-unit/empty/empty.less'), 'utf8')); | ||
| }, 10); | ||
| return; | ||
| } | ||
| } | ||
| // Handle redirect test - simulate needle's automatic redirect handling | ||
| if (url.includes('example.com/redirect.less')) { | ||
| setTimeout(() => { | ||
| // Simulate the final response after needle automatically follows the redirect | ||
| callback(null, { statusCode: 200 }, 'h1 { color: blue; }'); | ||
| }, 10); | ||
| return; | ||
| } | ||
| if (url.includes('example.com/target.less')) { | ||
| setTimeout(() => { | ||
| callback(null, { statusCode: 200 }, 'h1 { color: blue; }'); | ||
| }, 10); | ||
| return; | ||
| } | ||
| // Default error for unmocked URLs | ||
| setTimeout(() => { | ||
| callback(new Error('Unmocked URL: ' + url), null, null); | ||
| }, 10); | ||
| } | ||
| }; | ||
| } | ||
| return originalRequire.apply(this, arguments); | ||
| }; | ||
| // Now load other modules after mocking is set up | ||
| var path = require('path'), | ||
| fs = require('fs'), | ||
| lessTest = require('./less-test'), | ||
| stylize = require('../lib/less-node/lessc-helper').stylize; | ||
| // Parse command line arguments for test filtering | ||
| var args = process.argv.slice(2); | ||
| var testFilter = args.length > 0 ? args[0] : null; | ||
| // Create the test runner with the filter | ||
| var lessTester = lessTest(testFilter); | ||
| // HTTP mocking is now handled by needle mocking above | ||
| // Test HTTP redirect functionality | ||
| function testHttpRedirects() { | ||
| const less = require('../lib/less-node').default; | ||
| console.log('🧪 Testing HTTP redirect functionality...'); | ||
| const redirectTest = ` | ||
| @import "https://example.com/redirect.less"; | ||
| h1 { color: red; } | ||
| `; | ||
| return less.render(redirectTest, { | ||
| filename: 'test-redirect.less' | ||
| }).then(result => { | ||
| console.log('✅ HTTP redirect test SUCCESS:'); | ||
| console.log(result.css); | ||
| // Check if both imported and local content are present | ||
| if (result.css.includes('color: blue') && result.css.includes('color: red')) { | ||
| console.log('🎉 HTTP redirect test PASSED - both imported and local content found'); | ||
| return true; | ||
| } else { | ||
| console.log('❌ HTTP redirect test FAILED - missing expected content'); | ||
| return false; | ||
| } | ||
| }).catch(err => { | ||
| console.log('❌ HTTP redirect test ERROR:'); | ||
| console.log(err.message); | ||
| return false; | ||
| }); | ||
| } | ||
| // Test import-remote functionality | ||
| function testImportRemote() { | ||
| const less = require('../lib/less-node').default; | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| console.log('🧪 Testing import-remote functionality...'); | ||
| const testFile = path.join(__dirname, '../../test-data/tests-unit/import/import-remote.less'); | ||
| const expectedFile = path.join(__dirname, '../../test-data/tests-unit/import/import-remote.css'); | ||
| const content = fs.readFileSync(testFile, 'utf8'); | ||
| const expected = fs.readFileSync(expectedFile, 'utf8'); | ||
| return less.render(content, { | ||
| filename: testFile | ||
| }).then(result => { | ||
| console.log('✅ Import-remote test SUCCESS:'); | ||
| console.log('Expected:', expected.trim()); | ||
| console.log('Actual:', result.css.trim()); | ||
| if (result.css.trim() === expected.trim()) { | ||
| console.log('🎉 Import-remote test PASSED - CDN imports and variable resolution working'); | ||
| return true; | ||
| } else { | ||
| console.log('❌ Import-remote test FAILED - output mismatch'); | ||
| return false; | ||
| } | ||
| }).catch(err => { | ||
| console.log('❌ Import-remote test ERROR:'); | ||
| console.log(err.message); | ||
| return false; | ||
| }); | ||
| } | ||
| console.log('\n' + stylize('Less', 'underline') + '\n'); | ||
| if (testFilter) { | ||
| console.log('Running tests matching: ' + testFilter + '\n'); | ||
| } | ||
| // Glob patterns for main test runs (excluding problematic tests that will run separately) | ||
| var globPatterns = [ | ||
| 'tests-config/*/*.less', | ||
| 'tests-unit/*/*.less', | ||
| // Debug tests have nested subdirectories (comments/, mediaquery/, all/) | ||
| 'tests-config/debug/*/linenumbers-*.less', | ||
| '!tests-config/sourcemaps/**/*.less', // Exclude sourcemaps (need special handling) | ||
| '!tests-config/sourcemaps-empty/*', // Exclude sourcemaps-empty (need special handling) | ||
| '!tests-config/sourcemaps-disable-annotation/*', // Exclude sourcemaps-disable-annotation (need special handling) | ||
| '!tests-config/sourcemaps-variable-selector/*', // Exclude sourcemaps-variable-selector (need special handling) | ||
| '!tests-config/globalVars/*', // Exclude globalVars (need JSON config handling) | ||
| '!tests-config/modifyVars/*', // Exclude modifyVars (need JSON config handling) | ||
| '!tests-config/js-type-errors/*', // Exclude js-type-errors (need special test function) | ||
| '!tests-config/no-js-errors/*', // Exclude no-js-errors (need special test function) | ||
| '!tests-unit/import/import-remote.less', // Exclude import-remote (tested separately in isolation) | ||
| // HTTP import tests are now included since we have needle mocking | ||
| ]; | ||
| var testMap = [ | ||
| [{ | ||
| // TODO: Change this to rewriteUrls: 'all' once the relativeUrls option is removed | ||
| relativeUrls: true, | ||
| silent: true, | ||
| javascriptEnabled: true | ||
| }, '_main/'], | ||
| [{}, 'namespacing/'], | ||
| [{ | ||
| math: 'parens' | ||
| }, 'math/strict/'], | ||
| [{ | ||
| math: 'parens-division' | ||
| }, 'math/parens-division/'], | ||
| [{ | ||
| math: 'always' | ||
| }, 'math/always/'], | ||
| // Use legacy strictMath: true here to demonstrate it still works | ||
| [{strictMath: true, strictUnits: true, javascriptEnabled: true}, '../errors/eval/', | ||
| lessTester.testErrors, null], | ||
| [{strictMath: true, strictUnits: true, javascriptEnabled: true}, '../errors/parse/', | ||
| lessTester.testErrors, null], | ||
| [{math: 'strict', strictUnits: true, javascriptEnabled: true}, 'js-type-errors/', | ||
| lessTester.testTypeErrors, null], | ||
| [{math: 'strict', strictUnits: true, javascriptEnabled: false}, 'no-js-errors/', | ||
| lessTester.testErrors, null], | ||
| [{math: 'strict', dumpLineNumbers: 'comments'}, 'debug/', null, | ||
| function(name) { return name + '-comments'; }], | ||
| [{math: 'strict', dumpLineNumbers: 'mediaquery'}, 'debug/', null, | ||
| function(name) { return name + '-mediaquery'; }], | ||
| [{math: 'strict', dumpLineNumbers: 'all'}, 'debug/', null, | ||
| function(name) { return name + '-all'; }], | ||
| // TODO: Change this to rewriteUrls: false once the relativeUrls option is removed | ||
| [{math: 'strict', relativeUrls: false, rootpath: 'folder (1)/'}, 'static-urls/'], | ||
| [{math: 'strict', compress: true}, 'compression/'], | ||
| // Main test runs using glob patterns (cosmiconfig handles configs) | ||
| { | ||
| patterns: globPatterns | ||
| }, | ||
| [{math: 0, strictUnits: true}, 'units/strict/'], | ||
| [{math: 0, strictUnits: false}, 'units/no-strict/'], | ||
| // Error tests | ||
| { | ||
| patterns: ['tests-error/eval/*.less'], | ||
| verifyFunction: lessTester.testErrors | ||
| }, | ||
| { | ||
| patterns: ['tests-error/parse/*.less'], | ||
| verifyFunction: lessTester.testErrors | ||
| }, | ||
| [{math: 'strict', strictUnits: true, sourceMap: true, globalVars: true }, 'sourcemaps/', | ||
| lessTester.testSourcemap, null, null, | ||
| function(filename, type, baseFolder) { | ||
| // Special test cases with specific handling | ||
| { | ||
| patterns: ['tests-config/js-type-errors/*.less'], | ||
| verifyFunction: lessTester.testTypeErrors | ||
| }, | ||
| { | ||
| patterns: ['tests-config/no-js-errors/*.less'], | ||
| verifyFunction: lessTester.testErrors | ||
| }, | ||
| // Sourcemap tests with special handling | ||
| { | ||
| patterns: [ | ||
| 'tests-config/sourcemaps/**/*.less', | ||
| 'tests-config/sourcemaps-url/**/*.less', | ||
| 'tests-config/sourcemaps-rootpath/**/*.less', | ||
| 'tests-config/sourcemaps-basepath/**/*.less', | ||
| 'tests-config/sourcemaps-include-source/**/*.less' | ||
| ], | ||
| verifyFunction: lessTester.testSourcemap, | ||
| getFilename: function(filename, type, baseFolder) { | ||
| if (type === 'vars') { | ||
| return path.join(baseFolder, filename) + '.json'; | ||
| } | ||
| return path.join('test/sourcemaps', filename) + '.json'; | ||
| }], | ||
| // Extract just the filename (without directory) for the JSON file | ||
| var jsonFilename = path.basename(filename); | ||
| // For sourcemap type, return path relative to test directory | ||
| if (type === 'sourcemap') { | ||
| return path.join('test/sourcemaps', jsonFilename) + '.json'; | ||
| } | ||
| return path.join('test/sourcemaps', jsonFilename) + '.json'; | ||
| } | ||
| }, | ||
| { | ||
| patterns: ['tests-config/sourcemaps-empty/*.less'], | ||
| verifyFunction: lessTester.testEmptySourcemap | ||
| }, | ||
| { | ||
| patterns: ['tests-config/sourcemaps-disable-annotation/*.less'], | ||
| verifyFunction: lessTester.testSourcemapWithoutUrlAnnotation | ||
| }, | ||
| { | ||
| patterns: ['tests-config/sourcemaps-variable-selector/*.less'], | ||
| verifyFunction: lessTester.testSourcemapWithVariableInSelector | ||
| }, | ||
| [{math: 'strict', strictUnits: true, globalVars: true }, '_main/import/json/', | ||
| lessTester.testImports, null, true, | ||
| function(filename, type, baseFolder) { | ||
| return path.join(baseFolder, filename) + '.json'; | ||
| }], | ||
| [{math: 'strict', strictUnits: true, sourceMap: {sourceMapFileInline: true}}, | ||
| '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/', | ||
| null, null, null, function(name, type, baseFolder) { return path.join(baseFolder, name) + '.json'; }], | ||
| [{urlArgs: '424242'}, 'url-args/'], | ||
| [{rewriteUrls: 'all'}, 'rewrite-urls-all/'], | ||
| [{rewriteUrls: 'local'}, 'rewrite-urls-local/'], | ||
| [{rootpath: 'http://example.com/assets/css/', rewriteUrls: 'all'}, 'rootpath-rewrite-urls-all/'], | ||
| [{rootpath: 'http://example.com/assets/css/', rewriteUrls: 'local'}, 'rootpath-rewrite-urls-local/'], | ||
| [{paths: ['data/', '_main/import/']}, 'include-path/'], | ||
| [{paths: 'data/'}, 'include-path-string/'], | ||
| [{plugin: 'test/plugins/postprocess/'}, 'postProcessorPlugin/'], | ||
| [{plugin: 'test/plugins/preprocess/'}, 'preProcessorPlugin/'], | ||
| [{plugin: 'test/plugins/visitor/'}, 'visitorPlugin/'], | ||
| [{plugin: 'test/plugins/filemanager/'}, 'filemanagerPlugin/'], | ||
| [{math: 0}, '3rd-party/'], | ||
| [{ processImports: false }, 'process-imports/'] | ||
| // Import tests with JSON configs | ||
| { | ||
| patterns: ['tests-config/globalVars/*.less'], | ||
| lessOptions: { | ||
| globalVars: function(file) { | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const basename = path.basename(file, '.less'); | ||
| const jsonPath = path.join(path.dirname(file), basename + '.json'); | ||
| try { | ||
| return JSON.parse(fs.readFileSync(jsonPath, 'utf8')); | ||
| } catch (e) { | ||
| return {}; | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| patterns: ['tests-config/modifyVars/*.less'], | ||
| lessOptions: { | ||
| modifyVars: function(file) { | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const basename = path.basename(file, '.less'); | ||
| const jsonPath = path.join(path.dirname(file), basename + '.json'); | ||
| try { | ||
| return JSON.parse(fs.readFileSync(jsonPath, 'utf8')); | ||
| } catch (e) { | ||
| return {}; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ]; | ||
| testMap.forEach(function(args) { | ||
| lessTester.runTestSet.apply(lessTester, args) | ||
| // Note: needle mocking is set up globally at the top of the file | ||
| testMap.forEach(function(testConfig) { | ||
| // For glob patterns, pass lessOptions as the first parameter and patterns as the second | ||
| if (testConfig.patterns) { | ||
| lessTester.runTestSet( | ||
| testConfig.lessOptions || {}, // First param: options (including lessOptions) | ||
| testConfig.patterns, // Second param: patterns | ||
| testConfig.verifyFunction || null, // Third param: verifyFunction | ||
| testConfig.nameModifier || null, // Fourth param: nameModifier | ||
| testConfig.doReplacements || null, // Fifth param: doReplacements | ||
| testConfig.getFilename || null // Sixth param: getFilename | ||
| ); | ||
| } else { | ||
| // Legacy format for non-glob tests | ||
| var args = [ | ||
| testConfig.options || {}, // First param: options | ||
| testConfig.foldername, // Second param: foldername | ||
| testConfig.verifyFunction || null, // Third param: verifyFunction | ||
| testConfig.nameModifier || null, // Fourth param: nameModifier | ||
| testConfig.doReplacements || null, // Fifth param: doReplacements | ||
| testConfig.getFilename || null // Sixth param: getFilename | ||
| ]; | ||
| lessTester.runTestSet.apply(lessTester, args); | ||
| } | ||
| }); | ||
| lessTester.testSyncronous({syncImport: true}, '_main/import'); | ||
| lessTester.testSyncronous({syncImport: true}, '_main/plugin'); | ||
| lessTester.testSyncronous({syncImport: true}, 'math/strict/css'); | ||
| // Special synchronous tests | ||
| lessTester.testSyncronous({syncImport: true}, 'tests-unit/import/import'); | ||
| lessTester.testSyncronous({syncImport: true}, 'tests-config/math-strict/css'); | ||
| lessTester.testNoOptions(); | ||
@@ -97,17 +298,11 @@ lessTester.testDisablePluginRule(); | ||
| (() => { | ||
| // Create new tester, since tests are not independent and tests | ||
| // above modify tester in a way that breaks remote imports. | ||
| lessTester = lessTest(); | ||
| var scope = nock('https://example.com') | ||
| .get('/redirect.less').query(true) | ||
| .reply(301, null, { location: '/target.less' }) | ||
| .get('/target.less').query(true) | ||
| .reply(200); | ||
| lessTester.runTestSet( | ||
| {}, | ||
| 'import-redirect/', | ||
| lessTester.testImportRedirect(scope) | ||
| ); | ||
| lessTester.finished(); | ||
| })(); | ||
| // Test HTTP redirect functionality | ||
| console.log('\nTesting HTTP redirect functionality...'); | ||
| testHttpRedirects(); | ||
| console.log('HTTP redirect test completed'); | ||
| // Test import-remote functionality in isolation | ||
| console.log('\nTesting import-remote functionality...'); | ||
| testImportRemote(); | ||
| console.log('Import-remote test completed'); |
+453
-94
| /* jshint latedef: nofunc */ | ||
| var semver = require('semver'); | ||
| var logger = require('../lib/less/logger').default; | ||
| var { cosmiconfigSync } = require('cosmiconfig'); | ||
| var glob = require('glob'); | ||
@@ -21,3 +23,3 @@ var isVerbose = process.env.npm_config_loglevel !== 'concise'; | ||
| module.exports = function() { | ||
| module.exports = function(testFilter) { | ||
| var path = require('path'), | ||
@@ -33,7 +35,7 @@ fs = require('fs'), | ||
| var oneTestOnly = process.argv[2], | ||
| var oneTestOnly = testFilter || process.argv[2], | ||
| isFinished = false; | ||
| var testFolder = path.dirname(require.resolve('@less/test-data')); | ||
| var lessFolder = path.join(testFolder, 'less'); | ||
| var lessFolder = testFolder; | ||
@@ -88,3 +90,89 @@ // Define String.prototype.endsWith if it doesn't exist (in older versions of node) | ||
| function testSourcemap(name, err, compiledLess, doReplacements, sourcemap, baseFolder) { | ||
| function validateSourcemapMappings(sourcemap, lessFile, compiledCSS) { | ||
| // Validate sourcemap mappings using SourceMapConsumer | ||
| var SourceMapConsumer = require('source-map').SourceMapConsumer; | ||
| // sourcemap can be either a string or already parsed object | ||
| var sourceMapObj = typeof sourcemap === 'string' ? JSON.parse(sourcemap) : sourcemap; | ||
| var consumer = new SourceMapConsumer(sourceMapObj); | ||
| // Read the LESS source file | ||
| var lessSource = fs.readFileSync(lessFile, 'utf8'); | ||
| var lessLines = lessSource.split('\n'); | ||
| // Use the compiled CSS (remove sourcemap annotation for validation) | ||
| var cssSource = compiledCSS.replace(/\/\*# sourceMappingURL=.*\*\/\s*$/, '').trim(); | ||
| var cssLines = cssSource.split('\n'); | ||
| var errors = []; | ||
| var validatedMappings = 0; | ||
| // Validate mappings for each line in the CSS | ||
| for (var cssLine = 1; cssLine <= cssLines.length; cssLine++) { | ||
| var cssLineContent = cssLines[cssLine - 1]; | ||
| // Skip empty lines | ||
| if (!cssLineContent.trim()) { | ||
| continue; | ||
| } | ||
| // Check mapping for the start of this CSS line | ||
| var mapping = consumer.originalPositionFor({ | ||
| line: cssLine, | ||
| column: 0 | ||
| }); | ||
| if (mapping.source) { | ||
| validatedMappings++; | ||
| // Verify the source file exists in the sourcemap | ||
| if (!sourceMapObj.sources || sourceMapObj.sources.indexOf(mapping.source) === -1) { | ||
| errors.push('Line ' + cssLine + ': mapped to source "' + mapping.source + '" which is not in sources array'); | ||
| } | ||
| // Verify the line number is valid | ||
| if (mapping.line && mapping.line > 0) { | ||
| // If we can find the source file, validate the line exists | ||
| var sourceIndex = sourceMapObj.sources.indexOf(mapping.source); | ||
| if (sourceIndex >= 0 && sourceMapObj.sourcesContent && sourceMapObj.sourcesContent[sourceIndex] !== undefined && sourceMapObj.sourcesContent[sourceIndex] !== null) { | ||
| var sourceContent = sourceMapObj.sourcesContent[sourceIndex]; | ||
| // Ensure sourceContent is a string (it should be, but be defensive) | ||
| if (typeof sourceContent !== 'string') { | ||
| sourceContent = String(sourceContent); | ||
| } | ||
| // Split by newline - handle both \n and \r\n | ||
| var sourceLines = sourceContent.split(/\r?\n/); | ||
| if (mapping.line > sourceLines.length) { | ||
| errors.push('Line ' + cssLine + ': mapped to line ' + mapping.line + ' in "' + mapping.source + '" but source only has ' + sourceLines.length + ' lines'); | ||
| } | ||
| } else if (sourceIndex >= 0) { | ||
| // Source content not embedded, try to validate against the actual file if it matches | ||
| // This is a best-effort validation | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // Validate that all sources in the sourcemap are valid | ||
| if (sourceMapObj.sources) { | ||
| sourceMapObj.sources.forEach(function(source, index) { | ||
| if (sourceMapObj.sourcesContent && sourceMapObj.sourcesContent[index]) { | ||
| // Source content is embedded, validate it's not empty | ||
| if (!sourceMapObj.sourcesContent[index].trim()) { | ||
| errors.push('Source "' + source + '" has empty content'); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| if (consumer.destroy && typeof consumer.destroy === 'function') { | ||
| consumer.destroy(); | ||
| } | ||
| return { | ||
| valid: errors.length === 0, | ||
| errors: errors, | ||
| mappingsValidated: validatedMappings | ||
| }; | ||
| } | ||
| function testSourcemap(name, err, compiledLess, doReplacements, sourcemap, baseFolder, getFilename) { | ||
| if (err) { | ||
@@ -95,23 +183,106 @@ fail('ERROR: ' + (err && err.message)); | ||
| // Check the sourceMappingURL at the bottom of the file | ||
| var expectedSourceMapURL = name + '.css.map', | ||
| sourceMappingPrefix = '/*# sourceMappingURL=', | ||
| sourceMappingSuffix = ' */', | ||
| expectedCSSAppendage = sourceMappingPrefix + expectedSourceMapURL + sourceMappingSuffix; | ||
| if (!compiledLess.endsWith(expectedCSSAppendage)) { | ||
| // To display a better error message, we need to figure out what the actual sourceMappingURL value was, if it was even present | ||
| var indexOfSourceMappingPrefix = compiledLess.indexOf(sourceMappingPrefix); | ||
| if (indexOfSourceMappingPrefix === -1) { | ||
| fail('ERROR: sourceMappingURL was not found in ' + baseFolder + '/' + name + '.css.'); | ||
| return; | ||
| } | ||
| // Default expected URL is name + '.css.map', but can be overridden by sourceMapURL option | ||
| var sourceMappingPrefix = '/*# sourceMappingURL=', | ||
| sourceMappingSuffix = ' */'; | ||
| var indexOfSourceMappingPrefix = compiledLess.indexOf(sourceMappingPrefix); | ||
| if (indexOfSourceMappingPrefix === -1) { | ||
| fail('ERROR: sourceMappingURL was not found in ' + baseFolder + '/' + name + '.css.'); | ||
| return; | ||
| } | ||
| var startOfSourceMappingValue = indexOfSourceMappingPrefix + sourceMappingPrefix.length, | ||
| indexOfSuffix = compiledLess.indexOf(sourceMappingSuffix, startOfSourceMappingValue), | ||
| actualSourceMapURL = compiledLess.substring(startOfSourceMappingValue, indexOfSuffix === -1 ? compiledLess.length : indexOfSuffix).trim(); | ||
| // For tests with custom sourceMapURL, we just verify it exists and is non-empty | ||
| // The actual value will be validated by comparing the sourcemap JSON | ||
| if (!actualSourceMapURL) { | ||
| fail('ERROR: sourceMappingURL is empty in ' + baseFolder + '/' + name + '.css.'); | ||
| return; | ||
| } | ||
| var startOfSourceMappingValue = indexOfSourceMappingPrefix + sourceMappingPrefix.length, | ||
| indexOfNextSpace = compiledLess.indexOf(' ', startOfSourceMappingValue), | ||
| actualSourceMapURL = compiledLess.substring(startOfSourceMappingValue, indexOfNextSpace === -1 ? compiledLess.length : indexOfNextSpace); | ||
| fail('ERROR: sourceMappingURL should be "' + expectedSourceMapURL + '" but is "' + actualSourceMapURL + '".'); | ||
| // Use getFilename if available (for sourcemap tests with subdirectories) | ||
| var jsonPath; | ||
| if (getFilename && typeof getFilename === 'function') { | ||
| jsonPath = getFilename(name, 'sourcemap', baseFolder); | ||
| } else { | ||
| // Fallback: extract just the filename for sourcemap JSON files | ||
| var jsonFilename = path.basename(name); | ||
| jsonPath = path.join('test/sourcemaps', jsonFilename) + '.json'; | ||
| } | ||
| fs.readFile(path.join('test/', name) + '.json', 'utf8', function (e, expectedSourcemap) { | ||
| fs.readFile(jsonPath, 'utf8', function (e, expectedSourcemap) { | ||
| process.stdout.write('- ' + path.join(baseFolder, name) + ': '); | ||
| if (sourcemap === expectedSourcemap) { | ||
| if (e) { | ||
| fail('ERROR: Could not read expected sourcemap file: ' + jsonPath + ' - ' + e.message); | ||
| return; | ||
| } | ||
| // Apply doReplacements to the expected sourcemap to handle {path} placeholders | ||
| // This normalizes absolute paths that differ between environments | ||
| // For sourcemaps, we need to ensure {path} uses forward slashes to avoid breaking JSON | ||
| // (backslashes in JSON strings need escaping, and sourcemaps should use forward slashes anyway) | ||
| var replacementPath = path.join(path.dirname(path.join(baseFolder, name) + '.less'), '/'); | ||
| // Normalize to forward slashes for sourcemap JSON (web-compatible) | ||
| replacementPath = replacementPath.replace(/\\/g, '/'); | ||
| // Replace {path} with normalized forward-slash path BEFORE calling doReplacements | ||
| // This ensures the JSON is always valid and uses web-compatible paths | ||
| expectedSourcemap = expectedSourcemap.replace(/\{path\}/g, replacementPath); | ||
| // Also handle other placeholders that might be in the sourcemap (but {path} is already done) | ||
| expectedSourcemap = doReplacements(expectedSourcemap, baseFolder, path.join(baseFolder, name) + '.less'); | ||
| // Normalize paths in sourcemap JSON to use forward slashes (web-compatible) | ||
| // We need to parse the JSON, normalize the file property, then stringify for comparison | ||
| // This avoids breaking escape sequences like \n in the JSON string | ||
| function normalizeSourcemapPaths(sm) { | ||
| try { | ||
| var parsed = typeof sm === 'string' ? JSON.parse(sm) : sm; | ||
| if (parsed.file) { | ||
| parsed.file = parsed.file.replace(/\\/g, '/'); | ||
| } | ||
| // Also normalize paths in sources array | ||
| if (parsed.sources && Array.isArray(parsed.sources)) { | ||
| parsed.sources = parsed.sources.map(function(src) { | ||
| return src.replace(/\\/g, '/'); | ||
| }); | ||
| } | ||
| return JSON.stringify(parsed, null, 0); | ||
| } catch (parseErr) { | ||
| // If parsing fails, return original (shouldn't happen) | ||
| return sm; | ||
| } | ||
| } | ||
| var normalizedSourcemap = normalizeSourcemapPaths(sourcemap); | ||
| var normalizedExpected = normalizeSourcemapPaths(expectedSourcemap); | ||
| if (normalizedSourcemap === normalizedExpected) { | ||
| // Validate the sourcemap mappings are correct | ||
| // Find the actual LESS file - it might be in a subdirectory | ||
| var nameParts = name.split('/'); | ||
| var lessFileName = nameParts[nameParts.length - 1]; | ||
| var lessFileDir = nameParts.length > 1 ? nameParts.slice(0, -1).join('/') : ''; | ||
| var lessFile = path.join(lessFolder, lessFileDir, lessFileName) + '.less'; | ||
| // Only validate if the LESS file exists | ||
| if (fs.existsSync(lessFile)) { | ||
| try { | ||
| // Parse the sourcemap once for validation (avoid re-parsing) | ||
| // Use the original sourcemap string, not the normalized one | ||
| var sourceMapObjForValidation = typeof sourcemap === 'string' ? JSON.parse(sourcemap) : sourcemap; | ||
| var validation = validateSourcemapMappings(sourceMapObjForValidation, lessFile, compiledLess); | ||
| if (!validation.valid) { | ||
| fail('ERROR: Sourcemap validation failed:\n' + validation.errors.join('\n')); | ||
| return; | ||
| } | ||
| if (isVerbose && validation.mappingsValidated > 0) { | ||
| process.stdout.write(' (validated ' + validation.mappingsValidated + ' mappings)'); | ||
| } | ||
| } catch (validationErr) { | ||
| if (isVerbose) { | ||
| process.stdout.write(' (validation error: ' + validationErr.message + ')'); | ||
| } | ||
| // Don't fail the test if validation has an error, just log it | ||
| } | ||
| } | ||
| ok('OK'); | ||
@@ -125,3 +296,3 @@ } else if (err) { | ||
| } else { | ||
| difference('FAIL', expectedSourcemap, sourcemap); | ||
| difference('FAIL', normalizedExpected, normalizedSourcemap); | ||
| } | ||
@@ -289,3 +460,3 @@ }); | ||
| var expected = '@charset "utf-8";\n'; | ||
| toCSS({}, path.join(lessFolder, 'root-registry', 'root.less'), function(error, output) { | ||
| toCSS({}, path.join(lessFolder, 'tests-config', 'root-registry', 'root.less'), function(error, output) { | ||
| if (error) { | ||
@@ -303,5 +474,38 @@ return fail('ERROR: ' + error); | ||
| var path = require('path'); | ||
| var p = filename ? path.join(path.dirname(filename), '/') : directory, | ||
| pathimport = path.join(directory + 'import/'), | ||
| pathesc = p.replace(/[.:/\\]/g, function(a) { return '\\' + (a == '\\' ? '\/' : a); }), | ||
| var p = filename ? path.join(path.dirname(filename), '/') : directory; | ||
| // For debug tests in subdirectories (comments/, mediaquery/, all/), | ||
| // the import/ directory and main linenumbers.less file are at the parent debug/ level, not in the subdirectory | ||
| var isDebugSubdirectory = false; | ||
| var debugParentPath = null; | ||
| if (directory) { | ||
| // Normalize directory path separators for matching | ||
| var normalizedDir = directory.replace(/\\/g, '/'); | ||
| // Check if we're in a debug subdirectory | ||
| if (normalizedDir.includes('/debug/') && (normalizedDir.includes('/comments/') || normalizedDir.includes('/mediaquery/') || normalizedDir.includes('/all/'))) { | ||
| isDebugSubdirectory = true; | ||
| // Extract the debug/ directory path (parent of the subdirectory) | ||
| // Match everything up to and including /debug/ (works with both absolute and relative paths) | ||
| var debugMatch = normalizedDir.match(/(.+\/debug)\//); | ||
| if (debugMatch) { | ||
| debugParentPath = debugMatch[1]; | ||
| } | ||
| } | ||
| } | ||
| if (isDebugSubdirectory && debugParentPath) { | ||
| // For {path} placeholder, use the parent debug/ directory | ||
| // Convert back to native path format | ||
| p = debugParentPath.replace(/\//g, path.sep) + path.sep; | ||
| } | ||
| var pathimport; | ||
| if (isDebugSubdirectory && debugParentPath) { | ||
| pathimport = path.join(debugParentPath.replace(/\//g, path.sep), 'import') + path.sep; | ||
| } else { | ||
| pathimport = path.join(directory + 'import/'); | ||
| } | ||
| var pathesc = p.replace(/[.:/\\]/g, function(a) { return '\\' + (a == '\\' ? '\/' : a); }), | ||
| pathimportesc = pathimport.replace(/[.:/\\]/g, function(a) { return '\\' + (a == '\\' ? '\/' : a); }); | ||
@@ -350,3 +554,14 @@ | ||
| function runTestSet(options, foldername, verifyFunction, nameModifier, doReplacements, getFilename) { | ||
| options = options ? clone(options) : {}; | ||
| // Handle case where first parameter is glob patterns (no options object) | ||
| if (Array.isArray(options)) { | ||
| // First parameter is glob patterns, no options object | ||
| foldername = options; | ||
| options = {}; | ||
| } else if (typeof options === 'string') { | ||
| // First parameter is foldername (no options object) | ||
| foldername = options; | ||
| options = {}; | ||
| } else { | ||
| options = options ? clone(options) : {}; | ||
| } | ||
| runTestSetInternal(lessFolder, options, foldername, verifyFunction, nameModifier, doReplacements, getFilename); | ||
@@ -368,16 +583,96 @@ } | ||
| function getBasename(file) { | ||
| return foldername + path.basename(file, '.less'); | ||
| // Handle glob patterns with exclusions | ||
| if (Array.isArray(foldername)) { | ||
| var patterns = foldername; | ||
| var includePatterns = []; | ||
| var excludePatterns = []; | ||
| patterns.forEach(function(pattern) { | ||
| if (pattern.startsWith('!')) { | ||
| excludePatterns.push(pattern.substring(1)); | ||
| } else { | ||
| includePatterns.push(pattern); | ||
| } | ||
| }); | ||
| // Use glob to find all matching files, excluding the excluded patterns | ||
| var allFiles = []; | ||
| includePatterns.forEach(function(pattern) { | ||
| var files = glob.sync(pattern, { | ||
| cwd: baseFolder, | ||
| absolute: true, | ||
| ignore: excludePatterns | ||
| }); | ||
| allFiles = allFiles.concat(files); | ||
| }); | ||
| // Note: needle mocking is set up globally in index.js | ||
| // Process each .less file found | ||
| allFiles.forEach(function(filePath) { | ||
| if (/\.less$/.test(filePath)) { | ||
| var file = path.basename(filePath); | ||
| // For glob patterns, we need to construct the relative path differently | ||
| // The filePath is absolute, so we need to get the path relative to the test-data directory | ||
| var relativePath = path.relative(baseFolder, path.dirname(filePath)) + '/'; | ||
| // Only process files that have corresponding .css files (these are the actual tests) | ||
| var cssPath = path.join(path.dirname(filePath), path.basename(file, '.less') + '.css'); | ||
| if (fs.existsSync(cssPath)) { | ||
| // Process this file using the existing logic | ||
| processFileWithInfo({ | ||
| file: file, | ||
| fullPath: filePath, | ||
| relativePath: relativePath | ||
| }); | ||
| } | ||
| } | ||
| }); | ||
| return; | ||
| } | ||
| fs.readdirSync(path.join(baseFolder, foldername)).forEach(function (file) { | ||
| if (!/\.less$/.test(file)) { return; } | ||
| function processFileWithInfo(fileInfo) { | ||
| var file = fileInfo.file; | ||
| var fullPath = fileInfo.fullPath; | ||
| var relativePath = fileInfo.relativePath; | ||
| // Load config for this specific file using cosmiconfig | ||
| var configResult = cosmiconfigSync('styles').search(path.dirname(fullPath)); | ||
| // Deep clone the original options to prevent Less from modifying shared objects | ||
| var options = JSON.parse(JSON.stringify(originalOptions || {})); | ||
| if (configResult && configResult.config && configResult.config.language && configResult.config.language.less) { | ||
| // Deep clone and merge the language.less settings with the original options | ||
| var lessConfig = JSON.parse(JSON.stringify(configResult.config.language.less)); | ||
| Object.keys(lessConfig).forEach(function(key) { | ||
| options[key] = lessConfig[key]; | ||
| }); | ||
| } | ||
| // Merge any lessOptions from the testMap (for dynamic options like getVars functions) | ||
| if (originalOptions && originalOptions.lessOptions) { | ||
| Object.keys(originalOptions.lessOptions).forEach(function(key) { | ||
| var value = originalOptions.lessOptions[key]; | ||
| if (typeof value === 'function') { | ||
| // For functions, call them with the file path | ||
| var result = value(fullPath); | ||
| options[key] = result; | ||
| } else { | ||
| // For static values, use them directly | ||
| options[key] = value; | ||
| } | ||
| }); | ||
| } | ||
| var options = clone(originalOptions); | ||
| // Don't pass stylize to less.render as it's not a valid option | ||
| options.stylize = stylize; | ||
| var name = getBasename(file, relativePath); | ||
| var name = getBasename(file); | ||
| if (oneTestOnly && name !== oneTestOnly) { | ||
| if (oneTestOnly && typeof oneTestOnly === 'string' && !name.includes(oneTestOnly)) { | ||
| return; | ||
@@ -389,12 +684,16 @@ } | ||
| if (options.sourceMap && !options.sourceMap.sourceMapFileInline) { | ||
| options.sourceMap = { | ||
| sourceMapOutputFilename: name + '.css', | ||
| sourceMapBasepath: baseFolder, | ||
| sourceMapRootpath: 'testweb/', | ||
| disableSourcemapAnnotation: options.sourceMap.disableSourcemapAnnotation | ||
| }; | ||
| // This options is normally set by the bin/lessc script. Setting it causes the sourceMappingURL comment to be appended to the CSS | ||
| // output. The value is designed to allow the sourceMapBasepath option to be tested, as it should be removed by less before | ||
| // setting the sourceMappingURL value, leaving just the sourceMapOutputFilename and .map extension. | ||
| options.sourceMap.sourceMapFilename = options.sourceMap.sourceMapBasepath + '/' + options.sourceMap.sourceMapOutputFilename + '.map'; | ||
| // Set test infrastructure defaults only if not already set by styles.config.cjs | ||
| // Less.js core (parse-tree.js) will handle normalization of: | ||
| // - sourceMapBasepath (defaults to input file's directory) | ||
| // - sourceMapInputFilename (defaults to options.filename) | ||
| // - sourceMapFilename (derived from sourceMapOutputFilename or input filename) | ||
| // - sourceMapOutputFilename (derived from input filename if not set) | ||
| if (!options.sourceMap.sourceMapOutputFilename) { | ||
| // Needed for sourcemap file name in JSON output | ||
| options.sourceMap.sourceMapOutputFilename = name + '.css'; | ||
| } | ||
| if (!options.sourceMap.sourceMapRootpath) { | ||
| // Test-specific default for consistent test output paths | ||
| options.sourceMap.sourceMapRootpath = 'testweb/'; | ||
| } | ||
| } | ||
@@ -404,3 +703,3 @@ | ||
| try { | ||
| return JSON.parse(fs.readFileSync(getFilename(getBasename(file), 'vars', baseFolder), 'utf8')); | ||
| return JSON.parse(fs.readFileSync(getFilename(getBasename(file, relativePath), 'vars', baseFolder), 'utf8')); | ||
| } | ||
@@ -414,3 +713,3 @@ catch (e) { | ||
| queue(function() { | ||
| toCSS(options, path.join(baseFolder, foldername + file), function (err, result) { | ||
| toCSS(options, fullPath, function (err, result) { | ||
@@ -431,3 +730,3 @@ if (doubleCallCheck) { | ||
| var verificationResult = verifyFunction( | ||
| name, err, result && result.css, doReplacements, result && result.map, baseFolder, result && result.imports | ||
| name, err, result && result.css, doReplacements, result && result.map, baseFolder, result && result.imports, getFilename | ||
| ); | ||
@@ -455,14 +754,65 @@ release(); | ||
| fs.readFile(path.join(testFolder, 'css', css_name) + '.css', 'utf8', function (e, css) { | ||
| process.stdout.write('- ' + path.join(baseFolder, css_name) + ': '); | ||
| // Check if we're using the new co-located structure (tests-unit/ or tests-config/) or the old separated structure | ||
| var cssPath; | ||
| if (relativePath.startsWith('tests-unit/') || relativePath.startsWith('tests-config/')) { | ||
| // New co-located structure: CSS file is in the same directory as LESS file | ||
| cssPath = path.join(path.dirname(fullPath), path.basename(file, '.less') + '.css'); | ||
| } else { | ||
| // Old separated structure: CSS file is in separate css/ folder | ||
| // Windows compatibility: css_name may already contain path separators | ||
| // Use path.join with empty string to let path.join handle normalization | ||
| cssPath = path.join(testFolder, css_name) + '.css'; | ||
| } | ||
| css = css && doReplacements(css, path.join(baseFolder, foldername)); | ||
| if (result.css === css) { ok('OK'); } | ||
| else { | ||
| difference('FAIL', css, result.css); | ||
| // For the new structure, we need to handle replacements differently | ||
| var replacementPath; | ||
| if (relativePath.startsWith('tests-unit/') || relativePath.startsWith('tests-config/')) { | ||
| replacementPath = path.dirname(fullPath); | ||
| // Ensure replacementPath ends with a path separator for consistent matching | ||
| if (!replacementPath.endsWith(path.sep)) { | ||
| replacementPath += path.sep; | ||
| } | ||
| release(); | ||
| }); | ||
| } else { | ||
| replacementPath = path.join(baseFolder, relativePath); | ||
| } | ||
| var testName = fullPath.replace(/\.less$/, ''); | ||
| process.stdout.write('- ' + testName + ': '); | ||
| var css = fs.readFileSync(cssPath, 'utf8'); | ||
| css = css && doReplacements(css, replacementPath); | ||
| if (result.css === css) { ok('OK'); } | ||
| else { | ||
| difference('FAIL', css, result.css); | ||
| } | ||
| release(); | ||
| }); | ||
| }); | ||
| } | ||
| function getBasename(file, relativePath) { | ||
| var basePath = relativePath || foldername; | ||
| // Ensure basePath ends with a slash for proper path construction | ||
| if (basePath.charAt(basePath.length - 1) !== '/') { | ||
| basePath = basePath + '/'; | ||
| } | ||
| return basePath + path.basename(file, '.less'); | ||
| } | ||
| // This function is only called for non-glob patterns now | ||
| // For glob patterns, we use the glob library in the calling code | ||
| var dirPath = path.join(baseFolder, foldername); | ||
| var items = fs.readdirSync(dirPath); | ||
| items.forEach(function(item) { | ||
| if (/\.less$/.test(item)) { | ||
| processFileWithInfo({ | ||
| file: item, | ||
| fullPath: path.join(dirPath, item), | ||
| relativePath: foldername | ||
| }); | ||
| } | ||
| }); | ||
@@ -472,11 +822,19 @@ } | ||
| function diff(left, right) { | ||
| require('diff').diffLines(left, right).forEach(function(item) { | ||
| if (item.added || item.removed) { | ||
| var text = item.value && item.value.replace('\n', String.fromCharCode(182) + '\n').replace('\ufeff', '[[BOM]]'); | ||
| process.stdout.write(stylize(text, item.added ? 'green' : 'red')); | ||
| } else { | ||
| process.stdout.write(item.value && item.value.replace('\ufeff', '[[BOM]]')); | ||
| } | ||
| // Configure chalk to always show colors | ||
| var chalk = require('chalk'); | ||
| chalk.level = 3; // Force colors on | ||
| // Use jest-diff for much clearer output like Vitest | ||
| var diffResult = require('jest-diff').diffStringsUnified(left || '', right || '', { | ||
| expand: false, | ||
| includeChangeCounts: true, | ||
| contextLines: 1, | ||
| aColor: chalk.red, | ||
| bColor: chalk.green, | ||
| changeColor: chalk.inverse, | ||
| commonColor: chalk.dim | ||
| }); | ||
| process.stdout.write('\n'); | ||
| // jest-diff returns a string with ANSI colors, so we can output it directly | ||
| process.stdout.write(diffResult + '\n'); | ||
| } | ||
@@ -494,2 +852,5 @@ | ||
| // Only show the diff, not the full text | ||
| process.stdout.write(stylize('Diff:', 'yellow') + '\n'); | ||
| diff(left || '', right || ''); | ||
@@ -547,23 +908,37 @@ endTest(); | ||
| function toCSS(options, filePath, callback) { | ||
| options = options || {}; | ||
| // Deep clone options to prevent modifying the original, but preserve functions | ||
| var originalOptions = options || {}; | ||
| options = JSON.parse(JSON.stringify(originalOptions)); | ||
| // Restore functions that were lost in JSON serialization | ||
| if (originalOptions.getVars) { | ||
| options.getVars = originalOptions.getVars; | ||
| } | ||
| var str = fs.readFileSync(filePath, 'utf8'), addPath = path.dirname(filePath); | ||
| // Initialize paths array if it doesn't exist | ||
| if (typeof options.paths !== 'string') { | ||
| options.paths = options.paths || []; | ||
| if (!contains(options.paths, addPath)) { | ||
| options.paths.push(addPath); | ||
| } | ||
| } else { | ||
| options.paths = [options.paths] | ||
| options.paths = [options.paths]; | ||
| } | ||
| // Add the current directory to paths if not already present | ||
| if (!contains(options.paths, addPath)) { | ||
| options.paths.push(addPath); | ||
| } | ||
| // Resolve all paths relative to the test file's directory | ||
| options.paths = options.paths.map(searchPath => { | ||
| return path.resolve(lessFolder, searchPath) | ||
| if (path.isAbsolute(searchPath)) { | ||
| return searchPath; | ||
| } | ||
| // Resolve relative to the test file's directory | ||
| return path.resolve(path.dirname(filePath), searchPath); | ||
| }) | ||
| options.filename = path.resolve(process.cwd(), filePath); | ||
| options.optimization = options.optimization || 0; | ||
| if (options.globalVars) { | ||
| options.globalVars = options.getVars(filePath); | ||
| } else if (options.modifyVars) { | ||
| options.modifyVars = options.getVars(filePath); | ||
| } | ||
| // Note: globalVars and modifyVars are now handled via styles.config.cjs or lessOptions | ||
| if (options.plugin) { | ||
@@ -591,18 +966,3 @@ var Plugin = require(path.resolve(process.cwd(), options.plugin)); | ||
| function testImportRedirect(nockScope) { | ||
| return (name, err, css, doReplacements, sourcemap, baseFolder) => { | ||
| process.stdout.write('- ' + path.join(baseFolder, name) + ': '); | ||
| if (err) { | ||
| fail('FAIL: ' + (err && err.message)); | ||
| return; | ||
| } | ||
| const expected = 'h1 {\n color: red;\n}\n'; | ||
| if (css !== expected) { | ||
| difference('FAIL', expected, css); | ||
| return; | ||
| } | ||
| nockScope.done(); | ||
| ok('OK'); | ||
| }; | ||
| } | ||
| // HTTP redirect testing is now handled directly in test/index.js | ||
@@ -636,3 +996,2 @@ function testDisablePluginRule() { | ||
| testImports: testImports, | ||
| testImportRedirect: testImportRedirect, | ||
| testEmptySourcemap: testEmptySourcemap, | ||
@@ -639,0 +998,0 @@ testNoOptions: testNoOptions, |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| // Split the input into chunks. | ||
| function default_1(input, fail) { | ||
| var len = input.length; | ||
| var level = 0; | ||
| var parenLevel = 0; | ||
| var lastOpening; | ||
| var lastOpeningParen; | ||
| var lastMultiComment; | ||
| var lastMultiCommentEndBrace; | ||
| var chunks = []; | ||
| var emitFrom = 0; | ||
| var chunkerCurrentIndex; | ||
| var currentChunkStartIndex; | ||
| var cc; | ||
| var cc2; | ||
| var matched; | ||
| function emitChunk(force) { | ||
| var len = chunkerCurrentIndex - emitFrom; | ||
| if (((len < 512) && !force) || !len) { | ||
| return; | ||
| } | ||
| chunks.push(input.slice(emitFrom, chunkerCurrentIndex + 1)); | ||
| emitFrom = chunkerCurrentIndex + 1; | ||
| } | ||
| for (chunkerCurrentIndex = 0; chunkerCurrentIndex < len; chunkerCurrentIndex++) { | ||
| cc = input.charCodeAt(chunkerCurrentIndex); | ||
| if (((cc >= 97) && (cc <= 122)) || (cc < 34)) { | ||
| // a-z or whitespace | ||
| continue; | ||
| } | ||
| switch (cc) { | ||
| case 40: // ( | ||
| parenLevel++; | ||
| lastOpeningParen = chunkerCurrentIndex; | ||
| continue; | ||
| case 41: // ) | ||
| if (--parenLevel < 0) { | ||
| return fail('missing opening `(`', chunkerCurrentIndex); | ||
| } | ||
| continue; | ||
| case 59: // ; | ||
| if (!parenLevel) { | ||
| emitChunk(); | ||
| } | ||
| continue; | ||
| case 123: // { | ||
| level++; | ||
| lastOpening = chunkerCurrentIndex; | ||
| continue; | ||
| case 125: // } | ||
| if (--level < 0) { | ||
| return fail('missing opening `{`', chunkerCurrentIndex); | ||
| } | ||
| if (!level && !parenLevel) { | ||
| emitChunk(); | ||
| } | ||
| continue; | ||
| case 92: // \ | ||
| if (chunkerCurrentIndex < len - 1) { | ||
| chunkerCurrentIndex++; | ||
| continue; | ||
| } | ||
| return fail('unescaped `\\`', chunkerCurrentIndex); | ||
| case 34: | ||
| case 39: | ||
| case 96: // ", ' and ` | ||
| matched = 0; | ||
| currentChunkStartIndex = chunkerCurrentIndex; | ||
| for (chunkerCurrentIndex = chunkerCurrentIndex + 1; chunkerCurrentIndex < len; chunkerCurrentIndex++) { | ||
| cc2 = input.charCodeAt(chunkerCurrentIndex); | ||
| if (cc2 > 96) { | ||
| continue; | ||
| } | ||
| if (cc2 == cc) { | ||
| matched = 1; | ||
| break; | ||
| } | ||
| if (cc2 == 92) { // \ | ||
| if (chunkerCurrentIndex == len - 1) { | ||
| return fail('unescaped `\\`', chunkerCurrentIndex); | ||
| } | ||
| chunkerCurrentIndex++; | ||
| } | ||
| } | ||
| if (matched) { | ||
| continue; | ||
| } | ||
| return fail("unmatched `".concat(String.fromCharCode(cc), "`"), currentChunkStartIndex); | ||
| case 47: // /, check for comment | ||
| if (parenLevel || (chunkerCurrentIndex == len - 1)) { | ||
| continue; | ||
| } | ||
| cc2 = input.charCodeAt(chunkerCurrentIndex + 1); | ||
| if (cc2 == 47) { | ||
| // //, find lnfeed | ||
| for (chunkerCurrentIndex = chunkerCurrentIndex + 2; chunkerCurrentIndex < len; chunkerCurrentIndex++) { | ||
| cc2 = input.charCodeAt(chunkerCurrentIndex); | ||
| if ((cc2 <= 13) && ((cc2 == 10) || (cc2 == 13))) { | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| else if (cc2 == 42) { | ||
| // /*, find */ | ||
| lastMultiComment = currentChunkStartIndex = chunkerCurrentIndex; | ||
| for (chunkerCurrentIndex = chunkerCurrentIndex + 2; chunkerCurrentIndex < len - 1; chunkerCurrentIndex++) { | ||
| cc2 = input.charCodeAt(chunkerCurrentIndex); | ||
| if (cc2 == 125) { | ||
| lastMultiCommentEndBrace = chunkerCurrentIndex; | ||
| } | ||
| if (cc2 != 42) { | ||
| continue; | ||
| } | ||
| if (input.charCodeAt(chunkerCurrentIndex + 1) == 47) { | ||
| break; | ||
| } | ||
| } | ||
| if (chunkerCurrentIndex == len - 1) { | ||
| return fail('missing closing `*/`', currentChunkStartIndex); | ||
| } | ||
| chunkerCurrentIndex++; | ||
| } | ||
| continue; | ||
| case 42: // *, check for unmatched */ | ||
| if ((chunkerCurrentIndex < len - 1) && (input.charCodeAt(chunkerCurrentIndex + 1) == 47)) { | ||
| return fail('unmatched `/*`', chunkerCurrentIndex); | ||
| } | ||
| continue; | ||
| } | ||
| } | ||
| if (level !== 0) { | ||
| if ((lastMultiComment > lastOpening) && (lastMultiCommentEndBrace > lastMultiComment)) { | ||
| return fail('missing closing `}` or `*/`', lastOpening); | ||
| } | ||
| else { | ||
| return fail('missing closing `}`', lastOpening); | ||
| } | ||
| } | ||
| else if (parenLevel !== 0) { | ||
| return fail('missing closing `)`', lastOpeningParen); | ||
| } | ||
| emitChunk(true); | ||
| return chunks; | ||
| } | ||
| exports.default = default_1; | ||
| //# sourceMappingURL=chunker.js.map |
| {"version":3,"file":"chunker.js","sourceRoot":"","sources":["../../../src/less/parser/chunker.js"],"names":[],"mappings":";;AAAA,+BAA+B;AAC/B,mBAAyB,KAAK,EAAE,IAAI;IAChC,IAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC;IACzB,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAI,WAAW,CAAC;IAChB,IAAI,gBAAgB,CAAC;IACrB,IAAI,gBAAgB,CAAC;IACrB,IAAI,wBAAwB,CAAC;IAC7B,IAAM,MAAM,GAAG,EAAE,CAAC;IAClB,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,mBAAmB,CAAC;IACxB,IAAI,sBAAsB,CAAC;IAC3B,IAAI,EAAE,CAAC;IACP,IAAI,GAAG,CAAC;IACR,IAAI,OAAO,CAAC;IAEZ,SAAS,SAAS,CAAC,KAAK;QACpB,IAAM,GAAG,GAAG,mBAAmB,GAAG,QAAQ,CAAC;QAC3C,IAAI,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE;YACjC,OAAO;SACV;QACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,mBAAmB,GAAG,CAAC,CAAC,CAAC,CAAC;QAC5D,QAAQ,GAAG,mBAAmB,GAAG,CAAC,CAAC;IACvC,CAAC;IAED,KAAK,mBAAmB,GAAG,CAAC,EAAE,mBAAmB,GAAG,GAAG,EAAE,mBAAmB,EAAE,EAAE;QAC5E,EAAE,GAAG,KAAK,CAAC,UAAU,CAAC,mBAAmB,CAAC,CAAC;QAC3C,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE;YAC1C,oBAAoB;YACpB,SAAS;SACZ;QAED,QAAQ,EAAE,EAAE;YACR,KAAK,EAAE,EAAyB,IAAI;gBAChC,UAAU,EAAE,CAAC;gBACb,gBAAgB,GAAG,mBAAmB,CAAC;gBACvC,SAAS;YACb,KAAK,EAAE,EAAyB,IAAI;gBAChC,IAAI,EAAE,UAAU,GAAG,CAAC,EAAE;oBAClB,OAAO,IAAI,CAAC,qBAAqB,EAAE,mBAAmB,CAAC,CAAC;iBAC3D;gBACD,SAAS;YACb,KAAK,EAAE,EAAyB,IAAI;gBAChC,IAAI,CAAC,UAAU,EAAE;oBAAE,SAAS,EAAE,CAAC;iBAAE;gBACjC,SAAS;YACb,KAAK,GAAG,EAAwB,IAAI;gBAChC,KAAK,EAAE,CAAC;gBACR,WAAW,GAAG,mBAAmB,CAAC;gBAClC,SAAS;YACb,KAAK,GAAG,EAAwB,IAAI;gBAChC,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE;oBACb,OAAO,IAAI,CAAC,qBAAqB,EAAE,mBAAmB,CAAC,CAAC;iBAC3D;gBACD,IAAI,CAAC,KAAK,IAAI,CAAC,UAAU,EAAE;oBAAE,SAAS,EAAE,CAAC;iBAAE;gBAC3C,SAAS;YACb,KAAK,EAAE,EAAyB,IAAI;gBAChC,IAAI,mBAAmB,GAAG,GAAG,GAAG,CAAC,EAAE;oBAAE,mBAAmB,EAAE,CAAC;oBAAC,SAAS;iBAAE;gBACvE,OAAO,IAAI,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,CAAC;YACvD,KAAK,EAAE,CAAC;YACR,KAAK,EAAE,CAAC;YACR,KAAK,EAAE,EAAyB,aAAa;gBACzC,OAAO,GAAG,CAAC,CAAC;gBACZ,sBAAsB,GAAG,mBAAmB,CAAC;gBAC7C,KAAK,mBAAmB,GAAG,mBAAmB,GAAG,CAAC,EAAE,mBAAmB,GAAG,GAAG,EAAE,mBAAmB,EAAE,EAAE;oBAClG,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC,mBAAmB,CAAC,CAAC;oBAC5C,IAAI,GAAG,GAAG,EAAE,EAAE;wBAAE,SAAS;qBAAE;oBAC3B,IAAI,GAAG,IAAI,EAAE,EAAE;wBAAE,OAAO,GAAG,CAAC,CAAC;wBAAC,MAAM;qBAAE;oBACtC,IAAI,GAAG,IAAI,EAAE,EAAE,EAAS,IAAI;wBACxB,IAAI,mBAAmB,IAAI,GAAG,GAAG,CAAC,EAAE;4BAChC,OAAO,IAAI,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,CAAC;yBACtD;wBACD,mBAAmB,EAAE,CAAC;qBACzB;iBACJ;gBACD,IAAI,OAAO,EAAE;oBAAE,SAAS;iBAAE;gBAC1B,OAAO,IAAI,CAAC,qBAAe,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,MAAI,EAAE,sBAAsB,CAAC,CAAC;YACpF,KAAK,EAAE,EAAyB,uBAAuB;gBACnD,IAAI,UAAU,IAAI,CAAC,mBAAmB,IAAI,GAAG,GAAG,CAAC,CAAC,EAAE;oBAAE,SAAS;iBAAE;gBACjE,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC,mBAAmB,GAAG,CAAC,CAAC,CAAC;gBAChD,IAAI,GAAG,IAAI,EAAE,EAAE;oBACX,kBAAkB;oBAClB,KAAK,mBAAmB,GAAG,mBAAmB,GAAG,CAAC,EAAE,mBAAmB,GAAG,GAAG,EAAE,mBAAmB,EAAE,EAAE;wBAClG,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC,mBAAmB,CAAC,CAAC;wBAC5C,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,EAAE;4BAAE,MAAM;yBAAE;qBAC9D;iBACJ;qBAAM,IAAI,GAAG,IAAI,EAAE,EAAE;oBAClB,cAAc;oBACd,gBAAgB,GAAG,sBAAsB,GAAG,mBAAmB,CAAC;oBAChE,KAAK,mBAAmB,GAAG,mBAAmB,GAAG,CAAC,EAAE,mBAAmB,GAAG,GAAG,GAAG,CAAC,EAAE,mBAAmB,EAAE,EAAE;wBACtG,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC,mBAAmB,CAAC,CAAC;wBAC5C,IAAI,GAAG,IAAI,GAAG,EAAE;4BAAE,wBAAwB,GAAG,mBAAmB,CAAC;yBAAE;wBACnE,IAAI,GAAG,IAAI,EAAE,EAAE;4BAAE,SAAS;yBAAE;wBAC5B,IAAI,KAAK,CAAC,UAAU,CAAC,mBAAmB,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;4BAAE,MAAM;yBAAE;qBAClE;oBACD,IAAI,mBAAmB,IAAI,GAAG,GAAG,CAAC,EAAE;wBAChC,OAAO,IAAI,CAAC,sBAAsB,EAAE,sBAAsB,CAAC,CAAC;qBAC/D;oBACD,mBAAmB,EAAE,CAAC;iBACzB;gBACD,SAAS;YACb,KAAK,EAAE,EAAwB,4BAA4B;gBACvD,IAAI,CAAC,mBAAmB,GAAG,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,mBAAmB,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE;oBACtF,OAAO,IAAI,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,CAAC;iBACtD;gBACD,SAAS;SAChB;KACJ;IAED,IAAI,KAAK,KAAK,CAAC,EAAE;QACb,IAAI,CAAC,gBAAgB,GAAG,WAAW,CAAC,IAAI,CAAC,wBAAwB,GAAG,gBAAgB,CAAC,EAAE;YACnF,OAAO,IAAI,CAAC,6BAA6B,EAAE,WAAW,CAAC,CAAC;SAC3D;aAAM;YACH,OAAO,IAAI,CAAC,qBAAqB,EAAE,WAAW,CAAC,CAAC;SACnD;KACJ;SAAM,IAAI,UAAU,KAAK,CAAC,EAAE;QACzB,OAAO,IAAI,CAAC,qBAAqB,EAAE,gBAAgB,CAAC,CAAC;KACxD;IAED,SAAS,CAAC,IAAI,CAAC,CAAC;IAChB,OAAO,MAAM,CAAC;AAClB,CAAC;AAxHD,4BAwHC","sourcesContent":["// Split the input into chunks.\nexport default function (input, fail) {\n const len = input.length;\n let level = 0;\n let parenLevel = 0;\n let lastOpening;\n let lastOpeningParen;\n let lastMultiComment;\n let lastMultiCommentEndBrace;\n const chunks = [];\n let emitFrom = 0;\n let chunkerCurrentIndex;\n let currentChunkStartIndex;\n let cc;\n let cc2;\n let matched;\n\n function emitChunk(force) {\n const len = chunkerCurrentIndex - emitFrom;\n if (((len < 512) && !force) || !len) {\n return;\n }\n chunks.push(input.slice(emitFrom, chunkerCurrentIndex + 1));\n emitFrom = chunkerCurrentIndex + 1;\n }\n\n for (chunkerCurrentIndex = 0; chunkerCurrentIndex < len; chunkerCurrentIndex++) {\n cc = input.charCodeAt(chunkerCurrentIndex);\n if (((cc >= 97) && (cc <= 122)) || (cc < 34)) {\n // a-z or whitespace\n continue;\n }\n\n switch (cc) {\n case 40: // (\n parenLevel++;\n lastOpeningParen = chunkerCurrentIndex;\n continue;\n case 41: // )\n if (--parenLevel < 0) {\n return fail('missing opening `(`', chunkerCurrentIndex);\n }\n continue;\n case 59: // ;\n if (!parenLevel) { emitChunk(); }\n continue;\n case 123: // {\n level++;\n lastOpening = chunkerCurrentIndex;\n continue;\n case 125: // }\n if (--level < 0) {\n return fail('missing opening `{`', chunkerCurrentIndex);\n }\n if (!level && !parenLevel) { emitChunk(); }\n continue;\n case 92: // \\\n if (chunkerCurrentIndex < len - 1) { chunkerCurrentIndex++; continue; }\n return fail('unescaped `\\\\`', chunkerCurrentIndex);\n case 34:\n case 39:\n case 96: // \", ' and `\n matched = 0;\n currentChunkStartIndex = chunkerCurrentIndex;\n for (chunkerCurrentIndex = chunkerCurrentIndex + 1; chunkerCurrentIndex < len; chunkerCurrentIndex++) {\n cc2 = input.charCodeAt(chunkerCurrentIndex);\n if (cc2 > 96) { continue; }\n if (cc2 == cc) { matched = 1; break; }\n if (cc2 == 92) { // \\\n if (chunkerCurrentIndex == len - 1) {\n return fail('unescaped `\\\\`', chunkerCurrentIndex);\n }\n chunkerCurrentIndex++;\n }\n }\n if (matched) { continue; }\n return fail(`unmatched \\`${String.fromCharCode(cc)}\\``, currentChunkStartIndex);\n case 47: // /, check for comment\n if (parenLevel || (chunkerCurrentIndex == len - 1)) { continue; }\n cc2 = input.charCodeAt(chunkerCurrentIndex + 1);\n if (cc2 == 47) {\n // //, find lnfeed\n for (chunkerCurrentIndex = chunkerCurrentIndex + 2; chunkerCurrentIndex < len; chunkerCurrentIndex++) {\n cc2 = input.charCodeAt(chunkerCurrentIndex);\n if ((cc2 <= 13) && ((cc2 == 10) || (cc2 == 13))) { break; }\n }\n } else if (cc2 == 42) {\n // /*, find */\n lastMultiComment = currentChunkStartIndex = chunkerCurrentIndex;\n for (chunkerCurrentIndex = chunkerCurrentIndex + 2; chunkerCurrentIndex < len - 1; chunkerCurrentIndex++) {\n cc2 = input.charCodeAt(chunkerCurrentIndex);\n if (cc2 == 125) { lastMultiCommentEndBrace = chunkerCurrentIndex; }\n if (cc2 != 42) { continue; }\n if (input.charCodeAt(chunkerCurrentIndex + 1) == 47) { break; }\n }\n if (chunkerCurrentIndex == len - 1) {\n return fail('missing closing `*/`', currentChunkStartIndex);\n }\n chunkerCurrentIndex++;\n }\n continue;\n case 42: // *, check for unmatched */\n if ((chunkerCurrentIndex < len - 1) && (input.charCodeAt(chunkerCurrentIndex + 1) == 47)) {\n return fail('unmatched `/*`', chunkerCurrentIndex);\n }\n continue;\n }\n }\n\n if (level !== 0) {\n if ((lastMultiComment > lastOpening) && (lastMultiCommentEndBrace > lastMultiComment)) {\n return fail('missing closing `}` or `*/`', lastOpening);\n } else {\n return fail('missing closing `}`', lastOpening);\n }\n } else if (parenLevel !== 0) {\n return fail('missing closing `)`', lastOpeningParen);\n }\n\n emitChunk(true);\n return chunks;\n}\n"]} |
| var less = { | ||
| logLevel: 4, | ||
| errorReporting: 'console', | ||
| math: 'always', | ||
| strictUnits: false | ||
| }; |
| describe('less.js legacy tests', function() { | ||
| testLessEqualsInDocument(); | ||
| }); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Install scripts
Supply chain riskInstall scripts are run when the package is installed or built. Malicious packages often use scripts that run automatically to execute payloads or fetch additional code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 2 instances in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 12 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 2 instances in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 7 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
3015613
1.29%336
1.51%28565
2.57%50
6.38%1
Infinity%182
10.3%264
1.15%