vite-userscript-plugin 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -8
- package/dist/index.d.ts +7 -33
- package/dist/index.js +241 -143
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,7 +83,7 @@ Details: [types/README.md](./types/README.md).
|
|
|
83
83
|
|
|
84
84
|
`vite` prints `/{fileName}.dev.user.js` — install that URL once. HMR covers code and styles.
|
|
85
85
|
|
|
86
|
-
`server.file: true` skips HMR. The same `vite` watch-builds `{fileName}.js` (
|
|
86
|
+
`server.file: true` skips HMR. The same `vite` watch-builds `{fileName}.user.js` (headed IIFE), `{fileName}.js`, and `{fileName}.proxy.user.js` (`@require file://` to the IIFE). Install the printed `/{fileName}.user.js` URL (Firefox-safe). The `Proxy` line is for managers that can poll `file://`. See [examples/serve-file](./examples/serve-file).
|
|
87
87
|
|
|
88
88
|
> [!IMPORTANT]
|
|
89
89
|
> Changing `@match`, `@grant`, or `@name` needs a reinstall.
|
|
@@ -133,13 +133,12 @@ See [examples/sourcemap](./examples/sourcemap).
|
|
|
133
133
|
| Option | Default | Description |
|
|
134
134
|
| --- | --- | --- |
|
|
135
135
|
| `entry` | — | Userscript entry. Required. |
|
|
136
|
-
| `header` | — | Metablock. Required: `name`, `version`, `match`. |
|
|
136
|
+
| `header` | — | Metablock. Required: `name`, `version`, `match`. Relative `icon` / `require` / `resource` / `supportURL` / `updateURL` / `downloadURL` join `homepage` (`homepageURL` / `website` / `source`). Absolute `http(s):` URLs stay as-is. |
|
|
137
137
|
| `fileName` | sanitized `header.name` | Output base name (`{fileName}.user.js`). |
|
|
138
|
-
| `server.open` | `false` | Open the install target when Vite starts. HMR: `.dev.user.js`. `file`: `.
|
|
138
|
+
| `server.open` | `false` | Open the install target when Vite starts. HMR: `.dev.user.js`. `file`: `.user.js` URL. |
|
|
139
139
|
| `server.prefix` | `'server:'` | Prefix for `@name` in serve mode. `false` disables it. |
|
|
140
|
-
| `server.file` | `false` | Watch-build `{fileName}.js` + `{fileName}.proxy.user.js` (
|
|
141
|
-
| `
|
|
142
|
-
| `align` | `1` | Extra spaces after the longest `@key`. `false` — one space. |
|
|
140
|
+
| `server.file` | `false` | Watch-build `{fileName}.user.js` + `{fileName}.js` + `{fileName}.proxy.user.js`. Install `.user.js` (or the proxy if `file://` `@require` works). No HMR. |
|
|
141
|
+
| `headerAlign` | `1` | Extra spaces after the longest `@key`. `false` — one space. |
|
|
143
142
|
| `generate` | — | Rewrite the generated metablock. |
|
|
144
143
|
| `autoMetaUrls` | `false` | Fill empty `updateURL` / `downloadURL` from `homepage` / `homepageURL` / `website` / `source`. |
|
|
145
144
|
| `metaFile` | `true` | Emit `{fileName}.meta.js`. |
|
|
@@ -161,7 +160,7 @@ In serve mode the header lists every grant. In production the plugin scans the b
|
|
|
161
160
|
| [svelte](./examples/svelte) | SFC `<style>`. |
|
|
162
161
|
| [multiple-entries](./examples/multiple-entries) | Two scripts. |
|
|
163
162
|
| [sourcemap](./examples/sourcemap) | Inline map, HTML page, virtual module. |
|
|
164
|
-
| [serve-file](./examples/serve-file) | `server.file`, install the proxy from the printed
|
|
163
|
+
| [serve-file](./examples/serve-file) | `server.file`, install `.user.js` or the proxy from the printed URLs. |
|
|
165
164
|
|
|
166
165
|
## FAQ
|
|
167
166
|
|
|
@@ -181,6 +180,8 @@ In serve mode the header lists every grant. In production the plugin scans the b
|
|
|
181
180
|
|
|
182
181
|
> [!NOTE]
|
|
183
182
|
> Userscripts run on someone else’s origin. Import the file so Vite inlines it. `public/` only works for the `index.html` app on the Vite origin.
|
|
183
|
+
>
|
|
184
|
+
> `@icon`, `@require`, and `@resource` are different: the manager fetches them from the metablock URL, not from the `match` site. Write `icon: 'greasify.svg'` plus `homepage` (GitHub Pages, etc.) — the plugin joins them. Leave `https://…` as-is.
|
|
184
185
|
|
|
185
186
|
### `@run-at document-start` feels late in dev
|
|
186
187
|
|
|
@@ -190,7 +191,7 @@ In serve mode the header lists every grant. In production the plugin scans the b
|
|
|
190
191
|
### `file://` `@require` is blocked
|
|
191
192
|
|
|
192
193
|
> [!NOTE]
|
|
193
|
-
>
|
|
194
|
+
> Firefox extensions cannot read `file://`. Install the printed `{fileName}.user.js` URL instead of the proxy. Tampermonkey on Chromium must allow local file access for the proxy `@require`. Violentmonkey can poll the local IIFE after you install the proxy.
|
|
194
195
|
|
|
195
196
|
## Migration from v1
|
|
196
197
|
|
|
@@ -204,6 +205,8 @@ In serve mode the header lists every grant. In production the plugin scans the b
|
|
|
204
205
|
| Vite 3–7 | Vite 8 |
|
|
205
206
|
| `scripts` + shared `header` | `userscript([config, config, …])` |
|
|
206
207
|
| `ScriptOptions` | removed |
|
|
208
|
+
| `cssInject` | removed; imported CSS always appends a `<style>` node |
|
|
209
|
+
| `align` | `headerAlign` |
|
|
207
210
|
|
|
208
211
|
## License
|
|
209
212
|
|
package/dist/index.d.ts
CHANGED
|
@@ -147,7 +147,7 @@ type HeaderConfig = {
|
|
|
147
147
|
interface ServerConfig {
|
|
148
148
|
/**
|
|
149
149
|
* Open the install target when Vite starts.
|
|
150
|
-
* HMR: `.dev.user.js` URL. `file`: `{fileName}.
|
|
150
|
+
* HMR: `.dev.user.js` URL. `file`: `{fileName}.user.js` URL.
|
|
151
151
|
*
|
|
152
152
|
* @default false
|
|
153
153
|
*/
|
|
@@ -160,9 +160,9 @@ interface ServerConfig {
|
|
|
160
160
|
*/
|
|
161
161
|
prefix?: string | false;
|
|
162
162
|
/**
|
|
163
|
-
* Watch-build into `outDir`:
|
|
164
|
-
* with `@require file://` pointing at the IIFE.
|
|
165
|
-
* Install
|
|
163
|
+
* Watch-build into `outDir`: `{fileName}.user.js`, headerless `{fileName}.js`,
|
|
164
|
+
* and `{fileName}.proxy.user.js` with `@require file://` pointing at the IIFE.
|
|
165
|
+
* Install `/{fileName}.user.js` (Firefox-safe) or the proxy URL. No HMR for this script.
|
|
166
166
|
*
|
|
167
167
|
* @default false
|
|
168
168
|
*/
|
|
@@ -173,7 +173,6 @@ interface HeaderGenerateContext {
|
|
|
173
173
|
userscript: string;
|
|
174
174
|
mode: HeaderMode;
|
|
175
175
|
}
|
|
176
|
-
type CssInject = 'auto' | string | ((css: string) => void);
|
|
177
176
|
/**
|
|
178
177
|
* One userscript. Pass an object, or an array of these, to {@link UserscriptPluginConfig}.
|
|
179
178
|
*/
|
|
@@ -196,18 +195,12 @@ interface UserscriptConfig {
|
|
|
196
195
|
* Serve-mode options.
|
|
197
196
|
*/
|
|
198
197
|
server?: ServerConfig;
|
|
199
|
-
/**
|
|
200
|
-
* How to inject collected CSS in production builds.
|
|
201
|
-
*
|
|
202
|
-
* @default 'auto'
|
|
203
|
-
*/
|
|
204
|
-
cssInject?: CssInject;
|
|
205
198
|
/**
|
|
206
199
|
* Extra spaces after the longest `@key`, or `false` for a single space.
|
|
207
200
|
*
|
|
208
201
|
* @default 1
|
|
209
202
|
*/
|
|
210
|
-
|
|
203
|
+
headerAlign?: number | false;
|
|
211
204
|
/**
|
|
212
205
|
* Rewrite the generated metablock.
|
|
213
206
|
*/
|
|
@@ -240,32 +233,13 @@ interface ResolvedScript {
|
|
|
240
233
|
prefix: string | false;
|
|
241
234
|
file: boolean;
|
|
242
235
|
};
|
|
243
|
-
|
|
244
|
-
align: number | false;
|
|
236
|
+
headerAlign: number | false;
|
|
245
237
|
generate?: (ctx: HeaderGenerateContext) => string;
|
|
246
238
|
autoMetaUrls: boolean;
|
|
247
239
|
metaFile: boolean;
|
|
248
240
|
}
|
|
249
241
|
//#endregion
|
|
250
|
-
//#region src/header.d.ts
|
|
251
|
-
interface HeaderOptions {
|
|
252
|
-
align?: number | false;
|
|
253
|
-
autoMetaUrls?: boolean;
|
|
254
|
-
fileName?: string;
|
|
255
|
-
generate?: (ctx: HeaderGenerateContext) => string;
|
|
256
|
-
mode?: HeaderMode;
|
|
257
|
-
}
|
|
258
|
-
declare function resolveHomePage(header: HeaderConfig): string | undefined;
|
|
259
|
-
declare function resolvePublicFileUrl(header: HeaderConfig, fileName: string): string | undefined;
|
|
260
|
-
declare function generateHeader(config: HeaderConfig, options?: HeaderOptions): string;
|
|
261
|
-
declare class Header {
|
|
262
|
-
private readonly config;
|
|
263
|
-
private readonly options;
|
|
264
|
-
constructor(config: HeaderConfig, options?: HeaderOptions);
|
|
265
|
-
generate(): string;
|
|
266
|
-
}
|
|
267
|
-
//#endregion
|
|
268
242
|
//#region src/plugin.d.ts
|
|
269
243
|
declare function UserscriptPlugin(config: UserscriptPluginConfig): Plugin[];
|
|
270
244
|
//#endregion
|
|
271
|
-
export { type
|
|
245
|
+
export { type HeaderConfig, type HeaderGenerateContext, type ResolvedScript, type ServerConfig, type UserscriptConfig, type UserscriptPluginConfig, UserscriptPlugin as default };
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,91 @@ import { basename, posix, relative, resolve, sep } from "node:path";
|
|
|
2
2
|
import { build } from "vite";
|
|
3
3
|
import { Buffer } from "node:buffer";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import { existsSync } from "node:fs";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
6
|
import { styleText } from "node:util";
|
|
7
7
|
import openLink from "open";
|
|
8
|
+
//#region src/grants/catalog.ts
|
|
9
|
+
const GM = [
|
|
10
|
+
"setValue",
|
|
11
|
+
"getValue",
|
|
12
|
+
"deleteValue",
|
|
13
|
+
"listValues",
|
|
14
|
+
"setValues",
|
|
15
|
+
"getValues",
|
|
16
|
+
"deleteValues",
|
|
17
|
+
"setClipboard",
|
|
18
|
+
"addStyle",
|
|
19
|
+
"addElement",
|
|
20
|
+
"addValueChangeListener",
|
|
21
|
+
"removeValueChangeListener",
|
|
22
|
+
"registerMenuCommand",
|
|
23
|
+
"unregisterMenuCommand",
|
|
24
|
+
"download",
|
|
25
|
+
"getTab",
|
|
26
|
+
"getTabs",
|
|
27
|
+
"saveTab",
|
|
28
|
+
"openInTab",
|
|
29
|
+
"notification",
|
|
30
|
+
"getResourceURL",
|
|
31
|
+
"getResourceText",
|
|
32
|
+
"xmlhttpRequest",
|
|
33
|
+
"webRequest",
|
|
34
|
+
"cookie",
|
|
35
|
+
"audio",
|
|
36
|
+
"log",
|
|
37
|
+
"info"
|
|
38
|
+
];
|
|
39
|
+
const GMwindow = [
|
|
40
|
+
"unsafeWindow",
|
|
41
|
+
"window.onurlchange",
|
|
42
|
+
"window.focus",
|
|
43
|
+
"window.close"
|
|
44
|
+
];
|
|
45
|
+
const GM_DOT_ALIASES = ["GM.xmlHttpRequest", "GM.getResourceUrl"];
|
|
46
|
+
const grants = [
|
|
47
|
+
...GM.flatMap((grant) => [`GM_${grant}`, `GM.${grant}`]),
|
|
48
|
+
...GMwindow,
|
|
49
|
+
...GM_DOT_ALIASES
|
|
50
|
+
];
|
|
51
|
+
const gmIdentifiers = [
|
|
52
|
+
"GM",
|
|
53
|
+
"unsafeWindow",
|
|
54
|
+
...GM.map((grant) => `GM_${grant}`)
|
|
55
|
+
];
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/grants/scan.ts
|
|
58
|
+
const grantMatchers = grants.map((grant) => ({
|
|
59
|
+
grant,
|
|
60
|
+
pattern: new RegExp(`\\b${grant.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`)
|
|
61
|
+
}));
|
|
62
|
+
function removeDuplicates(arr) {
|
|
63
|
+
if (Array.isArray(arr)) return [...new Set(arr)];
|
|
64
|
+
return arr ? [arr] : [];
|
|
65
|
+
}
|
|
66
|
+
function defineGrants(code) {
|
|
67
|
+
return grantMatchers.filter(({ pattern }) => pattern.test(code)).map(({ grant }) => grant);
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/grants/policy.ts
|
|
71
|
+
function withServeGrants(header) {
|
|
72
|
+
if (header.grant === "none") return header;
|
|
73
|
+
return {
|
|
74
|
+
...header,
|
|
75
|
+
grant: [.../* @__PURE__ */ new Set([...header.grant ?? [], ...grants])]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function withBuildGrants(header, code, extraGrants = []) {
|
|
79
|
+
if (header.grant === "none") return header;
|
|
80
|
+
return {
|
|
81
|
+
...header,
|
|
82
|
+
grant: removeDuplicates([
|
|
83
|
+
...defineGrants(code),
|
|
84
|
+
...removeDuplicates(header.grant),
|
|
85
|
+
...extraGrants
|
|
86
|
+
])
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
8
90
|
//#region src/names.ts
|
|
9
91
|
function sanitizeFileName(name) {
|
|
10
92
|
return name.replace(/[<>:"/\\|?*\u0000-\u001F]+/g, "-").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "userscript";
|
|
@@ -16,6 +98,18 @@ function toIdentifier(name) {
|
|
|
16
98
|
}
|
|
17
99
|
//#endregion
|
|
18
100
|
//#region src/header.ts
|
|
101
|
+
const ABSOLUTE_URL_RE = /^[a-z][a-z0-9+.-]*:/i;
|
|
102
|
+
const HOMEPAGE_RELATIVE_FIELDS = [
|
|
103
|
+
"icon",
|
|
104
|
+
"iconURL",
|
|
105
|
+
"defaulticon",
|
|
106
|
+
"icon64",
|
|
107
|
+
"icon64URL",
|
|
108
|
+
"require",
|
|
109
|
+
"supportURL",
|
|
110
|
+
"updateURL",
|
|
111
|
+
"downloadURL"
|
|
112
|
+
];
|
|
19
113
|
function ensureTrailingSlash(url) {
|
|
20
114
|
return url.endsWith("/") ? url : `${url}/`;
|
|
21
115
|
}
|
|
@@ -25,6 +119,29 @@ function resolveHomePage(header) {
|
|
|
25
119
|
const trimmed = homePage.trim();
|
|
26
120
|
return trimmed === "" ? void 0 : trimmed;
|
|
27
121
|
}
|
|
122
|
+
function isResolvableHeaderPath(value) {
|
|
123
|
+
const trimmed = value.trim();
|
|
124
|
+
if (!trimmed) return false;
|
|
125
|
+
if (ABSOLUTE_URL_RE.test(trimmed) || trimmed.startsWith("//") || trimmed.startsWith("/")) return false;
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
function containsResolvableHeaderPath(value) {
|
|
129
|
+
if (typeof value === "string") return isResolvableHeaderPath(value);
|
|
130
|
+
if (!Array.isArray(value)) return false;
|
|
131
|
+
return value.some((item) => typeof item === "string" && isResolvableHeaderPath(item));
|
|
132
|
+
}
|
|
133
|
+
function containsResolvableResourcePath(resource) {
|
|
134
|
+
if (!Array.isArray(resource)) return false;
|
|
135
|
+
return resource.some((item) => {
|
|
136
|
+
return Array.isArray(item) && typeof item[1] === "string" && isResolvableHeaderPath(item[1]);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function listHomepageRelativeFields(header) {
|
|
140
|
+
const fields = [];
|
|
141
|
+
for (const key of HOMEPAGE_RELATIVE_FIELDS) if (containsResolvableHeaderPath(header[key])) fields.push(key);
|
|
142
|
+
if (containsResolvableResourcePath(header.resource)) fields.push("resource");
|
|
143
|
+
return fields;
|
|
144
|
+
}
|
|
28
145
|
function resolvePublicFileUrl(header, fileName) {
|
|
29
146
|
const homePage = resolveHomePage(header);
|
|
30
147
|
if (!homePage) return;
|
|
@@ -32,6 +149,31 @@ function resolvePublicFileUrl(header, fileName) {
|
|
|
32
149
|
return new URL(fileName, ensureTrailingSlash(homePage)).href;
|
|
33
150
|
} catch {}
|
|
34
151
|
}
|
|
152
|
+
function resolveHeaderUrlValue(header, value) {
|
|
153
|
+
if (typeof value === "string") {
|
|
154
|
+
if (!isResolvableHeaderPath(value)) return value;
|
|
155
|
+
return resolvePublicFileUrl(header, value.trim()) ?? value;
|
|
156
|
+
}
|
|
157
|
+
if (Array.isArray(value)) return value.map((item) => typeof item === "string" ? resolveHeaderUrlValue(header, item) : item);
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
function resolveResourceUrls(header, resource) {
|
|
161
|
+
if (!Array.isArray(resource)) return resource;
|
|
162
|
+
return resource.map((item) => {
|
|
163
|
+
if (!Array.isArray(item) || typeof item[1] !== "string") return item;
|
|
164
|
+
const [key, url] = item;
|
|
165
|
+
if (!isResolvableHeaderPath(url)) return item;
|
|
166
|
+
const resolved = resolvePublicFileUrl(header, url.trim());
|
|
167
|
+
return resolved ? [key, resolved] : item;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function applyHomepageRelativeUrls(header) {
|
|
171
|
+
if (!resolveHomePage(header)) return header;
|
|
172
|
+
const next = { ...header };
|
|
173
|
+
for (const key of HOMEPAGE_RELATIVE_FIELDS) if (next[key] != null) Object.assign(next, { [key]: resolveHeaderUrlValue(next, next[key]) });
|
|
174
|
+
if (next.resource != null) next.resource = resolveResourceUrls(next, next.resource);
|
|
175
|
+
return next;
|
|
176
|
+
}
|
|
35
177
|
function applyAutoMetaUrls(header, fileName) {
|
|
36
178
|
const updateURL = header.updateURL ?? resolvePublicFileUrl(header, `${fileName}.meta.js`);
|
|
37
179
|
const downloadURL = header.downloadURL ?? resolvePublicFileUrl(header, `${fileName}.user.js`);
|
|
@@ -53,7 +195,8 @@ function formatValue(value) {
|
|
|
53
195
|
}
|
|
54
196
|
function generateHeader(config, options = {}) {
|
|
55
197
|
const fileName = options.fileName ?? sanitizeFileName(config.name);
|
|
56
|
-
const
|
|
198
|
+
const withRelativeUrls = applyHomepageRelativeUrls({ ...config });
|
|
199
|
+
const header = options.autoMetaUrls ? applyAutoMetaUrls(withRelativeUrls, fileName) : withRelativeUrls;
|
|
57
200
|
const keys = Object.keys(header).filter((key) => {
|
|
58
201
|
const value = header[key];
|
|
59
202
|
return value !== void 0 && value !== null && value !== false;
|
|
@@ -88,99 +231,6 @@ function generateHeader(config, options = {}) {
|
|
|
88
231
|
mode: options.mode ?? "build"
|
|
89
232
|
});
|
|
90
233
|
}
|
|
91
|
-
var Header = class {
|
|
92
|
-
config;
|
|
93
|
-
options;
|
|
94
|
-
constructor(config, options = {}) {
|
|
95
|
-
this.config = config;
|
|
96
|
-
this.options = options;
|
|
97
|
-
}
|
|
98
|
-
generate() {
|
|
99
|
-
return generateHeader(this.config, this.options);
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
//#endregion
|
|
103
|
-
//#region src/grants/catalog.ts
|
|
104
|
-
const GM = [
|
|
105
|
-
"setValue",
|
|
106
|
-
"getValue",
|
|
107
|
-
"deleteValue",
|
|
108
|
-
"listValues",
|
|
109
|
-
"setValues",
|
|
110
|
-
"getValues",
|
|
111
|
-
"deleteValues",
|
|
112
|
-
"setClipboard",
|
|
113
|
-
"addStyle",
|
|
114
|
-
"addElement",
|
|
115
|
-
"addValueChangeListener",
|
|
116
|
-
"removeValueChangeListener",
|
|
117
|
-
"registerMenuCommand",
|
|
118
|
-
"unregisterMenuCommand",
|
|
119
|
-
"download",
|
|
120
|
-
"getTab",
|
|
121
|
-
"getTabs",
|
|
122
|
-
"saveTab",
|
|
123
|
-
"openInTab",
|
|
124
|
-
"notification",
|
|
125
|
-
"getResourceURL",
|
|
126
|
-
"getResourceText",
|
|
127
|
-
"xmlhttpRequest",
|
|
128
|
-
"webRequest",
|
|
129
|
-
"cookie",
|
|
130
|
-
"audio",
|
|
131
|
-
"log",
|
|
132
|
-
"info"
|
|
133
|
-
];
|
|
134
|
-
const GMwindow = [
|
|
135
|
-
"unsafeWindow",
|
|
136
|
-
"window.onurlchange",
|
|
137
|
-
"window.focus",
|
|
138
|
-
"window.close"
|
|
139
|
-
];
|
|
140
|
-
const GM_DOT_ALIASES = ["GM.xmlHttpRequest", "GM.getResourceUrl"];
|
|
141
|
-
const grants = [
|
|
142
|
-
...GM.flatMap((grant) => [`GM_${grant}`, `GM.${grant}`]),
|
|
143
|
-
...GMwindow,
|
|
144
|
-
...GM_DOT_ALIASES
|
|
145
|
-
];
|
|
146
|
-
const gmIdentifiers = [
|
|
147
|
-
"GM",
|
|
148
|
-
"unsafeWindow",
|
|
149
|
-
...GM.map((grant) => `GM_${grant}`)
|
|
150
|
-
];
|
|
151
|
-
//#endregion
|
|
152
|
-
//#region src/grants/scan.ts
|
|
153
|
-
const grantMatchers = grants.map((grant) => ({
|
|
154
|
-
grant,
|
|
155
|
-
pattern: new RegExp(`\\b${grant.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`)
|
|
156
|
-
}));
|
|
157
|
-
function removeDuplicates(arr) {
|
|
158
|
-
if (Array.isArray(arr)) return [...new Set(arr)];
|
|
159
|
-
return arr ? [arr] : [];
|
|
160
|
-
}
|
|
161
|
-
function defineGrants(code) {
|
|
162
|
-
return grantMatchers.filter(({ pattern }) => pattern.test(code)).map(({ grant }) => grant);
|
|
163
|
-
}
|
|
164
|
-
//#endregion
|
|
165
|
-
//#region src/grants/policy.ts
|
|
166
|
-
function withServeGrants(header) {
|
|
167
|
-
if (header.grant === "none") return header;
|
|
168
|
-
return {
|
|
169
|
-
...header,
|
|
170
|
-
grant: [.../* @__PURE__ */ new Set([...header.grant ?? [], ...grants])]
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
function withBuildGrants(header, code, extraGrants = []) {
|
|
174
|
-
if (header.grant === "none") return header;
|
|
175
|
-
return {
|
|
176
|
-
...header,
|
|
177
|
-
grant: removeDuplicates([
|
|
178
|
-
...defineGrants(code),
|
|
179
|
-
...removeDuplicates(header.grant),
|
|
180
|
-
...extraGrants
|
|
181
|
-
])
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
234
|
//#endregion
|
|
185
235
|
//#region src/sourcemap.ts
|
|
186
236
|
function countHeaderLines(prefix) {
|
|
@@ -264,13 +314,9 @@ function isAsset(item) {
|
|
|
264
314
|
//#endregion
|
|
265
315
|
//#region src/build/css.ts
|
|
266
316
|
const defaultCssInjector = `(function (css) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
var style = document.createElement('style')
|
|
271
|
-
style.textContent = css
|
|
272
|
-
;(document.head || document.documentElement).appendChild(style)
|
|
273
|
-
}
|
|
317
|
+
var style = document.createElement('style')
|
|
318
|
+
style.textContent = css
|
|
319
|
+
;(document.head || document.documentElement).appendChild(style)
|
|
274
320
|
})`;
|
|
275
321
|
function collectImportedCss(chunk, bundle, seen = /* @__PURE__ */ new Set()) {
|
|
276
322
|
const files = [...chunk.viteMetadata?.importedCss ?? []];
|
|
@@ -297,11 +343,8 @@ function collectCss(chunk, bundle) {
|
|
|
297
343
|
files
|
|
298
344
|
};
|
|
299
345
|
}
|
|
300
|
-
function createCssInject(css
|
|
301
|
-
|
|
302
|
-
if (cssInject === "auto") return `${defaultCssInjector}(${payload});\n`;
|
|
303
|
-
if (typeof cssInject === "function") return `(${cssInject.toString()})(${payload});\n`;
|
|
304
|
-
return `(${cssInject})(${payload});\n`;
|
|
346
|
+
function createCssInject(css) {
|
|
347
|
+
return `${defaultCssInjector}(${JSON.stringify(css)});\n`;
|
|
305
348
|
}
|
|
306
349
|
//#endregion
|
|
307
350
|
//#region src/build/iife.ts
|
|
@@ -345,7 +388,7 @@ function createWatchProxyHeader(script, jsAbsPath) {
|
|
|
345
388
|
}
|
|
346
389
|
function generateWatchProxy(script, jsAbsPath) {
|
|
347
390
|
return generateHeader(createWatchProxyHeader(script, jsAbsPath), {
|
|
348
|
-
align: script.
|
|
391
|
+
align: script.headerAlign,
|
|
349
392
|
autoMetaUrls: false,
|
|
350
393
|
fileName: script.fileName,
|
|
351
394
|
generate: script.generate,
|
|
@@ -406,6 +449,28 @@ function withSourceMaps(fileNames) {
|
|
|
406
449
|
function findScriptForChunk(chunk, fileName, scripts) {
|
|
407
450
|
return scripts.find((script) => chunk.name === script.fileName || fileName === `${script.fileName}.js` || fileName === `${script.fileName}.user.js`);
|
|
408
451
|
}
|
|
452
|
+
function createHeadedUserscript(script, options) {
|
|
453
|
+
const headerConfig = withBuildGrants(script.header, options.wrapped);
|
|
454
|
+
const prefix = `${generateHeader(headerConfig, {
|
|
455
|
+
align: script.headerAlign,
|
|
456
|
+
autoMetaUrls: script.autoMetaUrls,
|
|
457
|
+
fileName: script.fileName,
|
|
458
|
+
generate: script.generate,
|
|
459
|
+
mode: "build"
|
|
460
|
+
})}\n\n`;
|
|
461
|
+
const nextFileName = `${script.fileName}.user.js`;
|
|
462
|
+
let nextCode = `${prefix}${options.code}`;
|
|
463
|
+
if (options.map) {
|
|
464
|
+
const wrapOffset = isAlreadyIife(stripSourceMappingUrl(options.body)) ? 0 : 1;
|
|
465
|
+
const lineOffset = countHeaderLines(prefix) + countHeaderLines(options.cssPrelude) + wrapOffset + countHeaderLines(options.inlined);
|
|
466
|
+
const map = stripVendorSourcesContent(offsetSourceMap(options.map, lineOffset, nextFileName));
|
|
467
|
+
nextCode = `${stripSourceMappingUrl(nextCode).replace(/\n+$/g, "\n")}//# sourceMappingURL=${toInlineSourceMappingUrl(map)}\n`;
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
headerConfig,
|
|
471
|
+
code: nextCode.endsWith("\n") ? nextCode : `${nextCode}\n`
|
|
472
|
+
};
|
|
473
|
+
}
|
|
409
474
|
function deleteBundleFiles(bundle, fileNames) {
|
|
410
475
|
for (const fileName of fileNames) delete bundle[fileName];
|
|
411
476
|
}
|
|
@@ -435,13 +500,20 @@ function applyUserscriptBundle(bundle, config, context) {
|
|
|
435
500
|
const inlined = stripSourceMappingUrl(inlineImportedChunks(chunk, bundle));
|
|
436
501
|
const { css, files: cssFiles } = collectCss(chunk, bundle);
|
|
437
502
|
for (const cssFile of cssFiles) if (!keptCss.has(cssFile)) leftoverAssets.push(...withSourceMaps([cssFile]));
|
|
438
|
-
const cssPrelude = css ? createCssInject(css
|
|
503
|
+
const cssPrelude = css ? createCssInject(css) : "";
|
|
439
504
|
const body = `${inlined}${chunk.code}`;
|
|
440
505
|
const wrapped = ensureIife(body);
|
|
441
|
-
const extraGrants = css && script.cssInject === "auto" ? ["GM_addStyle"] : [];
|
|
442
506
|
const code = `${cssPrelude}${wrapped}`;
|
|
443
507
|
const emitFileProxy = Boolean(context.emitProxy && context.outDir && script.server.file);
|
|
444
508
|
leftoverAssets.push(`${fileName}.map`);
|
|
509
|
+
const headed = createHeadedUserscript(script, {
|
|
510
|
+
body,
|
|
511
|
+
code,
|
|
512
|
+
cssPrelude,
|
|
513
|
+
inlined,
|
|
514
|
+
map: chunk.map,
|
|
515
|
+
wrapped
|
|
516
|
+
});
|
|
445
517
|
if (emitFileProxy) {
|
|
446
518
|
const requireName = toRequireFileName(script.fileName);
|
|
447
519
|
let nextCode = code.endsWith("\n") ? code : `${code}\n`;
|
|
@@ -454,28 +526,13 @@ function applyUserscriptBundle(bundle, config, context) {
|
|
|
454
526
|
chunk.code = nextCode;
|
|
455
527
|
chunk.fileName = requireName;
|
|
456
528
|
emitFile(toProxyFileName(script.fileName), `${generateWatchProxy(script, resolve(context.outDir, requireName))}\n`);
|
|
529
|
+
emitFile(`${script.fileName}.user.js`, headed.code);
|
|
457
530
|
continue;
|
|
458
531
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
fileName: script.fileName,
|
|
464
|
-
generate: script.generate,
|
|
465
|
-
mode: "build"
|
|
466
|
-
})}\n\n`;
|
|
467
|
-
const nextFileName = `${script.fileName}.user.js`;
|
|
468
|
-
let nextCode = `${prefix}${code}`;
|
|
469
|
-
if (chunk.map) {
|
|
470
|
-
const wrapOffset = isAlreadyIife(stripSourceMappingUrl(body)) ? 0 : 1;
|
|
471
|
-
const lineOffset = countHeaderLines(prefix) + countHeaderLines(cssPrelude) + wrapOffset + countHeaderLines(inlined);
|
|
472
|
-
chunk.map = stripVendorSourcesContent(offsetSourceMap(chunk.map, lineOffset, nextFileName));
|
|
473
|
-
nextCode = `${stripSourceMappingUrl(nextCode).replace(/\n+$/g, "\n")}//# sourceMappingURL=${toInlineSourceMappingUrl(chunk.map)}\n`;
|
|
474
|
-
}
|
|
475
|
-
chunk.code = nextCode;
|
|
476
|
-
chunk.fileName = nextFileName;
|
|
477
|
-
if (script.metaFile) emitFile(`${script.fileName}.meta.js`, generateHeader(headerConfig, {
|
|
478
|
-
align: script.align,
|
|
532
|
+
chunk.code = headed.code;
|
|
533
|
+
chunk.fileName = `${script.fileName}.user.js`;
|
|
534
|
+
if (script.metaFile) emitFile(`${script.fileName}.meta.js`, generateHeader(headed.headerConfig, {
|
|
535
|
+
align: script.headerAlign,
|
|
479
536
|
autoMetaUrls: script.autoMetaUrls,
|
|
480
537
|
fileName: script.fileName,
|
|
481
538
|
generate: script.generate,
|
|
@@ -495,7 +552,7 @@ function createClientSnapshot(scripts, command) {
|
|
|
495
552
|
return scripts.map((script) => {
|
|
496
553
|
let suffix = ".dev.user.js";
|
|
497
554
|
if (command === "build") suffix = ".user.js";
|
|
498
|
-
else if (script.server.file) suffix = ".
|
|
555
|
+
else if (script.server.file) suffix = ".user.js";
|
|
499
556
|
return {
|
|
500
557
|
name: script.header.name,
|
|
501
558
|
version: script.header.version,
|
|
@@ -596,8 +653,7 @@ function toResolvedScript(config) {
|
|
|
596
653
|
prefix: config.server?.prefix ?? "server:",
|
|
597
654
|
file: config.server?.file ?? false
|
|
598
655
|
},
|
|
599
|
-
|
|
600
|
-
align: config.align ?? 1,
|
|
656
|
+
headerAlign: config.headerAlign ?? 1,
|
|
601
657
|
generate: config.generate,
|
|
602
658
|
autoMetaUrls: config.autoMetaUrls ?? false,
|
|
603
659
|
metaFile: config.metaFile ?? true
|
|
@@ -612,6 +668,19 @@ function collectAutoMetaUrlsWarnings(config) {
|
|
|
612
668
|
}
|
|
613
669
|
return warnings;
|
|
614
670
|
}
|
|
671
|
+
function collectHomepageRelativeUrlWarnings(config) {
|
|
672
|
+
const warnings = [];
|
|
673
|
+
for (const script of config.scripts) {
|
|
674
|
+
if (resolveHomePage(script.header)) continue;
|
|
675
|
+
const fields = listHomepageRelativeFields(script.header);
|
|
676
|
+
if (!fields.length) continue;
|
|
677
|
+
warnings.push(`[${PLUGIN_NAME}] "${script.fileName}" has relative header URLs (${fields.join(", ")}) but no homepage, homepageURL, website, or source`);
|
|
678
|
+
}
|
|
679
|
+
return warnings;
|
|
680
|
+
}
|
|
681
|
+
function collectConfigWarnings(config) {
|
|
682
|
+
return [...collectAutoMetaUrlsWarnings(config), ...collectHomepageRelativeUrlWarnings(config)];
|
|
683
|
+
}
|
|
615
684
|
function resolvePluginConfig(config) {
|
|
616
685
|
const items = Array.isArray(config) ? config : [config];
|
|
617
686
|
if (!items.length) throw new Error(`[${PLUGIN_NAME}] Provide a userscript config or a non-empty array`);
|
|
@@ -648,9 +717,10 @@ function shimModule(code, id) {
|
|
|
648
717
|
}
|
|
649
718
|
//#endregion
|
|
650
719
|
//#region src/serve/logger.ts
|
|
651
|
-
function formatInstallLine(installUrl) {
|
|
720
|
+
function formatInstallLine(installUrl, label = "Userscript") {
|
|
652
721
|
const coloredUrl = styleText("cyan", installUrl.replace(/:(\d+)\//, (_match, port) => `:${styleText("bold", port)}/`));
|
|
653
|
-
|
|
722
|
+
const padded = label.padEnd(10);
|
|
723
|
+
return ` ${styleText("green", "➜")} ${styleText("bold", padded)}: ${coloredUrl}`;
|
|
654
724
|
}
|
|
655
725
|
function formatRebuildLine(elapsedMs) {
|
|
656
726
|
return `${styleText("green", "Userscript rebuilt")} ${styleText("dim", `(${elapsedMs}ms)`)}`;
|
|
@@ -720,9 +790,16 @@ function matchDevUserscript(url, fileName) {
|
|
|
720
790
|
function matchProxyUserscript(url, fileName) {
|
|
721
791
|
return (url.split("?")[0] ?? "") === `/${toProxyFileName(fileName)}`;
|
|
722
792
|
}
|
|
723
|
-
function
|
|
724
|
-
|
|
725
|
-
|
|
793
|
+
function matchFileUserscript(url, fileName) {
|
|
794
|
+
return (url.split("?")[0] ?? "") === `/${fileName}.user.js`;
|
|
795
|
+
}
|
|
796
|
+
function toInstallUrl(origin, fileName, kind = "dev") {
|
|
797
|
+
const names = {
|
|
798
|
+
dev: `${fileName}.dev.user.js`,
|
|
799
|
+
proxy: toProxyFileName(fileName),
|
|
800
|
+
user: `${fileName}.user.js`
|
|
801
|
+
};
|
|
802
|
+
return `${origin.replace(/\/$/, "")}/${names[kind]}`;
|
|
726
803
|
}
|
|
727
804
|
function toServeEntryPath(root, entry) {
|
|
728
805
|
const absolute = resolve(root, entry);
|
|
@@ -779,6 +856,9 @@ function findDevScript(url, scripts) {
|
|
|
779
856
|
function findProxyScript(url, scripts) {
|
|
780
857
|
return scripts.find((script) => script.server.file && matchProxyUserscript(url, script.fileName));
|
|
781
858
|
}
|
|
859
|
+
function findFileUserscript(url, scripts) {
|
|
860
|
+
return scripts.find((script) => script.server.file && matchFileUserscript(url, script.fileName));
|
|
861
|
+
}
|
|
782
862
|
function createDevUserscript(options) {
|
|
783
863
|
return generateDevUserscript({
|
|
784
864
|
script: options.script,
|
|
@@ -786,7 +866,7 @@ function createDevUserscript(options) {
|
|
|
786
866
|
root: options.root,
|
|
787
867
|
prefix: options.script.server.prefix,
|
|
788
868
|
headerOptions: {
|
|
789
|
-
align: options.script.
|
|
869
|
+
align: options.script.headerAlign,
|
|
790
870
|
autoMetaUrls: options.script.autoMetaUrls,
|
|
791
871
|
generate: options.script.generate
|
|
792
872
|
},
|
|
@@ -824,6 +904,17 @@ function configureDevServer(server, resolved, reactPreamble) {
|
|
|
824
904
|
writeScript(res, createReactBootstrapModule(entry));
|
|
825
905
|
return;
|
|
826
906
|
}
|
|
907
|
+
const fileScript = findFileUserscript(url, resolved.scripts);
|
|
908
|
+
if (fileScript) {
|
|
909
|
+
const userJsPath = resolve(server.config.root, server.config.build.outDir, `${fileScript.fileName}.user.js`);
|
|
910
|
+
try {
|
|
911
|
+
writeScript(res, readFileSync(userJsPath, "utf8"));
|
|
912
|
+
} catch {
|
|
913
|
+
res.statusCode = 404;
|
|
914
|
+
res.end();
|
|
915
|
+
}
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
827
918
|
const proxyScript = findProxyScript(url, resolved.scripts);
|
|
828
919
|
if (proxyScript) {
|
|
829
920
|
writeScript(res, `${generateWatchProxy(proxyScript, resolve(server.config.root, server.config.build.outDir, toRequireFileName(proxyScript.fileName)))}\n`);
|
|
@@ -848,7 +939,14 @@ function configureDevServer(server, resolved, reactPreamble) {
|
|
|
848
939
|
let origins = [];
|
|
849
940
|
if (urls) origins = urls.local.length ? urls.local : urls.network;
|
|
850
941
|
const printInstall = () => {
|
|
851
|
-
for (const origin of origins) for (const script of resolved.scripts)
|
|
942
|
+
for (const origin of origins) for (const script of resolved.scripts) {
|
|
943
|
+
if (script.server.file) {
|
|
944
|
+
info(formatInstallLine(toInstallUrl(origin, script.fileName, "user")));
|
|
945
|
+
info(formatInstallLine(toInstallUrl(origin, script.fileName, "proxy"), "Proxy"));
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
info(formatInstallLine(toInstallUrl(origin, script.fileName)));
|
|
949
|
+
}
|
|
852
950
|
info(formatFaqHint());
|
|
853
951
|
};
|
|
854
952
|
const logger = createAfterLocalLogger(info, urls?.local.length ?? 0, printInstall);
|
|
@@ -866,7 +964,7 @@ function configureDevServer(server, resolved, reactPreamble) {
|
|
|
866
964
|
if (!toOpen.length) return;
|
|
867
965
|
queueMicrotask(() => {
|
|
868
966
|
const origin = resolveServerOrigin(server.resolvedUrls);
|
|
869
|
-
for (const script of toOpen) openLink(toInstallUrl(origin, script.fileName, script.server.file));
|
|
967
|
+
for (const script of toOpen) openLink(toInstallUrl(origin, script.fileName, script.server.file ? "user" : "dev"));
|
|
870
968
|
});
|
|
871
969
|
});
|
|
872
970
|
}
|
|
@@ -984,7 +1082,7 @@ function UserscriptPlugin(config) {
|
|
|
984
1082
|
command = viteConfig.command;
|
|
985
1083
|
resolved = absolutizeEntries(resolved, viteConfig.root);
|
|
986
1084
|
reactPreamble = hasReactRefreshPlugin(viteConfig.plugins);
|
|
987
|
-
for (const message of
|
|
1085
|
+
for (const message of collectConfigWarnings(resolved)) viteConfig.logger.warn(message);
|
|
988
1086
|
}
|
|
989
1087
|
},
|
|
990
1088
|
{
|
|
@@ -1042,4 +1140,4 @@ function UserscriptPlugin(config) {
|
|
|
1042
1140
|
];
|
|
1043
1141
|
}
|
|
1044
1142
|
//#endregion
|
|
1045
|
-
export {
|
|
1143
|
+
export { UserscriptPlugin as default };
|