vite-userscript-plugin 2.2.0 → 2.3.1

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,9 +133,9 @@ 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`. Relative `icon` / `require` / `resource` / `supportURL` / `updateURL` / `downloadURL` join `homepage` (`homepageURL` / `website` / `source`). Absolute `http(s):` URLs stay as-is. |
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. `updateURL` / `downloadURL` of `none` disable updates and are not joined. |
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
141
  | `headerAlign` | `1` | Extra spaces after the longest `@key`. `false` — one space. |
package/dist/index.d.ts CHANGED
@@ -63,18 +63,33 @@ type HeaderConfig = {
63
63
  */
64
64
  'author'?: string;
65
65
  /**
66
+ * Script homepage. Also the base for relative `icon`, `iconURL`,
67
+ * `defaulticon`, `icon64`, `icon64URL`, `require`, `resource`, `supportURL`,
68
+ * `updateURL`, and `downloadURL`. Absolute `http(s):` URLs stay as-is.
69
+ * `updateURL` / `downloadURL` of `none` are not joined.
70
+ *
71
+ * Fallback order: `homepage`, `homepageURL`, `website`, `source`.
72
+ *
66
73
  * @see https://www.tampermonkey.net/documentation.php#meta:homepage
67
74
  */
68
75
  'homepage'?: string;
69
76
  /**
77
+ * Alias of `homepage`. Used as the relative-URL base when `homepage` is empty.
78
+ *
70
79
  * @see https://www.tampermonkey.net/documentation.php#meta:homepage
71
80
  */
72
81
  'homepageURL'?: string;
73
82
  /**
83
+ * Alias of `homepage`. Used as the relative-URL base when `homepage` and
84
+ * `homepageURL` are empty.
85
+ *
74
86
  * @see https://www.tampermonkey.net/documentation.php#meta:homepage
75
87
  */
76
88
  'website'?: string;
77
89
  /**
90
+ * Alias of `homepage`. Used as the relative-URL base when `homepage`,
91
+ * `homepageURL`, and `website` are empty.
92
+ *
78
93
  * @see https://www.tampermonkey.net/documentation.php#meta:homepage
79
94
  */
80
95
  'source'?: string;
@@ -124,13 +139,17 @@ type HeaderConfig = {
124
139
  */
125
140
  'noframes'?: boolean;
126
141
  /**
142
+ * `none` disables update checks. It is not joined with homepage.
143
+ *
127
144
  * @see https://www.tampermonkey.net/documentation.php#meta:updateURL
128
145
  */
129
- 'updateURL'?: string;
146
+ 'updateURL'?: 'none' | (string & {});
130
147
  /**
148
+ * `none` disables update checks (pinned / non-latest installs). It is not joined with homepage.
149
+ *
131
150
  * @see https://www.tampermonkey.net/documentation.php#meta:downloadURL
132
151
  */
133
- 'downloadURL'?: string;
152
+ 'downloadURL'?: 'none' | (string & {});
134
153
  /**
135
154
  * @see https://www.tampermonkey.net/documentation.php#meta:supportURL
136
155
  */
@@ -144,14 +163,17 @@ type HeaderConfig = {
144
163
  */
145
164
  'unwrap'?: boolean;
146
165
  };
166
+ type ServerOpen = boolean | 'user' | 'proxy';
167
+ type ResolvedServerOpen = false | 'dev' | 'user' | 'proxy';
147
168
  interface ServerConfig {
148
169
  /**
149
170
  * Open the install target when Vite starts.
150
- * HMR: `.dev.user.js` URL. `file`: `{fileName}.user.js` URL.
171
+ * `true`: HMR `.dev.user.js`; `file` `{fileName}.user.js`.
172
+ * `'user'` / `'proxy'`: file-mode install URL. Vite opens one path.
151
173
  *
152
174
  * @default false
153
175
  */
154
- open?: boolean;
176
+ open?: ServerOpen;
155
177
  /**
156
178
  * Prefix applied to `@name` in serve mode.
157
179
  * Set `false` to disable.
@@ -229,7 +251,7 @@ interface ResolvedScript {
229
251
  iifeName: string;
230
252
  header: HeaderConfig;
231
253
  server: {
232
- open: boolean;
254
+ open: ResolvedServerOpen;
233
255
  prefix: string | false;
234
256
  file: boolean;
235
257
  };
@@ -242,4 +264,4 @@ interface ResolvedScript {
242
264
  //#region src/plugin.d.ts
243
265
  declare function UserscriptPlugin(config: UserscriptPluginConfig): Plugin[];
244
266
  //#endregion
245
- export { type HeaderConfig, type HeaderGenerateContext, type ResolvedScript, type ServerConfig, type UserscriptConfig, type UserscriptPluginConfig, UserscriptPlugin as default };
267
+ 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,6 @@ 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";
8
7
  //#region src/grants/catalog.ts
9
8
  const GM = [
10
9
  "setValue",
@@ -119,16 +118,17 @@ function resolveHomePage(header) {
119
118
  const trimmed = homePage.trim();
120
119
  return trimmed === "" ? void 0 : trimmed;
121
120
  }
122
- function isResolvableHeaderPath(value) {
121
+ function isResolvableHeaderPath(value, field) {
123
122
  const trimmed = value.trim();
124
123
  if (!trimmed) return false;
124
+ if ((field === "updateURL" || field === "downloadURL") && trimmed.toLowerCase() === "none") return false;
125
125
  if (ABSOLUTE_URL_RE.test(trimmed) || trimmed.startsWith("//") || trimmed.startsWith("/")) return false;
126
126
  return true;
127
127
  }
128
- function containsResolvableHeaderPath(value) {
129
- if (typeof value === "string") return isResolvableHeaderPath(value);
128
+ function containsResolvableHeaderPath(value, field) {
129
+ if (typeof value === "string") return isResolvableHeaderPath(value, field);
130
130
  if (!Array.isArray(value)) return false;
131
- return value.some((item) => typeof item === "string" && isResolvableHeaderPath(item));
131
+ return value.some((item) => typeof item === "string" && isResolvableHeaderPath(item, field));
132
132
  }
133
133
  function containsResolvableResourcePath(resource) {
134
134
  if (!Array.isArray(resource)) return false;
@@ -138,7 +138,7 @@ function containsResolvableResourcePath(resource) {
138
138
  }
139
139
  function listHomepageRelativeFields(header) {
140
140
  const fields = [];
141
- for (const key of HOMEPAGE_RELATIVE_FIELDS) if (containsResolvableHeaderPath(header[key])) fields.push(key);
141
+ for (const key of HOMEPAGE_RELATIVE_FIELDS) if (containsResolvableHeaderPath(header[key], key)) fields.push(key);
142
142
  if (containsResolvableResourcePath(header.resource)) fields.push("resource");
143
143
  return fields;
144
144
  }
@@ -149,12 +149,12 @@ function resolvePublicFileUrl(header, fileName) {
149
149
  return new URL(fileName, ensureTrailingSlash(homePage)).href;
150
150
  } catch {}
151
151
  }
152
- function resolveHeaderUrlValue(header, value) {
152
+ function resolveHeaderUrlValue(header, value, field) {
153
153
  if (typeof value === "string") {
154
- if (!isResolvableHeaderPath(value)) return value;
154
+ if (!isResolvableHeaderPath(value, field)) return value;
155
155
  return resolvePublicFileUrl(header, value.trim()) ?? value;
156
156
  }
157
- if (Array.isArray(value)) return value.map((item) => typeof item === "string" ? resolveHeaderUrlValue(header, item) : item);
157
+ if (Array.isArray(value)) return value.map((item) => typeof item === "string" ? resolveHeaderUrlValue(header, item, field) : item);
158
158
  return value;
159
159
  }
160
160
  function resolveResourceUrls(header, resource) {
@@ -170,7 +170,7 @@ function resolveResourceUrls(header, resource) {
170
170
  function applyHomepageRelativeUrls(header) {
171
171
  if (!resolveHomePage(header)) return header;
172
172
  const next = { ...header };
173
- for (const key of HOMEPAGE_RELATIVE_FIELDS) if (next[key] != null) Object.assign(next, { [key]: resolveHeaderUrlValue(next, next[key]) });
173
+ for (const key of HOMEPAGE_RELATIVE_FIELDS) if (next[key] != null) Object.assign(next, { [key]: resolveHeaderUrlValue(next, next[key], key) });
174
174
  if (next.resource != null) next.resource = resolveResourceUrls(next, next.resource);
175
175
  return next;
176
176
  }
@@ -639,19 +639,25 @@ function assertHeader(header, label) {
639
639
  "match"
640
640
  ]) if (isEmptyHeaderField(header[field])) throw new Error(`[${PLUGIN_NAME}] ${label} is missing required header.${field}`);
641
641
  }
642
+ function resolveServerOpen(open, file) {
643
+ if (!open) return false;
644
+ if (open === true) return file ? "user" : "dev";
645
+ return file ? open : "dev";
646
+ }
642
647
  function toResolvedScript(config) {
643
648
  if (!config.entry) throw new Error(`[${PLUGIN_NAME}] Provide an "entry" for each userscript`);
644
649
  assertHeader(config.header ?? {}, config.entry);
645
650
  const fileName = sanitizeFileName(config.fileName ?? config.header.name);
651
+ const file = config.server?.file ?? false;
646
652
  return {
647
653
  entry: config.entry,
648
654
  fileName,
649
655
  iifeName: toIdentifier(fileName),
650
656
  header: config.header,
651
657
  server: {
652
- open: config.server?.open ?? false,
658
+ open: resolveServerOpen(config.server?.open, file),
653
659
  prefix: config.server?.prefix ?? "server:",
654
- file: config.server?.file ?? false
660
+ file
655
661
  },
656
662
  headerAlign: config.headerAlign ?? 1,
657
663
  generate: config.generate,
@@ -678,8 +684,23 @@ function collectHomepageRelativeUrlWarnings(config) {
678
684
  }
679
685
  return warnings;
680
686
  }
681
- function collectConfigWarnings(config) {
682
- return [...collectAutoMetaUrlsWarnings(config), ...collectHomepageRelativeUrlWarnings(config)];
687
+ function collectServerOpenWarnings(config, input) {
688
+ const warnings = [];
689
+ const items = Array.isArray(input) ? input : [input];
690
+ for (const [index, script] of config.scripts.entries()) {
691
+ const open = items[index]?.server?.open;
692
+ 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`);
693
+ }
694
+ const opened = config.scripts.filter((script) => script.server.open);
695
+ if (opened.length > 1) warnings.push(`[${PLUGIN_NAME}] server.open is set on multiple scripts; Vite opens only "${opened[0]?.fileName}"`);
696
+ return warnings;
697
+ }
698
+ function collectConfigWarnings(config, input) {
699
+ return [
700
+ ...collectAutoMetaUrlsWarnings(config),
701
+ ...collectHomepageRelativeUrlWarnings(config),
702
+ ...collectServerOpenWarnings(config, input)
703
+ ];
683
704
  }
