Less.js: Local Variable Scoping in Import Files

Created on 10 Feb 2015  路  13Comments  路  Source: less/less.js

Thanks for the project, and taking time to read this request. I enjoy LESS immensely.

I'm running into a critical issue with variable scoping that's preventing me from using @import to include multiple components in the same project.

Test Case

The actual implementation is a bit different, but it can be boiled down to a simple test case.

Consider a master file for outputting a concatenated release of components that imports several different components.

/* project.less */
@import "button.less";
@import "header.less";

And two components

/* button.less */
@component: 'button';
.debug {
  component: @component;
}
/* header.less */
@component: 'header';
.debug {
  component: @component;
}

The expected output is

.debug {
  component: 'button';
}
.debug {
  component: 'header';
}

But the actual output is

.debug {
  component: 'header';
}
.debug {
  component: 'header';
}

I've added a test case in zip format here

Rationale

I'm working on an open source component framework. Each component may include variable names that may be used in other components, but should be scoped to each component so that they do not conflict.

Variable overlap is inevitable with distributed component design since individual authors cannot be aware of the variables used in other components.

Currently the only way I can make my library work is to sandbox each component by compiling them separately, and informing developers that they cannot directly import components from my library in their less files.

For more details on the specific implementation that I'm running into trouble with you can see this overview, or peruse how a component loads a theme

Most helpful comment

@import (scoped) "package";

we already have that:

& {@import "package";}

It's actually even better to move the & {} part into the "package" file itself if the isolated scope is its initial requirement.

