Open joneff opened 2 years ago
What about simplifying it and using relative paths for the imports instead? I had it working with includePaths and loadPaths with WebCompiler 2022, but the. Visual Studio 2022 doesn’t understand includePaths or loadPaths, so it gives undeclared variable errors and does not give intellisense for bariable names; but if you replace all the ~ imports with relative paths, it compiles perfectly with WebCompiler 2022, Visual Studio 2022 can follow the imports, so no errors are reported, and Intellisense autocomplete variable names.
Wanting to use Telerik Blazor UI, and costumize it, seems that Kendo + boostrap with node-sass is what was used before to acchive this together with gulp. Would be really nice, if for example, https://demos.telerik.com/blazor-dashboard-app would be updated to showcase the proper modern theme development workflow.
To correctly and fully answer how kendo themes can be compiled in .net environment, I must first explain what the themes are, what sass is, talk about existing solutions then get to answer the question.
It's a good place to introduce the concept of "framework native" -- the native way of doing something depending on the framework of choice. A few examples: npm is the framework native package manager of node. nuget is the framework native package manager for .net and so on and so forth.
It's not like I can't bring .net libraries / packages into a node project, but I would have to use
nuget
for fetching anddotnet
executable for compiling and make sure it integrates well with my workflow.This is going to be long, but the context is important.
What are Kendo UI themes (kendo-themes)?
kendo-themes are themes for Kendo UI (for angular, jquery, react, angular, vue) and Telerik UI for Blazor, written in sass and are currently built (compiled) and distributed around node and npm.
What is sass?
TLDR: sass is a stylesheet language and a superset of css that compiles to css. Sass has extensions over the standard css syntax like function, mixins, loops, that enables nesting selectors, code reuse or code generation.
There are two main versions of the syntax: sass, which relies on indentation and scss which resembles css a lot more.
Sass compilers, as the name suggests, compile sass to css.
The state of sass
The current state of sass is fairly complicated:
There are numerous other projects like rsass for rust; a nouvelle dart-sass implementation for ruby; a libsass implementation for ruby and so on and so forth.
On top, you have a new modern sass syntax with module system that does away with
@import
and switches to@use
and@forward
.There are two js APIs to interact with the compiler: v1, that hasn't really changed since the introduction of node-sass and v2, that looks much more mature. The former being called
legacy
and the latter calledmodern
. Needless to say, the API's are not compatible.In short, there are two major implementations, two syntaxes and two js APIs, each with their own quirk and compatibility issues.
A quick note on the compiler API
The API allows to set certain options for the compiler that affect the output (css), like line ending, indentation etc.
There are also "plugin" points that allow for providing custom functions or even special behaviour when encountering certain file types on import.
A great example of such extension is the compass ruby gem, which had the ability to import images, to use for sprites, and create class names for each corresponding image.
Another great use, this time for node, is to use tilde (
~
) for pointing tonode_modules
and nudging the compiler in the right direction to load the files correctly.Existing solutions for compiling sass in the .net world
Here are some of the most popular existing solutions, with each having advantages and shortcomings.
node
Probably the least popular among .net developers. I have no idea why, because this will yield the best results. That's the "framework native" way of the themes, yet an alien in the .net world.
In all fairness, using node does require that initial
package.json
file, but then again, what is apackage.json
file compared to a proper MSBuild file, like.csproj
?On the upside, there is Task Runner Explorer in Visual Studio that detects gulp tasks and npm scripts, so there is even UI to run them.
On the downside, .net projects are organized a bit differently and while it's possible to have a root (as in where the
.sln
is, and all the.csproj
are children) and execute from that folder, the reality is that a developer could have a solution that spans multiple volumes, multiple repos even...WebCompiler Visual Studio extension(s)
Probably, the most well know solution is WebCompiler by Mads Kristensen (@madskristensen), which enables compiling sass (and not only) directly in Visual Studio. The extension does so by bundling node with it and using the CLI.
While technically the CLI for node-sass does allow for custom functions and importers to be passed, the extension does not. So no
~
imports, no caching... Not that tilde imports would work or make sense if there isn't a node_modules folder in the right location.Working with the extension on a daily basis with a huge codebase, one of the most useful features I found was having the files listed in a json format. Hell, I could even write my own json, and then right click and compile all!
Unfortunately, time has not been kind to this extension and it hasn't aged well...
But on the other had, that aging has spawn WebCompiler 2022+ by Jason Moore (@failwyn). WebCompiler 2022+ uses pretty much the same code base (it's forked), but is updated to work with both node-sass and dart-sass. It can also have different options for different files.
Still, the original WebCompiler was great piece of software, without a doubt, and a game changer!
Everything from Andrey Taritsyn
To me, one of the most prolific developers in the sass .net scene is Andrey Taritsyn (@Taritsyn). He has created multiple libraries that deal with wrapping sass -- libsasshost and dartsasshost, as well as libraries that consume them like BundleTransformer.SassAndScss.
In fact, one more entry here depends on dartsasshost: LigerShark.WebOptimizer.Sass.
Both libsasshost and dartsasshost are hosts (duh), but take different approaches. libsasshost is wrapper around the native libsass. dartsasshost, on the other hand, executes the transpiled javascript in a js engine host.
Both of them are brilliant and a probably the next best thing.
The only downside I see is that neither support passing custom functions or importers.
As for BundleTransformer.SassAndScss -- it supports only .net 4, but apart from that is pretty much on par, or better with almost everything else. And yes, still no custom function or importers.
Sidenote: I would very much like to see a proper dartsasshost, the way libsasshost is, with support for function and importers.
A note on js hosts
Running javascript in a host ... sounds promising at first, but then there are plenty of non-javascript aspects in a npm package. For starters, built in modules like
fs
,path
etc; then you have npm itself with package resolution, deduping ...; and finally you have packages that are wrapped around native bindings likeesbuild
and evensass-embedded
.AspNetCore.SassCompiler nuget
A very good, and cross-platform, solution is AspNetCore.SassCompiler by @koenvzeijl.
The library author took a different approach and provides OS specific builds of dart-sass and compiles sass trough child processes.
On the upside, it integrates especially well with blazor. In addition, because the project employs tasks, one should probably be able to include it in a MSBuild target.
On the downside, there is limited configurability; no option to pass functions or importers and it works only with dart-sass (the native bindings, not the node module).
LigerShark.WebOptimizer.Sass nuget
A star infused team (LigerShark) consisting of Mads Kristensen and Scott Hanselman, among others.
To me it feels like WebOptimizer picked up from where BundleTransformer stopped -- it supports newer versions of .net and has everything that dartsasshost has.
Which means, it is again not able to receive custom functions and imports.
Microsoft.AspNetCore.NodeServices
That's a sort of "gateway" to executing javascript in actual node, but unfortunately it's obsolete and Microsoft.AspNetCore.SpaServices.Extensions should be used instead.
It would have been the perfect solution, because it would allow to interact with node in a civilized manner that's native to .net developers.
MSBuild targets
Targets are native to MSBuild (read
.csproj
), so it offers the best integration with CLI and IDE's like Visual Studio.There are plenty of examples with targets that either run node or use any of the aforementioned tools.
On the downside, targets are not immediately visible, obvious or easy to comprehend. The MSBuild syntax is surprisingly powerful and convoluted with a learning curve to match.
It's hard, for instance, to decide in which part of the lifecycle a target should run. How often, for instance, does one want to do initial npm install or npm update or actually compile sass files?
It's evident that there are plenty of libraries and extensions that try to solve sass compilation and most of them do fine or good enough. But none matches the versatility a properly configured build for
node-sass
orsass
.To illustrate why I am so hell bent on being able to pass custom functions and importers to the sass compiler, lets run and measure each of the following, with
node-sass
,sass
andsass-embedded
:The results will differ, based on actual machine load, but here are my results on MacBook Pro 2021 w/ M1 Pro:
node-sass
dart-sass
sass-embedded
To get a better understanding of the difference between the time it took
node-sass
,sass
andsass-embedded
to compile the files, we must first look at the files:dist/all.scss
is a single file that contains the complete themesrc/grid/_index.scss
imports the styles for grid component, as well as all required or depended upon components, for instance the various inputs, buttons, window etc. Those components in term do the same for their dependencies until we are left with "leaf" components that don't rely on other components.src/all.scss
imports the styles for every component and it's depending components. Obviously that results in a huge overhead that results in many components being loaded many times. I don't have the exact metrics around here, but I do recall about 40-50 k files in the resulting tree.So why the difference between in compile time? I am guessing, and this is just guessing, that it comes to the difference in implementation:
node-sass
is a wrapper around native libsass, where assass
is this huge chunk of dart to js. And I am speculating that I/O issass
's arch-nemesis. Kryptonite, in a way. So it should come as no surprise whensass-embedded
, which is also a wrapper (around native dart-sass), just blows pastsass
and is on par or better thannode-sass
.How is this an argument for custom importers or functions? It's simple: not all problems can be solved by using faster implementation. Sometimes using a different implementation is not possible. For those cases we can use custom functions and importers.
For instance, we could have an importer that keep track of all processed files and suggest to the compiler that the file it requests (if already processed) is empty, which will just skip the entire processing of the file, along with any child imports and their child imports... True, the
sass
compiler is sort of not ok with said nudging in certain scenarios, but thenode-sass
is quite happy.Mentioned quite early in this piece, compass gem enables to import images and generate sprite class names.
How about beieng able to import json files and generate variables. There could even be a strongly typed object that serializes to said json. Wouldn't that be something?
So that's why I am so hell bent on having the ability to supply custom functions and importers.
So how do you compile kendo-themes (or any sass) in .net environment?
Well that's the million dollar question, innit?
In many projects and organizations, there is standardization. If there is an established procedure for compilation, developers should adhere to it. If the procedure feels clunky, perhaps it can be improved.
Bottom line is that every developer should be able use the method of compilation that feels best (as long as that method of compilation provides the same output), but should follow practices, if present.
Any sass file
The answer is "it depends".
If it's a locally written file, then any of the above should suffice and perhaps WebCompiler or WebCompiler 2022+ will provide the best possible experience.
However, if we are talking about compiling a file that contains external dependency, then its best to see the origin (read eco system) of that dependency and use a pipeline that feels native to it.
For instance, bootstrap provide both a node package and a nuget. Considering we are talking about .net the nuget should be used. However, as of this writing the node package is v5.2.0 and the nuget is v5.1.3, so even this approach is not without it's limitations.
kendo themes
Speaking for kendo-themes, if the desired result is a full theme, the best way is to use the
dist/all.scss
. As mentioned above, it's a single file that contains the entire theme. This file will work in any scenario, regardless of the method used.If the the desired result is part of the theme, then the above example about speed should be taken into consideration.
I cannot stress this enough: kendo-themes we use
~
imports for dependencies. In other words, currently, it's next to impossible to compile kendo-themes in certain scenarios outside of node.(And though we provide documentation how to compile themes, we do sometimes forget to update it. But thanks to vigilant users like @smchargue who spotted the discrepancies, we corrected the mistake and updated the article.)
A few closing words
Obviously, node packages are not the perfect medium for .net projects. Yes, it's an acceptable medium, but not the perfect. That's why we'll begin shipping nugets of our themes, so it fits better within the .net eco system.
That's not going to be 1:1 port, but rather tailored to .net. For instance,
~
imports don't seem to be a huge thing in .net, so we'll strip it and instead rely on supplyinginclude_paths
orload_paths
option (depending on the compiler).We are working additional tooling that should make compiling easier and friendlier not only to .net developers, but developers as a whole.
One part is that we are experimenting with a json format that describes the files for building and settings for them, a lot like
webcompilerconfig.json
does. The difference being that we use a set of common settings to abstract the differences between compilers.Here is a quick example: the legacy API has an
outputStyle
setting with 4 possible values --nested
,expanded
,compact
andcompressed
; the modern API has astyle
setting with 2 possible values --expanded
andcompressed
. Our abstraction is calledminify
and is a boolean setting which switches betweenexpanded
andcompressed
.That about sums it up.
Happy coding!