less / less.js

Less. The dynamic stylesheet language.
http://lesscss.org
Apache License 2.0
17.02k stars 3.41k forks source link

How to replace a Declaration node By an array of Declaration and a Ruleset when using visitor to modify AST ? #3601

Closed arvinxx closed 2 years ago

arvinxx commented 3 years ago

There is a much issue problem about this issue -> #3600 .

TL;DR

I want to make a less plugin to make less variable dynamic with css variable, since less variable can't use in browser runtime. But I meet some difficulties when deal with less AST.

There are:

Target

less is a comilping-runtime language,so less variables don't exist on browser runtime. My idea is to change less variables to css variable when comilping with right order(So it can be compatible with less function). Then we can use css variable in browser to have a dynamic result.

a simple example is below:

// dynamtic-variable.config.js
// configuration to decide what variables should be dynamic
module.exports = {
  variable: ['base-number', 'multiply-number'],
};

input:

// multiplyTwo is a function to multiply number by 2, just for example
@base-number: 10;
@multiply-number: multiplyTwo(@base-number);

[scope='local'] {
  @base-number: 2;
}

.use {
  base: @base-number;
  multiply: @multiply-number;
}

outputs:

:root {
  --base-number: 10;
  --multiply-number: 20;
}

:root[scope='local'] {
  --base-number: 2;
  --multiply-number: 4;
}

.use {
  color: var(--base-number);
  multiply: var(--multiply-number);
}

With this idea, I try to work on the less-plugin-dynamic-variable to implement it.

Steps to achieve this example

Add target css variable

after configuartion, I want to add the target css variable with less variable.So when in comile

// from 
@base-number: 10;

// middle
@base-number: 10;
:root {
  --base-number: @base-number;
}

// final to css
:root {
  --base-number: 10;
}

variables with less funtion:

// from 
@multiply-number: multiplyTwo(@base-number);

// middle
@multiply-number: multiplyTwo(@base-number);
:root {
  --multiply-number: @multiply-number;
}

// final to css
:root {
  --multiply-number: 20;
}

I have referred to less-plugin-custom-properties about Declaration node, but I failed. Because I don't know the right way to replace a node by array. My code is below:

const declaration = new this.tree.Declaration(
  node.name.replace(/^@/, '--'),
  node.value,
);

  return [
    node,
    new Ruleset(
      [new Selector([new Element(new Combinator(' '), ':root')], [])],
      [declaration],
    ),
  ];

And it seems there are little information about less AST structure. (Really struggle with it 😞)

handle less variable in usage

It seems much easier below:

// from 
  base: @base-number;

// to
  color: var(--base-number);

I need to know which properties to replace, but visitVariable don't has context about it. So I just fail again

How can I deal with it?

matthew-dean commented 3 years ago

I want to make a less plugin to make less variable dynamic with css variable, since less variable can't use in browser runtime.

Side note: being able to have variables be dynamic at runtime is one reason I'm building Jess.

In terms of visitors and the Less AST, IIRC, returning an array should be possible, but the safest thing to do when visiting a node is returning a node. Note that you need to have isReplacing set to true if your visitor is replacing nodes (and then all visited rules need to return the node it entered with or a new node).

Another trick you can use instead of returning an array is returning a Ruleset with the selector as a single & element. That should collapse? As long as your visitor is a pre-eval visitor? On evaluation (and later rule visiting), arrays and rulesets should be merged.

Does that help? Did you have both isPreEvalVisitor and isReplacing set to true?

arvinxx commented 3 years ago

Did you have both isPreEvalVisitor and isReplacing set to true?

Yeah ,I have set both isPreEvalVisitor and isReplacing to true.

The question is I don't know the struct syntax of a Node. For example

returning a Ruleset with the selector as a single & element

I'm just wondering the right way to do this. Is the below a right structure?

new Ruleset(
  [new Selector(new Element('&'))],
  [
    node, // the input node
    new Ruleset(
      [new Selector([new Element(new Combinator(' '), ':root')], [])],
      [declaration], // new 
    ),
  ],
)

or just use an element node ?

new Ruleset(
  new Element('&'),
  [
    node, // the input node
    new Ruleset(
      [new Selector([new Element(new Combinator(' '), ':root')], [])],
      [declaration], // new 
    ),
  ],
)

I try in both way, but all failed with error below. Here are these codes

Error: error evaluating function `multiplyTwo`: variable @base-number is undefined

If you want to try, just clone and install, then run npm run jest or yarn jest, the failed test is in test/e2e.test.ts named transform less variable to css variable with function'

I think it may be the relpace rule doesn't work right. It's a huge need to know intermediate state of less code. for example, I really need to know whether@base-number: 10; is transformed to

@base-number: 10;
:root {
  --base-number: @base-number;
}

However there seems to be no way to check intermediate code (or just I don't know)

That's a really pain in ast development😰


Side note: being able to have variables be dynamic at runtime is one reason I'm building Jess.

That seems interesting, look forward to it :)

matthew-dean commented 3 years ago

I don't understand why you don't just do this:

.mixin(@base-number) {
  @base: @base-number;
  @multiply-number: multiplyTwo(@base);
  --base-number: @{base};
  --multiply-number: @{multiply-number};
}
:root {
  .mixin(10);
}

:root[scope='local'] {
  .mixin(2);
}

.use {
  color: var(--base-number);
  multiply: var(--multiply-number);
}

Note: looks like there might be a bug in evaluating variables in custom properties in mixin scope, but by aliasing @base-number to @base it seems to work.

matthew-dean commented 3 years ago

@arvinxx

Error: error evaluating function multiplyTwo: variable @base-number is undefined

I wonder if you're encountering the same bug as above with trying to evaluate @base-number in a custom property? It's possible you're doing everything else correctly.

arvinxx commented 3 years ago

@matthew-dean

The reason i don't use mixin is that I need this plugin to be compatible with existed codebase ( for example antd).

I have try a visitor:

 visitDeclaration(node) {
    if (!(typeof node.name === 'string') || !node.name.match(/^@/)) {
      return node;
    }

    const declaration = new this.tree.Declaration(
      node.name.replace(/^@/, '--'),
      node.value,
    );

    if (!node.parent) return declaration;
    if (node.parent.root) {
      const { Ruleset, Selector, Element } = this.tree;

      return new Ruleset(
        new Element('&'),
        [
          node,
          new Ruleset([new Selector([new Element(':root')])], [declaration]),
        ],
      );
    }

    return declaration;
  }

and use the test case below:

@bg: #000;
@fg: #fff;

@media (print) {
  @bg: #fff; // Override
  @fg: #000;
}

I expect to get result of

:root {
  --bg: #000;
}
:root {
  --fg: #fff;
}
@media (print) {
  :root {
    --bg: #fff;
  }
  :root {
    --fg: #000;
  }
}

but the result I get finally is below

@media (print) {
  --bg: #fff;
  --fg: #000;
}

Is there something wrong with the previous visitor function?

matthew-dean commented 3 years ago

@arvinxx .parent is not a reliable property; don't use it. If you need the root, store a reference to it when it's first visited (the first visitRuleset, I believe).