Tutorial 14 of 21 · for anyone who needs a real library and has no package.json

Add a library without npm

You'll add Chart.js to an app that has no node_modules, draw a chart with it and learn what FSR does instead of a package manager. Then you'll hit the one failure everybody hits and fix it.

Before you start

Straight from crates.io. No Node, 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/. No lockfile, no transitive tree, no install step for anyone who clones your repo, because the file is committed.

fsr add also wrote the import map for you:

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.

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} />;
}

Place it in browser mode, because useEffect and a live canvas are exactly what server mode cannot do:

tsx
<Island when="visible" mode="browser">
  <Sales labels={data.months} values={data.units} />
</Island>

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, never shipped. types/ is gitignored, so a fresh clone runs fsr types once.

The failure everybody hits

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. That's deliberate: every package in the import map must have declarations, so an untyped import can't quietly become any across your codebase.

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

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
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 is FSR telling you those declarations are yours rather than the registry's.

Leave something out on purpose

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.

No package.json

No package.json, no lockfile, no node_modules for CI to restore, no postinstall script. Every dependency is a committed file at a pinned version, so nothing resolves differently on another machine.

Next up: 095. Google Analytics behind a consent banner.

Built with SnapFire FSR. Pure Rust runtime, zero Node.js on the server.

Proudly Created by Excerion Sun LLC