Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3.0 Roadmap #10

Open
matthew-dean opened this issue Jan 13, 2016 · 14 comments
Open

3.0 Roadmap #10

matthew-dean opened this issue Jan 13, 2016 · 14 comments

Comments

@matthew-dean
Copy link
Member

This is intended as just a starting point for discussion (and not an official plan).
#3.0 Roadmap Proposal

Some possible items to address for 3.0. (Copied from WIP document - https://github.com/less/less-meta/blob/master/proposal/3.0.md)

Many of these are breaking changes or relate to breaking changes, hence targeting it for a full point release.

Less API

For the last item above, the Less API and plugin architecture suffers from having too large a scope. Right now everything everything is customizable in the same way that you can customize any part of Linux. That is, the fact that you can in theory doesn't mean an average person can in actuality.

Even though Less JavaScript plugins have been around much longer than PostCSS's, with a theoretical "greater capability", PostCSS's plugins growth has been rapid, I'd say for a number of reasons, including:

  • The API is extremely limited. There are a total of 4 main node types. (Less has 28 node types, by the looks of things.) And the methods for those nodes are simple and consistent. (Many of Less's methods on nodes are not even directly callable without having knowledge of contexts and frames, making them largely unusable.)
  • PostCSS's API is well-documented. (Less's API is largely undocumented.)
  • PostCSS has "best practices" and guidelines for plugin development.
  • PostCSS has under its org "starter repos" for plugins, which include tests (and instructions for setting up those tests) that the plugin must pass.

Less.js doesn't need to be like PostCSS (or at the extreme suggestion of some users, ditch any plugin features and just adopt PostCSS), but PostCSS is an example of successful plugin architecture, so it's worth looking at.

Keeping things simple

I think we should:

  1. Look at what parts of Less have been extended by plugins, and consider limiting the API around those.
  2. Fix node methods to be evaluatable without passing in environment / context / frames whatever. (e.g. Ruleset.toCSS() or Ruleset.eval() working without parameters)
  3. Document said API methods, which should be easier to accomplish with a limited set.

I also think that the @plugin syntax could be more successful than the "installable" plugins, because it's easier to use (and more limited, which is actually better), so it would make sense to make that more of the focus of the "plugin" feature.

Thoughts on this? Any other large targets for a major release?

UPDATE: More ideas on the Less API: #15

@seven-phases-max
Copy link
Member

seven-phases-max commented Jan 13, 2016

I also think that the @plugin syntax could be more successful than the "installable" plugins, because it's easier to use (and more limited, which is actually better), so it would make sense to make that more of the focus of the "plugin" feature.

This. I'm actually very interested to bring more consistency there (so that @plugin "some"; and lessc --less-plugin-some could do the same (where possible of course because of limited scope of the former)). Though I cannot say I am to implement this or say it will be easy (so far both are implemented in too orthogonal ways)).


The other problem (just to tone down "PostCSS API excitement") is a different nature of Less and PostCSS plugins in general. For example taking AST processing only, notice the major differences between PostCSS tree and Less tree. PostCSS tree represents only parsed tokens (i.e. = raw strings), while Less tree represents already evaluated (or-ready-to-be-evaluated) "objects" of certain type. E.g. Less Color is a Color object, PostCSS "Color" is just a string that plugin has to convert to an object of interest on its own (for example https://github.com/btholt/postcss-colorblind/blob/master/src/color-transformer.js#L22) - and same for every language entity (for instance yet more PostCSS humiliating example: "Less extract (and Sass nth) vs. PostCSS nth" - not even counting that the latter code has serious limits with nested comma/space lists).

That's why it's 28 nodes vs. 4.

Both methods have their pros and cons, but it's important to realize that the API is strongly dictated by the tree structure, thus we can't simplify the API w/o simplifying (maybe with major refactoring) the tree itself (maybe even with some minor language changes). Though in many cases it's not really a problem of the API itself, but a problem of the documentation and then more aggressive (hopefully not using the same dirty manipulative tricks as PostCSS authors do) advertising (and those are coupled to human resources available).

@matthew-dean
Copy link
Member Author

This. I'm actually very interested to bring more consistency there (so that @plugin "some"; and lessc --less-plugin-some could do the same

YES. In fact, I would like it if we could even deprecate less-plugin-something in favor of @plugin because
a) it's just so frakking easy for the end-user,
b) you can assign a plugin to a specific scope,
c) it greatly simplifies a library having plugin dependencies.

I'm of the mind that if something doesn't make sense with @plugin, then maybe that would be part of what would be deprecated. But, the greatest reason to do this is that having 2 completely distinct methods of "plugins" does not help the current plugin confusion that Less has.

@rjgotten
Copy link

