Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/less/V5-STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ emits nested CSS) instead of always flattening. Opt into flattened output with
| `math` modes | ✅ | ✅ | `always` / `parens-division` (default) / `parens`. |
| `unitMode` (formerly `strictUnits`) | ✅ | ✅ | `loose` / `preserve` (default) / `strict`. |
| `compress` | ✅ | ✅ | Minified, but not byte-identical to Less 4 `-x` (nesting preserved by default). |
| Source maps (`sourceMap`) | ✅ | ✅ | Returns `result.map`; annotation + `outputSourceFiles` supported. Path-variant parity is ⏳. |
| Source maps (`sourceMap`) | ✅ | ✅ | Returns `result.map`; annotation, inline data URI, `outputSourceFiles`, and the `rootpath`/`basepath`/`url` path variants all supported. |
| URL rewriting (`rewriteUrls` / `rootpath` / `urlArgs`) | ✅ | ✅ | Rewrites `url(...)` references. |
| `globalVars` / `modifyVars` injection | ✅ | ❌ | Not supported — these throw rather than silently no-op. |
| `javascriptEnabled` | ✅ | ❌ | JavaScript evaluation is not supported. |
Expand All @@ -61,7 +61,7 @@ emits nested CSS) instead of always flattening. Opt into flattened output with
| `@use` / `@compose` modules | ➖ | ⏳ | Member access (namespaced functions/mixins) designed, not yet wired. |
| Browser build (`window.less`) | ✅ | ✅ | `dist/less-browser-dev.js` ships and powers the playground; full 4.x browser-API parity is ⏳. |
| `lessc` CLI (compile) | ✅ | ✅ | Compiles files. |
| `lessc` CLI **flags** for the newer options | ✅ | ⏳ | `--compress`, `--source-map`, `--rewrite-urls` / `--rootpath` / `--url-args`, `--math` are not wired yet — use the `less.render()` API for these until the CLI catches up. |
| `lessc` CLI **flags** for the newer options | ✅ | ✅ | `--compress`/`-x`, `--source-map[=file]` (+ `--source-map-inline` / `-include-source` / `-rootpath` / `-basepath` / `-url`), `--rewrite-urls` / `--rootpath` / `--url-args`, `--math` are all wired. |
| Diagnostics (`file:line:column` + excerpt) | ➖ | ✅ | Precise diagnostics, not raw parser offsets. |

---
Expand Down
125 changes: 112 additions & 13 deletions packages/less/bin/lessc
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,15 @@ let output = null;
let silent = false;
let quiet = false;
let verbose = false;
let sourceMapEnabled = false;
let sourceMapExplicitFile = null;
const sourceMapOptions = {};

