Tutorial 20 of 36 · for anyone who needs a third-party JavaScript library · Markdown

Add a JavaScript library

You'll add Chart.js with fsr add, draw a chart with it and see how FSR vendors the library and names it in the import map. Then you'll hit the missing-declarations failure and fix it.

Before you start

Install from crates.io. You need no Node and no package manager.

cargo install snapfire_compiler
cargo install snapfire_fsr_cli
fsr --version

Every command and screenshot on this page was captured with fsr 0.x.

Add it#

$ fsr add shop/app chart.js@4.4.7
added     chart.js                     chart.js/chart.bundle.mjs  202502 bytes

One file landed in app/vendor/chart.js/. The file is committed, so anyone who clones the repo already has the library.

fsr add also wrote the import map for you:

app/importmap.json
{
  "imports": {
    "react": "/static/js/vendor/react/react.bundle.mjs",
    "chart.js": "/static/js/vendor/chart.js/chart.bundle.mjs"
  }
}

That map is what the browser uses to resolve import { Chart } from "chart.js". It is also a build-time allowlist: a bare import with no entry fails the build instead of 404ing in someone's browser.

Add a subpath#

Chart.js wants its auto-registering entry, which is a separate specifier. Ask for it by name:

$ fsr add shop/app chart.js@4.4.7/auto
added     chart.js/auto                chart.js/auto.bundle.mjs  196 bytes

Now import { Chart } from "chart.js/auto" resolves.

Use it in a browser island#

A chart needs a canvas and a layout pass, so this one belongs in the browser. That's the other island mode from 020.

app/src/ui/Sales.tsx
import { useEffect, useRef } from "react";
import { Chart } from "chart.js/auto";

export function Sales({ labels, values }: { labels: string[]; values: number[] }) {
  const canvas = useRef<HTMLCanvasElement>(null);
  useEffect(() => {
    if (!canvas.current) return;
    const chart = new Chart(canvas.current, {
      type: "bar",
      data: { labels, datasets: [{ label: "Units", data: values }] },
    });
    return () => chart.destroy();
  }, [labels, values]);
  return <canvas ref={canvas} height={160} />;
}

Give it a page. The loader returns the figures:

app/routes/sales/page.loader.ts
import type { Ctx } from "@snapfire/fsr";

export async function load(_ctx: Ctx<"/sales">) {
  return {
    months: ["Jul", "Aug", "Sep"],
    units: [42, 57, 61],
  };
}

The page places the chart as a browser island, with no mode, because useEffect and a live canvas are exactly what server mode cannot do:

app/routes/sales/page.tsx
import type { SalesProps } from "@generated/client";
import { Island } from "@snapfire/fsr-client/react";
import { Sales } from "@src/ui/Sales";

export default function SalesPage({ months, units }: SalesProps) {
  return (
    <section>
      <h1>Sales</h1>
      <Island when="visible">
        <Sales labels={months} values={units} />
      </Island>
    </section>
  );
}

when="visible" means the module isn't fetched until the chart scrolls into view. The page around it stays server-rendered.

Where the types came from#

$ fsr types shop/app
types     chart.js                     chart.js 4.5.1
kept      react
kept      react-dom

FSR pulled the declarations out of the published package and wrote them under app/types/. Those are for your editor and for tsc and are never shipped. types/ is gitignored, so a fresh clone runs fsr types once.

A package with no declarations#

Try adding a package that publishes no usable declarations:

$ fsr add shop/app date-fns@4.1.0
added     date-fns                     date-fns/date-fns.bundle.mjs  8128 bytes

$ fsr types shop/app
missing   date-fns                     no `types` in the package and nothing on DefinitelyTyped

$ fsr build shop/app
no declarations for date-fns; run `fsr types`

The build refuses, because every package in the import map must have declarations, so an untyped import can't quietly become any across your codebase.

Either remove the package or declare the part of it you actually use. The second takes about four lines:

app/types/date-fns/index.d.ts
export declare function format(date: Date | number, pattern: string): string;
export declare function formatDistanceToNow(date: Date | number): string;

Save it as app/types/date-fns/index.d.ts and build again:

$ fsr build shop/app --verbose
types     chart.js               types/chart.js  chart.js 4.5.1
          date-fns               types/date-fns
typecheck tsc 7.0.2 from cache, clean

The date-fns row now has a path and no version, which marks those declarations as yours and not the registry's.

Mark a library external#

If a library should come from the page that embeds your component rather than from your bundle, mark it external:

$ fsr add shop/app my-widget@2.0.0 --external react

The bundle then imports react instead of carrying it. The embedding page supplies it.

Next up: 340. Add Google Analytics with a consent banner.