npm scripts reference
Every entry in package.json scripts, what it runs, what it writes, and when it applies.
prepare
huskyLifecycle script; npm runs it automatically after npm install. Installs the git hooks in .husky/:
| Hook | Command |
|---|---|
pre-commit | npx lint-staged |
commit-msg | npx --no -- commitlint --edit "$1" |
lint-staged (configured in package.json) runs eslint --fix on staged .ts, .tsx, .js, .mjs, .cjs files, and prettier --write on those plus .json, .md, .css, .xhtml.
commitlint uses commitlint.config.mjs: @commitlint/config-conventional with type-enum narrowed to feat, fix, improve, hotfix, chore, docs, test. A commit with any other type is rejected.
clean:profile
node scripts/clean-dev-profile.mjsTwo cleanups against this checkout's dev profile. The path comes from an exported ZOTERO_PLUGIN_PROFILE_PATH if there is one, otherwise from .env -- the same precedence the scaffold uses, so the script always aims at the instance npm start actually launched:
- Kills the Zotero holding this checkout's dev profile, and nothing else. It reads
ps -ww -e -o pid=,args=, keeps the processes whoseargv[0]ends inzoteroorzotero-bin, and among those kills the ones carrying-profile <resolved profile path>— the argumentzotero-plugin servelaunches with (ZoteroRunner.startZoteroInstanceinzotero-plugin-scaffold). Logsclean-dev-profile: killed the dev Zotero holding <path> (pid N). Zotero's-contentprocchildren carry no-profileof their own and are not matched; they exit with the parent. Matching onargv[0]rather than the whole line keeps a shell command that merely mentions the path from matching itself. - Takes the same resolved profile path, opens
<profile>/session.json, and drops every tab whosetypestarts withzoterolinkedmindmaps-(theconfig.addonRefvalue plus a hyphen). If it removed tabs and none of the survivors is markedselected, it selects the last remaining tab, then rewritessession.json.
If ZOTERO_PLUGIN_PROFILE_PATH is set neither in the environment nor in .env, the script logs clean-dev-profile: ZOTERO_PLUGIN_PROFILE_PATH not set anywhere, skipping the process check and session.json cleanup and does neither. Both steps identify the profile by that path, and killing a Zotero without it would be a guess at which one is yours. A missing session.json is not an error.
Run it directly when you want the cleanup without launching Zotero.
prestart
node scripts/clean-dev-profile.mjsIdentical to clean:profile. npm runs it automatically before start, so npm start always begins with no stale process and no stale mindmap tab in the session file.
start
zotero-plugin serveBuilds src/ and addon/ into .scaffold/build/, launches the Zotero binary at ZOTERO_PLUGIN_ZOTERO_BIN_PATH against the profile at ZOTERO_PLUGIN_PROFILE_PATH with --purgecaches no-remote, installs the build as a temporary plugin, and then watches the source directories. An edit under src/ or addon/ triggers a rebuild and reload without restarting Zotero.
Devtools are on (server.devtools defaults to true) and the profile is created if it does not exist (server.createProfileIfMissing defaults to true). Requires .env; see development-setup.md.
Do not run two npm start instances against the same profile path.
start:headless
env -u WAYLAND_DISPLAY xvfb-run -a npm startThe same serve on a virtual display, so the dev Zotero does not take over the desktop. An agent working unattended uses this; a human watching the UI uses npm start.
xvfb-run alone does not make Zotero headless on a Wayland session. The Zotero launcher exports MOZ_ENABLE_WAYLAND=1, so Gecko connects to the compositor named by the inherited WAYLAND_DISPLAY and paints on the real screen while DISPLAY points at an Xvfb nothing ever draws on, silently. The launcher's own export cannot be overridden from outside, so removing WAYLAND_DISPLAY is the lever. Measured 2026-09-04: under a bare xvfb-run -a the Zotero process still carried WAYLAND_DISPLAY=wayland-0 and the Xvfb root had 0 children; with it unset the same probe listed 18 windows.
To check which display a run actually used, read the Zotero process's own environ for WAYLAND_DISPLAY and count children of the Xvfb root with xwininfo -root -children. Pass XAUTHORITY from the Xvfb process's -auth argument or the query fails on the cookie rather than telling you anything.
build
zotero-plugin build && tsc --noEmitzotero-plugin build writes to .scaffold/build/:
.scaffold/build/addon/withaddon/**/*.*copied across and the__addonName__-style tokens in those files substituted fromzotero-plugin.config.tsbuild.define..scaffold/build/addon/content/scripts/zoterolinkedmindmaps.js, the esbuild bundle ofsrc/index.ts(bundled, targetfirefox115)..scaffold/build/addon/manifest.json, merged with generatedapplications.zotero.idandupdate_url.- A packed
.xpiandupdate.json/update-beta.json.
It also regenerates two typings files in place from the Fluent and prefs sources: typings/i10n.d.ts and typings/prefs.d.ts. Expect those to show up as working-tree changes after a build.
tsc --noEmit then type-checks using the root tsconfig.json, which extends zotero-types/entries/sandbox/ (that preset sets "strict": true) and has include: ["src", "typings"].
test/ is not in that include list, so npm run build still does not type-check the test suite. npm run typecheck is what covers both: it runs tsc --noEmit against the root config and then tsc --noEmit -p test against test/tsconfig.json, which extends it. scripts/verify.sh runs that as its own stage, so a changed export signature that breaks a spec fails there rather than waiting for the live suite.
lint:check
prettier --check . && eslint .Reports formatting deviations and lint errors across the whole repo without writing anything. Exits nonzero on the first of the two that fails, so a Prettier failure hides ESLint output until it is fixed. This is what CI runs in the lint job.
Prettier options live in package.json: printWidth 80, tabWidth 2, endOfLine lf, plus htmlWhitespaceSensitivity: "css" for *.xhtml.
ESLint uses eslint.config.mjs: the @zotero-plugin/eslint-config preset with two overrides. @typescript-eslint/no-unused-vars is turned off for all **/*.ts (a TODO(TASK-3) in the config marks this as temporary, left from the template's example code). Files under scripts/** get Node globals, since they run under plain node rather than in the Zotero sandbox.
lint:fix
prettier --write . && eslint . --fixSame two tools, writing fixes. Reformats every Prettier-eligible file in the repo and applies ESLint autofixes. Anything ESLint cannot fix automatically is still reported and still exits nonzero.
typecheck
tsc --noEmit && tsc --noEmit -p testType-checks src/ and typings/ through the root tsconfig.json, then test/ through test/tsconfig.json, which extends it. The second half is the point: npm run build only ever checks the root config's include, so before this existed a changed export signature broke specs silently and only surfaced when the live suite ran. scripts/verify.sh runs this as its own stage, after lint and before Zotero is launched.
test
zotero-plugin testBuilds the plugin, bundles test/ into a temporary tester plugin, empties and recreates .scaffold/test/profile, .scaffold/test/data, and .scaffold/test/resource, then launches Zotero with both plugins installed and runs the Mocha suite inside it. Results stream back over HTTP to the CLI process on a free port chosen at launch; each pass, fail, and pending prints as it arrives, and the run ends with Test run completed - N passed or Test run completed - N passed, M failed.
Configured in zotero-plugin.config.ts under test, which sets only waitForPlugin: "() => Zotero.ZoteroLinkedMindmaps.data.initialized". Scaffold defaults supply the rest: entries "test", Mocha timeout 10000 ms, startupDelay 1000 ms, abortOnFail false, headless false, watch true.
Watch is the part that surprises people. Without --exit-on-finish or --no-watch, zotero-plugin test keeps Zotero open after the suite finishes and re-runs on changes to src/ or test/. It is a watch session, not a hang. Under CI (isCI), the runner forces watch to false and headless to true, so the process exits with 0 or 1 on its own.
test:fast
node scripts/run-tests.mjsSpawns npx zotero-plugin test with detached: true, pipes its stdout through unchanged, and watches for /Test run completed - (\d+) passed(?:, (\d+) failed)?/. On a match it logs run-tests: completion line seen, killing Zotero instead of waiting for its own exit (failed=N), SIGKILLs the child's process group, and exits 1 if any test failed, 0 otherwise.
Because it kills a process group rather than matching on process names, it leaves every Zotero it did not start alone, so it is safe to run alongside npm start or a test run in another worktree. Detaching also means Ctrl-C no longer reaches Zotero through the terminal, so the script traps SIGINT and SIGTERM and kills the group itself.
A 900-second timer starts at launch. If no completion line has appeared by then, the script prints run-tests: no completion line after 900s, treating as a hang and exits 1. The timer covers the whole suite, not the gap since the last line, because several tests wait on Zotero's own notification timing and cannot be shortened.
If the child exits before the completion line appears, the script exits with the child's code, or 1 if the code is null.
Use this instead of npm test for a one-shot run. See testing-howto.md for when the difference matters and testing-explanation.md for why the wrapper exists.
release
zotero-plugin releaseBumps the version (interactive prompt by default, preid beta), runs npm run build through release.bumpp.execute, commits as chore(publish): release v%s, tags v%s, and pushes both. GitHub publishing defaults to "ci", so running this locally stops at the push.
The tag push triggers .github/workflows/release.yml, which calls the reusable zotero-plugin-dev/workflows/.github/workflows/release-plugin.yml and runs this same script inside Actions. There it skips the bump, builds, generates a changelog, and creates the v<version> release with the .xpi attached plus the release release holding update.json and update-beta.json.
The release URLs are templated from repository.url in package.json. A wrong owner or repo there produces a release pointing at the wrong place, with no error. See configuration-reference.md.
releasing-howto.md walks through an actual release, including the arguments for a beta and what to do when a step fails.
update-deps
npm update --saveUpdates dependencies within their declared semver ranges and writes the new ranges back to package.json. Two dependencies are pinned to exact prerelease versions and will not move: zotero-plugin-toolkit at 5.1.0-beta.13 and zotero-types at 4.1.0-beta.4. Everything else uses a caret range.
Adding a new dependency is a separate decision, not something this script covers.