P.S. Aside from above, the problem of @import (scoped) "package"; is the same as I pointed somewere around there - we should not throw a responsibility of providing a required library environment at the library user, it should be in hands of the library author (thus if we need something like this it should be inside "package" not out of it... and while (reference) has its reasonable excuse for being the way it is (simply because it's primarily designed to be used with external non-modifiable CSS libraries), (scoped) would have no).

All 13 comments

LESS variables behave like CSS properties: the latest definition in a scope "wins".

You can address this in your example by surrounding imports in a mixin scope.

.import-header() {
    @import "header";
}
.import-button() {
    @import "button";
}
.import-header();
.import-button();

LESS variables behave like CSS properties: the latest definition in a scope "wins".

Yes, that should be the case, but then why are two files imported separately into one "master" file in the same scope? How could one file possibly know of the other's existence?

Shouldnt there be some way to specify in an @import statement to specify that the file should be treated as a separate scope?

Consider that any third party code distributed in LESS has to work in a host environment without knowing what strange and unknown variables could be defined there. How could packages distributed in LESS ever work under a single global scope?

Less is not unusual in this way. For example, an imported JavaScript file runs the same whether included as an external file or included inline.

How could packages distributed in LESS ever work under a single global scope?

Variables can be name-spaced, or the library itself can be wrapped in a mixin. So, there are ways to solve it, and it does behave as expected. However, it sounds like you're proposing a feature like:

@import (scoped) "package";

... which would wrap the import in a local scope, sort of like Node modules. Something like that?

Less is not unusual in this way. For example, an imported JavaScript file runs the same whether included as an external file or included inline.

Agreed that scoping is similar, but what is different is that all variable assignments in LESS, not just declarations are hoisted to the top of scope. This means that variables assigned in other files _after_ importing a file adjust their values in previous files.

If you reassign the value of a variable in javascript it is expected that change will only affect code included _afterward_. Removing this linearity makes it difficult to implement inheritance, since whatever a value is defined to last becomes what its defined as everywhere in the file.

My proposal would be to add scoped imports, or to only hoist variable declarations and not assignments, or simple remove variable hoisting altogether.

Since LESS does not separate var foo = baz; declarations from foo = baz; assignments the latter may be more difficult. It's worth noting however, ECMAScript 6 replaces var declarations with let that does not hoist.

If you reassign the value of a variable in javascript it is expected that change will only affect code included afterward. Removing this linearity makes it difficult to implement inheritance, since whatever a value is defined to last in a file becomes what its defined as everywhere in the file.

Yes, JavaScript works that way, so you're talking about two different things.

  1. Scope: Imported files are in the same scope as the calling file. (JavaScript was my example, but technically all of CSS shares a "global" scope as well.)
  2. Inheritance: Later assignments override previous assignments. (CSS was my example.)

If you're trying to make a case that Less should behave differently, that can't happen, as there are a lot of features that rely on the inheritance model. And the fact that it behaves differently than a scripting language is no accident. Like CSS, Less is not a scripting language. All declarations are final, you might say. They don't "run" in order and have different values at different points. Variables in each scope end up with a final value for that scope. So, they aren't "hoisted", because hoisting is a concept of imperative languages. So asking if Less can "hoist" some things and not others is illogical, because that's not what's happening.

If you're trying to figure out how you can implement something that isolates scope, I've given some options.

If you want to propose a feature, that's fine, but it sounds like you're talking mostly about how you expect Less to behave. I would recommend looking at the docs at http://lesscss.org/features/#variables-feature

I do think scoping imports might be useful, but I'd want some other people to weigh in on it. Opening for any further discussion on:

@import (scoped) "package";

Thanks. I hope you'll consider this.

@import (scoped) "package";

we already have that:

& {@import "package";}

It's actually even better to move the & {} part into the "package" file itself if the isolated scope is its initial requirement.

P.S. Aside from above, the problem of @import (scoped) "package"; is the same as I pointed somewere around there - we should not throw a responsibility of providing a required library environment at the library user, it should be in hands of the library author (thus if we need something like this it should be inside "package" not out of it... and while (reference) has its reasonable excuse for being the way it is (simply because it's primarily designed to be used with external non-modifiable CSS libraries), (scoped) would have no).

Lazy-evaluation (and declarative nature of almost anything in general) is out-of-consideration since this is simply what (among a few other things of course) makes Less to be Less.

@jlukic

http://learnsemantic.com/themes/overview.html

I'm afraid this component loading scheme is designed solely with Sass in mind (or with just whatever imperative C-like variable evaluation in general) - basically it's the same pitfall as in https://github.com/less/less.js/issues/1706#issuecomment-72377331.
So (with all respect) I really really would suggest you to get yourself familiar with Less basics (and variable lazy-loading and last-definition-wins concepts are indeed the very fundamentals of the language) _before_ designing such library mechanisms:


So while "scoped" import like above will probably do the trick for you in this particular "component" case you'll certainly face more troubles if you continue to ignore these fundamental Less differences.

To illustrate how such "component loading mechanismus" might look like if it would take modern Less into consideration, here're few examples:


[1]. This is how "Semantic-UI" component looks like by now (simplified code with all of its import trickery expanded):

// some auxiliary stuff defined in button.less itself
@component: 'button';

// variables (and related stuff) that supposed to be customizable 
// imported via something like "../@{theme}/@{component}.less"

@color: red;

// actual CSS styles:
.button {
    color: @color;
}

This approach have the advantage of using human-readable variable names (e.g. @color), on the other hand the disadvantage obviously is that this way you can't share the same namespace for all components so you have to do something to isolate them from each other but what is more important is that as soon you do this you kill the possibility to override things within your project files the way it is supposed to be in Less, e.g.:

@import "button.less";
@color: blue;

can't work for more than one component.
(For those who are lazy to look at "Semantic-UI" sources, they work it around by providing additional empty "../@{theme}/@{component}.overrides" file imported with that "../@{theme}/@{component}.less" file and this is where you just must place any custom values you need... (and btw., this is the kludge where #2145 came from)).


[2] No, this is not the solution example yet. This is an example of how a more Less friendly library (e.g. "Bootstrap") handles this kind of stuff currently (to show the big picture before introducing the third example):

// variables (and related stuff) usually defined in some shared "variables.less"

@button-color: red;

// actual CSS styles:
.button {
   color: @button-color;
}

this approach has exactly opposite pros and cons, you may override things whereever you want but those customizible variables have to have unique names (e.g. @nav-tabs-justified-active-link-border-color).


[3] The solution is obviously in namespaces... Actually while "namespaced variables" still have some room for improvement (#1848) this particular use-case does not require anything new, the following approach is available since Less 1.4.x:

// variables (and related stuff) defined whereever suitable:
.button-stuff { // any appropriate name (also see in comments below)
   @color: green;
}

// actual CSS styles:

& { 

// using .button-stuff namespace here
.button-stuff();

.button {
    color: @color;
}

} // end-of &

and to be used like:

@import "button.less";

.button-stuff {
    @color: yellow;
}

Exact code may vary as always. For instance since .button-stuff is a mixin you can provide some extra flexibility for theme customization stuff via mixin arguments (via pattern-matching/guards) and still (optionally) combine all that with "theme importing hooks" (like those "../@{theme}/@{component}.less" above) if necessary. For example the "customization namespace" mixins may have any of the following forms:
.theme-name-button()
.theme-name(button)
.theme(theme-name, button)
.button(theme-name)
.theme-name.button()
etc. etc.
(that is: a single customization mixin may match multiple themes and/or components and vice-versa)
More specific and inspiring approaches can be found in #1848.

& {@import "package";}

Ah, I wasn't aware of that trick. You know all the tricks, @seven-phases-max. :-)

Also, yes this:

we should not throw a responsibility of providing a required library environment at the library user, it should be in hands of the library author

Closing again as having multiple methods to achieve the same effect, both in a library and by wrapping an import. Thanks, for weighing in, Max.

Just wanted to say thanks @seven-phases-max for spending your time to thoroughly explain scoping and possible implementations which was above and beyond.

I've added the mentioned scoping fix to each definition file and it works like a charm. The library will finally have a standard less import file in the next release.

It's worth noting that my original intent when adding precompiled css was to write code that could be maintained in LESS and ported by script to SCSS, which is something which bootstrap does to be platform neutral. I've however run into issues with SCSS not supporting some features that I took as the lowest common denominator for pre-processors, like permitting variables names inside @import statements which is used for theme includes.

So it looks like LESS comes out ahead for me in many regards (including support) :+1: Kudos!

Any updates on this problem?

Was this page helpful?
0 / 5 - 0 ratings