Aw shucks. All the thumbs up for @plugin make we feel all fuzzy and warm inside. ^_^

Realistically, there are a few things fully blown tree-visitor command-line plugins can do that @plugin in its current inception will never be able to do though. Compiler system-level additions, such as adding on additional file managers or new types of tree-visitors that pre- or post-process the parse tree.

However, that's not necessarily a bad thing. It's actually a good idea to keep those type of constructs that act on a global level separate from plugins that only add additional functions on a local level.

With a clear separation of what-does-what, you run substantially less risk that a third-party framework will force heavy-duty system-level additions on a user just to facilitate one or two additional functions.


Ofcourse; it would be nice if there were a way to extend @plugin to atleast allow more than just custom functions. But perhaps something could be done to allow post-visit of a scoped part of the parse-tree only and adding additional behavior to it: the addition of custom semantics for code that is already syntax compatible.

One quite clean mechanism would be additional semantics added to custom at-directives. The following @root directive is a simple example that would hoist nested selectors all the way back to the root level.

#ns {
  @plugin "at-root";

  .mixin() {
    .descendant-selector { ... }

    @root {
      .top-level-selector { ... }
    }
  }
}

.selector { 
  #ns.mixin()
}
.selector .descendant-selector { ... }
.top-level-selector { ... } 

Infact, this could also be usable to solve another problem: the creation of compound variables.
It can be used to form maps or dictionaries.

In the following example @map is rewritten to generate a variable node with a new value-type Map node. This new node type is populated by taking the existing ruleset in the at-directive block and converting all its properties into key-value pairs.

In turn a map variable can be manipulated via a keys() function that the plugin adds and an overridden extract() function, which augments the built-in to not only handle numerical keys for lists but also handle alphanumerical keys for maps.

#fonticons {
  @plugin "at-map";

  .base ( @font-family ) { ... }
  .icons( @icons ) {
    @keys : keys( @icons );

    ._( length( @keys ));
    ._( @index ) when ( @index != 0 ) { ._(@index - 1) }
    ._( @index ) when ( @index != 0 ) {
      @key : extract( @keys, @index );
      @code : extract( @icons, @key );

      &[data-icon='@{key}']::before { content : "\@{code}"; }
    }
  }
}


.icon {
  @plugin "at-map";

  @map icons {
    chevron-up    : e800;
    chevron-down  : e801;
    chevron-left  : e802;
    chevron-right : e803;
  }

  #fonticons.base( "my-icons" );
  #fonticons.icons( @icons );
}

It's these kind of limited code transformations, that don't pollute the global environment, which can add just that little bit more usability and readability to Less that you sometimes end up looking for.

@matthew-dean
Copy link
Member Author

If there's one thing I would plead for, it would be for an elimination of any reason to do mixin loop hacks like:

 ._( length( @keys ));
    ._( @index ) when ( @index != 0 ) { ._(@index - 1) }
    ._( @index ) when ( @index != 0 ) { ... }

This is my least favorite type of Less example, and I personally wish we wouldn't use that pattern, except for the fact that there's nothing currently to replace it.

