Plugins

Scripts

By default, any javascript you write will be interpreted server-side to generate the page markup. To execute javascript in the browser, we could insert a <script> tag pointing to a script in the static/ folder. But there are several drawbacks :

  • If you have multiple scripts, you will have to bundle them in a separate process. This means that besides Frugal, you will have another process (like esbuild) running to bundle your scripts.
  • If the script source change during development, you'll have to refresh the browser manually to get the updated bundle once the bundling from the second process is done.

It could work for a small number of scripts that do not change often, but this does not scale. To help you, Frugal comes with a script plugin to handle them for you.

Basic usage

First you'll have to register the plugin :

frugal.config
...
import { script } from "https://deno.land/x/frugal@0.9.6/plugins/script.ts";

export default {
    ...
    plugins: [script()]
    ...
}
1
2
3
4
5
6
7
8

Now each js module imported by a page matching /.script.[tj]sx?$/ will be considered as scripts and will be bundled.

Conditional execution with import.meta.environment

Since your script needs to be imported by the page to be bundled, the top level of your script will be executed server-side. All usages of browser-specific API will fail.

To fix that, Frugal add an environment value in import.meta. This value indicates if the script is executed client-side or server-side :

main.script.ts
// this is executed both in the server and the browser.
export const ID = 'my-id'

if (import.meta.environment === 'client') {
    // this is executed only in the browser.
    document.getElementById(ID).style.color = 'blue'
}
1
2
3
4
5
6
7

That way, you can safely use browser-specific API while sharing data with the server. Here we export the ID variable to be used in the markup: script and markup can be kept in sync.

page.ts
import { ID } from './main.script.ts'

...

export function render() {
    return `<!DOCTYPE html>
<html>
    <body>
        <h1 id="${ID}">Hello world</h1>
    </body>
</html>`
}
1
2
3
4
5
6
7
8
9
10
11
12

Loading scripts

Frugal will bundle all your script using esbuild, and output them in a public directory in the outdir directory. But you still have to insert a <script> in your markup :

page.ts
...

export function render({ assets, descriptor }: RenderContext<typeof route>) {
    return `<!DOCTYPE html>
<html>
    <head>
        <script src="${assets['script'][descriptor]}" />
    </head>
    <body>
        <h1 id="${ID}">Hello world</h1>
    </body>
</html>`
}
1
2
3
4
5
6
7
8
9
10
11
12
13

The style plugin will set the "script" key of the assets object to an map of all bundle associated to their descriptor id. To get the bundle of the page, you'll have to use assets['script'][descriptor]

Configuration

The plugin accepts a configuration object :

type ScriptOptions = {
    filter: RegExp;
};
1
2
3

filter

The regexp used to determine if a module is a script. Defaults to /.script.[tj]sx?$/.

Css Module

Frugal load standard CSS stylesheet out of the box thanks to esbuild. But you might want to "componentize" your styles with CSS Modules. This plugin allows you to compile CSS Modules.

Basic usage

First, you'll have to register the plugin.

frugal.config
...
import { cssModule } from "https://deno.land/x/frugal@0.9.6/plugins/cssModule.ts";

export default {
    ...
    plugins: [cssModule()]
    ...
}
1
2
3
4
5
6
7
8

With the plugin registered, given the following CSS module :

style.module.css
.foo {
    color: red;
}

.bar {
    composes: foo;
    background: blue;
}
1
2
3
4
5
6
7
8

Importing it in our page module will give us a default import containing an object with all the classes defined :

page.ts
import style from './style.module.css'

console.log(style['foo']) // contains something like "l6pGIG_foo"
console.log(style['bar']) // contains something like "l6pGIG_bar l6pGIG_foo"
console.log(style['baz']) // undefined
1
2
3
4
5

Configuration

This plugin accepts a configuration object :

type ScriptOptions = {
    dashedIdents?: boolean;
    filter: RegExp;
    pattern?: string;
};
1
2
3
4
5

dashedIdents

Whether to rename dashed identifiers, e.g. custom properties (see lighnigcss doc for more information).

