Less.js: How to trigger sync and async imports

Created on 7 Dec 2014  ·  26Comments  ·  Source: less/less.js

How does less decide to call loadFile or loadFileSync of the current fileManager? According to this code it requires an isSync-parameter to be set. But a short search for a call to getFileManager reveals that this option is only used by lib/less/functions/data-uri.js.

Is it possible to set this option explicitly when calling render()?

feature request medium priority stale

Most helpful comment

Is there a renderSync yet? If not, is there a workaround for a synchronous render?

Edit: Nvm. For any future person who stumbles upon this, this is what I did:

less.renderSync = function (input, options) {
    if (!options || typeof options != "object") options = {};
    options.sync = true;
    var css;
    this.render(input, options, function (err, result) {
        if (err) throw err;
        css = result.css;
    });
    return css;
};

All 26 comments

isSync can be passed in the options object to render. data-uri is always
sync because functions cannot be async.

But the importManager doesn't pass any option to getFileManager...

Its done here

https://github.com/less/less.js/blob/32dbbee247f76af00eb7577053eccad2ee5f6110/lib/less-browser/file-manager.js#L61

Its because that option is ignored by less core and is specific only to the browser. So the browser always gets called async and may depending on options use a sync or async mechanism to get the file.

I can see how that's confusing. Is it causing a problem?

One thing I see immediately is that it will onyl be effected by the options at page load, so if it is set afterwards or passed in as an option it will be ignored..

Well, probably i have to explain my use-case:

I've written a less-loader for webpack. Since webpack has an own resolving mechanism I'm using a plugin to hook into less' file resolving via a file manager.

Webpack supports sync and async loaders but I haven't found a way to tell less to render all files sync or asynchronously. Currently it always calls loadFile. Thus I used a dirty hack to call loadFileSync when a synchronous compilation is requested. Luckily less works synchronously when the callback is called synchronously (which shouldn't be done under normal circumstances of course).

I see...

I think we should move an async option to be on the less context, then use that to determine which call to make as you suggest. It may need a few changes to the browser file manager, but I think it will be a good refactor.

Cool!

There is just one problem: It is very unusual to do something synchronously while accepting a callback (as the render-function does). You're suggesting a sync-option like this:

less.render(
    input,
    { ... sync: true ... },
    function (err, result) {
    }
);

?

Imho it's weird to have the callback being called synchronously. I'd expect an api like this:

// async
less.render(input, option, callback);

// sync
var result = less.renderSync(input, option);

Yes you are right, that's better.

+1
It seems this is also very useful to consider implementing renderSync to not restrict code execution in the callback only. In the callback scope many methods such as console.err or throw new Error() or deliberate JavaScript error will not print anything to the console, only stop the code execution, potentially leading to bugs that cant be traced. I would imagine this behaviour should not happen.

In my particular case renderSync would be used in a command-line utility and rather than controlling the callback flow using promises to ensure all output and errors are printed in order each time, I would rather just use renderSync and not have to worry about it.

