Style loader

The style loader works with the styled utility (inspired from the api of styled-component). Every module targeted by this loader should export class names generated with the className utility:

import { className } from 'https://deno.land/x/frugal/styled.ts';
import * as colors from './colors';
import * as grid from './grid';

export const item = className('item').styled`
    color: ${colors.red};
`;

export const selected = className('selected').styled`
    color: ${colors.blue};
`;

export const list = className('list').styled`
    padding: ${2 * grid.base}px;
`;

If this module is caught by the style loader (meaning its name matches the loader pattern), the following css will be generated:

.item-l6cy2y {
    color: #FF0000;
}

.selected-ug9bde {
    color: #0000FF;
}

.list-vngyoe {
    padding: 16px
}

You can then import your style module and use it in your markup (using the cx utility, to generate a string from a smorgasbord of class names):

import { cx } from 'https://deno.land/x/frugal/styled.ts';
import { item, list, selected } from './MyComponent.style.ts';

export function MyComponent(items: any[]) {
    return `<ul className="${cx(list)}"}>
        ${items.map(item => {
            return `<li className="${cx(item, item.isSelected && selected)}>
        })}
    </ul>`;
}

The style loader will provide to the loaderContext a string containing the url of the generated css file. You can therefore get the url of the css file in the getContent function of your page descriptor:

export function getContent(
    { loaderContext }: frugal.GetContentParams<Path, Data>,
) {
    const cssFileUrl = loaderContext.get('style');

    // ...
}

Usage

The className utility will generate unique class names. You can control the prefix of the class name, for easier debugging. Those class will be ordered by declaration order:

  • within the same module, class names declared first are outputted first
  • amongst modules, class names from modules imported first are outputted first.

The createGlobalStyle utility will generate global style, without scoping.

The atImport utility allows you to include an external stylesheet by url.

The keyframes utility allows you to declare @keyframes rules for animations.

Both global styles and imported stylesheet will be outputted first. Then all keyframes and finally scoped class names.

Transformer

The style loader has no notion of css syntax, it simply aggregates what is given to him. This means that you can "customize" the flavor of css you want, via the transform function. Here for example, we use the stylis preprocessor:

import * as frugal from 'https://deno.land/x/frugal/core.ts';
import { StyleLoader } from 'https://deno.land/x/frugal/loader_style.ts';
import * as stylis from 'https://esm.sh/stylis@4.0.13';

import * as myPage from './pages/myPage.ts';

const self = new URL(import.meta.url);

export const config: frugal.Config = {
    self,
    outputDir: './dist',
    pages: [
        frugal.page(myPage),
    ],
    loader: [
        new StyleLoader({
            test: (url) => /\.style\.ts$/.test(url.toString()),
            transform: (content) => {
                return stylis.serialize(
                    stylis.compile(content),
                    stylis.middleware([stylis.prefixer, stylis.stringify]),
                );
            },
        }),
    ],
};

With this setup, the following module, using non-standard syntax (nested selectors):

import { className } from 'https://deno.land/x/frugal/styled.ts';

export const item = className('item').styled`
    color: red;
`;

export const list = className('list').styled`
    padding: 0;

    ${item} {
        color: blue;
    }
`;

Should output the following style

.item-l6cy2y {
    color: red;
}

.list-vngyoe {
    padding: 0
}

.list-vngyoe .item-l6cy2y {
    padding: 0
}

Interaction with script loader

If you import a style module in a script (if you have to toggle a class name generated in the style module, using JS for example) the script loader will bundle the style module without complaining. This means that your bundle will now contain numerous non-compressible strings describing some styles that are useless to your bundle, since those styles are already in the .css file generated by the style loader.

When bundling a style module, we only want to bundle the generated class names, not what was used to generate them. For this style module:

import { className } from 'https://deno.land/x/frugal/styled.ts';

export const item = className('item').styled`
    color: red;
    padding: 10em;
    width: 100px
    height: 30px;
    position: absolute;
`;

const base = className('base').styled`
    font-size: 1rem;
    font-weight: 300;
    transform: translate(-50%, 50%);
`;

export const list = className('list').extends(base).styled`
    padding: 0;
`;

We want to bundle only this:

export const item = 'item-l6cy2y';
export const list = 'list-vngyoe base-ucdg1u';

In order to do so, the script loader accepts some transformers. Each transformer will run on modules matching a test function, and transform the code of the module before bundling. You can use the styleTransformer exposed by the style loader module to transform any style modules:

import { ScriptLoader } from 'https://deno.land/x/frugal/loader_script.ts';
import { StyleLoader, styleTransformer } from 'https://deno.land/x/frugal/loader_style.ts';

function isStyleModule(url: string|URL) {
    return /\.style\.ts$/.test(url.toString())
}

const config = {
    //...
    loaders: [
        new StyleLoader({
            test: isStyleModule,
        })
        new ScriptLoader({
            test: (url) => /\.script\.ts$/.test(url.toString()),
            formats: ['esm'],
            transformers: [{
                test: isStyleModule,
                transform: styleTransformer
            }],
        })
    ]
}