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:
{
"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.
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:
<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:
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.