(Not demeaning the issue itself, but just to ensure it won't get even more weird than it is):

Imho it's weird to have the callback being called synchronously.

This is an exaggeration... In this line of the code you're not assuming the config is set asynchronously, are you?
Callbacks are just callbacks, by themselves they have nothing to do with sync/async stuff.

Ok, than we need to talk about the term callback :wink:.

I know, some do call functions passed to forEach a callback (like MDN). I wouldn't call it so, because for me a callback is something that is called when a task has been finished.

And if the callback follows node's error convention with the first argument being null or an error, than there are good reasons to _always_ call it asynchronously.

I know, some do call functions passed to forEach a callback`

Someone is just using too much node ;) Everybody name this thing a "callback". So if one is about "a callback function called when a task has been finished" it's no more no less than "asynchronous callback".

But never mind, sorry, I did not mean to sound like a CO and start this purely linguistic debate (just wanted to ensure we speak the same language and the docs actually mention less.render is synchronous).

P.S. Just to clarify more:

@kwketh

This less.render form was there for years, you can't just take and change it out of nothing to break zillion snippets out there.

@seven-phases-max

Many thanks for your answers, they are all helpful. I agree with all your points regarding callbacks. I had a very brief look at the .render code and you are right, the form it is written, it all revolves around the callbacks and doesn't seem neither easy or reasonable to have renderSync.

My issue is somehow different but related (https://github.com/less/less.js/issues/2546). Implementing renderSync feature would resolve my issue but it wouldn't be the ultimate solution.

Do you mind having a quick look? I would really appreciate.

Thanks.

Someone is just using too much node

Well, that's true :grin:

But node's callback convention is well-established. I'd assume the callback to always be called asynchronously in this case.

In addition: How are errors handled when there was an error? Is it thrown (like most sync APIs) or passed as argument to the callback?

It's just that the current API is ambiguous (at least imho)

Is there a renderSync yet? If not, is there a workaround for a synchronous render?

Edit: Nvm. For any future person who stumbles upon this, this is what I did:

less.renderSync = function (input, options) {
    if (!options || typeof options != "object") options = {};
    options.sync = true;
    var css;
    this.render(input, options, function (err, result) {
        if (err) throw err;
        css = result.css;
    });
    return css;
};

Is this feature actually implemented? Is there any documentation? I can only find the async-option.

Idk if it's actually supported, but I know what I did works for me currently, so.... shrug

@Aarilight thank you very much, your code helped a lot

This synchronous callback behaviour is really counter-intuitive :confused:

@Aarilight it doesn't work for me=(
I tried

less.render(css, {sync : true}, (e, result) =>{
        if(e) {
         console.error(e)
    }

        output = result;
        return result
    });

and logged https://github.com/less/less.js/blob/master/lib/less/render.js

            console.log('1')
            this.parse(input, options, function(err, root, imports, options) {
                console.log('2')
                if (err) { return callback(err); }

                var result;
                console.log('3')
                try {
                    console.log('4')
                    var parseTree = new ParseTree(root, imports);
                    console.log('5')
                    result = parseTree.toCSS(options);
                }
                catch (err) { 
                    console.log('6')
                    return callback(err); 
                }

                console.log('7')
                callback(null, result);
            });
            console.log('8')

And I see 1, 8 and then 2,3,4,5,6,7 for some files

-1 to splitting render into render and renderSync. It's a Node.js convention that is awkward. And it doesn't allow sending a sync option to grunt / gulp / accord or other workflows integrated into less that pass in a JS object to a designated function. IMO it's fine to pass in an optional callback when using an async option.

Another option: what I've seen libraries start to do is actually return a promise in either case. Then, all that changes between sync / async is when the promise is fulfilled, but the code handling the result is exactly the same.

And btw.:

{sync: true}

There's no such option.

-1 to splitting render into render and renderSync. It's a Node.js convention that is awkward.

Isn't that a completely subjective viewpoint? IMHO combining async and sync in a single function is (with few exceptions) a terrible anti-pattern. It creates code that is cluttered with conditional statements, is harder to maintain and is even more confusing for the user than a clearly defined and documented function that does one thing well. just my 2c

Isn't that a completely subjective viewpoint?

Yes.

Regardless, my other point is not subjective. That is, that Less is used in build processes that might need to update if a function is split. For instance: Accord (https://github.com/jenius/accord), which I currently use for one project, abstracts out different compilers into a single API, and typically passes in an object to whatever function that engine requires. So, it's probably not a huge deal to switch which function is used based on the Less options a developer specifies, but I'm not sure how many libraries that would affect. It's just something to be aware of.

As of now, adding syncImport: true to my options fixed this for me.

(It's not in the documentation... I was just lucky enough to stumble across it in the source code)

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rejas picture rejas  ·  6Comments

renoth picture renoth  ·  6Comments

heavyk picture heavyk  ·  3Comments

pknepper picture pknepper  ·  3Comments

briandipalma picture briandipalma  ·  6Comments