684
705
  function resolvePluginConfig(config) {
685
706
  const items = Array.isArray(config) ? config : [config];
@@ -717,16 +738,24 @@ function shimModule(code, id) {
717
738
  }
718
739
  //#endregion
719
740
  //#region src/serve/logger.ts
741
+ const INSTALL_LABEL_WIDTH = 10;
742
+ function colonPadFor(label) {
743
+ return " ".repeat(Math.max(0, INSTALL_LABEL_WIDTH - label.length) + 1);
744
+ }
745
+ function formatLabeledLine(label, value) {
746
+ return ` ${styleText("green", "➜")} ${styleText("bold", label)}:${colonPadFor(label)}${value}`;
747
+ }
748
+ function alignViteUrlLine(message) {
749
+ return message.replace(/(Local(?:\u001B\[[0-9;]*m)*:)(\s+)/, (_match, prefix) => `${prefix}${colonPadFor("Local")}`);
750
+ }
720
751
  function formatInstallLine(installUrl, label = "Userscript") {
721
- const coloredUrl = styleText("cyan", installUrl.replace(/:(\d+)\//, (_match, port) => `:${styleText("bold", port)}/`));
722
- const padded = label.padEnd(10);
723
- return ` ${styleText("green", "➜")} ${styleText("bold", padded)}: ${coloredUrl}`;
752
+ return formatLabeledLine(label, styleText("cyan", installUrl.replace(/:(\d+)\//, (_match, port) => `:${styleText("bold", port)}/`)));
724
753
  }
725
754
  function formatRebuildLine(elapsedMs) {
726
755
  return `${styleText("green", "Userscript rebuilt")} ${styleText("dim", `(${elapsedMs}ms)`)}`;
727
756
  }
728
757
  function formatFaqHint() {
729
- return `${` ${styleText("green", "➜")} ${styleText("bold", "FAQ")}: `}${styleText("cyan", FAQ_URL)}\n`;
758
+ return `${formatLabeledLine("FAQ", styleText("cyan", FAQ_URL))}\n`;
730
759
  }
731
760
  function stripAnsi(text) {
732
761
  return text.replace(/\u001B\[[0-9;]*m/g, "");
@@ -744,7 +773,7 @@ function createAfterLocalLogger(info, localCount, onAfterLocal) {
744
773
  };
745
774
  return {
746
775
  info: (msg, options) => {
747
- info(msg, options);
776
+ info(typeof msg === "string" ? alignViteUrlLine(msg) : msg, options);
748
777
  if (remaining > 0 && isViteLocalUrlLine(String(msg))) {
749
778
  remaining -= 1;
750
779
  if (remaining === 0) flush();
@@ -793,13 +822,18 @@ function matchProxyUserscript(url, fileName) {
793
822
  function matchFileUserscript(url, fileName) {
794
823
  return (url.split("?")[0] ?? "") === `/${fileName}.user.js`;
795
824
  }
796
- function toInstallUrl(origin, fileName, kind = "dev") {
797
- const names = {
825
+ function toInstallFileName(fileName, kind) {
826
+ return {
798
827
  dev: `${fileName}.dev.user.js`,
799
828
  proxy: toProxyFileName(fileName),
800
829
  user: `${fileName}.user.js`
801
- };
802
- return `${origin.replace(/\/$/, "")}/${names[kind]}`;
830
+ }[kind];
831
+ }
832
+ function toInstallPath(fileName, kind = "dev") {
833
+ return `/${toInstallFileName(fileName, kind)}`;
834
+ }
835
+ function toInstallUrl(origin, fileName, kind = "dev") {
836
+ return `${origin.replace(/\/$/, "")}${toInstallPath(fileName, kind)}`;
803
837
  }
804
838
  function toServeEntryPath(root, entry) {
805
839
  const absolute = resolve(root, entry);
@@ -959,14 +993,6 @@ function configureDevServer(server, resolved, reactPreamble) {
959
993
  logger.flush();
960
994
  }
961
995
  };
962
- server.httpServer?.once("listening", () => {
963
- const toOpen = resolved.scripts.filter((script) => script.server.open);
964
- if (!toOpen.length) return;
965
- queueMicrotask(() => {
966
- const origin = resolveServerOrigin(server.resolvedUrls);
967
- for (const script of toOpen) openLink(toInstallUrl(origin, script.fileName, script.server.file ? "user" : "dev"));
968
- });
969
- });
970
996
  }
971
997
  //#endregion
972
998
  //#region src/plugin.ts
@@ -1053,13 +1079,18 @@ function UserscriptPlugin(config) {
1053
1079
  userConfig.build.rolldownOptions.input = input;
1054
1080
  const userOutput = userConfig.build.rolldownOptions.output;
1055
1081
  const userEntryFileNames = userOutput && !Array.isArray(userOutput) ? userOutput.entryFileNames : void 0;
1082
+ const openScript = resolved.scripts.find((script) => script.server.open);
1083
+ const openPath = openScript?.server.open ? toInstallPath(openScript.fileName, openScript.server.open) : void 0;
1056
1084
  return {
1057
1085
  appType: userConfig.appType ?? (hasHtml ? "spa" : "custom"),
1058
1086
  optimizeDeps: {
1059
1087
  entries: Object.values(input),
1060
1088
  exclude: [VIRTUAL_MODULE_ID]
1061
1089
  },
1062
- server: { cors: userConfig.server?.cors ?? true },
1090
+ server: {
1091
+ cors: userConfig.server?.cors ?? true,
1092
+ ...openPath ? { open: openPath } : {}
1093
+ },
1063
1094
  build: {
1064
1095
  minify: userConfig.build?.minify ?? false,
1065
1096
  assetsInlineLimit: userConfig.build?.assetsInlineLimit ?? Number.MAX_SAFE_INTEGER,
@@ -1082,7 +1113,7 @@ function UserscriptPlugin(config) {
1082
1113
  command = viteConfig.command;
1083
1114
  resolved = absolutizeEntries(resolved, viteConfig.root);
1084
1115
  reactPreamble = hasReactRefreshPlugin(viteConfig.plugins);
1085
- for (const message of collectConfigWarnings(resolved)) viteConfig.logger.warn(message);
1116
+ for (const message of collectConfigWarnings(resolved, config)) viteConfig.logger.warn(message);
1086
1117
  }
1087
1118
  },
1088
1119
  {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vite-userscript-plugin",
3
3
  "type": "module",
4
- "version": "2.2.0",
4
+ "version": "2.3.1",
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",