-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Provide api for custom importers #530
Comments
@jhnns looks like a cool feature, thanks for raising the issue here. Before you go ahead and make a PR though, I'd wait to see what happens with libsass. The issue hasn't been closed yet - and it looks like no implementation has been done on the libsass side of things. Let's wait for that and then focus on getting it implemented in node-sass 😺 |
Yep, sure. There's especially a problem with asynchronousness. |
@jhnns why does it need to be done synchronously? Is there no way we can do this with a callback? importer: function (filename, callback) {
callback({
filename: "..."
filesource: "..."
});
} |
The problem is, that the C-code needs to be paused until node called back with a result. I don't know if that is possible with libuv. It would be possible if libsass provided a callback. |
The api is implemented in libsass now. I guess I'll implement the easiest use-case for now: An api to rewrite the import paths. |
@jhnns, with V8 API, this can be done in a async callback manner. I have couple of ideas how to implement in our binding. For one, see http://stackoverflow.com/a/13904693/863980. Going by your example: importer: function (filename) {
return {
filename: "..."
filesource: "..."
};
} I am not sure if it allows to set source-content (but only the souce-file paths). At least I can't find that in the new API docs. However, we can make it so to get the sourceContents back in importer's callback. But the sole purpose of importer is to feed the import files to libsass, isn't it? Here call back seems like we have another candidate to success event, which would be redundant / incorrect. Perhaps, @mgreter can shed some light on this: what is the use case for the callback in import function? Nonetheless, please wait till #543 is merged, or you can start by pulling my master branch, since the current binding code is rendered obsolete. |
Not really sure what the question really is. You get called for each import statement and can return either the filename with or without its source (libsass will try to load it itself if no source is provided). It's as simple as that. Not sure what the confusion about this hook/callback is!? |
Thanks! Actually, I am confused about the use case of |
OK, let me try to explain it. The cookie can be used to store anything you'd like with the importer (it's completely up to the implementer). Maybe you want to attach some configuration that can be used inside the importer. Or you can store a function reference to a node.js function and call it inside the importer and use the return value. You probably will not be able to use node.js function directly for the importer callback, since you pretty sure will need to convert some data between C and node.js before/afterwards. |
Oh ok, so basically, its scope is just confined to when our importer is called. Perfect! 👍 I will try to implement it, so we let user pass a JS object, which can contain key-value pair, where value can be an inline / local function. |
@mgreter, I have pushed the code in a separate branch: am11@00057b7. The compilation is succeeding but apparently it is not calling the JavaScript function. We are expecting either JS object literal //# this is nodejs interactive console, pwd == node-sass' root directory
require("./").render({
file: "C:/temp/example.scss",
success: function (css,map) { console.log("css: " + css); console.log("map: " + map) },
error: function (err) { console.log("err: " + JSON.stringify(err)) },
importer: function (file) { console.log("importer: " + file); return file; }
}) Changing Here is another example, a valid case with both //# this is nodejs interactive console, pwd == node-sass' root directory
require("./").render({
file: "C:/temp/example.scss",
success: function (css,map) { console.log("css: " + css); console.log("map: " + map) },
error: function (err) { console.log("err: " + JSON.stringify(err)) },
importer: function (file) {
console.log("importer: " + file);
return {
path: '/my/random/file.scss',
contents: 'body{ color: red }'
};
}
}) The debug output is never printed to the console. I need to dig it further what's going wrong. |
I'm currently looking into it, but first it just segfaultet for me because, as it seems,
Will now check if I can get the basic example working ... |
Oh yes! For now, you can say, it is just implemented for async methods only (not the renderSync and renderFileSync). :) |
So you mean for sync methods only? For me a basic example works, sass_importer is getting called and the cookie is passed correctly. From there I cannot really help much as why your node.js function is not called. |
Fixed ctx_w issue with Async methods (without |
@am11 Ok, then I'll do nothing until your rewrite is done. Or are you implementing the importer API anyway (I'm ok with that)? I'm not experienced enough with v8 and libuv, just wanted to ensure if an async importer will be possible? importer: function (filename, callback) {
doSomeAsyncTask(function (err) {
callback({
filename: "..."
filesource: "..."
});
});
} |
@jhnns, I used the IMO, it's better that we keep it like this: importer: function (inputPath) {
return callSomeSyncFunc(inputPath); // returns object: {path, content}
} As working on objects in synchronous manner is comparatively straight-forward to implement. |
Well, the problem is that a synchronous importer is not always possible. For example you can't do a synchronous http request. Don't know if anybody wanted to that but if you're bound to do something synchronously there are not many possibilities in node.js. For now a synchronous api is ok. But in that case you probably only want to rewrite the path. I can't imagine a use-case where you want to pass the |
@mgreter, while debugging importer branch with the first chunk of JS code I posted above, it throws Memory Access violation exception in See: And here is the call stack:
Note: (see the dropdown next to ":arrow_forward: Continue" says |
@mgreter, the Could C++11 lambdas be used as function pointers, it would have made our callback code more rhetorical. Nonetheless, I have a breakthrough in calling and getting back JS results using libvu! am11@cbcacc2. (finally 😄) The next issue is with the uv_async_send, which requires the actual JS-context-aware loop, which we have and we can provide. But then it waits for the main thread to run.. which defeats the purpose. |
Status: It worked! ⚡ Credit goes to @txdv and @rvagg. Thank you guys for all the tips! Code is in my importer branch. Next, we need to find a way to support the JS callback function, which @txdv explained why it is necessary (to enable users run async functions within the importer function). My first try failed, because @rvagg, in order to store and forward the JS callback function reference, (without any modification in C code), is there a concise/neat way to do so using For instance: /**
* @param {String} file
* @param {Function} jsImporterCallback
*
* @return {Object} modifiedImport
*/
importer: function (file, jsImporterCallback) {
// this function is called by libuv intermittently, but synchronously, as in; one at a time
// do some magic with "file" string, make use of jsImporterCallback and return back to libuv
var modifiedImport = {
file: alteredVersionOf_file,
contents: contentReadFromSomeResource; // could be read from DB, web-service, file
// or any string containing SASS code
};
return modifiedImport;
} In the above code, I am referring to the parameter Having said that; do we need to cast it to |
See my comments inline about how Also see e.g. function invoker () {
var importantThing = 'very important'
var arg = 'some argument'
asyncWork(arg, function asyncCallback (err) {
otherFn(err, importantThing)
})
} If you follow that through from a GC perspective, the only thing that has a reference to |
Hello @rvagg, thanks for your detailed input. I now realize how important is it to avoid race condition in such a situation. The good news is, this is fixed by using @jhnns I am still (again) confused, what is the purpose of The following test is now working: //# in node interactive console
//# process.cwd == node-sass repo dir
require("./").render ({
file: "c:/temp/alpha/index.scss",
outFile: "c:/temp/alpha/index.css",
sourceMap: "c:/temp/alpha/index.css.map",
success: function(css, map){
console.warn(css); console.warn(map);
},
error: function(err, status){
console.error(status + ": " + err);
},
importer: function(file) {
console.log("I am in importer, yay!");
return {
file: "/some/random/path/file.scss", // non-existent, but it doesn't matter due to the next line
contents: "div {color: yellow;}"
}
}
}); If that suffice the purpose, we can merge in the importer branch. |
Since node.js is single-threaded – at least in the JS world – most APIs are async by default. Take the fs-API for example: var fs = require("fs");
sass.render({
...
importer: function (file) {
fs.readFile(file, "utf8", function (err, content) {
// oops, we're too late, function has already returned
});
return {
file: "/some/random/path/file.scss",
contents: ? // we can't pass anything here yet
};
}
}); Luckily there is a sync-version for every fs-method: importer: function (file) {
var content = fs.readFileSync(file, "utf8");
return {
file: "/some/random/path/file.scss",
contents: content
};
} But that's an exception, most I/O APIs are asynchronous. What if we wanted to perform an HTTP request? importer: function (url) {
http.get(url, function(res) {
// damn, we're too late again!
});
return {
file: "/some/random/path/file.scss",
contents: ?
};
} There is no |
Ok, how will you pass callback to importer in this case? |
@am11 I think the point is that if importer: function (file, callback) {
fs.readFile(file, "utf8", function (content) {
callback(null, {
file: "/some/random/path/file.scss",
contents: content
});
});
} importer: function (url, callback) {
http.get(url, function(res) {
callback(null, {
file: url,
contents: res.body,
});
});
} |
Aaaaaand..... its Here we go: fd8c7f7 I have used I will throw in couple of test cases for it subsequently. Now you can say: //# in node interactive console
//# process.cwd == node-sass repo dir
require("./").render ({
file: "c:/temp/alpha/index.scss",
outFile: "c:/temp/alpha/index.css",
sourceMap: "c:/temp/alpha/index.css.map",
success: function(css, map){
console.warn(css); console.warn(map);
},
error: function(err, status){
console.error(status + ": " + JSON.stringify(err)); // see #543, errors serialized as JSON.
},
importer: function(file, done) { // you can name params as you please
console.log("I am in importer, yay!");
done({
// each tuple consists of file and content
// meaning, here done() can take array
// of objects [{file:'',content:''},{file:'',content:''}, ..]
file: "/some/random/path/file.scss",
contents: "div {color: yellow;}"
});
}
}); Thank you all for your help and patience! |
congrats |
Aweseome!! 👍 I was afraid that async importers are not possible. Need to dig the source code, I'm curious how you've managed it. 😀 Is there any roadmap for v2? |
As soon as libsass gets new release. See sass/libsass#697. I actually read @rvagg's NAN repo's README this time. :) Then used synchronous example and drew the code from there. Once the synchronous method is called (done ()), it signals libuv to continue (via conditional variable). Not the pure Nan-based solution, which @rvagg mentioned above, but fiddling with libuv mixed with Nan helped us achieved the target. Nonetheless, we can always improve things over time.. |
I guess the code puts libsass in its own thread, so it can be stopped and continued at will, without interfering with the main event loop (which I guess is what libuv does)!? |
With 356d256, it now works with On a slightly related note, based on #547, we should probably return both css and map in case of |
If you change the api like this, you should probably also return the I've implemented it this way because the function did return only a string and I didn't want to change the api. But now – with your changes – I think it makes more sense to return the stats because they are the result of the compilation. You probably also don't need to nest these values in a stats-object at all and just return them on the same level as renderSync(blah);
// returns
{
css: "..."
map: "..."
importedFiles: [] // I've renamed them to importedFiles
// in the style of the new importer option
} |
var myStats = {};
var result = sass.renderSync ({
file: "c:/temp/alpha/index.scss",
outFile: "c:/temp/alpha/index.css",
sourceMap: "c:/temp/alpha/index.css.map",
stats: myStats,
importer: function(file, done) {
done({
file: "/some/random/path/file.scss",
contents: "div {color: yellow;}"
});
}
});
// result.css returns css string
// result.map returns source-map string
// if there is an error, it'll be thrown to stdout as JSON (object literal like, not stringified) Analogous to its async variant: var myStats = {};
sass.render ({
file: "c:/temp/alpha/index.scss",
outFile: "c:/temp/alpha/index.css",
sourceMap: "c:/temp/alpha/index.css.map",
stats: myStats,
success: function(css, map) {
// css is string
// map is string
// mystats is JS object literal
},
error: function(err, status){
// status is an integer
// err is JS object literal (parsed JSON, see #543)
},
importer: function(file, done) {
done({
file: "/some/random/path/file.scss",
contents: "div {color: yellow;}"
});
}
}); I have made the changes in my Appreciate your input. Lets continue discussion on this at #547. |
@am11 This time I didn't help much, however I want to add my two cents: IMO in renderSync the importer function should use "return { }" instead of a done callback. This is a better paradigm for blocking functions. Now you are mixing them. |
@txdv, the binding code would remain the same as the uv call will be deferred to main loop and wait for JS to return. It is similar to how mocha.js testing framework works; gives full control to user by passing the done callback on, and letting the user decide whether to use it asynchronously or not. We can certainly restrict it in our JS code (by keeping the done() propagating to end-user), but I don't see a significant gain there except for the fact that it is strictly logical. :) |
The final version looks like this: stats = {};
require("./").renderSync ({
file: "c:/temp/alpha/index.scss",
outFile: "c:/temp/alpha/index.css",
sourceMap: "c:/temp/alpha/index.css.map",
stats: stats,
importer: function(file, prev, done) {
console.log("Previous: " + prev);
done({
file: "/some/random/path/file.scss",
contents: "div {color: yellow;}"
});
}
}); Where |
I have to agree with @txdv: It looks strange, to use an async function signature within a synchronous call (and I didn't know that this is possible). But if it's working, I don't know why we should restrict the developer to synchronous functions. Is it still possible to just return the value? Like mocha is providing the possibility to either return a value or accept a done() callback for async tasks. |
The catch is, it does not reflect what underlying C++ implementation is doing: calling the Nan callback anyway. |
Cool 👍 |
v2.0.0-beta is just released. Thanks for all your support guys! |
Nice! Then we can update the sass-loader |
Remove unnecessary whitespace in rbg/rgba compressed output
With sass/libsass#21 libsass will provide the possibility to specify a custom importer. This is useful for build tools like webpack that provide a custom resolving algorithm.
We basically just have to wrap the js-callback so it can be called by libsass:
The libsass-api is a bit more complex to support more advanced use-cases as described here. I'm planing to make the js-api a bit more pleasant to work with.
For example you could do:
One problem I currently see is that if a
filesource
is provided, this needs to be done synchronously which might be impossible in certain situations.What do you think about it? If it's ok for you I could start a pull-request.
The text was updated successfully, but these errors were encountered: