npm packages
Browser code can use packages from npm: charts, maps, date pickers, editors, confetti. You don't need Node.js. PyWeb downloads the package from the npm registry, keeps only the files the browser loads, and serves them from your app.
#Add a package
Run this in the folder that holds app.pyweb:
pyweb add chart.js/auto @kurkle/color@0.3.4 1 file(s) (dependency)
chart.js@4.5.1 3 file(s)
pyweb.lock: 2 package(s); files in static/vendor/pyweb add does four things:
- It picks the newest version that matches the range you give (
pyweb add chart.js@^4,pyweb add lit@3.2), or the latest one. - It downloads the package and checks it against the registry's sha512 checksum.
- It follows the
importstatements from the package's browser entry point and copies only the files they reach intostatic/vendor/<name>@<version>/. Dependencies are installed the same way. - It records versions, checksums and file lists in
pyweb.lock.
Commit pyweb.lock and static/vendor/. Your app then builds and runs without network access. Many packages keep their files in a dist/ folder, so if your .gitignore has a bare dist/ line, change it to /dist/ (only the build output at the top) or the vendored files won't be committed. Projects made with pyweb new already do this. Other commands:
pyweb add reinstall exactly what pyweb.lock lists
pyweb add chart.js@^4 add, or change the requested range
pyweb remove chart.js remove a package and the files only it usedLocked versions are kept until you ask for a different range, so running pyweb add again never upgrades a package by surprise.
#Use it in browser code
npm(package, export) binds a name to one of the package's exports at the top of the file:
from pyweb import App, npm
Chart = npm("chart.js/auto") # the default export
confetti = npm("canvas-confetti")
dates = npm("date-fns", "*") # the whole module
app = App()
@app.page("/")
def Sales():
canvas = None
chart = None
total = 3
until = ""
def on_mount():
chart = Chart(canvas, {"type": "bar",
"data": {"labels": ["Mon", "Tue", "Wed"],
"datasets": [{"label": "Orders", "data": [1, 2, 3]}]}})
def add():
total += 1
chart.data.labels.append("Thu")
chart.data.datasets[0].data.append(total)
chart.update()
until = dates.format(dates.addDays(dates.startOfToday(), total), "EEE d MMM")
confetti(particleCount=80, spread=60)
<main>
<canvas ref={canvas}></canvas>
<button onclick={add}>Add a day</button>
<p>{total} orders, planned until {until}</p>
</main>What this compiles to
// Sales.js
import $npm_canvas_confetti_default_07a066 from "canvas-confetti";
import $npm_chart_js_auto_default_336847 from "chart.js/auto";
import * as $npm_date_fns___c92fd6 from "date-fns";
function Sales($s) {
const canvas = $signal("canvas" in $s ? $s["canvas"] : null);
const chart = $signal("chart" in $s ? $s["chart"] : null);
const total = $signal("total" in $s ? $s["total"] : 3);
const until = $signal("until" in $s ? $s["until"] : "");
function on_mount() {
chart($py.call($npm_chart_js_auto_default_336847, [canvas(), {"type": "bar", "data": {"labels": ["Mon", "Tue", "Wed"], "datasets": [{"label": "Orders", "data": [1, 2, 3]}]}}]));
}
function add() {
total($py.add(total(), 1));
$py.mut(chart, ["data", "labels"], ($v) => $py.m($v, "append", "Thu"));
$py.mut(chart, ["data", "datasets", 0, "data"], ($v) => $py.m($v, "append", total()));
$py.mut(chart, [], ($v) => $py.m($v, "update"));
until($py.callm($npm_date_fns___c92fd6, "format", [$py.callm($npm_date_fns___c92fd6, "addDays", [$py.callm($npm_date_fns___c92fd6, "startOfToday", []), total()]), "EEE d MMM"]));
$py.call($npm_canvas_confetti_default_07a066, [], {"particleCount": 80, "spread": 60});
}
$onMount(on_mount);
return [$h("main", null, () => [
$h("canvas", {"$ref": canvas}),
$h("button", {"onclick": add}, () => [$t("Add a day")]),
$h("p", null, () => [
$dyn(() => total()),
$t(" orders, planned until "),
$dyn(() => until())
])
])];
}
$mount("Sales", Sales);page Sales route=/
browser canvas reactive state: set to an element by ref= (line 31); literal initial value
browser chart reactive state: assigned in on_mount(); literal initial value
browser total reactive state: assigned in add(); literal initial value
browser until reactive state: assigned in add(); literal initial value
browser on_mount event handler (compiled to JavaScript)
browser add event handler (compiled to JavaScript)<main>
<canvas></canvas><button>Add a day</button>
<p>3 orders, planned until </p>
</main>The rules:
- Calling a class constructs it.
Chart(canvas, config)becomesnew Chart(canvas, config)in JavaScript, so you write Python call syntax for both functions and classes. - Keyword arguments become an options object.
confetti(particleCount=80, spread=60)callsconfetti({particleCount: 80, spread: 60}), the convention most JavaScript libraries use. - Objects are used as they are. Attributes, methods and lists on a package's objects are the real JavaScript ones. When browser code changes one that a page variable holds (
chart.value = 5), the page re-renders anything that shows it. - npm values only exist in the browser. Use them in event handlers,
on_mountand other browser functions. Using one directly in markup is a compile error, because the server renders markup first and has no copy of the package. Calling one from server code raises an error that says so. - Pages load only what they use. Each page imports just the packages its own code calls, so adding a charting library doesn't slow down pages without charts. For a large module such as
date-fns, a subpath (npm("date-fns/format")) loads only the one function instead of the whole library.
If a package isn't installed, the compiler points at the npm(...) line and tells you which pyweb add command to run.
#Elements: ref=
Many libraries need a DOM element to draw into. ref={name} sets a page variable to the element once it exists, which is in time for on_mount:
from pyweb import App
app = App()
@app.page("/")
def Focus():
box = None
def on_mount():
box.focus()
<input ref={box} placeholder="Focused on load" />What this compiles to
// Focus.js
function Focus($s) {
const box = $signal("box" in $s ? $s["box"] : null);
function on_mount() {
box().focus();
}
$onMount(on_mount);
return [$h("input", {"$ref": box, "placeholder": "Focused on load"})];
}
$mount("Focus", Focus);page Focus route=/
browser box reactive state: set to an element by ref= (line 13); literal initial value
browser on_mount event handler (compiled to JavaScript)<input placeholder="Focused on load">#Web components
Packages that define custom elements, such as Shoelace or Lit components, work too. Import the module for its side effect and use the tags in markup:
from pyweb import App, npm
shoelace = npm("@shoelace-style/shoelace/dist/components/button/button.js", "*")
app = App()
@app.page("/")
def Buttons():
clicks = 0
def on_mount():
print("loaded", shoelace)
def click():
clicks += 1
<sl-button variant="primary" onclick={click}>Clicked {clicks} times</sl-button>What this compiles to
// Buttons.js
import * as $npm__shoelace_style_shoelace_dist_components_button_button_js___0c7112 from "@shoelace-style/shoelace/dist/components/button/button.js";
function Buttons($s) {
const clicks = $signal("clicks" in $s ? $s["clicks"] : 0);
function on_mount() {
$py.print("loaded", $npm__shoelace_style_shoelace_dist_components_button_button_js___0c7112);
}
function click() {
clicks($py.add(clicks(), 1));
}
$onMount(on_mount);
return [$h("sl-button", {"variant": "primary", "onclick": click}, () => [
$t("Clicked "),
$dyn(() => clicks()),
$t(" times")
])];
}
$mount("Buttons", Buttons);page Buttons route=/
browser clicks reactive state: assigned in click(); literal initial value
browser on_mount event handler (compiled to JavaScript)
browser click event handler (compiled to JavaScript)<sl-button variant="primary">Clicked 0 times</sl-button>A page imports a package only if its code uses the bound name, which is why on_mount mentions shoelace here.
#Which packages work
Packages that ship ES modules for browsers work: most modern UI, charting, date, maths, animation and editor libraries. pyweb add explains the problem when one doesn't:
- CommonJS only (
module.exports,require()): look for an ES module build, often a package with-esoresmin its name (lodash-esinstead oflodash). - Node.js modules (
fs,path,crypto, ...): the package is meant for servers. Do that work in an@serverfunction in Python. - Version conflicts: PyWeb installs one version of each package. If two packages need incompatible versions of a dependency, it tells you which ones.
Packages are served by your app, so the default Content Security Policy (script-src 'self') still applies. The import map that tells the browser where each package lives is allowed by its hash.
#Editor support
pyweb lsp reads pyweb.lock. Hovering a name bound with npm() shows the installed version and, when the package ships TypeScript declarations, the signature (class Chart(item: ChartItem, config: ...)). Inside npm(" it completes installed packages, and after name. on a whole-module binding it completes the module's exports.
pyweb dts FILE.d.ts turns TypeScript declarations into Python dataclasses, for typing data that a package hands you.
#Building and deploying
pyweb build copies pyweb.lock and static/vendor/ into dist/. With --production your page code is minified and hashed as usual; vendored files are served as they are, with long-lived caching because their URLs include the version.