Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on:
push:
branches:
- master
pull_request:

jobs:
ci:
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- uses: actions/checkout@v7

- uses: pnpm/setup@v2
with:
runtime: node@22
cache: true

- run: pnpm type-check
- run: pnpm test
- run: pnpm lint
- run: pnpm build
- run: pnpm build:examples
1 change: 1 addition & 0 deletions .github/workflows/npm-publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
- run: pnpm test
- run: pnpm lint
- run: pnpm build
- run: pnpm build:examples

- name: Publish
run: pnpm publish --access public --no-git-checks
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ import './style.css'

`index.html` is a normal Vite app next to the userscript. `vite` serves it at `/`. `vite build` writes it to `dist/` beside `{fileName}.user.js`.

The plugin sets `build.assetsInlineLimit` very high so userscript assets become data URLs. The HTML app’s assets will too, unless you set `build.assetsInlineLimit` yourself.

> [!WARNING]
> Keep the page's `<script>` entries distinct from `entry`.

Expand Down Expand Up @@ -142,10 +144,30 @@ See [examples/sourcemap](./examples/sourcemap).
| `generate` | — | Rewrite the generated metablock. |
| `autoMetaUrls` | `false` | Fill empty `updateURL` / `downloadURL` from `homepage` / `homepageURL` / `website` / `source`. |
| `metaFile` | `true` | Emit `{fileName}.meta.js`. |
| `external` | — | Keep these packages out of the bundle and load them via `@require`. Keys are specifiers (`jquery`, `vue`). A string value is the CDN URL (global name from the specifier). Pass `{ global, url }` for `$` / `Vue`. Install `@types/…` for `tsc`; do not install the runtime package. See [examples/external-cdn](./examples/external-cdn). |

Everything else on `header` follows the manager metablock (`@grant`, `@require`, `@connect`, …).

In serve mode the header lists every grant. In production the plugin scans the bundle and writes only the grants in use. `grant: "none"` disables GM APIs and is never mixed with the scan.
```ts
userscript({
entry: 'src/index.ts',
header: {
name: pkg.name,
version: pkg.version,
match: 'https://example.com/*',
},
external: {
jquery: {
global: '$',
url: 'https://cdn.jsdelivr.net/npm/jquery@4.0.0/dist/jquery.min.js',
},
},
})
```

For types without bundling the package: add `@types/jquery` (not `jquery`) and a `d.ts` that references those types. The import type-checks; the plugin maps it to the CDN global. Full setup: [examples/external-cdn](./examples/external-cdn).

In serve mode the header lists every grant. In production the plugin scans the bundle and writes only the grants in use. `window.focus`, `window.close`, and `window.onurlchange` are **not** auto-detected (they collide with DOM APIs) — list them in `header.grant` when you need them. `grant: "none"` disables GM APIs and is never mixed with the scan.

> [!WARNING]
> Keep `metaFile: true` if you use `autoMetaUrls`. Otherwise `@updateURL` points at a file that is not emitted.
Expand All @@ -161,6 +183,7 @@ In serve mode the header lists every grant. In production the plugin scans the b
| [multiple-entries](./examples/multiple-entries) | Two scripts. |
| [sourcemap](./examples/sourcemap) | Inline map, HTML page, virtual module. |
| [serve-file](./examples/serve-file) | `server.file`, install `.user.js` or the proxy from the printed URLs. |
| [external-cdn](./examples/external-cdn) | `@types/jquery` only; runtime `$` from a CDN `@require`. |

## FAQ

Expand Down
27 changes: 27 additions & 0 deletions examples/external-cdn/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# External CDN example

`jquery` is **not** installed. `@types/jquery` is types-only. Runtime `$` comes from the CDN `@require` in `vite.config.ts`.

The widget on `https://example.com/` is built with `$`: version from `$.fn.jquery`, counter via `.on('click')`. That is the point — `import $ from 'jquery'` type-checks, the bundle never ships jQuery.

```ts
userscript({
entry: 'src/index.ts',
header: { name: 'external-cdn-example', version: '0.0.0', match: 'https://example.com/' },
external: {
jquery: {
global: '$',
url: 'https://cdn.jsdelivr.net/npm/jquery@4.0.0/dist/jquery.min.js',
},
},
})
```

[`src/jquery.d.ts`](./src/jquery.d.ts) points TypeScript at `@types/jquery`. `import $ from 'jquery'` type-checks; the plugin maps that import to the CDN global.

```bash
pnpm dev
pnpm build
```

