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 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
- | `cssInject` | `'auto'` | How production CSS is injected. `'auto'` uses `GM_addStyle` or a `<style>` node. |
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: `.dev.user.js` URL. `file`: `{fileName}.user.js` URL.
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?: boolean;
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
- align?: number | false;
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: boolean;
235
+ open: ResolvedServerOpen;
240
236
  prefix: string | false;
241
237
  file: boolean;
242
238
  };
243
- cssInject: CssInject;
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 CssInject, Header, type HeaderConfig, type HeaderGenerateContext, type ResolvedScript, type ServerConfig, type UserscriptConfig, type UserscriptPluginConfig, UserscriptPlugin as default, generateHeader, resolveHomePage, resolvePublicFileUrl };
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
- import openLink from "open";
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 header = options.autoMetaUrls ? applyAutoMetaUrls({ ...config }, fileName) : { ...config };
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
- if (typeof GM_addStyle === 'function') {
268
- GM_addStyle(css)
269
- } else {
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, cssInject = "auto") {
301
- const payload = JSON.stringify(css);
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.align,
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, options.extraGrants);
452
+ const headerConfig = withBuildGrants(script.header, options.wrapped);
411
453
  const prefix = `${generateHeader(headerConfig, {
412
- align: script.align,
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, script.cssInject) : "";
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.align,
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 ?? false,
657
+ open: resolveServerOpen(config.server?.open, file),
612
658
  prefix: config.server?.prefix ?? "server:",
613
- file: config.server?.file ?? false
659
+ file
614
660
  },
615
- cssInject: config.cssInject ?? "auto",
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
- const coloredUrl = styleText("cyan", installUrl.replace(/:(\d+)\//, (_match, port) => `:${styleText("bold", port)}/`));
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 `${` ${styleText("green", "➜")} ${styleText("bold", "FAQ")}: `}${styleText("cyan", FAQ_URL)}\n`;
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 toInstallUrl(origin, fileName, kind = "dev") {
744
- const names = {
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
- return `${origin.replace(/\/$/, "")}/${names[kind]}`;
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.align,
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: { cors: userConfig.server?.cors ?? true },
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 collectAutoMetaUrlsWarnings(resolved)) viteConfig.logger.warn(message);
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 { Header, UserscriptPlugin as default, generateHeader, resolveHomePage, resolvePublicFileUrl };
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.1.1",
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.56.10",
54
+ "svelte": "5.57.0",
58
55
  "tsdown": "0.22.14",
59
- "turbo": "2.10.11",
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.41"
60
+ "vue": "3.5.42"
64
61
  },
65
62
  "scripts": {
66
63
  "dev": "tsdown --watch",