vite plugin for GNU LibreJS
compliance.
Written in TypeScript, Deno and distributed on JSR.
This README is the documentation.
GNU LibreJS blocks JavaScript that does not carry a recognised free-software
license notice. This plugin automates all three compliance methods LibreJS
supports, applied automatically during vite build.
In practice, this plugin does three things:
- wraps emitted JavaScript chunks with machine-readable
@licensecomments - emits a
jslicense-labels1web labels page - injects a
rel="jslicense"link into every built HTML page
For further reading about why LibreJS exists and why you may want to care:
// vite.config.ts
import { defineConfig } from "npm:vite";
import { librejsPlugin } from "jsr:@urutau-ltd/vite-plugin-librejs";
export default defineConfig({
plugins: [
librejsPlugin({
license: "AGPL-3.0-or-later",
sourceBase: "https://example.com/src/",
}),
],
});If you are using this from a Deno-managed project, the import above is enough.
If you are using a regular Vite project, the important bit is still the same:
the plugin itself comes from JSR and Vite comes from npm:vite.
The smallest useful setup is:
import { defineConfig } from "npm:vite";
import { librejsPlugin } from "jsr:@urutau-ltd/vite-plugin-librejs";
export default defineConfig({
plugins: [
librejsPlugin({
license: "MIT",
}),
],
});That will:
- add inline LibreJS license comments to emitted JS chunks
- generate
about/javascript.html - inject a link to that page in built HTML
If your code is copyleft and you want proper @source links too:
import { defineConfig } from "npm:vite";
import { librejsPlugin } from "jsr:@urutau-ltd/vite-plugin-librejs";
export default defineConfig({
plugins: [
librejsPlugin({
license: "AGPL-3.0-or-later",
sourceBase: "https://example.com/src/",
}),
],
});Wraps every emitted JS chunk with the machine-readable comment pair LibreJS checks:
// @license magnet:?xt=urn:btih:...&dn=agpl-3.0.txt AGPL-3.0-or-later
// @source https://example.com/src/main.js
(() => {/* minified code */})();
// @license-endThe hook runs after esbuild/terser minification, so the comment survives
without being stripped, something heretical for the JS devs, which makes it even
cooler. The @source line is injected automatically when sourceBase is set,
satisfying the copyleft requirement to provide access to source code.
This currently applies to emitted .js, .mjs and .cjs chunks.
This emits an HTML asset at weblabelsPath (by default about/javascript.html)
containing the jslicense-labels1 table:
<table id="jslicense-labels1">
<tr>
<td><a href="../assets/main-A1b2.js">main-A1b2.js</a></td>
<td>
<a href="https://www.gnu.org/licenses/agpl-3.0.html">
GNU Affero General Public License version 3 or later
</a>
</td>
<td><a href="https://example.com/src/main.js">main.js</a></td>
</tr>
</table>LibreJS falls back to this table when a script has no inline comment, or when something in front of your app strips or rewrites comments.
One behavior detail that matters: script links in the generated table are relative to the web labels page itself. This means nested output layouts work correctly instead of assuming everything lives at web root.
Appends to the <body> of every HTML page:
<a
href="../about/javascript.html"
rel="jslicense"
style="font-size: 0.8em; opacity: 0.7"
>
JavaScript license information
</a>The rel="jslicense" attribute is how LibreJS discovers the web labels page for
a given site.
The injected href is also relative to each HTML file, which matters if your
build emits pages into nested directories.
Here is a more realistic setup where your own code is AGPL, but a vendor chunk needs a different license label and a different source link:
import { defineConfig } from "npm:vite";
import { librejsPlugin } from "jsr:@urutau-ltd/vite-plugin-librejs";
export default defineConfig({
build: {
sourcemap: false,
},
plugins: [
librejsPlugin({
license: "AGPL-3.0-or-later",
sourceBase: "https://example.com/src/",
weblabelsPath: "about/javascript.html",
licensePageTitle: "JavaScript license information",
chunks: {
vendor: {
license: "MIT",
source:
"https://registry.npmjs.org/some-dep/-/some-dep-1.0.0.tgz",
},
},
}),
],
});This is not magic. The plugin can only label what Vite emits, and the source URLs you provide still need to actually exist and be accessible.
interface ChunkLicense {
license?: string;
magnet?: string;
source?: string;
}
interface LibreJSOptions {
license: string;
magnet?: string;
sourceBase?: string;
weblabelsPath?: string;
inlineComments?: boolean;
weblabels?: boolean;
injectLicenseLink?: boolean;
licensePageTitle?: string;
chunks?: Record<string, ChunkLicense>;
}The default SPDX identifier applied to every JS chunk.
Examples:
AGPL-3.0-or-laterGPL-3.0-or-laterMITApache-2.0
This must either:
- exist in the built-in license map
- or be paired with a
magnetoption
If not, plugin creation throws immediately and the build fails early.
Custom LibreJS magnet URI for the default license.
Use this when your chosen SPDX identifier is not in the built-in map:
librejsPlugin({
license: "EUPL-1.2",
magnet:
"magnet:?xt=urn:btih:e4ae48d8b484fce88a6cf1d33d4a4bfcbc1fdcf1&dn=eupl-1.2.txt",
});LibreJS identifies licenses by the SHA-1 info-hash embedded in the magnet URI, not by the human-readable SPDX string.
Base URL used to generate automatic @source links:
sourceBase: "https://example.com/src/";For a chunk named main, the generated source URL becomes:
https://example.com/src/main.jsThis is convenient for simple deployments, but it is based on chunk.name, not
the final hashed file name.
If you need exact per-file source URLs, use chunks[].source.
Output path for the web labels HTML page, relative to Vite's outDir.
Default:
about/javascript.htmlExamples:
about/javascript.htmllegal/javascript.htmljavascript.html
The plugin computes relative links against this path, so moving it deeper into the output tree is supported.
Whether to inject @license and @license-end comments into emitted JS chunks.
Default:
inlineComments: true;Disable it if you only want the web labels page:
librejsPlugin({
license: "MIT",
inlineComments: false,
});Whether to emit the jslicense-labels1 HTML page.
Default:
weblabels: true;Whether to append the rel="jslicense" link to built HTML pages.
Default:
injectLicenseLink: true;If you disable this, the page can still be generated, but LibreJS will not discover it automatically unless you add the link yourself.
Visible text of the injected license link and the <title> / <h1> of the
generated web labels page.
Default:
JavaScript license informationExample:
librejsPlugin({
license: "MIT",
licensePageTitle: "LibreJS metadata",
});Per-chunk overrides.
Each entry can override:
licensemagnetsource
Keys can be either:
- the final emitted
chunk.fileName, for exampleassets/vendor-Bca12345.js - the logical
chunk.name, for examplevendor
Example keyed by chunk name:
librejsPlugin({
license: "AGPL-3.0-or-later",
sourceBase: "https://example.com/src/",
chunks: {
vendor: {
license: "MIT",
source: "https://registry.npmjs.org/some-dep/-/some-dep-1.0.0.tgz",
},
},
});Example keyed by final emitted file name:
librejsPlugin({
license: "AGPL-3.0-or-later",
chunks: {
"assets/vendor-Bca12345.js": {
license: "MIT",
},
},
});When both are present, exact fileName matches win over chunk.name matches.
If a chunk declares a license that is not in the built-in map, that chunk must
also define a matching magnet. The plugin now fails fast instead of silently
reusing the global magnet, because silent wrong metadata is garbage.
For every emitted JS chunk, the plugin resolves metadata in this order:
- pick a per-chunk override, first by exact
fileName, then bychunk.name - resolve the SPDX identifier from
chunks[].licenseor the globallicense - resolve the magnet from
chunks[].magnet, built-in license metadata, or the global default magnet if the chunk still uses the global license - resolve the source URL from
chunks[].sourceorsourceBase
This means:
- the global
licenseis your default chunksis the escape hatch for special cases- unknown per-chunk licenses must bring their own
magnet
During vite build, the plugin hooks into:
renderChunkgenerateBundletransformIndexHtml
It is a build-time plugin. It is not doing anything interesting during dev server operation, and that is intentional.
The plugin name reported to Vite is:
vite-plugin-librejsThe plugin uses enforce: "post" so comment injection happens after minifiers
have already done their thing.
Built-in license support includes:
| SPDX Identifier | License Name |
|---|---|
AGPL-3.0-only |
GNU Affero General Public License version 3 |
AGPL-3.0-or-later |
GNU Affero General Public License version 3 or later |
GPL-2.0-only |
GNU General Public License version 2 |
GPL-2.0-or-later |
GNU General Public License version 2 or later |
GPL-3.0-only |
GNU General Public License version 3 |
GPL-3.0-or-later |
GNU General Public License version 3 or later |
LGPL-2.1-only |
GNU Lesser General Public License version 2.1 |
LGPL-2.1-or-later |
GNU Lesser General Public License version 2.1 or later |
LGPL-3.0-only |
GNU Lesser General Public License version 3 |
LGPL-3.0-or-later |
GNU Lesser General Public License version 3 or later |
MIT |
Expat (MIT) License |
Apache-2.0 |
Apache License 2.0 |
Artistic-2.0 |
Artistic License 2.0 |
BSD-3-Clause |
BSD 3-Clause License |
ISC |
ISC License |
MPL-2.0 |
Mozilla Public License 2.0 |
CC0-1.0 |
Creative Commons Zero v1.0 Universal |
Deprecated bare identifiers are also accepted for compatibility with older
package.json files:
AGPL-3.0GPL-2.0GPL-3.0LGPL-2.1LGPL-3.0
Canonical magnet links are sourced from: https://www.gnu.org/software/librejs/manual/html_node/Free-Licenses-Detection.html
If you need a license that is not listed here, supply a custom magnet. The
plugin does not try to be a universal SPDX resolver because that would be
pretentious and wrong.
Supported:
- Vite 6.x
- Vite 7.x
- Vite 8.x
Vite 7 and 8 moved more of their bundler surface toward Rolldown. This plugin
imports OutputBundle, OutputChunk, RenderedChunk and
NormalizedOutputOptions from rolldown directly because those types are not
consistently re-exported by vite across supported versions.
That import is type-level only. It is not there for decoration.
Everything is exported from mod.ts:
export { librejsPlugin } from "./plugin.ts";
export type { ChunkLicense, LibreJSOptions } from "./plugin.ts";
export { getLicense, LICENSES } from "./licenses.ts";
export type { LicenseInfo, LicenseMap } from "./licenses.ts";
export { generateWeblabelsHtml } from "./weblabels.ts";
export type { WeblabelEntry } from "./weblabels.ts";Creates the Vite plugin.
import { librejsPlugin } from "jsr:@urutau-ltd/vite-plugin-librejs";Immutable map of built-in SPDX identifiers to LibreJS metadata.
import { LICENSES } from "jsr:@urutau-ltd/vite-plugin-librejs";
console.log(LICENSES["MIT"]);Looks up one built-in license by SPDX identifier.
import { getLicense } from "jsr:@urutau-ltd/vite-plugin-librejs";
const info = getLicense("MIT");
// -> { magnet, label, url }Returns undefined for licenses not present in the built-in map.
Generates a complete standalone web labels document.
import { generateWeblabelsHtml } from "jsr:@urutau-ltd/vite-plugin-librejs";
const html = generateWeblabelsHtml([
{
scriptPath: "/assets/app.js",
scriptName: "app.js",
licenseLabel: "GNU General Public License version 3 or later",
licenseUrl: "https://www.gnu.org/licenses/gpl-3.0.html",
sourcePath: "https://example.com/src/app.js",
},
]);This is useful if you need custom build scripts outside Vite.
Because LibreJS supports both, and sites in the real world are messy. Inline comments are direct and nice. Web labels are a fallback and a discovery mechanism. Doing both is the most reliable option.
This plugin is meant for build output. The interesting behavior happens during
vite build.
Not always.
- For permissive licenses, maybe not.
- For copyleft licenses, you probably do want source links.
- For complex setups, use per-chunk
sourceinstead ofsourceBase.
Because the point of @source is to point to corresponding source code, not to
the built artifact. Using chunk.name is the simplest stable default.
If that mapping is wrong for your project, override it with chunks[].source.
Because silently inheriting the global magnet for a different license is wrong. Wrong compliance metadata is worse than a failing build.
Because nested output directories exist, and absolute-root assumptions break there. Relative links are the correct default for emitted static artifacts.
[vite-plugin-librejs] License "X" is not in the built-in map.
Supply a "magnet" URI in the plugin options.Your global license is unknown to the built-in map. Provide magnet.
[vite-plugin-librejs] Chunk "..." declares unsupported license "X".
Supply "chunks....magnet" ...One chunk override uses an SPDX identifier the plugin does not know. Add a
matching magnet for that specific chunk.
You are probably relying on sourceBase but your output naming does not map
cleanly to chunk.name. Use explicit chunks[].source values.
Check:
- that
injectLicenseLinkis enabled, or that you manually addedrel="jslicense" - that
weblabelsis enabled - that the generated path actually matches your deployed output
- that another transform or proxy is not rewriting the HTML
That is intentional. The plugin now emits relative links so the generated files work when pages are not served from root.
This repository now has tests for:
- inline comment injection
- chunk-name overrides
- unsupported per-chunk license failures
- relative web labels links
- relative HTML link injection
The package metadata is set for 1.0.0, and the repo includes Deno tasks for:
deno task checkdeno task fmtdeno task lintdeno task test
This project is under the terms of the GNU Affero General Public License version
3 or later (AGPL-3.0-or-later).