Install the printed `*.dev.user.js` URL. HTTPS and `public/` notes: [FAQ](../../README.md#faq).
16 changes: 16 additions & 0 deletions examples/external-cdn/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "external-cdn-example",
"type": "module",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"type-check": "tsc --noEmit -p tsconfig.json"
},
"devDependencies": {
"@types/jquery": "4.0.1",
"vite": "catalog:",
"vite-userscript-plugin": "workspace:*"
}
}
9 changes: 9 additions & 0 deletions examples/external-cdn/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import $ from 'jquery'
import { createWidget } from './widget'
import './style.css'

if (document.body) {
$('body')
.attr('data-cdn-jquery', $.fn.jquery)
.append(createWidget())
}
8 changes: 8 additions & 0 deletions examples/external-cdn/src/jquery.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* `jquery` is not a runtime dependency. Types come from `@types/jquery`.
* The script itself is the CDN `@require` in `vite.config.ts` (`external.jquery`).
*
* `import $ from 'jquery'` type-checks against `@types/jquery` (`export = jQuery`).
* Serve and build map that import to the global `$` from the CDN.
*/
/// <reference types="jquery" />
12 changes: 12 additions & 0 deletions examples/external-cdn/src/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.external-cdn-userscript {
border-top: 1px solid black;
}

.external-cdn-userscript .meta {
color: #555;
}

.actions {
display: flex;
gap: 0.5rem;
}
4 changes: 4 additions & 0 deletions examples/external-cdn/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/// <reference types="vite/client" />
/// <reference types="vite-userscript-plugin/virtual" />
/// <reference types="vite-userscript-plugin/types/tampermonkey" />
/// <reference path="./jquery.d.ts" />
49 changes: 49 additions & 0 deletions examples/external-cdn/src/widget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import $ from 'jquery'

function createButton(text: string, onClick: () => void) {
return $('<button type="button">')
.text(text)
.on('click', onClick)
}

export function createWidget() {
let count = 0

const root = $('<div class="external-cdn-userscript">')
const title = $('<h1>').text('External CDN userscript')
const meta = $('<p class="meta">').text(
`jQuery ${$.fn.jquery} loaded from jsDelivr via @require`,
)
const label = $('<p>')
const actions = $('<div class="actions">')

const render = () => {
label.text(`Count: ${count}`)
}

const increment = () => {
count += 1
render()
}

const decrement = () => {
count -= 1
render()
}

const reset = () => {
count = 0
render()
}

actions.append(
createButton('-', decrement),
createButton('Reset', reset),
createButton('+', increment),
)

root.append(title, meta, label, actions)
render()

return root.get(0)!
}
10 changes: 10 additions & 0 deletions examples/external-cdn/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"paths": {
"vite-userscript-plugin": ["../../src/index.ts"]
},
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*", "vite.config.ts"]
}
22 changes: 22 additions & 0 deletions examples/external-cdn/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { defineConfig } from 'vite'
import userscript from 'vite-userscript-plugin'
import pkg from './package.json' with { type: 'json' }

export default defineConfig({
plugins: [
userscript({
entry: 'src/index.ts',
header: {
name: pkg.name,
version: pkg.version,
match: 'https://example.com/',
},
external: {
jquery: {
global: '$',
url: 'https://cdn.jsdelivr.net/npm/jquery@4.0.0/dist/jquery.min.js',
},
},
}),
],
})
2 changes: 1 addition & 1 deletion examples/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"devDependencies": {
"@types/react": "19.2.18",
"@types/react-dom": "19.2.5",
"@types/react-dom": "19.2.7",
"@vitejs/plugin-react": "6.1.1",
"react": "19.2.8",
"react-dom": "19.2.8",
Expand Down
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "vite-userscript-plugin",
"type": "module",
"version": "2.3.1",
"packageManager": "pnpm@11.22.0",
"version": "2.4.0",
"packageManager": "pnpm@11.25.0",
"author": {
"name": "Vitalij Ryndin",
"url": "https://github.com/crashmax-dev"
Expand Down Expand Up @@ -46,7 +46,8 @@
"build:examples": "turbo run build --filter=./examples/*",
"test": "vitest",
"test:ui": "vitest --ui --watch",
"type-check": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p scripts/tsconfig.json && turbo run type-check --filter=./examples/*",
"type-check": "tsc --noEmit -p tsconfig.json && turbo run type-check --filter=./examples/*",
"check-update": "taze major -Irl",
"sync-types": "node scripts/sync-types.ts",
"lint": "eslint . --cache",
"lint:fix": "eslint . --fix --cache"
Expand All @@ -65,7 +66,8 @@
"eslint-plugin-format": "2.0.1",
"prettier-plugin-css-order": "2.2.0",
"svelte": "catalog:",
"tsdown": "0.22.14",
"taze": "21.1.0",
"tsdown": "0.23.0",
"turbo": "2.10.12",
"typescript": "npm:@typescript/typescript6@6.0.2",
"vite": "catalog:",
Expand Down
Loading
Loading