const unsupportedOptions = new Map([
['--source-map', 'source maps are not supported'],
['--source-map-map-inline', 'source maps are not supported'],
['--source-map-include-source', 'source maps are not supported'],
['--source-map-rootpath', 'source maps are not supported'],
['--source-map-basepath', 'source maps are not supported'],
['--source-map-url', 'source maps are not supported'],
['--plugin', 'legacy lessc plugin flags are not supported'],
['--depends', 'dependency-only output is not supported'],
['--lint', 'lint-only mode is not supported'],
['--compress', 'compressed output is not supported'],
['-x', 'compressed output is not supported'],
['--clean-css', 'clean-css compression is not supported'],
['--rewrite-urls', 'URL rewriting is not supported'],
['--rootpath', 'URL rootpath rewriting is not supported'],
['--url-args', 'URL argument rewriting is not supported'],
['--global-var', 'global variable injection is not supported'],
['--modify-var', 'modify-var injection is not supported'],
['--js', 'JavaScript evaluation is not supported'],
Expand Down Expand Up @@ -136,8 +128,74 @@ function parseArgs() {
verbose = true;
continue;
}
if (arg === '--collapse-nesting') {
options.collapseNesting = true;
const collapseNestingMatch = arg.match(/^--collapse-nesting(?:=(.+))?$/);
if (collapseNestingMatch) {
options.collapseNesting = collapseNestingMatch[1] ?? true;
continue;
}
if (arg === '--compress' || arg === '-x') {
options.compress = true;
continue;
}
const mathMatch = arg.match(/^--math=(.+)$/);
if (mathMatch) {
options.math = mathMatch[1];
continue;
}
// URL rewriting (forwarded to the Less plugin). `--rewrite-urls` with no
// value is Less 4.x's `all`.
const rewriteUrlsMatch = arg.match(/^--rewrite-urls(?:=(.+))?$/);
if (rewriteUrlsMatch) {
options.rewriteUrls = rewriteUrlsMatch[1] ?? 'all';
continue;
}
const rootpathMatch = arg.match(/^--rootpath=(.+)$/);
if (rootpathMatch) {
options.rootpath = rootpathMatch[1];
continue;
}
const urlArgsMatch = arg.match(/^--url-args=(.+)$/);
if (urlArgsMatch) {
options.urlArgs = urlArgsMatch[1];
continue;
}
// Source maps. The `--source-map-*` sub-flags are matched before the bare
// `--source-map[=file]` (their `$`-anchored patterns don't overlap, but the
// ordering documents intent). Each sub-flag also enables source maps.
if (arg === '--source-map-inline' || arg === '--source-map-map-inline') {
sourceMapEnabled = true;
sourceMapOptions.sourceMapFileInline = true;
continue;
}
if (arg === '--source-map-include-source') {
sourceMapEnabled = true;
sourceMapOptions.outputSourceFiles = true;
continue;
}
const smRootpathMatch = arg.match(/^--source-map-rootpath=(.+)$/);
if (smRootpathMatch) {
sourceMapEnabled = true;
sourceMapOptions.sourceMapRootpath = smRootpathMatch[1];
continue;
}
const smBasepathMatch = arg.match(/^--source-map-basepath=(.+)$/);
if (smBasepathMatch) {
sourceMapEnabled = true;
sourceMapOptions.sourceMapBasepath = smBasepathMatch[1];
continue;
}
const smUrlMatch = arg.match(/^--source-map-url=(.+)$/);
if (smUrlMatch) {
sourceMapEnabled = true;
sourceMapOptions.sourceMapURL = smUrlMatch[1];
continue;
}
const sourceMapMatch = arg.match(/^--source-map(?:=(.+))?$/);
if (sourceMapMatch) {
sourceMapEnabled = true;
if (sourceMapMatch[1]) {
sourceMapExplicitFile = sourceMapMatch[1];
}
continue;
}
// Deprecated 4.x spellings (--strict-units, --strict-units=on|off, -su=on|off)
Expand Down Expand Up @@ -190,6 +248,39 @@ function parseArgs() {
}
}

// Assemble the `sourceMap` render option from the CLI source-map flags and
// return the sidecar `.map` path to write (null when inlined or unset). The
// engine writes the `sourceMappingURL` annotation into the CSS and returns
// `result.map`; the CLI owns writing that map to disk, mirroring Less 4.x lessc.
function resolveSourceMap() {
if (!sourceMapEnabled) {
return null;
}
let mapFilePath = null;
if (sourceMapOptions.sourceMapFileInline !== true) {
if (sourceMapExplicitFile) {
mapFilePath = path.resolve(process.cwd(), sourceMapExplicitFile);
} else if (output) {
mapFilePath = `${output}.map`;
} else {
// Writing CSS to stdout leaves nowhere for a sidecar; inline the map.
sourceMapOptions.sourceMapFileInline = true;
}
}
if (mapFilePath) {
// Annotation URL defaults to the map's basename (unless --source-map-url set
// it); the map's `file` field to the CSS output name.
if (sourceMapOptions.sourceMapURL === undefined) {
sourceMapOptions.sourceMapFilename = path.basename(mapFilePath);
Comment thread
matthew-dean marked this conversation as resolved.
Outdated
}
if (output) {
sourceMapOptions.sourceMapOutputFilename = path.basename(output);
}
}
options.sourceMap = sourceMapOptions;
return mapFilePath;
}

async function run() {
parseArgs();

Expand All @@ -199,6 +290,8 @@ async function run() {
printUsage();
}

const mapFilePath = resolveSourceMap();

less.logger.addListener({
info(msg) {
if (verbose) console.log(msg);
Expand Down Expand Up @@ -237,6 +330,12 @@ async function run() {
if (!silent) outputJessDiagnostics([], result.warnings || []);
process.stdout.write(result.css);
}

if (mapFilePath && result.map) {
fs.mkdirSync(path.dirname(mapFilePath), { recursive: true });
fs.writeFileSync(mapFilePath, result.map, 'utf8');
if (!silent) console.log(`lessc: wrote ${mapFilePath}`);
}
} catch (err) {
if (!silent) {
if (!outputJessError(err)) {
Expand Down
16 changes: 13 additions & 3 deletions packages/less/lib/lessc-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,23 @@ const lesscHelper = {
console.log(' --quiet Suppresses output of warnings.');
console.log(' -v, --version Prints version number and exit.');
console.log(' --verbose Be verbose.');
console.log(' --collapse-nesting Flatten nested rules after preserving source-order cascade.');
console.log(' --collapse-nesting[=MODE] Flatten nested rules: native (default), or compact.');
console.log(' -x, --compress Compress output by removing whitespace.');
console.log(' --math=MODE Math mode: parens-division (default), always, or parens.');
console.log(' --unit-mode=MODE Unit handling in math: preserve (default), strict, or loose (Less 4.x guessing).');
console.log(' --strict-units[=on|off] Deprecated: on is --unit-mode=strict, off is the default (preserve).');
console.log(' --source-map[=FILE] Emit a source map (FILE, or <destination>.map by default).');
console.log(' --source-map-inline Embed the source map as a data URI instead of a file.');
console.log(' --source-map-include-source Embed the source files in the map (sourcesContent).');
console.log(' --source-map-rootpath=PATH Prepend PATH to every source in the map.');
console.log(' --source-map-basepath=PATH Strip PATH from the front of every source in the map.');
console.log(' --source-map-url=URL Override the sourceMappingURL annotation.');
console.log(' --rewrite-urls[=all|local|off] Rewrite url(...) references in imported files.');
console.log(' --rootpath=PATH Prepend PATH to url(...) and import references.');
console.log(' --url-args=ARGS Append ARGS (e.g. cache-buster) to every url(...).');
console.log('');
console.log('This release intentionally supports a smaller CLI surface.');
console.log('Source maps, browser compilation, legacy plugin flags, lint-only mode, and');
console.log('URL rewrite flags will be revisited in later alphas.');
console.log('Browser compilation, legacy plugin flags, and lint-only mode are not supported.');
console.log('');
console.log('Report bugs to: http://github.com/less/less.js/issues');
console.log('Home page: <http://lesscss.org/>');
Expand Down
77 changes: 74 additions & 3 deletions packages/less/test/lessc-alpha.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,14 @@ try {
'lessc help documents the supported alpha nesting flag');
assert.match(help.stdout, /This release intentionally supports a smaller CLI surface/,
'lessc help explicitly scopes the supported CLI surface');
assert.doesNotMatch(help.stdout, /--source-map/,
'lessc help must not advertise unsupported source-map flags in alpha.1');
assert.match(help.stdout, /--source-map\[=FILE\]/,
'lessc help documents the supported source-map flag');
assert.match(help.stdout, /--compress/,
'lessc help documents the supported compress flag');
assert.doesNotMatch(help.stdout, /--plugin=/,
'lessc help must not advertise unsupported plugin flags in alpha.1');

for (const flag of ['--source-map', '--plugin=less-plugin-clean-css', '--bogus']) {
for (const flag of ['--clean-css', '--plugin=less-plugin-clean-css', '--bogus']) {
const unsupported = await runLessc([flag, '-'], '.unsupported { color: red; }\n');
assert.equal(unsupported.code, 1, `${flag} must fail instead of silently no-oping`);
assert.equal(unsupported.stdout, '', `${flag} must not emit CSS after rejecting the option`);
Expand Down Expand Up @@ -279,6 +281,75 @@ try {
assert.equal(await readFile(nestedOutput, 'utf8'), collapsedCss,
'file-mode lessc preserves declaration source order while collapsing nesting');

// --compress / --math / --url-args / --rootpath forward to the render options
// the API already supports (see alpha-support.mjs for the API-level coverage).
const urlInput = path.join(tempDir, 'urls.less');
await writeFile(urlInput, '.a { color: red; background: url(img.png); }\n');

const compressed = await runLessc(['--compress', urlInput]);
assert.equal(compressed.code, 0, compressed.stderr);
assert.doesNotMatch(compressed.stdout, /\n\s*\n/, '--compress strips blank lines');
assert.match(compressed.stdout, /color:red/, '--compress minifies declarations');

const mathAlways = await runLessc(['--math=always', '-'], '.a { width: 2 + 3 * 4; }\n');
assert.equal(mathAlways.code, 0, mathAlways.stderr);
assert.match(mathAlways.stdout, /width:\s*14/, '--math=always evaluates unparenthesized math');

const urlArgs = await runLessc(['--url-args=v=9', urlInput]);
assert.equal(urlArgs.code, 0, urlArgs.stderr);
assert.match(urlArgs.stdout, /url\(img\.png\?v=9\)/, '--url-args appends the query');

const rootpath = await runLessc(['--rootpath=/cdn/', urlInput]);
assert.equal(rootpath.code, 0, rootpath.stderr);
assert.match(rootpath.stdout, /url\(\/cdn\/img\.png\)/, '--rootpath prepends to url() references');

// Source maps: `--source-map` writes a sidecar <output>.map and annotates the
// CSS with its basename; `--source-map-inline` embeds a data URI instead.
const smOutput = path.join(tempDir, 'sm.css');
const sm = await runLessc(['--source-map', input, smOutput]);
assert.equal(sm.code, 0, sm.stderr);
assert.match(sm.stdout, /lessc: wrote .+sm\.css\.map\n/, '--source-map reports the sidecar map it wrote');
const smCss = await readFile(smOutput, 'utf8');
assert.match(smCss, /\/\*# sourceMappingURL=sm\.css\.map \*\/\n$/,
'--source-map annotates the CSS with the sidecar map basename');
const smMap = JSON.parse(await readFile(`${smOutput}.map`, 'utf8'));
assert.equal(smMap.version, 3, 'the sidecar map is source-map v3');
assert.equal(smMap.file, 'sm.css', 'map.file is the CSS output name');
assert.ok(smMap.sources.some(s => s.endsWith('input.less')), 'the map carries the input source');

const inlineOut = path.join(tempDir, 'inline.css');
const inlineSm = await runLessc(['--source-map-inline', input, inlineOut]);
assert.equal(inlineSm.code, 0, inlineSm.stderr);
assert.match(await readFile(inlineOut, 'utf8'), /sourceMappingURL=data:application\/json;base64,/,
'--source-map-inline embeds the map as a data URI');
await assert.rejects(readFile(`${inlineOut}.map`, 'utf8'),
'--source-map-inline writes no sidecar .map file');

// Path variants: basepath strips, rootpath prepends, include-source embeds
// content, --source-map-url overrides the annotation. basepath must match the
// real (symlink-resolved) tempDir the compiler records for the source.
const realTempDir = await realpath(tempDir);
const variantMapPath = path.join(tempDir, 'variant.map');
const variantOut = path.join(tempDir, 'variant.css');
const variant = await runLessc([
`--source-map=${variantMapPath}`,
`--source-map-basepath=${realTempDir}`,
'--source-map-rootpath=http://cdn/',
'--source-map-include-source',
'--source-map-url=/assets/variant.map',
input, variantOut
]);
assert.equal(variant.code, 0, variant.stderr);
assert.match(await readFile(variantOut, 'utf8'), /sourceMappingURL=\/assets\/variant\.map \*\//,
'--source-map-url overrides the annotation URL');
const variantMap = JSON.parse(await readFile(variantMapPath, 'utf8'));
assert.ok(variantMap.sources.length > 0 && variantMap.sources.every(s => s.startsWith('http://cdn/')),
'--source-map-rootpath prepends every source');
assert.ok(variantMap.sources.every(s => !s.includes(realTempDir)),
'--source-map-basepath strips the base from every source');
assert.ok(Array.isArray(variantMap.sourcesContent) && variantMap.sourcesContent.length > 0,
'--source-map-include-source embeds sourcesContent');

const file = await runLessc([input, output]);
assert.equal(file.code, 0, file.stderr);
assert.match(file.stdout, /^lessc: wrote .+output\.css\n$/);
Expand Down
Loading