(This is further discussed in the @each discussion that I can't find right now.)

So, back on topic to this:

However, that's not necessarily a bad thing. It's actually a good idea to keep those type of constructs that act on a global level separate from plugins that only add additional functions on a local level.

That's actually a good guideline, although I would change "additional functions" to "additional features", which you mentioned just after. The use cases for each should be separated. If you want to add syntax features, that would be plugin type A (@plugin), and if you want to fundamentally change the environment for Less.js, that would be plugin type B (command line), and the API signature for each should be a bit different.

Of course; it would be nice if there were a way to extend @plugin to at least allow more than just custom functions.

Yes, this. Most of the use cases that have popped into my head for @plugin are not for functions, and actually, have been very very similar to your example! That is, nested variable sets, or maps, or namespaced vars, however that's framed, but the ability to logically group vars and either replace a single item from a group, or a group (or sub-group).

(I feel like this has been a desire from the very beginning of Less, from the time when people started using Less to calculate background offsets for use with image sprites. We're getting closer, though. See: less/less.js#2767 (comment))

But, yeah, I've wanted to add "root-level" functions, which is impossible with current function syntax. I like your @map example, though IIRC, @seven-phases-max has pointed out in the past that a map like that is basically just a detached ruleset, as in:

  @icons: {
    chevron-up    : e800;
    chevron-down  : e801;
    chevron-left  : e802;
    chevron-right : e803;
  };

Which I think we should perhaps write as:

  @ruleset icons {
    chevron-up    : e800;
    chevron-down  : e801;
    chevron-left  : e802;
    chevron-right : e803;
  }
  @icons();  

Since we can already construct a block like that, the main thing missing is reference syntax. The current proposal would be something like: @icons$chevron-up to reference an individual member. (See: property selectors: less/less.js#2654).

The other thing missing would be the keys() and values() functions, along with a proper each method.

@matthew-dean
Copy link
Member Author

@rjgotten I do like the idea of plugins being able to add arbitrary @-rules. For instance, a plugin author could create something like:

@plugin "strong-typing-checker.js";
@color my-color #FF0000;
@color other-color 20px;   // Throw type mismatch error

@rjgotten
Copy link

a proper each method.

Like this?

.mixin(@list) {
  @plugin "at-foreach";
  @foreach ( @value, @index ) of ( @list ) {
    > :nth-child(@{index})::before { content : "@{value}" }
  }
}

.selector {
  .mixin(a b c);
}
.selector > :nth-child(1)::before { content : "a" }
.selector > :nth-child(2)::before { content : "b" }
.selector > :nth-child(3)::before { content : "c" }

It's basically the same scoped re-writing idea.

I was already half-debating whether I should've added it into my previous example. Guess I should have, right? ;-)

@matthew-dean
Copy link
Member Author

@rjgotten I remember us a little all over the map as far as syntax, but yep, that's the gist. Good point that a plugin could add that as an @-rule. I even think it could exist in Less core, but could be "switched on" as a plugin. That would allow us to document Less features as logical (and optional) sections.

@rjgotten
Copy link

Heh...
Something that just popped into my brain: if both map and foreach plugin would be aware of one another, you could even do something like

#fonticons {
  @plugin "at-foreach";
  @plugin "at-map";

  .base ( @font-family ) { ... }
  .icons( @icons ) {
    @foreach ( @code, @key ) of ( @icons ) {
      &[data-icon='@{key}']::before { content : "\@{code}"; }
    }
  }
}

and make the key->value extraction-process completely transparant.


Regarding extending plugin to cover semantics for at-directives: wasn't there an open issue that discussed how unknown at-directives should be handled?

If the initial parse logic can atleast deliver a sane stream of lexed tokens, then handling an unknown at-directive becomes a matter of looking up the name of the directive in a scoped registry (like the current function registry) to handle further parsing of those lexed tokens and handle any code-rewriting it wants to do. In the event that no matching name is found, the at-directive node will just emit its lexed tokens back as text with variables replaced by their expanded values.

If the plumbing is done right, this feature of 'plugin directives' should be ridiculously easy to add in.

@matthew-dean
Copy link
Member Author

if both map and foreach plugin would be aware of one another

Which is why it should probably be core. Otherwise you end up with a scenario like PostCSS, where plugins have to be declared in a certain order and, in some cases, the same plugin has to be declared again somewhere else in the stack. We could have "optional core plugins", but if we do, they should import any other optional dependencies they might need. If you only consider Node / NPM, it would seem trivial, but we need to keep in mind that Less also runs in-browser, so you don't want or can't have a large dependency tree.

@matthew-dean
Copy link
Member Author

In other words, Less plugins, ideally, shouldn't really have any other Less plugin dependencies. I would imagine it being flatter like jQuery plugins.

If you have a bunch of post-processing steps for Less-to-CSS builds, then a build tool like PostCSS would probably be fine, in conjunction with a single postcss-less plugin.

@rjgotten
Copy link

In other words, Less plugins, ideally, shouldn't really have any other Less plugin dependencies.

I was specifically avoiding the word 'dependency' because in this case, it wouldn't need a hard dependency. ;-)

A hypothetical at-foreach plugin could examine the node type (which is available as a string) of its of argument and if that equals "Map", then it first looks up the existence of a keys method in its function registry. (Existence of keys would indicate that extract was also patched to support maps.)

Only if it finds that these additional preconditions are satisfied, it can create a functioning code rewriting. If not, it throws an error that states its argument is not iterable.

@matthew-dean
Copy link
Member Author

Hmm... I think you could do it more generically. That is, it returns something like an "Iterable". And any node type that implements iterable has to have specific methods including keys, values and some kind of iterator function. That way, it doesn't directly have to know about "Map".

All that being said, I don't know that there's a big enough demand to create several types of iterable collections. Like I said, rulesets are already collections of key/value pairs, so I don't know how much we need yet another ruleset-like structure that's again only superficially different from the ruleset-like structures that already exist.

Buuuuut... to be a devil's advocate against myself, a stable and simple API doesn't have to worry about such things. If someone wants a specialized collection, they could create one, just like they could create any other type of @-rule.

@matthew-dean
Copy link
Member Author

Should we discuss sometime on https://gitter.im/less/less.js ?

@matthew-dean
Copy link
Member Author

@rjgotten I've implemented custom-atrule functions in a pull request. It's good to go. See: less/less.js#2852

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants