0

How to create a file by file custom SCSS build including dependencies, using pos...

 2 years ago
source link: https://itnext.io/how-to-create-a-file-by-file-custom-scss-build-including-dependencies-using-postcss-and-optional-c83f8e5677d8
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

How to create a file by file custom SCSS build including dependencies, using postcss and optional css vars

While this sounds like a common problem, I was not able to find a good solution for it on the web. So I thought writing an article about it to save others some time is a good idea.

While there are many use cases where you can rely on e.g. webpack to create fitting CSS split chunks out of the box for you, there are scenarios where you need to go from scratch instead to create a 1 input → 1 output based solution.

I strongly recommend to read the article first, but for the curious ones who want to dive into the code right away:
buildScripts/buildScss.js#L89 (you can skip the first 90 lines)

Content

  1. Motivation: JS related cross app split chunks
  2. Why can’t we use webpack for the theme build?
  3. The SCSS structure
  4. The desired output using css variables
  5. The desired output without css variables
  6. How to build the SCSS?
  7. The core algorithm
  8. How long does the build take?
  9. Could we add support for CSS in JS in neo.mjs as well?
  10. What is coming next?
  11. Final thoughts

1. Motivation: JS related cross app split chunks

I am definitely a fan of webpack. The cross app split chunks inside the worker scope work very well. The same goes for browsers themselves: Dynamically importing files will never load the same JS module more than once.

You can see it inside the app worker console logs (in case you look close: when moving an existing component tree into a new window, you can see the modules for the empty viewport & app.mjs loading in parallel to the next main window tab body):

Cross app split chunks means, that we as well put multiple apps on one page. In this case webpack enables us to create chunks for the shared JS modules, so we won’t load more code as needed.

I created an in depth article about this a while ago:

neo.mjs was still using a monolithic CSS output, which did not do the project justice. Motivation enough to create a new version which is on the same level as the JS side!

2. Why can’t we use webpack for the theme build?

There are two reasons.

While we could generate CSS split chunks which match the JS ones, this is not enough for the multi window context using SharedWorkers. In case you did watch the last video closely, you have seen that we can move entire component trees into different windows.

A component tree could contain any possible combination of components, which makes a component file based output mandatory. In case we move a tree, we want to load exactly the required CSS files inside the target window and nothing else.

Reason 2 is that we are loading components into the application worker. We would need to customise webpack to match the loading of a JS chunk into a worker with adding the related CSS split chunk to the matching main thread. We would also need to adjust the chunk builds itself, so that there is an awareness of the app worker — main thread connection.

However, reason 1 does not apply for the non shared workers scope. Single page apps written in neo.mjs could make use of this approach. Unfortunately my roadmap is already too intense to give it a try. You are welcome to open a ticket though!

My goal was to create a new solution which fits both scopes for consistency reasons.

3. The SCSS structure

To get an idea of what we are dealing with:
resources/scss

find mixins      -type f | wc -l //   1 file
find src -type f | wc -l // 125 files
find theme-dark -type f | wc -l // 72 files
find theme-light -type f | wc -l // 72 files

The old build had the ability to optionally use css vars, which is definitely something we want to keep. To achieve this, we need a couple of functions.

mixins/_all.scss:

Let us take a look of a rather simple component: a button

[Side node] Enhancing the content is on my todo list, e.g. only using .em based sizes to make components scalable. Please keep an open mind here.

scss/theme-dark/button/_Base.scss:

We are defining scss variables inside a map, afterwards there is a check if we do want to use css variables. If so, the neo() call will give us the value of the matching scss variable.

scss/src/button/_Base.scss:

The v() function will return the name of the css variable or the value of the matching scss variable.

4. The desired output using css variables

dist/development/css/theme-dark/button/Base.css:

dist/development/css/src/button.Base.css:

The nice part here is that themes have a very little file size. In case you want to switch themes inside your apps dynamically or use different themes for different parts of your view structure, this approach fits well.

You can apply a theme on any level of the virtual DOM.

It is also nice that you can change css variables at run time.

Obviously this requires to maintain a src structure which fits for all themes. In case you want to create an own theme and need more variables, feel free to open tickets or send PRs.

You can of course create custom components extending framework base classes and style them any way you like to as well.

The dist/production output does not use source maps and is minified.

5. The desired output without css variables

dist/development/css-no-vars/theme-dark/button/Base.css:

In this scenario we just get one file which contains the values of the scss variables.

If you don’t want to change css variables at run time and just use one theme, the file size is a bit smaller. As soon as you use a second theme or more, this version no longer makes sense.

6. How to build the SCSS?

We are using the dart-sass npm package (got renamed to sass).

We will also use postcss, including the autoprefixer and cssnano plugins.

Before finally diving into the code, let us think about two problems which I encountered.

The old monolithic build had specific entry points. To show a few:

scss_structure.scss

@use "sass:map";
$neoMap: ();

$useCssVars: true;

@import "../../../../resources/scss/mixins/all";
@import "../../../../resources/scss/src/all";

theme_dark.scss

@use "sass:map";
$neoMap: ();

$useCssVars: true;

@import "../../../../resources/scss/mixins/all";
@import "../../../../resources/scss/theme-dark/all";

theme_dark.noCssVars.scss

@use "sass:map";
$neoMap: ();

$useCssVars: false;

@import "../../../../resources/scss/mixins/all";
@import "../../../../resources/scss/theme-dark/all";
@import "../../../../resources/scss/src/all";

We definitely do not want to create multiple new entry points for each single scss file, so using sass.render() starting from a file won’t work.

Looking into the JS API:
[side note] It is a little bit outdated, since fibers won’t work in node v16+

We are in luck, since we can use a data property instead which accepts file buffers (stdin).

We only want to import the mixins file once per target.

The real problem was, that src files can not rely on only importing the parent classes. E.g. a SplitButton could use Button variables.

The classic example: a container.Toolbar file is using Button variables as well, but is not related to the Button class hierarchy.

To make this bullet proof, each var file needs to import the variable files of all components. Now if we want to build 72 var files which each import the theme-dark/all file which uses @import sass imports to fetch the other 71 theme variable files, we end up with 72 * 72 sass imports.

It was actually not as slow as I thought, but we can definitely reduce the build time by several seconds with not doing it.

Instead, we are dynamically creating a merged version of all theme variable files once.

Using a recursive function does the trick.

sassImportRegex = /@import[^'"]+?['"](.+?)['"];?/g

We are replacing the import statements with the content of the files.

We are not saving the result into a file. A local variable is sufficient.

The program itself supports command line options. If not set, we are using the inquirer to

  1. Ask for themes (all, dark, light)
  2. Environment (all, development, production)
  3. Use CSS vars? (all, yes, no) // you will most likely never use "all”

Once the options are set, we run into:

Which leads to the buildEnv() function:

No css vars → no src build needed and we are collecting all entry points using getAllScssFiles() .

For now, I duplicated the resources/scss folder into scss_new.

The idea is that non entry point files start with an underscore
(like _all.scss ) and entry point files do not (like Button.scss ).

This way we can easily parse a nested folder structure to fetch all entry point files recursively.

Once we have the files, we will call parseScssFiles()

7. The core algorithm

We are using the previously mentioned scssCombine() method once per theme we want to build.

We can just drop it into the data build property, which is beautiful.

We can also add the needed variables & mixins there.

The rest is pretty straight forward:

We are using thesourceMap option only for the development env.

Once sass.render() is done, we are triggering postcss().process() using the autoprefixer for both environments and cssnano only for the production output.

Once this is done, we are saving the CSS files for the output and this is it.

8. How long does the build take?

theme-dark dev     vars: 2.66s
theme-dark prod vars: 3.54s
theme-dark dev no vars: 3.68s
theme-dark prod no vars: 4.28stheme-dark all vars: 5.38s
theme-dark all no vars: 7.18sall all vars: 8.46s

9. Could we add support for CSS in JS in neo.mjs as well?

I have been thinking about this topic a lot, since projects like emotion are getting more and more popular.

neo.mjs has the design goal that the development mode runs inside the browser without any builds or transpilations on the JS side.

We definitely want to use postcss and the autoprefixer plugin for this as well. So, to make it work, we would need a tool like browserify to enable a compilation directly inside the browser. This would result in some overhead (bigger file size).

Inside the dist/development environment it is definitely easier.

My roadmap is pretty intense, so I am not sure if I will find time for diving into it anytime soon. I am mostly focussing on the framework foundation (core & ecosystem) to make it easier for others to build things on top of it.

In case you would like to see this happen, definitely open a ticket:
https://github.com/neomjs/neo/issues

You are also very welcome to work on this one if you like to. The only requirement on my end is that it is optional. Doable, since we already have optional main thread addons in place.

10. What is coming next?

I think I spent around 40h within the last 3 days to get the new build running. While the resulting code got pretty short, the devil was in the details :)

Obviously the next step is to include the new output into the framework. I created a project for this here: neomjs/neo/projects/25.

It should be straight forward: We create a map inside the application worker like Neo.cssMap[appName][className] . Each class needs flags if it has a theme src and / or var file. component.Base has an appName config (matching the main thread → browser window as well), so we can use afterSetAppName() to check the map and trigger the main thread Stylesheet addon to load new files as needed. Constructing a new instance will trigger an appName change. Adding a component to a container will do the same. container.Base will adjust the direct child items appName config as well in case there is a change, so this part feels easy.

Once done I will deploy it to the online examples and write a quick follow up article.

We also need to add the new build and output into your workspaces created by npx neo-app . Workspaces have their own resources folder to enable you creating theme files for your own components there. These files need to get included (like it works inside the current build) and the build output has to get saved into the workspace dist folder.

Once this is done, we have reached the v2.1 release. After more testing, I will add this version into the create-app repo then and remove (replace) the old monolithic build. I kept the old build in place during this transition to ensure there are no problems on your end when working inside your existing workspaces.

11. Final thoughts

In case you have not looked into the neo.mjs project yet, I can strongly recommend doing it. The project is very disruptive and maybe too far ahead of its time, but it does run well in todays browsers.

Design goals:

  1. Dedicated or shared workers are the main actors
  2. You can run the JS code as it is inside the dev mode (no builds or transpilations required to work with JS modules)
  3. JSON based virtual dom creates a blazing fast performance
  4. The optional multi browser window mode enables unique features, like sharing the application state across windows or dynamically moving component trees across windows while keeping the same JS instances.

The project is in need for more contributors as well as sponsors.

Best regards & happy coding,
Tobias

Preview image:

1*Lx-bPYfTPvzSHBlpfuzmZA.png?q=20
how-to-create-a-file-by-file-custom-scss-build-including-dependencies-using-postcss-and-optional-c83f8e5677d8

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK