A Webpack plugin for userscript projects 🙈
npm i webpack-userscript -D
Import and configure the plugin in the webpack.config.js
as the following example.
const { UserscriptPlugin } = require('webpack-userscript');
module.exports = {
plugins: [new UserscriptPlugin(/* optionally provide more options here */)],
};
See UserscriptOptions for all configurations.
The main purpose of this plugin is to generate userscript headers, and prepend them as a comment block into output entry scripts whose names are conventionally ending with .user.js
.
There are several userscript engines on the internet and some of them call userscript headers in different names; but don't worry because they share the same concept and almost the same format.
Here are some references to headers definitions of userscript engines:
- TamperMonkey: userscript headers
- GreaseMonkey: metadata block
- GreasyFork: meta keys
- ViolentMonkey: metadata block
Besides prepending headers to entry scripts, it can optionally generate metadata files which are userscript files without codes; that is, they contain headers only. Metadata files are used to save bandwidth when checking updates. By convention, their names are ending with .meta.js
.
The concept of proxyscript is introduced by this plugin, unlike userscripts and metadata files which are commonly known. The name of a proxyscript should end with .proxy.user.js
. It is mainly designed to work around the caching behavior of userscript engines like TamperMonkey. It was a pain point for userscript developers who set up a development environment with Webpack Dev Server and require fresh reloads to test their scripts.
See more details in issue #63
It is worth mentioning that with ViolentMonkey you might experience a better reload story, according to a feedback in issue #63.
Headers can be provided directly as an object, a string referencing to a file, or a function returning an object of headers.
The plugin will also try to load initial headers from the following fields, name
, description
, version
, author
, homepage
and bugs
in package.json
.
Headers files are added as a file dependency; therefore, changes to headers files are watched by Webpack during developing in the watch mode.
The main purpose is to fix misspelling or wrong letter case.
updateUrl
=>updateURL
iconUrl
=>iconURL
icon64Url
=>icon64URL
installUrl
=>installURL
supportUrl
=>supportURL
downloadUrl
=>downloadURL
homepageUrl
=>homepageURL
Base URLs are resolved for downloadURL
and updateURL
.
If updateBaseURL
is not provided, downloadBaseURL
will be used; if metajs is disabled, updateURL
will point to the file of userjs.
Subresource Integrity is used to ensure the 3rd-party assets do not get mocked by the man in the middle.
URLs in @require
and @resource
tags can have their SSRIs be generated and locked in a SSRI lock file whose name is default to ssri-lock.json
.
Missing SSRIs will be computed right in the compilation, which indicates that developers have to ensure their 3rd-party assets to be trustable during compilation.
If one cannot ensure 3rd-party assets to be trustable, he can modify the lock file himself with trustable integrities of assets.
Note that the lock file should be commited into the version control system just like
package-lock.json
.
If there is no any @include
or @match
tag provided, a wildcard @match
, *://*/*
, is used.
The content of a proxyscript looks similar to a metajs except that its @require
tag will include an URL linked to its userjs file and it won't have any one of these tags, downloadURL
, updateURL
and installURL
.
Leaf values of headers can be interpolable templates. Template variables can be represented in a format, [var]
, just like how template strings in Webpack output options look like.
Possible template variables are as follows.
[name]
: chunk name[buildNo]
: build number starting from 1 at beginning of watch mode[buildTime]
: the timestamp in millisecond when the compilation starts[file]
: full path of the file- =
[dirname]
+[basename]
+[extname]
+[query]
- =
[filename]
: file path- =
[basename]
+[extname]
- =
[dirname]
: directory path[basename]
: file base name[extname]
: file extension starting with.
[query]
: query string starting with?
Note that
[buildNo]
starts from 0 and will increase during developing in the watch mode. Once exiting from the watch mode, it will be reset.
For example, one can embed the build time into @version
tag via the following configuration.
new UserscriptPlugin({
headers: {
version: '0.0.1-beta.[buildTime]'
},
})
Headers will be transformed and validated with the help of class-transformer
and class-validator
.
The configuration defaults to strict mode, which means extra tags are not allowed and type checking to headers values are performed.
One can provide headersClass
option to override the default Headers
class; but it is suggested to inherit from the original one.
Note that the
headersClass
is used for both main headers and i18n headers. Check the default implementation before customizing your own.
Headers in all locales are merged and rendered.
There are 2 useful options here, pretty
and tagOrder
.
The pretty
option is a boolean deciding whether to render the headers as a table or not.
The tagOrder
option is a precedence list of tag names which should be followed. Listed tags are rendered first; unlisted tags are rendered after listed ones, in ASCII order.
The following example can be used in development mode with the help of webpack-dev-server
.
webpack-dev-server
will build the userscript in watch mode. Each time the project is built, the buildNo
variable will increase by 1.
Notes:
buildNo
will be reset to 0 if the dev server is terminated. In this case, if you expect the build version to be persisted during dev server restarting, you can use thebuildTime
variable instead.
In the following configuration, a portion of the version
contains the buildNo
; therefore, each time there is a build, the version
is also increased so as to indicate a new update available for the script engine like Tampermonkey or GreaseMonkey.
After the first time starting the webpack-dev-server
, you can install the script via http://localhost:8080/<project-name>.user.js
(the URL is actually refered to your configuration of webpack-dev-server
). Once installed, there is no need to manually reinstall the script until you stop the server. To update the script, the script engine has an update button on the GUI for you.
webpack.config.dev.js
const path = require('path');
const { UserscriptPlugin } = require('webpack-userscript');
const dev = process.env.NODE_ENV === 'development';
module.exports = {
mode: dev ? 'development' : 'production',
entry: path.resolve(__dirname, 'src', 'index.js'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: '<project-name>.user.js',
},
devServer: {
contentBase: path.join(__dirname, 'dist'),
},
plugins: [
new UserscriptPlugin({
headers(original) {
if (dev) {
return {
...original,
version: `${original.version}-build.[buildNo]`,
}
}
return original;
},
}),
],
};
If you feel tired with firing the update button on TamperMonkey GUI, maybe you can have a try at proxy script.
A proxy script actually looks similar with the content of *.meta.js
except that it contains additional @require
field to include the main userscript. A proxy script is used since TamperMonkey has an option that makes external scripts always be update-to-date without caching, and external scripts are included into userscripts via the @require
meta field. (You may also want to read this issue, Tampermonkey/tampermonkey#767)
To avoid caching and make the main script always be updated after each page refresh, we have to make our main script "an external resource". That is where the proxy script comes in, it provides TamperMonkey with a @require
pointint to the URL of the main script on the dev server, and each time you reload your testing page, it will trigger the update.
Actually it requires 2 reloads for each change to take effect on the page. The first reload trigger the update of external script but without execution (it runs the legacy version of the script), the second reload will start to run the updated script.
I have no idea why TamperMonkey is desinged this way. But..., it's up to you!
To enable the proxy script, provide a proxyScript
configuration to the plugin constructor.
baseUrl
should be the base URL of the dev server, and the filename
is for the proxy script.
Note:
filename
will be interpolated.
After starting the dev server, you can find your proxy script under <baseUrl>/<filename>
. In the example below, assume the entry filename is index.js
, you should visit http://127.0.0.1:12345/index.proxy.user.js
to install the proxy script on TamperMonkey.
See Issue#63 for more information.
new WebpackUserscript({
// <...your other configs...>,
proxyScript: {
baseUrl: 'http://127.0.0.1:12345',
filename: '[basename].proxy.user.js',
},
});
I18n headers can be provided as an object, a string (a.k.a headers file) or a function (a.k.a headers provider), just like the main headers.
new UserscriptPlugin({
headers: {
name: 'this is the main script name'
},
i18n: {
// headers object
'en-US': {
name: 'this is a localized name'
},
},
})
new UserscriptPlugin({
headers: {
name: 'this is the main script name'
},
i18n: {
// headers file
'en-US': '/dir/to/headers.json' // whose content is `{"name": "this is a localized name"}`
},
})
new UserscriptPlugin({
headers: {
name: 'this is the main script name'
},
i18n: {
// headers provider
'en-US': (headers) => ({
...headers,
name: 'this is a localized name'
}),
},
})
With the above configurations will generate the following headers,
// ==UserScript==
// @name this is the main script name
// @name:en-US this is a localized name
// @version 0.0.0
// @match *://*/*
// ==/UserScript==