filter

The regexp used to determine if a module is a CSS module. Defaults to /\.module.css$/.

pattern

The pattern to use when renaming class names and other identifiers (see lighnigcss doc for more information)

Google Fonts optimization

This plugin will detect stylesheets from Google Fonts, download each font and transform the stylesheet to reference the downloaded local fonts.

Basic usage

First, you'll have to register the plugin :

frugal.config
...
import { googleFonts } from "https://deno.land/x/frugal@0.9.6/plugins/cssModule.ts";

export default {
    ...
    plugins: [googleFonts()]
    ...
}
1
2
3
4
5
6
7
8

Now any @import coming from Google Fonts will be modified by the plugin and inlined in your stylesheet.

Configuration

This plugin accepts a configuration object :

type ScriptOptions = {
    type?: "local" | "external";
};
1
2
3

type

Whether to download the fonts to make them local to your server or keep the url to Google Fonts' CDN. Defaults to "local".

SVG sprites

When using SVG icons, it might be beneficial to generate sprites: a large SVG file containing all your icons as <symbol> (learn more here about the technique). Frugal can automate the process for you.

Basic usage

First, you'll have to register the plugin :

frugal.config
...
import { svg } from "https://deno.land/x/frugal@0.9.6/plugins/svg.ts";

export default {
    ...
    plugins: [svg()]
    ...
}
1
2
3
4
5
6
7
8

Now you can import .svg files inside your modules and receive an object { href:string, viewBox:string } as a default export. You can use those values to generate a "reference" to the symbol inside the sprite sheet :

import icon from "./icon.svg";

const svg = `<svg viewBox="${icon.viewBox}">
    <use href="${icon.href} />
</svg>`;
1
2
3
4
5

Frugal will consider that two SVGs are in the same sprite sheet if they are in the same directory.

Configuration

This plugin accepts a configuration object :

type SvgOptions = {
    filter: RegExp;
};
1
2
3

filter

The regexp used to determine if a module is an SVG file. Defaults to /\.svg$/.

Svelte

Warning

This plugin is experimental. No extensive tests were done to ensure it worked in all conditions, but it should work in most cases.

If you wish to use Svelte as a UI framework, you will need this plugin to import .svelte files.

Basic usage

First, you'll have to register the plugin :

frugal.config
...
import { svelte } from "https://deno.land/x/frugal@0.9.6/plugins/svelte.ts";

export default {
    ...
    plugins: [svelte()]
    ...
}
1
2
3
4
5
6
7
8

Now you can import .svelte files inside your modules, and you'll receive as a default export either a Client Side Component or a Server Side Component depending if the .svelte file was imported in a script or not.

Configuration

This plugin accepts a configuration object :

type SvelteOptions = {
    filter: RegExp;
    preprocess?: PreprocessorGroup | PreprocessorGroup[];
};
1
2
3
4

filter

The regexp used to determine if a module is a Svelte file. Defaults to /\.svelte$/.

preprocess

If you need to register preprocessor for svelte.

Writing your own plugin

A Frugal plugin is an esbuild plugin with an extra step :

type Plugin = (context: PluginContext) => esbuild.Plugin;

interface PluginContext {
    config: FrugalConfig;
    url: (args: { namespace: string; path: string }) => URL;
    load: (specifier: URL) => Promise<Uint8Array>;
    output: (type: string, output: any) => void;
    collect: (filter: RegExp, metafile: esbuild.Metafile) => Asset[];
}

type Asset = {
    entrypoint: string;
    url: URL;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

A Frugal plugin is a function that receives a context object and returns an esbuild plugin. The context object exposes a few properties.

config

Contains the current frugal config.

url

A utility function to get an URL from the namespace and path given by esbuild.

load

A method that will load the file's content at the given URL (either via fetch or readFile).

output

The function used to set values in the assets object passed to page descriptor: type will be the key and output the value that will be set in assets.

collect

Given a filter and a metafile, this method will collect all matching files that esbuild output, keeping track of their entrypoints.