{"id":15254656,"url":"https://github.com/guansss/webpack-monkey","last_synced_at":"2025-04-10T02:22:47.291Z","repository":{"id":154764834,"uuid":"610667102","full_name":"guansss/webpack-monkey","owner":"guansss","description":"A webpack plugin for developing your userscripts with a modern workflow, featuring HMR, meta generation, and more.","archived":false,"fork":false,"pushed_at":"2024-11-29T20:28:31.000Z","size":1065,"stargazers_count":38,"open_issues_count":4,"forks_count":3,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-03T00:06:17.651Z","etag":null,"topics":["tampermonkey","userscript","webpack"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/guansss.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":null,"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"lfx_crowdfunding":null,"custom":"https://raw.githubusercontent.com/guansss/.github/main/alipay.jpg"}},"created_at":"2023-03-07T08:35:06.000Z","updated_at":"2024-11-29T20:28:35.000Z","dependencies_parsed_at":"2023-11-09T11:53:00.846Z","dependency_job_id":"9db6bdec-005a-4645-b55a-2acf9eacd43c","html_url":"https://github.com/guansss/webpack-monkey","commit_stats":{"total_commits":153,"total_committers":2,"mean_commits":76.5,"dds":0.0065359477124182774,"last_synced_commit":"e18153546eb15c9de83e5ed9089a079f13c3b0ef"},"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guansss%2Fwebpack-monkey","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guansss%2Fwebpack-monkey/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guansss%2Fwebpack-monkey/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guansss%2Fwebpack-monkey/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/guansss","download_url":"https://codeload.github.com/guansss/webpack-monkey/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248143249,"owners_count":21054736,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["tampermonkey","userscript","webpack"],"created_at":"2024-09-29T23:05:28.674Z","updated_at":"2025-04-10T02:22:47.273Z","avatar_url":"https://github.com/guansss.png","language":"TypeScript","funding_links":["https://raw.githubusercontent.com/guansss/.github/main/alipay.jpg"],"categories":[],"sub_categories":[],"readme":"# webpack-monkey\n\n[![npm](https://img.shields.io/npm/v/webpack-monkey)](https://www.npmjs.com/package/webpack-monkey)\n[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/guansss/webpack-monkey/test.yaml?logo=github\u0026label=test)](https://github.com/guansss/webpack-monkey/actions)\n\nA webpack plugin for developing your userscripts with a modern workflow.\n\nFocusing on support for [Tampermonkey](https://www.tampermonkey.net/) and [Violentmonkey](https://violentmonkey.github.io/).\n\n- [Features](#features)\n- [Installation](#installation)\n- [Quick start](#quick-start)\n  - [1. Set up the project](#1-set-up-the-project)\n  - [2. Install the dev script](#2-install-the-dev-script)\n  - [3. Start developing](#3-start-developing)\n  - [4. Build for release](#4-build-for-release)\n  - [Advanced: multiple userscripts](#advanced-multiple-userscripts)\n  - [More examples](#more-examples)\n- [API / Configuration](#api--configuration)\n  - [`monkey(config)`](#monkeyconfig)\n  - [`module.hot.monkeyReload(options)`](#modulehotmonkeyreloadoptions)\n- [Meta generation](#meta-generation)\n- [External dependencies (@require)](#external-dependencies-require)\n- [External assets (@resource)](#external-assets-resource)\n- [CSS](#css)\n- [TypeScript](#typescript)\n- [Working with HMR](#working-with-hmr)\n- [Comparison with vite-plugin-monkey](#comparison-with-vite-plugin-monkey)\n- [Development Notes](DEVELOPMENT.md)\n\n## Features\n\n- **HMR (Hot Module Replacement)**: Easily apply changes without page reload.\n- **CSP bypassing**: No worries about CSP restrictions during development.\n- **Meta generation**: Generate userscript meta blocks programmatically.\n- **Multiple userscripts**: Develop multiple userscripts at the same time.\n- **Clean output**: Comply with the userscript hosting sites' no-minification requirement.\n\nThe modern workflow also allows:\n\n- **Compiling**: Use the latest JavaScript features and even TypeScript.\n- **Code splitting**: Split your code into multiple files, and share code between userscripts.\n\n## Installation\n\n```sh\nnpm install webpack-monkey\n\n# peer dependencies\nnpm install webpack webpack-dev-server\n```\n\n## Quick start\n\nHere is a preview of the final file structure:\n\n```\n.\n├── dist\n│   └── hello.user.js\n├── src\n│   ├── index.js\n│   └── meta.js\n├── webpack.config.js\n└── package.json\n```\n\n### 1. Set up the project\n\nCreate a new project and initialize it with npm:\n\n```sh\nnpm init -y\nnpm install webpack webpack-cli webpack-dev-server webpack-monkey\n```\n\nCreate `src/index.js`:\n\n```js\nGM_log(\"Hello world!\")\n\n// enable HMR, for more details please check the HMR section below\nif (module.hot) {\n  module.hot.monkeyReload()\n}\n```\n\nCreate `src/meta.js`:\n\n```js\nmodule.exports = {\n  name: \"Hello world\",\n  version: \"1.0.0\",\n  match: [\"*://example.com/\"],\n}\n```\n\nCreate `webpack.config.js`:\n\n```js\nconst path = require(\"path\")\nconst { monkey } = require(\"webpack-monkey\")\n\nmodule.exports = monkey({\n  entry: {\n    hello: \"./src/index.js\",\n  },\n  output: {\n    path: path.resolve(__dirname, \"dist\"),\n  },\n})\n```\n\nFinally, add the following scripts to `package.json`:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"webpack serve --mode development\",\n    \"build\": \"webpack --mode production\"\n  }\n}\n```\n\n### 2. Install the dev script\n\nRun `npm run dev`. When the dev server is ready, you should see a message like this:\n\n```\n[MonkeyPlugin] Dev script hosted at: http://localhost:xxxx/monkey-dev.user.js\n```\n\nNow open the URL in your browser and install the dev script.\n\n\u003e [!IMPORTANT]  \n\u003e Unless the dev server's _port_ has changed, you don't need to reinstall the dev script after running `npm run dev` next time.\n\n### 3. Start developing\n\nGo to `http://example.com` and open the console, you should see the message `\"Hello world!\"`.\n\nNow edit `index.js` and try to change the message text in `GM_log()`, save the file, and you'll see the new message printed in the console without page reload!\n\n### 4. Build for release\n\nRun `npm run build`, and the userscript will be built to `dist/hello.user.js`:\n\n```js\n// ==UserScript==\n// @name     Hello world\n// @grant    GM_log\n// @match    *://example.com/\n// @version  1.0.0\n// ==/UserScript==\n\n;(() =\u003e {\n  GM_log(\"Hello, world!\")\n})()\n```\n\nNote that the `GM_log` function is automatically added to `@grant`.\n\n### Advanced: multiple userscripts\n\nSet up your project as follows to develop multiple userscripts at the same time:\n\n```\n.\n├── dist\n│   ├── foo.user.js\n│   └── bar.user.js\n├── src\n│   ├── foo\n│   │   ├── index.js\n│   │   └── meta.js\n│   └── bar\n│       ├── index.js\n│       └── meta.js\n├── webpack.config.js\n└── package.json\n```\n\nAnd update `webpack.config.js` as:\n\n```js\nconst path = require(\"path\")\nconst { monkey } = require(\"webpack-monkey\")\n\nmodule.exports = monkey({\n  entry: {\n    foo: \"./src/foo/index.js\",\n    bar: \"./src/bar/index.js\",\n  },\n  output: {\n    path: path.resolve(__dirname, \"dist\"),\n  },\n})\n```\n\nNow you can run `npm run dev` and both userscripts will be served, no need to install the dev scripts again.\n\n### More examples\n\n- This project's [examples](examples)\n- This project's [playground](playground)\n- My own userscripts repo: [userscripts](https://github.com/guansss/userscripts) (in pure TypeScript)\n\n## API / Configuration\n\n### `monkey(config)`\n\nTakes a webpack config object and returns a cloned config object with plugins added and some options modified.\n\n\u003e [!NOTE]  \n\u003e The followings are some major changes to the config, for more details please check [the source code](src/node/monkey.ts).\n\u003e\n\u003e - Adds some sensible defaults for userscript development.\n\u003e - Adds `MonkeyPlugin` to the plugins list.\n\u003e - Replaces the default minimizer `TerserPlugin` with `MonkeyMinimizer`, which extends `TerserPlugin` with some extra features.\n\nThe monkey options are passed as the `monkey` property of the webpack config object:\n\n```js\nmodule.exports = monkey({\n  // normal webpack options\n  entry: \"./src/index.js\",\n  output: {\n    path: path.resolve(__dirname, \"dist\"),\n  },\n\n  // monkey options\n  monkey: {\n    debug: true,\n  },\n})\n```\n\n**Options**\n\n- **[`debug`](#debug)**\n- **[`meta.resolve`](#metaresolve)**\n- **[`meta.load`](#metaload)**\n- **[`meta.transform`](#metatransform)**\n- **[`require.provider`](#requireprovider)**\n- **[`require.lockVersions`](#requirelockversions)**\n- **[`require.exportsFromUnnamed`](#requireexportsfromunnamed)**\n- **[`require.resolve`](#requireresolve)**\n- **[`devScript.meta`](#devscriptmeta)**\n- **[`devScript.transform`](#devscripttransform)**\n- **[`beautify.prettier`](#beautifyprettier)**\n- **[`terserPluginOptions`](#terserpluginoptions)**\n\nYou'll find some options that can be a function with a context object as the second argument. The context object has the following type:\n\n```ts\ninterface OptionFunctionContext {\n  logger: WebpackLogger | Console\n}\n```\n\n#### `debug`\n\nType: `boolean`\\\nDefault: `false`\n\nWhen enabled, some debug messages will be printed in the console (just a few for now).\n\n#### `meta.resolve`\n\nType: `string | string[] | (arg: { entryName: string; entry: string }, context) =\u003e string | undefined | Promise\u003cstring | undefined\u003e`\\\nDefault: `[\"meta.js\", \"meta.ts\", \"meta.json\"]`\n\nThe path of meta file to be used by `meta.load` later. If an array, the first file that matches will be used.\n\nYou can pass a custom function as the resolver:\n\n```ts\nmonkey({\n  monkey: {\n    meta: {\n      resolve({ entry }) {\n        return path.resolve(path.dirname(entry), \"meta.txt\")\n\n        // if undefined, this entry will not be treated as a userscript\n        // return undefined\n      },\n    },\n  },\n})\n```\n\n#### `meta.load`\n\nType: `(arg: { file: string }, context) =\u003e UserscriptMeta | Promise\u003cUserscriptMeta\u003e`\\\nDefault: `require()`\n\nFunction to load the meta file and return the meta object. The default function uses `require()` to load the meta file with supported extensions: `.js`, `.ts`, `.json`.\n\n```ts\nmonkey({\n  monkey: {\n    meta: {\n      load({ file }) {\n        // read JSON from a meta.txt given by the above meta.resolve example\n        return JSON.stringify(fs.readFileSync(file, \"utf-8\"))\n      },\n    },\n  },\n})\n```\n\n#### `meta.transform`\n\nType: `(arg: { meta: UserscriptMeta }, context) =\u003e UserscriptMeta | Promise\u003cUserscriptMeta\u003e`\\\nDefault: `undefined`\n\nFunction to transform the meta object before using it for serving or building. Can be used to add or modify meta properties.\n\n#### `meta.generateFile`\n\nType: `boolean`\\\nDefault: `true`\n\nGenerating `*.meta.js` for lightweight update checking when self-hosting userscripts, e.g. GitHub Pages\n\n#### `require.provider`\n\nType: `\"jsdelivr\" | \"unpkg\"`\\\nDefault: `\"unpkg\"`\n\nWhen [importing modules using webpack externals](#4-webpack-externals-with-global-variable-most-flexible), the module will be resolved to `// @require \u003cCDN\u003e/\u003cmoduleName\u003e@\u003cversion\u003e`, where `\u003cCDN\u003e` is the provider's URL prefix (for example `https://cdn.jsdelivr.net/npm`), and `\u003cversion\u003e` is the version of the installed package, or, if not found, the version range specified in the project's `package.json` (can be controlled with `require.lockVersions`).\n\n#### `require.lockVersions`\n\nType: `boolean`\\\nDefault: `true`\n\nWhen using a CDN provider, this option controls whether to generate URLs with the versions of installed packages, or the version ranges specified in the project's `package.json`. For example:\n\n- `false`: `https://unpkg.com/jquery@^3.5.0` (version range specified in `package.json`)\n- `true`: `https://unpkg.com/jquery@3.6.0` (version actually installed)\n\n#### `require.exportsFromUnnamed`\n\nType: `boolean`\\\nDefault: `false`\n\nWhen importing an external module with URL, whether to allow using its exports without specifying a global variable name for it. For example:\n\n```js\nimport { ajax } from \"https://unpkg.com/jquery\"\n```\n\nThe above code will cause a runtime error because a global variable (`$`) is not specified along with the URL. In this case, webpack-monkey will print a warning in development mode and throw an error in production mode. Setting this option to `true` suppresses the warning and error.\n\n#### `require.resolve`\n\nType:\n\n```ts\ntype RequireResolver = (\n  arg: {\n    name: string // the module name\n    externalType: string // you would't need this if you don't know what it is\n    version?: string // the installed version, or undefined if not found\n    packageVersion?: string // the version specified in package.json, or undefined if not found\n    url?: string // the URL if the module is imported with URL, otherwise undefined\n  },\n  context: object,\n) =\u003e string | undefined | Promise\u003cstring | undefined\u003e\n```\n\nDefault: `undefined`\n\nCustom resolver for external dependencies. The function should return a URL string, or undefined if the module should not produce a `@require`.\n\n```js\nmonkey({\n  monkey: {\n    require: {\n      resolve({ name, url }) {\n        if (name.includes(\"dev-tools\")) {\n          return undefined\n        }\n\n        return url || \"https://unpkg.com/\" + name\n      },\n    },\n  },\n})\n```\n\n#### `devScript.meta`\n\nType: `UserscriptMeta | ((arg: { meta: UserscriptMeta }) =\u003e UserscriptMeta)`\\\nDefault: `undefined`\n\nMeta object for the dev script. If an object, it will be merged with the default meta object; if a function, it will be called with the default meta object as the argument, and the returned object will be used as a replacement.\n\n#### `devScript.transform`\n\nType: `(arg: { content: string }, context) =\u003e string`\\\nDefault: `undefined`\n\nFunction to transform the dev script content before serving. Can be used to add or modify the script content.\n\n#### `beautify.prettier`\n\nType: `boolean`\\\nDefault: `undefined`\n\nWhen enabled, the output will be formatted using [Prettier](https://prettier.io/) with your Prettier config. When undefined, it will be enabled if Prettier is found installed.\n\n#### `terserPluginOptions`\n\nType: `object`\\\nDefault: `undefined`\n\nCustom options for the minimizer [`TerserPlugin`](https://webpack.js.org/plugins/terser-webpack-plugin/).\n\n### `module.hot.monkeyReload(options)`\n\nWorks the same as `module.hot.accept()`, except that it'll **reload the whole userscript** instead of the changed module and the modules that depend on it. See [Working with HMR](#working-with-hmr) section for more details.\n\n**Options**\n\n- **[`ignore`](#ignore)**\n\n#### `ignore`\n\nType: `(string | RegExp)[] | (moduleId: string | number) =\u003e boolean`\\\nDefault: `[\"node_modules\"]`\n\nModules to ignore when reloading. Can be a list of strings or regular expressions, or a function that returns a boolean.\n\nNote that this only affects the modules that webpack-monkey tries to additionally reload, and does not affect the modules that webpack would reload according to its own rules.\n\n## Meta generation\n\nThe meta object is provided in a separate file. By default, webpack-monkey will look for `meta.js`, `meta.ts`, or `meta.json` in the same directory as the entry file, and load it with `require()`.\n\nYou can customize this behavior with the `monkey.meta.resolve` and `monkey.meta.load` options.\n\n\u003e [!NOTE]\n\u003e The meta file is evaluated in the Node.js environment, so you can `require()` other modules in it, and cannot use browser APIs such as `window`.\n\nThe meta fields have different types as shown below:\n\n\u003ctable\u003e\n\u003cthead\u003e\n  \u003ctr\u003e\n    \u003cth\u003eType\u003c/th\u003e\n    \u003cth\u003eFields\u003c/th\u003e\n    \u003cth\u003eExample\u003c/th\u003e\n    \u003cth\u003eExample output\u003c/th\u003e\n  \u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n  \u003ctr\u003e\n    \u003ctd\u003eBoolean\u003c/td\u003e\n    \u003ctd\u003e\u003ccode\u003enoframes\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\n\n```js\nmodule.exports = {\n  noframes: true,\n}\n```\n\n\u003c/td\u003e\n\u003ctd\u003e\n\n```js\n// @noframes\n```\n\n\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003eArray\u003c/td\u003e\n    \u003ctd\u003e\u003ccode\u003egrant\u003c/code\u003e\u003cbr\u003e\u003ccode\u003ematch\u003c/code\u003e\u003cbr\u003e\u003ccode\u003einclude\u003c/code\u003e\u003cbr\u003e\u003ccode\u003eexclude\u003c/code\u003e\u003cbr\u003e\u003ccode\u003erequire\u003c/code\u003e\u003cbr\u003e\u003ccode\u003eresource\u003c/code\u003e\u003cbr\u003e\u003ccode\u003econnect\u003c/code\u003e\u003cbr\u003e\u003ccode\u003ewebRequest\u003c/code\u003e\u003cbr\u003e\u003c/td\u003e\n\u003ctd\u003e\n\n\u003c!-- prettier-ignore --\u003e\n```js\nmodule.exports = {\n  match: [\n    \"*://example.com/\",\n    \"*://example.org/\"\n  ],\n  // can be a string if only one item\n  require: \"https://example.com/foo.js\",\n  // empty array will be omitted\n  resource: [],\n}\n```\n\n\u003c/td\u003e\n\u003ctd\u003e\n\n```js\n// @match   *://example.com/\n// @match   *://example.org/\n// @require https://example.com/foo.js\n```\n\n\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003eObject\u003cbr\u003e(I18n)\u003c/td\u003e\n    \u003ctd\u003e\u003ccode\u003ename\u003c/code\u003e\u003cbr\u003e\u003ccode\u003edescription\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\n\n```js\nmodule.exports = {\n  name: {\n    default: \"Hello world\",\n    \"zh-CN\": \"你好世界\",\n  },\n  // can be a string if only one item\n  description: \"Say hello to the world!\",\n}\n```\n\n\u003c/td\u003e\n\u003ctd\u003e\n\n```js\n// @name        Hello world\n// @name:zh-CN  你好世界\n// @description Say hello to the world!\n```\n\n\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003eString\u003c/td\u003e\n    \u003ctd\u003eAll the others\u003c/td\u003e\n\u003ctd\u003e\n\n```js\nmodule.exports = {\n  version: \"0.1\",\n}\n```\n\n\u003c/td\u003e\n\u003ctd\u003e\n\n```js\n//@version 0.1\n```\n\n\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\n## External dependencies (@require)\n\nThere are several ways to handle external dependencies, please choose the one that suits you best.\n\n1. [meta.require (simple)](#1-metarequire-simple)\n2. [import with URL (good for tree-shaking)](#2-import-with-url-good-for-tree-shaking)\n3. [Webpack externals with URL (good for tree-shaking and TypeScript)](#3-webpack-externals-with-url-good-for-tree-shaking-and-typescript) \u003c- recommended\n4. [Webpack externals with global variable (most flexible)](#4-webpack-externals-with-global-variable-most-flexible)\n\n### 1. meta.require (simple)\n\nThe simplest way is to put the URL in the `require` meta property:\n\n```js\n// meta.js\nmodule.exports = {\n  require: [\n    \"https://unpkg.com/jquery@3.6.0\",\n\n    // to load a specific file instead of the default entry, specify its full path\n    \"https://unpkg.com/lodash@4.17.21/lodash.min.js\",\n  ],\n}\n\n// index.js\n$(\".foo\").text(_.capitalize(\"hello world\"))\n```\n\n### 2. import with URL (good for tree-shaking)\n\nYou can directly import an external script with URL:\n\n```js\n// index.js\nimport \"https://unpkg.com/jquery@3.6.0\"\n\n$(\".foo\")\n```\n\nYou can also use [other import forms](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#forms_of_import_declarations). When using an import form other than _Side effect import_, the external script will be treated as a module, and you must provide a global variable name to reference the module in the format of `\"\u003cglobalVar\u003e@\u003cURL\u003e\"`, for example:\n\n```js\nimport jq, { ajax } from \"$@https://unpkg.com/jquery@3.6.0\"\n\njq(\".foo\").click(() =\u003e ajax(\"/api\"))\n```\n\nThe above code is roughly equivalent to the following:\n\n```js\nimport \"https://unpkg.com/jquery@3.6.0\"\n\nconst jq = $\nconst { ajax } = $\n```\n\n... except that the global variable `$` is treated as a module object, so webpack generates a [shim](https://webpack.js.org/guides/shimming/) for it - not much a thing to worry about though.\n\nNote that you don't need to specify an import name that is different from the global variable name, because webpack will (always) rename it to a longer form. For example, you can write `import $ from \"$@...\"`, and webpack will generate an output like `const external_$_namespaceObject = $`.\n\n### 3. Webpack externals with URL (good for tree-shaking and TypeScript)\n\n```js\n// webpack.config.js\nmodule.exports = {\n  externals: {\n    // same rule as in [2. import with URL], specify a global variable name if needed\n    jquery: \"https://unpkg.com/jquery@3.6.0\",\n    lodash: \"_@https://unpkg.com/lodash\",\n  },\n}\n\n// index.js\nimport \"jquery\"\nimport _ from \"lodash\"\n```\n\n### 4. Webpack externals with global variable (most flexible)\n\n```js\n// webpack.config.js\nmodule.exports = {\n  externals: {\n    jquery: \"$\",\n  },\n}\n\n// index.js\nimport \"jquery\"\n// or\nimport $ from \"jquery\"\n```\n\nIn this case, the module will be resolved to a URL according to the [`require`](#require) option. If `require.resolve` is specified, it will be used; otherwise, a CDN provider will be used according to `require.provider`.\n\n## External assets (@resource)\n\nNot well supported yet, coming soon. For now, you can put the asset URLs in the meta object, and manually fetch them during development:\n\n```js\n// meta.js\nmodule.exports = {\n  resource: [\"myText   https://example.com/my-text.txt\"],\n}\n\n// index.js\nasync function main() {\n  const myText =\n    process.env.NODE_ENV === \"development\"\n      ? await fetch(\"https://example.com/my-text.txt\").then((res) =\u003e res.text())\n      : GM_getResourceText(\"myText\")\n\n  console.log(myText)\n}\n```\n\n## CSS\n\nYou can import CSS files in your js files (check out [webpack's guide](https://webpack.js.org/guides/asset-management/#loading-css)), and webpack-monkey will bundle them into the userscript:\n\n**index.js**\n\n```js\nimport \"./styles.css\"\n\nGM_log(\"Hello world!\")\n```\n\n**styles.css**\n\n```css\nbody {\n  color: red;\n}\n```\n\n**dist/hello.user.js**\n\n```js\n// ==UserScript==\n// @name     Hello world\n// @grant    GM_log\n// @grant    GM_addStyle\n// @match    *://*/*\n// @version  1.0.0\n// ==/UserScript==\n\n;(() =\u003e {\n  GM_log(\"Hello, world!\")\n})()\n\nGM_addStyle(`\nbody {\n  color: red;\n}\n`)\n```\n\nThe CSS content will be wrapped in a `GM_addStyle()` at the end of the userscript, so if you or your users are inspecting the code, you can see the JavaScript code from the beginning, without having to scroll over a massive CSS block.\n\nBonus: when writing styles for your custom DOM elements, a good practice is to use [CSS Modules](https://github.com/css-modules/css-modules), which ensures that your class names will not conflict with other userscripts or the page itself. Check out [webpack's guide](https://webpack.js.org/loaders/css-loader/#modules).\n\n## TypeScript\n\nTypeScript is supported out of the box. Just set up the TypeScript environment as usual, and you're good to go. Check out [webpack's guide](https://webpack.js.org/guides/typescript/) if you're not familiar with it.\n\nNote that only `ts-loader` and `babel-loader` are tested. Other loaders are supposed to work, otherwise please let me know by opening an issue.\n\nBonus: install `@types/tampermonkey` to get the types for `GM_*`.\n\n### meta.ts\n\nYou can place the meta object in a `meta.ts` as well:\n\n```ts\n// note: Meta is an alias of UserscriptMeta\nimport { Meta } from \"webpack-monkey\"\n\nexport default {\n  version: \"1.0\",\n  name: \"Hello world\",\n} satisfies Meta\n```\n\nHowever, since this TypeScript file will be loaded with `require()`, you'll need to set up your environment to support it. Here's an example using [ts-node](https://typestrong.org/ts-node/):\n\n1. Install `ts-node`: `npm install ts-node`\n2. In your `tsconfig.json`, set `esModuleInterop: true` and add a `ts-node` object that sets `module: \"commonjs\"`:\n\n   ```json\n   {\n     \"compilerOptions\": {\n       \"esModuleInterop\": true\n     },\n\n     \"ts-node\": {\n       \"compilerOptions\": {\n         \"module\": \"commonjs\"\n       }\n     }\n   }\n   ```\n\n3. Do either of the following:\n\n   - Rename `webpack.config.js` to `webpack.config.ts` (maybe need some rewriting), then webpack will do the rest for you.\n   - Install `cross-env`. Then in your `package.json`, prepend a [node flag](https://typestrong.org/ts-node/docs/usage/#node-flags-and-other-tools) to the webpack commands:\n\n     ```diff\n       {\n         \"scripts\": {\n     -     \"dev\": \"webpack serve --mode development\",\n     -     \"build\": \"webpack --mode production\"\n     +     \"dev\": \"cross-env NODE_OPTIONS=\\\"-r ts-node/register --no-warnings\\\" webpack serve --mode development\",\n     +     \"build\": \"cross-env NODE_OPTIONS=\\\"-r ts-node/register --no-warnings\\\" webpack --mode production\"\n         }\n       }\n     ```\n\n### meta.js with JSDoc\n\nIf you find the above method too complicated, you can also use a `meta.js` with JSDoc comments:\n\n```js\n/**\n * @type {import(\"webpack-monkey\").Meta}\n */\nconst meta = {\n  name: \"Hello world\",\n  version: \"1.0.0\",\n}\n\nmodule.exports = meta\n```\n\nIf you're using WebStorm, the JSDoc type checking seems to be supported out of the box so you're good to go.\n\nIf you're using VSCode, some [extra steps](https://code.visualstudio.com/docs/nodejs/working-with-javascript#_type-checking-javascript) are needed.\n\n### Typing external dependencies\n\nTo type an external dependency, you are most likely to locally install it (`npm install xxx`), or if the types are not built-in, install only its types package (`@types/xxx`).\n\nThen, if using the #3 or #4 method of [handling external dependencies](#external-dependencies-require), TypeScript will automatically recognize the types when imported:\n\n```ts\nimport $ from \"jquery\"\n```\n\nAnd if using #1 or #2, you need to manually declare the global variables:\n\n```ts\n/// \u003creference types=\"jquery\" /\u003e\n\ndeclare global {\n  var $: JQueryStatic\n\n  // you can also use an inline import\n  var mitt: typeof import(\"mitt\").default\n}\n\n$(\".foo\")\nmitt()\n```\n\n## Working with HMR\n\nIf you don't know what HMR is, check out [webpack's introduction](https://webpack.js.org/concepts/hot-module-replacement/).\n\n### TL;DR\n\nwebpack-monkey extends webpack's HMR API with `module.hot.monkeyReload()` to reload all the modules in a userscript when any of them is changed.\n\n`module.hot.monkeyReload()` should be added to each userscript's **entry file**, and then you can clear side effects with `module.hot.dispose()` in any dependent modules. Example:\n\n**index.js**\n\n```js\nimport { foo } from \"./foo\"\n\nconst element = $(\"\u003cdiv\u003e\").text(foo).appendTo(\"body\")\n\nif (module.hot) {\n  module.hot.monkeyReload()\n  module.hot.dispose(() =\u003e {\n    element.remove()\n  })\n}\n```\n\n**foo.js**\n\n```js\nimport { bar } from \"./bar\"\n\nexport const foo = \"foo\"\n\nbar()\n```\n\n**bar.js**\n\n```js\nexport function bar() {\n  const timer = setInterval(() =\u003e console.log(Date.now()), 1000)\n\n  if (module.hot) {\n    module.hot.dispose(() =\u003e {\n      clearInterval(timer)\n    })\n  }\n}\n```\n\n### Why HMR?\n\nWhen developing userscripts, you'll most likely want to enable HMR to prevent page reloads, because you have no direct access to the target page's states and will lose them all when reloading, for example the position in an infinite scroll list, which is very annoying and slows down the development.\n\n### webpack's standard method\n\nYou've probably already used some frameworks such as React and Vue that have the HMR support built in for you, and it's pretty straightforward to change your _components' code_ and see the changes applied without page reload. However, if you change the _code outside of components_, a full page reload is performed because webpack cannot magically clear the outdated code's (possible) side effects.\n\nSo how to prevent full reloads without such frameworks? Webpack has a [HMR guide](https://webpack.js.org/guides/hot-module-replacement/) for the standard method, but it's too complicated to understand, and too hard to set up without making mistakes.\n\nLet's take the example from that guide to start with:\n\n```js\nimport _ from \"lodash\"\nimport printMe from \"./print.js\"\n\nfunction component() {\n  // ...\n\n  return element\n}\n\nlet element = component() // Store the element to re-render on print.js changes\ndocument.body.appendChild(element)\n\nif (module.hot) {\n  module.hot.accept(\"./print.js\", function () {\n    console.log(\"Accepting the updated printMe module!\")\n    document.body.removeChild(element)\n    element = component() // Re-render the \"component\" to update the click handler\n    document.body.appendChild(element)\n  })\n}\n```\n\nThis works, but accepting dependencies is super tedious and error-prone, because you have to figure out each dependency's side effects and the way to clear them, and you have to write each dependency's path without the help of IDE's auto-completion and auto-renaming, which is a nightmare for maintenance.\n\nSo a better way is to self-accept the current module and only clear itself's side effects:\n\n```diff\n  if (module.hot) {\n-   module.hot.accept(\"./print.js\", function () {\n-     console.log(\"Accepting the updated printMe module!\")\n-     document.body.removeChild(element)\n-     element = component() // Re-render the \"component\" to update the click handler\n-     document.body.appendChild(element)\n-   })\n+   module.hot.accept()\n+   module.hot.dispose(() =\u003e {\n+     document.body.removeChild(element)\n+   })\n  }\n```\n\nBut some other problems arise. Let's take another example:\n\n**index.js**\n\n```js\nimport { onResize } from \"./helper\"\n\nonResize(() =\u003e console.log(\"resized\"))\n\nif (module.hot) {\n  module.hot.accept()\n}\n```\n\n**helper.js**\n\n```js\nexport function onResize(listener) {\n  window.addEventListener(\"resize\", listener)\n}\n```\n\nHow to clear this side effect? It happens inside `helper.js`, but we cannot clear it with a `.dispose()` there, because if `index.js` is updated, then `helper.js` will not be reloaded, and its `.dispose()` will not be called. So we need to hoist this responsibility onto `index.js`:\n\n**index.js**\n\n```diff\n  import { onResize } from \"./helper\"\n\n- onResize(() =\u003e console.log(\"resized\"))\n+ const offResize = onResize(() =\u003e console.log(\"resized\"))\n\n  if (module.hot) {\n    module.hot.accept()\n+   module.hot.dispose(() =\u003e {\n+     offResize()\n+   })\n  }\n```\n\n**helper.js**\n\n```diff\n  export function onResize(listener) {\n    window.addEventListener(\"resize\", listener)\n\n+   return () =\u003e {\n+     window.removeEventListener(\"resize\", listener)\n+   }\n  }\n```\n\nThis is still quite annoying:\n\n1. When building for release, the `return () =\u003e ...` part is unused, but still bundled into the production code, making the size unnecessarily large.\n2. If `onResize()` is called multiple times, we'll have to keep track of all the returned functions and call them all in `.dispose()`.\n3. If we want `onResize()` to return something else, we'll have to make other dirty workarounds.\n\n### webpack-monkey's method\n\nwebpack-monkey provides a simple solution for this by extending webpack's HMR API. You only need to put these few lines in your userscript's **entry file**:\n\n```js\nif (module.hot) {\n  module.hot.monkeyReload()\n}\n```\n\n`module.hot.monkeyReload()` works the same as `module.hot.accept()` except that it'll **reload the whole userscript** instead of the changed module and the modules that depend on it, as shown below:\n\n![HMR diagram](docs/img/diagram-hmr.png)\n\nThis means you no longer have to worry about the relationship between modules, you only focus on clearing the side effects for each individual module.\n\nWith this feature, we can rewrite the above example as:\n\n**index.js**\n\n```diff\n  import { onResize } from \"./helper\"\n\n- const offResize = onResize(() =\u003e console.log(\"resized\"))\n+ onResize(() =\u003e console.log(\"resized\"))\n\n  if (module.hot) {\n-   module.hot.accept()\n-   module.hot.dispose(() =\u003e {\n-     offResize()\n-   })\n+   module.hot.monkeyReload()\n  }\n```\n\n**helper.js**\n\n```diff\n  export function onResize(listener) {\n    window.addEventListener(\"resize\", listener)\n\n-   return () =\u003e {\n-     window.removeEventListener(\"resize\", listener)\n-   }\n+   if (module.hot) {\n+     module.hot.dispose(() =\u003e {\n+       window.removeEventListener(\"resize\", listener)\n+     })\n+   }\n  }\n```\n\nNow `helper.js` will be reloaded when `index.js` is updated, so we can place the cleanup code immediately after the side effect code, which is very intuitive and easy to maintain.\n\nThe `if (module.hot)` block will also be removed when building for release, so no more unused code.\n\n## Comparison with vite-plugin-monkey\n\n[vite-plugin-monkey](https://github.com/lisonge/vite-plugin-monkey) is another great plugin for developing userscripts but with Vite. This plugin and webpack-monkey basically share the same goal - to develop userscripts with bundling and HMR support, but they have different approaches.\n\n### Vite vs. webpack\n\nVite is a next-generation build tool and is faster than webpack. However, Vite has a limitation that it only emits ES modules in development mode, meaning that they must be loaded with `\u003cscript type=\"module\"\u003e`, which will be blocked by the page's CSP if it has one. A notable example of CSP-enabled sites is `github.com`.\n\nThere is a workaround though - you can disable CSP with a browser extension. But it's a risky move because CSP is a security feature and is there for a reason, also you may forget to re-enable it after development. More importantly, if you are maintaining an open-source userscript, your contributors will be required to disable CSP as well, which is not a good experience.\n\nWebpack, on the other hand, emits CommonJS modules in development mode, which are capable to be evaluated in userscript scope and will never be affected by CSP.\n\n### Plugin differences\n\nvite-plugin-monkey is quite a mature project and has been well tested by the community. It has some features that webpack-monkey doesn't have yet, such as:\n\n- Loading external assets (@resource)\n- Greasemonkey support (GM.\\*)\n\nwebpack-monkey is still in early development but has some exclusive features:\n\n- CSP bypassing\n- Developing multiple userscripts by installing a single dev script\n- Reloading userscripts instead of the page when performing HMR with side effects\n\n### Is webpack-monkey a plagiarism?\n\nI have to put this here because I guess this kind of thought can easily come to one's mind.\n\nThe answer is no. They have many similar concepts and features, but these are some features that a decent userscript development tool _should_ have. It's like all planets are round.\n\nIn fact, a bit earlier than vite-plugin-monkey, I once spent a lot of time using Vite to set up a development environment for my own [userscripts](https://github.com/guansss/userscripts), which was somewhat inspired by [rollup-userscript-template](https://github.com/cvzi/rollup-userscript-template). It worked well until I found the CSP issue when developing a userscript for GitHub, and I was very frustrated because there's no choice but to disable CSP. So I migrated the framework to webpack and eventually decided to extract the code as a plugin.\n\nOkay but, is the name a plagiarism? I don't think so. I was thinking about _webpack-userscript_, but apparently it's already taken by another plugin (with quite different features so I'm not writing a comparison here). Then I came up with _webpack-monkey_, and then searched for name conflicts and discovered vite-plugin-monkey. I was a bit surprised but I think it's just a coincidence and decided to keep the name as it sounds good. I don't like monkeys though. I like cats.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fguansss%2Fwebpack-monkey","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fguansss%2Fwebpack-monkey","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fguansss%2Fwebpack-monkey/lists"}