vite-userscript-plugin 2.1.1 → 2.3.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 +7 -4
- package/dist/index.d.ts +9 -32
- package/dist/index.js +222 -139
- package/package.json +4 -7
package/README.md
CHANGED
|
@@ -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`: `.user.js` URL. |
|
|
138
|
+
| `server.open` | `false` | Open the install target when Vite starts (`true`, `'user'`, `'proxy'`). HMR: `.dev.user.js`. `file`: `.user.js` or `.proxy.user.js`. Vite opens one URL. |
|
|
139
139
|
| `server.prefix` | `'server:'` | Prefix for `@name` in serve mode. `false` disables it. |
|
|
140
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
|
-
| `
|
|
142
|
-
| `align` | `1` | Extra spaces after the longest `@key`. `false` — one space. |
|
|
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`. |
|
|
@@ -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
|
|
|
@@ -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
|
@@ -144,14 +144,17 @@ type HeaderConfig = {
|
|
|
144
144
|
*/
|
|
145
145
|
'unwrap'?: boolean;
|
|
146
146
|
};
|
|
147
|
+
type ServerOpen = boolean | 'user' | 'proxy';
|
|
148
|
+
type ResolvedServerOpen = false | 'dev' | 'user' | 'proxy';
|
|
147
149
|
interface ServerConfig {
|
|
148
150
|
/**
|
|
149
151
|
* Open the install target when Vite starts.
|
|
150
|
-
* HMR
|
|
152
|
+
* `true`: HMR → `.dev.user.js`; `file` → `{fileName}.user.js`.
|
|
153
|
+
* `'user'` / `'proxy'`: file-mode install URL. Vite opens one path.
|
|
151
154
|
*
|
|
152
155
|
* @default false
|
|
153
156
|
*/
|
|
154
|
-
open?:
|
|
157
|
+
open?: ServerOpen;
|
|
155
158
|
/**
|
|
156
159
|
* Prefix applied to `@name` in serve mode.
|
|
157
160
|
* Set `false` to disable.
|
|
@@ -173,7 +176,6 @@ interface HeaderGenerateContext {
|
|
|
173
176
|
userscript: string;
|
|
174
177
|
mode: HeaderMode;
|
|
175
178
|
}
|
|
176
|
-
type CssInject = 'auto' | string | ((css: string) => void);
|
|
177
179
|
/**
|
|
178
180
|
* One userscript. Pass an object, or an array of these, to {@link UserscriptPluginConfig}.
|
|
179
181
|
*/
|
|
@@ -196,18 +198,12 @@ interface UserscriptConfig {
|
|
|
196
198
|
* Serve-mode options.
|
|
197
199
|
*/
|
|
198
200
|
server?: ServerConfig;
|
|
199
|
-
/**
|
|
200
|
-
* How to inject collected CSS in production builds.
|
|
201
|
-
*
|
|
202
|
-
* @default 'auto'
|
|
203
|
-
*/
|
|
204
|
-
cssInject?: CssInject;
|
|
205
201
|
/**
|
|
206
202
|
* Extra spaces after the longest `@key`, or `false` for a single space.
|
|
207
203
|
*
|
|
208
204
|
* @default 1
|
|
209
205
|
*/
|
|
210
|
-
|
|
206
|
+
headerAlign?: number | false;
|
|
211
207
|
/**
|
|
212
208
|
* Rewrite the generated metablock.
|
|
213
209
|
*/
|
|
@@ -236,36 +232,17 @@ interface ResolvedScript {
|
|
|
236
232
|
iifeName: string;
|
|
237
233
|
header: HeaderConfig;
|
|
238
234
|
server: {
|
|
239
|
-
open:
|
|
235
|
+
open: ResolvedServerOpen;
|
|
240
236
|
prefix: string | false;
|
|
241
237
|
file: boolean;
|
|
242
238
|
};
|
|
243
|
-
|
|
244
|
-
align: number | false;
|
|
239
|
+
headerAlign: number | false;
|
|
245
240
|
generate?: (ctx: HeaderGenerateContext) => string;
|
|
246
241
|
autoMetaUrls: boolean;
|
|
247
242
|
metaFile: boolean;
|
|
248
243
|
}
|
|
249
244
|
//#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
245
|
//#region src/plugin.d.ts
|
|
269
246
|
declare function UserscriptPlugin(config: UserscriptPluginConfig): Plugin[];
|
|
270
247
|
//#endregion
|
|
271
|
-
export { type
|
|
248
|
+
export { type HeaderConfig, type HeaderGenerateContext, type ResolvedScript, type ResolvedServerOpen, type ServerConfig, type ServerOpen, type UserscriptConfig, type UserscriptPluginConfig, UserscriptPlugin as default };
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,88 @@ import { Buffer } from "node:buffer";
|
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
6
|
import { styleText } from "node:util";
|
|
7
|
-
|
|
7
|
+
//#region src/grants/catalog.ts
|
|
8
|
+
const GM = [
|
|
9
|
+
"setValue",
|
|
10
|
+
"getValue",
|
|
11
|
+
"deleteValue",
|
|
12
|
+
"listValues",
|
|
13
|
+
"setValues",
|
|
14
|
+
"getValues",
|
|
15
|
+
"deleteValues",
|
|
16
|
+
"setClipboard",
|
|
17
|
+
"addStyle",
|
|
18
|
+
"addElement",
|
|
19
|
+
"addValueChangeListener",
|
|
20
|
+
"removeValueChangeListener",
|
|
21
|
+
"registerMenuCommand",
|
|
22
|
+
"unregisterMenuCommand",
|
|
23
|
+
"download",
|
|
24
|
+
"getTab",
|
|
25
|
+
"getTabs",
|
|
26
|
+
"saveTab",
|
|
27
|
+
"openInTab",
|
|
28
|
+
"notification",
|
|
29
|
+
"getResourceURL",
|
|
30
|
+
"getResourceText",
|
|
31
|
+
"xmlhttpRequest",
|
|
32
|
+
"webRequest",
|
|
33
|
+
"cookie",
|
|
34
|
+
"audio",
|
|
35
|
+
"log",
|
|
36
|
+
"info"
|
|
37
|
+
];
|
|
38
|
+
const GMwindow = [
|
|
39
|
+
"unsafeWindow",
|
|
40
|
+
"window.onurlchange",
|
|
41
|
+
"window.focus",
|
|
42
|
+
"window.close"
|
|
43
|
+
];
|
|
44
|
+
const GM_DOT_ALIASES = ["GM.xmlHttpRequest", "GM.getResourceUrl"];
|
|
45
|
+
const grants = [
|
|
46
|
+
...GM.flatMap((grant) => [`GM_${grant}`, `GM.${grant}`]),
|
|
47
|
+
...GMwindow,
|
|
48
|
+
...GM_DOT_ALIASES
|
|
49
|
+
];
|
|
50
|
+
const gmIdentifiers = [
|
|
51
|
+
"GM",
|
|
52
|
+
"unsafeWindow",
|
|
53
|
+
...GM.map((grant) => `GM_${grant}`)
|
|
54
|
+
];
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/grants/scan.ts
|
|
57
|
+
const grantMatchers = grants.map((grant) => ({
|
|
58
|
+
grant,
|
|
59
|
+
pattern: new RegExp(`\\b${grant.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`)
|
|
60
|
+
}));
|
|
61
|
+
function removeDuplicates(arr) {
|
|
62
|
+
if (Array.isArray(arr)) return [...new Set(arr)];
|
|
63
|
+
return arr ? [arr] : [];
|
|
64
|
+
}
|
|
65
|
+
function defineGrants(code) {
|
|
66
|
+
return grantMatchers.filter(({ pattern }) => pattern.test(code)).map(({ grant }) => grant);
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/grants/policy.ts
|
|
70
|
+
function withServeGrants(header) {
|
|
71
|
+
if (header.grant === "none") return header;
|
|
72
|
+
return {
|
|
73
|
+
...header,
|
|
74
|
+
grant: [.../* @__PURE__ */ new Set([...header.grant ?? [], ...grants])]
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function withBuildGrants(header, code, extraGrants = []) {
|
|
78
|
+
if (header.grant === "none") return header;
|
|
79
|
+
return {
|
|
80
|
+
...header,
|
|
81
|
+
grant: removeDuplicates([
|
|
82
|
+
...defineGrants(code),
|
|
83
|
+
...removeDuplicates(header.grant),
|
|
84
|
+
...extraGrants
|
|
85
|
+
])
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
8
89
|
//#region src/names.ts
|
|
9
90
|
function sanitizeFileName(name) {
|
|
10
91
|
return name.replace(/[<>:"/\\|?*\u0000-\u001F]+/g, "-").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "userscript";
|
|
@@ -16,6 +97,18 @@ function toIdentifier(name) {
|
|
|
16
97
|
}
|
|
17
98
|
//#endregion
|
|
18
99
|
//#region src/header.ts
|
|
100
|
+
const ABSOLUTE_URL_RE = /^[a-z][a-z0-9+.-]*:/i;
|
|
101
|
+
const HOMEPAGE_RELATIVE_FIELDS = [
|
|
102
|
+
"icon",
|
|
103
|
+
"iconURL",
|
|
104
|
+
"defaulticon",
|
|
105
|
+
"icon64",
|
|
106
|
+
"icon64URL",
|
|
107
|
+
"require",
|
|
108
|
+
"supportURL",
|
|
109
|
+
"updateURL",
|
|
110
|
+
"downloadURL"
|
|
111
|
+
];
|
|
19
112
|
function ensureTrailingSlash(url) {
|
|
20
113
|
return url.endsWith("/") ? url : `${url}/`;
|
|
21
114
|
}
|
|
@@ -25,6 +118,29 @@ function resolveHomePage(header) {
|
|
|
25
118
|
const trimmed = homePage.trim();
|
|
26
119
|
return trimmed === "" ? void 0 : trimmed;
|
|
27
120
|
}
|
|
121
|
+
function isResolvableHeaderPath(value) {
|
|
122
|
+
const trimmed = value.trim();
|
|
123
|
+
if (!trimmed) return false;
|
|
124
|
+
if (ABSOLUTE_URL_RE.test(trimmed) || trimmed.startsWith("//") || trimmed.startsWith("/")) return false;
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
function containsResolvableHeaderPath(value) {
|
|
128
|
+
if (typeof value === "string") return isResolvableHeaderPath(value);
|
|
129
|
+
if (!Array.isArray(value)) return false;
|
|
130
|
+
return value.some((item) => typeof item === "string" && isResolvableHeaderPath(item));
|
|
131
|
+
}
|
|
132
|
+
function containsResolvableResourcePath(resource) {
|
|
133
|
+
if (!Array.isArray(resource)) return false;
|
|
134
|
+
return resource.some((item) => {
|
|
135
|
+
return Array.isArray(item) && typeof item[1] === "string" && isResolvableHeaderPath(item[1]);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function listHomepageRelativeFields(header) {
|
|
139
|
+
const fields = [];
|
|
140
|
+
for (const key of HOMEPAGE_RELATIVE_FIELDS) if (containsResolvableHeaderPath(header[key])) fields.push(key);
|
|
141
|
+
if (containsResolvableResourcePath(header.resource)) fields.push("resource");
|
|
142
|
+
return fields;
|
|
143
|
+
}
|
|
28
144
|
function resolvePublicFileUrl(header, fileName) {
|
|
29
145
|
const homePage = resolveHomePage(header);
|
|
30
146
|
if (!homePage) return;
|
|
@@ -32,6 +148,31 @@ function resolvePublicFileUrl(header, fileName) {
|
|
|
32
148
|
return new URL(fileName, ensureTrailingSlash(homePage)).href;
|
|
33
149
|
} catch {}
|
|
34
150
|
}
|
|
151
|
+
function resolveHeaderUrlValue(header, value) {
|
|
152
|
+
if (typeof value === "string") {
|
|
153
|
+
if (!isResolvableHeaderPath(value)) return value;
|
|
154
|
+
return resolvePublicFileUrl(header, value.trim()) ?? value;
|
|
155
|
+
}
|
|
156
|
+
if (Array.isArray(value)) return value.map((item) => typeof item === "string" ? resolveHeaderUrlValue(header, item) : item);
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
function resolveResourceUrls(header, resource) {
|
|
160
|
+
if (!Array.isArray(resource)) return resource;
|
|
161
|
+
return resource.map((item) => {
|
|
162
|
+
if (!Array.isArray(item) || typeof item[1] !== "string") return item;
|
|
163
|
+
const [key, url] = item;
|
|
164
|
+
if (!isResolvableHeaderPath(url)) return item;
|
|
165
|
+
const resolved = resolvePublicFileUrl(header, url.trim());
|
|
166
|
+
return resolved ? [key, resolved] : item;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function applyHomepageRelativeUrls(header) {
|
|
170
|
+
if (!resolveHomePage(header)) return header;
|
|
171
|
+
const next = { ...header };
|
|
172
|
+
for (const key of HOMEPAGE_RELATIVE_FIELDS) if (next[key] != null) Object.assign(next, { [key]: resolveHeaderUrlValue(next, next[key]) });
|
|
173
|
+
if (next.resource != null) next.resource = resolveResourceUrls(next, next.resource);
|
|
174
|
+
return next;
|
|
175
|
+
}
|
|
35
176
|
function applyAutoMetaUrls(header, fileName) {
|
|
36
177
|
const updateURL = header.updateURL ?? resolvePublicFileUrl(header, `${fileName}.meta.js`);
|
|
37
178
|
const downloadURL = header.downloadURL ?? resolvePublicFileUrl(header, `${fileName}.user.js`);
|
|
@@ -53,7 +194,8 @@ function formatValue(value) {
|
|
|
53
194
|
}
|
|
54
195
|
function generateHeader(config, options = {}) {
|
|
55
196
|
const fileName = options.fileName ?? sanitizeFileName(config.name);
|
|
56
|
-
const
|
|
197
|
+
const withRelativeUrls = applyHomepageRelativeUrls({ ...config });
|
|
198
|
+
const header = options.autoMetaUrls ? applyAutoMetaUrls(withRelativeUrls, fileName) : withRelativeUrls;
|
|
57
199
|
const keys = Object.keys(header).filter((key) => {
|
|
58
200
|
const value = header[key];
|
|
59
201
|
return value !== void 0 && value !== null && value !== false;
|
|
@@ -88,99 +230,6 @@ function generateHeader(config, options = {}) {
|
|
|
88
230
|
mode: options.mode ?? "build"
|
|
89
231
|
});
|
|
90
232
|
}
|
|
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
233
|
//#endregion
|
|
185
234
|
//#region src/sourcemap.ts
|
|
186
235
|
function countHeaderLines(prefix) {
|
|
@@ -264,13 +313,9 @@ function isAsset(item) {
|
|
|
264
313
|
//#endregion
|
|
265
314
|
//#region src/build/css.ts
|
|
266
315
|
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
|
-
}
|
|
316
|
+
var style = document.createElement('style')
|
|
317
|
+
style.textContent = css
|
|
318
|
+
;(document.head || document.documentElement).appendChild(style)
|
|
274
319
|
})`;
|
|
275
320
|
function collectImportedCss(chunk, bundle, seen = /* @__PURE__ */ new Set()) {
|
|
276
321
|
const files = [...chunk.viteMetadata?.importedCss ?? []];
|
|
@@ -297,11 +342,8 @@ function collectCss(chunk, bundle) {
|
|
|
297
342
|
files
|
|
298
343
|
};
|
|
299
344
|
}
|
|
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`;
|
|
345
|
+
function createCssInject(css) {
|
|
346
|
+
return `${defaultCssInjector}(${JSON.stringify(css)});\n`;
|
|
305
347
|
}
|
|
306
348
|
//#endregion
|
|
307
349
|
//#region src/build/iife.ts
|
|
@@ -345,7 +387,7 @@ function createWatchProxyHeader(script, jsAbsPath) {
|
|
|
345
387
|
}
|
|
346
388
|
function generateWatchProxy(script, jsAbsPath) {
|
|
347
389
|
return generateHeader(createWatchProxyHeader(script, jsAbsPath), {
|
|
348
|
-
align: script.
|
|
390
|
+
align: script.headerAlign,
|
|
349
391
|
autoMetaUrls: false,
|
|
350
392
|
fileName: script.fileName,
|
|
351
393
|
generate: script.generate,
|
|
@@ -407,9 +449,9 @@ function findScriptForChunk(chunk, fileName, scripts) {
|
|
|
407
449
|
return scripts.find((script) => chunk.name === script.fileName || fileName === `${script.fileName}.js` || fileName === `${script.fileName}.user.js`);
|
|
408
450
|
}
|
|
409
451
|
function createHeadedUserscript(script, options) {
|
|
410
|
-
const headerConfig = withBuildGrants(script.header, options.wrapped
|
|
452
|
+
const headerConfig = withBuildGrants(script.header, options.wrapped);
|
|
411
453
|
const prefix = `${generateHeader(headerConfig, {
|
|
412
|
-
align: script.
|
|
454
|
+
align: script.headerAlign,
|
|
413
455
|
autoMetaUrls: script.autoMetaUrls,
|
|
414
456
|
fileName: script.fileName,
|
|
415
457
|
generate: script.generate,
|
|
@@ -457,10 +499,9 @@ function applyUserscriptBundle(bundle, config, context) {
|
|
|
457
499
|
const inlined = stripSourceMappingUrl(inlineImportedChunks(chunk, bundle));
|
|
458
500
|
const { css, files: cssFiles } = collectCss(chunk, bundle);
|
|
459
501
|
for (const cssFile of cssFiles) if (!keptCss.has(cssFile)) leftoverAssets.push(...withSourceMaps([cssFile]));
|
|
460
|
-
const cssPrelude = css ? createCssInject(css
|
|
502
|
+
const cssPrelude = css ? createCssInject(css) : "";
|
|
461
503
|
const body = `${inlined}${chunk.code}`;
|
|
462
504
|
const wrapped = ensureIife(body);
|
|
463
|
-
const extraGrants = css && script.cssInject === "auto" ? ["GM_addStyle"] : [];
|
|
464
505
|
const code = `${cssPrelude}${wrapped}`;
|
|
465
506
|
const emitFileProxy = Boolean(context.emitProxy && context.outDir && script.server.file);
|
|
466
507
|
leftoverAssets.push(`${fileName}.map`);
|
|
@@ -468,7 +509,6 @@ function applyUserscriptBundle(bundle, config, context) {
|
|
|
468
509
|
body,
|
|
469
510
|
code,
|
|
470
511
|
cssPrelude,
|
|
471
|
-
extraGrants,
|
|
472
512
|
inlined,
|
|
473
513
|
map: chunk.map,
|
|
474
514
|
wrapped
|
|
@@ -491,7 +531,7 @@ function applyUserscriptBundle(bundle, config, context) {
|
|
|
491
531
|
chunk.code = headed.code;
|
|
492
532
|
chunk.fileName = `${script.fileName}.user.js`;
|
|
493
533
|
if (script.metaFile) emitFile(`${script.fileName}.meta.js`, generateHeader(headed.headerConfig, {
|
|
494
|
-
align: script.
|
|
534
|
+
align: script.headerAlign,
|
|
495
535
|
autoMetaUrls: script.autoMetaUrls,
|
|
496
536
|
fileName: script.fileName,
|
|
497
537
|
generate: script.generate,
|
|
@@ -598,22 +638,27 @@ function assertHeader(header, label) {
|
|
|
598
638
|
"match"
|
|
599
639
|
]) if (isEmptyHeaderField(header[field])) throw new Error(`[${PLUGIN_NAME}] ${label} is missing required header.${field}`);
|
|
600
640
|
}
|
|
641
|
+
function resolveServerOpen(open, file) {
|
|
642
|
+
if (!open) return false;
|
|
643
|
+
if (open === true) return file ? "user" : "dev";
|
|
644
|
+
return file ? open : "dev";
|
|
645
|
+
}
|
|
601
646
|
function toResolvedScript(config) {
|
|
602
647
|
if (!config.entry) throw new Error(`[${PLUGIN_NAME}] Provide an "entry" for each userscript`);
|
|
603
648
|
assertHeader(config.header ?? {}, config.entry);
|
|
604
649
|
const fileName = sanitizeFileName(config.fileName ?? config.header.name);
|
|
650
|
+
const file = config.server?.file ?? false;
|
|
605
651
|
return {
|
|
606
652
|
entry: config.entry,
|
|
607
653
|
fileName,
|
|
608
654
|
iifeName: toIdentifier(fileName),
|
|
609
655
|
header: config.header,
|
|
610
656
|
server: {
|
|
611
|
-
open: config.server?.open
|
|
657
|
+
open: resolveServerOpen(config.server?.open, file),
|
|
612
658
|
prefix: config.server?.prefix ?? "server:",
|
|
613
|
-
file
|
|
659
|
+
file
|
|
614
660
|
},
|
|
615
|
-
|
|
616
|
-
align: config.align ?? 1,
|
|
661
|
+
headerAlign: config.headerAlign ?? 1,
|
|
617
662
|
generate: config.generate,
|
|
618
663
|
autoMetaUrls: config.autoMetaUrls ?? false,
|
|
619
664
|
metaFile: config.metaFile ?? true
|
|
@@ -628,6 +673,34 @@ function collectAutoMetaUrlsWarnings(config) {
|
|
|
628
673
|
}
|
|
629
674
|
return warnings;
|
|
630
675
|
}
|
|
676
|
+
function collectHomepageRelativeUrlWarnings(config) {
|
|
677
|
+
const warnings = [];
|
|
678
|
+
for (const script of config.scripts) {
|
|
679
|
+
if (resolveHomePage(script.header)) continue;
|
|
680
|
+
const fields = listHomepageRelativeFields(script.header);
|
|
681
|
+
if (!fields.length) continue;
|
|
682
|
+
warnings.push(`[${PLUGIN_NAME}] "${script.fileName}" has relative header URLs (${fields.join(", ")}) but no homepage, homepageURL, website, or source`);
|
|
683
|
+
}
|
|
684
|
+
return warnings;
|
|
685
|
+
}
|
|
686
|
+
function collectServerOpenWarnings(config, input) {
|
|
687
|
+
const warnings = [];
|
|
688
|
+
const items = Array.isArray(input) ? input : [input];
|
|
689
|
+
for (const [index, script] of config.scripts.entries()) {
|
|
690
|
+
const open = items[index]?.server?.open;
|
|
691
|
+
if ((open === "user" || open === "proxy") && !script.server.file) warnings.push(`[${PLUGIN_NAME}] server.open: "${open}" requires server.file for "${script.fileName}" — opening .dev.user.js instead`);
|
|
692
|
+
}
|
|
693
|
+
const opened = config.scripts.filter((script) => script.server.open);
|
|
694
|
+
if (opened.length > 1) warnings.push(`[${PLUGIN_NAME}] server.open is set on multiple scripts; Vite opens only "${opened[0]?.fileName}"`);
|
|
695
|
+
return warnings;
|
|
696
|
+
}
|
|
697
|
+
function collectConfigWarnings(config, input) {
|
|
698
|
+
return [
|
|
699
|
+
...collectAutoMetaUrlsWarnings(config),
|
|
700
|
+
...collectHomepageRelativeUrlWarnings(config),
|
|
701
|
+
...collectServerOpenWarnings(config, input)
|
|
702
|
+
];
|
|
703
|
+
}
|
|
631
704
|
function resolvePluginConfig(config) {
|
|
632
705
|
const items = Array.isArray(config) ? config : [config];
|
|
633
706
|
if (!items.length) throw new Error(`[${PLUGIN_NAME}] Provide a userscript config or a non-empty array`);
|
|
@@ -664,16 +737,24 @@ function shimModule(code, id) {
|
|
|
664
737
|
}
|
|
665
738
|
//#endregion
|
|
666
739
|
//#region src/serve/logger.ts
|
|
740
|
+
const INSTALL_LABEL_WIDTH = 10;
|
|
741
|
+
function colonPadFor(label) {
|
|
742
|
+
return " ".repeat(Math.max(0, INSTALL_LABEL_WIDTH - label.length) + 1);
|
|
743
|
+
}
|
|
744
|
+
function formatLabeledLine(label, value) {
|
|
745
|
+
return ` ${styleText("green", "➜")} ${styleText("bold", label)}:${colonPadFor(label)}${value}`;
|
|
746
|
+
}
|
|
747
|
+
function alignViteUrlLine(message) {
|
|
748
|
+
return message.replace(/(Local(?:\u001B\[[0-9;]*m)*:)(\s+)/, (_match, prefix) => `${prefix}${colonPadFor("Local")}`);
|
|
749
|
+
}
|
|
667
750
|
function formatInstallLine(installUrl, label = "Userscript") {
|
|
668
|
-
|
|
669
|
-
const padded = label.padEnd(10);
|
|
670
|
-
return ` ${styleText("green", "➜")} ${styleText("bold", padded)}: ${coloredUrl}`;
|
|
751
|
+
return formatLabeledLine(label, styleText("cyan", installUrl.replace(/:(\d+)\//, (_match, port) => `:${styleText("bold", port)}/`)));
|
|
671
752
|
}
|
|
672
753
|
function formatRebuildLine(elapsedMs) {
|
|
673
754
|
return `${styleText("green", "Userscript rebuilt")} ${styleText("dim", `(${elapsedMs}ms)`)}`;
|
|
674
755
|
}
|
|
675
756
|
function formatFaqHint() {
|
|
676
|
-
return `${
|
|
757
|
+
return `${formatLabeledLine("FAQ", styleText("cyan", FAQ_URL))}\n`;
|
|
677
758
|
}
|
|
678
759
|
function stripAnsi(text) {
|
|
679
760
|
return text.replace(/\u001B\[[0-9;]*m/g, "");
|
|
@@ -691,7 +772,7 @@ function createAfterLocalLogger(info, localCount, onAfterLocal) {
|
|
|
691
772
|
};
|
|
692
773
|
return {
|
|
693
774
|
info: (msg, options) => {
|
|
694
|
-
info(msg, options);
|
|
775
|
+
info(typeof msg === "string" ? alignViteUrlLine(msg) : msg, options);
|
|
695
776
|
if (remaining > 0 && isViteLocalUrlLine(String(msg))) {
|
|
696
777
|
remaining -= 1;
|
|
697
778
|
if (remaining === 0) flush();
|
|
@@ -740,13 +821,18 @@ function matchProxyUserscript(url, fileName) {
|
|
|
740
821
|
function matchFileUserscript(url, fileName) {
|
|
741
822
|
return (url.split("?")[0] ?? "") === `/${fileName}.user.js`;
|
|
742
823
|
}
|
|
743
|
-
function
|
|
744
|
-
|
|
824
|
+
function toInstallFileName(fileName, kind) {
|
|
825
|
+
return {
|
|
745
826
|
dev: `${fileName}.dev.user.js`,
|
|
746
827
|
proxy: toProxyFileName(fileName),
|
|
747
828
|
user: `${fileName}.user.js`
|
|
748
|
-
};
|
|
749
|
-
|
|
829
|
+
}[kind];
|
|
830
|
+
}
|
|
831
|
+
function toInstallPath(fileName, kind = "dev") {
|
|
832
|
+
return `/${toInstallFileName(fileName, kind)}`;
|
|
833
|
+
}
|
|
834
|
+
function toInstallUrl(origin, fileName, kind = "dev") {
|
|
835
|
+
return `${origin.replace(/\/$/, "")}${toInstallPath(fileName, kind)}`;
|
|
750
836
|
}
|
|
751
837
|
function toServeEntryPath(root, entry) {
|
|
752
838
|
const absolute = resolve(root, entry);
|
|
@@ -813,7 +899,7 @@ function createDevUserscript(options) {
|
|
|
813
899
|
root: options.root,
|
|
814
900
|
prefix: options.script.server.prefix,
|
|
815
901
|
headerOptions: {
|
|
816
|
-
align: options.script.
|
|
902
|
+
align: options.script.headerAlign,
|
|
817
903
|
autoMetaUrls: options.script.autoMetaUrls,
|
|
818
904
|
generate: options.script.generate
|
|
819
905
|
},
|
|
@@ -906,14 +992,6 @@ function configureDevServer(server, resolved, reactPreamble) {
|
|
|
906
992
|
logger.flush();
|
|
907
993
|
}
|
|
908
994
|
};
|
|
909
|
-
server.httpServer?.once("listening", () => {
|
|
910
|
-
const toOpen = resolved.scripts.filter((script) => script.server.open);
|
|
911
|
-
if (!toOpen.length) return;
|
|
912
|
-
queueMicrotask(() => {
|
|
913
|
-
const origin = resolveServerOrigin(server.resolvedUrls);
|
|
914
|
-
for (const script of toOpen) openLink(toInstallUrl(origin, script.fileName, script.server.file ? "user" : "dev"));
|
|
915
|
-
});
|
|
916
|
-
});
|
|
917
995
|
}
|
|
918
996
|
//#endregion
|
|
919
997
|
//#region src/plugin.ts
|
|
@@ -1000,13 +1078,18 @@ function UserscriptPlugin(config) {
|
|
|
1000
1078
|
userConfig.build.rolldownOptions.input = input;
|
|
1001
1079
|
const userOutput = userConfig.build.rolldownOptions.output;
|
|
1002
1080
|
const userEntryFileNames = userOutput && !Array.isArray(userOutput) ? userOutput.entryFileNames : void 0;
|
|
1081
|
+
const openScript = resolved.scripts.find((script) => script.server.open);
|
|
1082
|
+
const openPath = openScript?.server.open ? toInstallPath(openScript.fileName, openScript.server.open) : void 0;
|
|
1003
1083
|
return {
|
|
1004
1084
|
appType: userConfig.appType ?? (hasHtml ? "spa" : "custom"),
|
|
1005
1085
|
optimizeDeps: {
|
|
1006
1086
|
entries: Object.values(input),
|
|
1007
1087
|
exclude: [VIRTUAL_MODULE_ID]
|
|
1008
1088
|
},
|
|
1009
|
-
server: {
|
|
1089
|
+
server: {
|
|
1090
|
+
cors: userConfig.server?.cors ?? true,
|
|
1091
|
+
...openPath ? { open: openPath } : {}
|
|
1092
|
+
},
|
|
1010
1093
|
build: {
|
|
1011
1094
|
minify: userConfig.build?.minify ?? false,
|
|
1012
1095
|
assetsInlineLimit: userConfig.build?.assetsInlineLimit ?? Number.MAX_SAFE_INTEGER,
|
|
@@ -1029,7 +1112,7 @@ function UserscriptPlugin(config) {
|
|
|
1029
1112
|
command = viteConfig.command;
|
|
1030
1113
|
resolved = absolutizeEntries(resolved, viteConfig.root);
|
|
1031
1114
|
reactPreamble = hasReactRefreshPlugin(viteConfig.plugins);
|
|
1032
|
-
for (const message of
|
|
1115
|
+
for (const message of collectConfigWarnings(resolved, config)) viteConfig.logger.warn(message);
|
|
1033
1116
|
}
|
|
1034
1117
|
},
|
|
1035
1118
|
{
|
|
@@ -1087,4 +1170,4 @@ function UserscriptPlugin(config) {
|
|
|
1087
1170
|
];
|
|
1088
1171
|
}
|
|
1089
1172
|
//#endregion
|
|
1090
|
-
export {
|
|
1173
|
+
export { UserscriptPlugin as default };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-userscript-plugin",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Vitalij Ryndin",
|
|
7
7
|
"url": "https://github.com/crashmax-dev"
|
|
@@ -41,9 +41,6 @@
|
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"vite": "^8.0.0"
|
|
43
43
|
},
|
|
44
|
-
"dependencies": {
|
|
45
|
-
"open": "11.0.1"
|
|
46
|
-
},
|
|
47
44
|
"devDependencies": {
|
|
48
45
|
"@antfu/eslint-config": "7.2.0",
|
|
49
46
|
"@sveltejs/vite-plugin-svelte": "7.3.0",
|
|
@@ -54,13 +51,13 @@
|
|
|
54
51
|
"eslint": "9.39.2",
|
|
55
52
|
"eslint-plugin-format": "2.0.1",
|
|
56
53
|
"prettier-plugin-css-order": "2.2.0",
|
|
57
|
-
"svelte": "5.
|
|
54
|
+
"svelte": "5.57.0",
|
|
58
55
|
"tsdown": "0.22.14",
|
|
59
|
-
"turbo": "2.10.
|
|
56
|
+
"turbo": "2.10.12",
|
|
60
57
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
61
58
|
"vite": "8.2.2",
|
|
62
59
|
"vitest": "4.1.11",
|
|
63
|
-
"vue": "3.5.
|
|
60
|
+
"vue": "3.5.42"
|
|
64
61
|
},
|
|
65
62
|
"scripts": {
|
|
66
63
|
"dev": "tsdown --watch",
|