vite-userscript-plugin 1.11.0 → 2.1.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/dist/index.js CHANGED
@@ -1,295 +1,1045 @@
1
- // src/index.ts
2
- import { readFileSync, writeFileSync } from "node:fs";
3
- import { createServer } from "node:http";
4
- import { resolve } from "node:path";
5
- import getPort from "get-port";
1
+ import { basename, posix, relative, resolve, sep } from "node:path";
2
+ import { build } from "vite";
3
+ import { Buffer } from "node:buffer";
4
+ import { pathToFileURL } from "node:url";
5
+ import { existsSync } from "node:fs";
6
+ import { styleText } from "node:util";
6
7
  import openLink from "open";
7
- import colors from "picocolors";
8
- import serveHandler from "serve-handler";
9
- import { createLogger } from "vite";
10
- import { server } from "websocket";
11
-
12
- // src/banner.ts
13
- var Banner = class {
14
- constructor(config) {
15
- this.config = config;
16
- this.addHomepageMeta();
17
- this.maxKeyLength = Math.max(...Object.keys(this.config).map((key) => key.length)) + 1;
18
- }
19
- header = [];
20
- maxKeyLength;
21
- addHomepageMeta() {
22
- const homePage = this.config.homepage ?? this.config.homepageURL;
23
- if (homePage) {
24
- this.config.updateURL = new URL(
25
- `${this.config.name}.meta.js`,
26
- homePage
27
- ).href;
28
- this.config.downloadURL = new URL(
29
- `${this.config.name}.user.js`,
30
- homePage
31
- ).href;
32
- }
33
- }
34
- addSpaces(str) {
35
- return " ".repeat(this.maxKeyLength - str.length);
36
- }
37
- addMetadata(key, value) {
38
- value = Array.isArray(value) ? value.join(" ") : value === true ? "" : value;
39
- this.header.push(`// @${key}${this.addSpaces(key)}${value}`);
40
- }
41
- generate() {
42
- for (const [key, value] of Object.entries(this.config)) {
43
- if (Array.isArray(value)) {
44
- value.forEach((value2) => this.addMetadata(key, value2));
45
- } else {
46
- if (value === void 0) continue;
47
- this.addMetadata(key, value);
48
- }
49
- }
50
- return [
51
- "// ==UserScript==",
52
- ...this.header,
53
- "// ==/UserScript=="
54
- ].join("\n");
55
- }
8
+ //#region src/names.ts
9
+ function sanitizeFileName(name) {
10
+ return name.replace(/[<>:"/\\|?*\u0000-\u001F]+/g, "-").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "userscript";
11
+ }
12
+ function toIdentifier(name) {
13
+ let id = name.replace(/[^\w$]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
14
+ if (/^\d/.test(id)) id = `_${id}`;
15
+ return id || "userscript";
16
+ }
17
+ //#endregion
18
+ //#region src/header.ts
19
+ function ensureTrailingSlash(url) {
20
+ return url.endsWith("/") ? url : `${url}/`;
21
+ }
22
+ function resolveHomePage(header) {
23
+ const homePage = header.homepage ?? header.homepageURL ?? header.website ?? header.source;
24
+ if (typeof homePage !== "string") return;
25
+ const trimmed = homePage.trim();
26
+ return trimmed === "" ? void 0 : trimmed;
27
+ }
28
+ function resolvePublicFileUrl(header, fileName) {
29
+ const homePage = resolveHomePage(header);
30
+ if (!homePage) return;
31
+ try {
32
+ return new URL(fileName, ensureTrailingSlash(homePage)).href;
33
+ } catch {}
34
+ }
35
+ function applyAutoMetaUrls(header, fileName) {
36
+ const updateURL = header.updateURL ?? resolvePublicFileUrl(header, `${fileName}.meta.js`);
37
+ const downloadURL = header.downloadURL ?? resolvePublicFileUrl(header, `${fileName}.user.js`);
38
+ if (!updateURL && !downloadURL) return header;
39
+ return {
40
+ ...header,
41
+ ...updateURL ? { updateURL } : {},
42
+ ...downloadURL ? { downloadURL } : {}
43
+ };
44
+ }
45
+ function sanitizeMetaText(text) {
46
+ return text.replace(/[\r\n\u2028\u2029]+/g, " ");
47
+ }
48
+ function formatValue(value) {
49
+ if (Array.isArray(value)) return sanitizeMetaText(value.join(" "));
50
+ if (value === true) return "";
51
+ if (typeof value === "object") return;
52
+ return sanitizeMetaText(String(value));
53
+ }
54
+ function generateHeader(config, options = {}) {
55
+ const fileName = options.fileName ?? sanitizeFileName(config.name);
56
+ const header = options.autoMetaUrls ? applyAutoMetaUrls({ ...config }, fileName) : { ...config };
57
+ const keys = Object.keys(header).filter((key) => {
58
+ const value = header[key];
59
+ return value !== void 0 && value !== null && value !== false;
60
+ });
61
+ const align = options.align;
62
+ const maxKeyLength = align === false ? 0 : Math.max(...keys.map((key) => key.length), 0) + (align ?? 1);
63
+ const pad = (key) => {
64
+ if (align === false) return " ";
65
+ return " ".repeat(Math.max(1, maxKeyLength - key.length));
66
+ };
67
+ const lines = [];
68
+ const addMetadata = (key, value) => {
69
+ const formatted = formatValue(value);
70
+ if (formatted === void 0) return;
71
+ const safeKey = key.replace(/[\r\n\u2028\u2029]+/g, "");
72
+ if (!safeKey) return;
73
+ lines.push(`// @${safeKey}${pad(key)}${formatted}`);
74
+ };
75
+ for (const key of keys) {
76
+ const value = header[key];
77
+ if (Array.isArray(value)) value.forEach((item) => addMetadata(key, item));
78
+ else addMetadata(key, value);
79
+ }
80
+ const userscript = [
81
+ "// ==UserScript==",
82
+ ...lines,
83
+ "// ==/UserScript=="
84
+ ].join("\n");
85
+ if (!options.generate) return userscript;
86
+ return options.generate({
87
+ userscript,
88
+ mode: options.mode ?? "build"
89
+ });
90
+ }
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
+ }
56
101
  };
57
-
58
- // src/constants.ts
59
- import { dirname } from "node:path";
60
- import { fileURLToPath } from "node:url";
61
- var pluginDir = dirname(fileURLToPath(import.meta.url));
62
- var pluginName = "vite-userscript-plugin";
63
- var regexpScripts = new RegExp(/\.(t|j)sx?$/);
64
- var GM = [
65
- "setValue",
66
- "getValue",
67
- "deleteValue",
68
- "listValues",
69
- "setClipboard",
70
- "addStyle",
71
- "addElement",
72
- "addValueChangeListener",
73
- "removeValueChangeListener",
74
- "registerMenuCommand",
75
- "unregisterMenuCommand",
76
- "download",
77
- "getTab",
78
- "getTabs",
79
- "saveTab",
80
- "openInTab",
81
- "notification",
82
- "getResourceURL",
83
- "getResourceText",
84
- "xmlhttpRequest",
85
- "log",
86
- "info"
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"
87
133
  ];
88
- var GMwindow = [
89
- "unsafeWindow",
90
- "window.onurlchange",
91
- "window.focus",
92
- "window.close"
134
+ const GMwindow = [
135
+ "unsafeWindow",
136
+ "window.onurlchange",
137
+ "window.focus",
138
+ "window.close"
93
139
  ];
94
- var grants = GM.map((grant) => [
95
- `GM_${grant}`,
96
- `GM.${grant}`
97
- ]).flat();
98
- grants.push(...GMwindow);
99
-
100
- // src/helpers.ts
101
- import { transformWithEsbuild } from "vite";
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
+ }));
102
157
  function removeDuplicates(arr) {
103
- return [...new Set(Array.isArray(arr) ? arr : arr ? [arr] : [])];
104
- }
105
- async function transform({ minify, file, name, loader }, transformOptions) {
106
- const { code } = await transformWithEsbuild(file, name, {
107
- minify,
108
- loader,
109
- sourcemap: false,
110
- legalComments: "none",
111
- ...transformOptions
112
- });
113
- return code;
158
+ if (Array.isArray(arr)) return [...new Set(arr)];
159
+ return arr ? [arr] : [];
114
160
  }
115
161
  function defineGrants(code) {
116
- const definedGrants = [];
117
- for (const grant of grants) {
118
- if (code.indexOf(grant) !== -1) {
119
- definedGrants.push(grant);
120
- }
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
+ //#endregion
185
+ //#region src/sourcemap.ts
186
+ function countHeaderLines(prefix) {
187
+ if (!prefix) return 0;
188
+ return prefix.endsWith("\n") ? prefix.slice(0, -1).split("\n").length : prefix.split("\n").length;
189
+ }
190
+ const VLQ_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
191
+ function encodeVlq(value) {
192
+ let vlq = value < 0 ? -value << 1 | 1 : value << 1;
193
+ let encoded = "";
194
+ do {
195
+ let digit = vlq & 31;
196
+ vlq >>>= 5;
197
+ if (vlq > 0) digit |= 32;
198
+ encoded += VLQ_ALPHABET[digit];
199
+ } while (vlq > 0);
200
+ return encoded;
201
+ }
202
+ function isTokenBoundary(line, index) {
203
+ if (index === 0) return true;
204
+ return /\w/.test(line[index] ?? "") !== /\w/.test(line[index - 1] ?? "");
205
+ }
206
+ function identitySourceMap(code, file) {
207
+ const lineCount = countHeaderLines(code);
208
+ const sourceLines = code.split("\n");
209
+ let previousOriginalLine = 0;
210
+ let previousOriginalColumn = 0;
211
+ return {
212
+ version: 3,
213
+ file,
214
+ mappings: Array.from({ length: lineCount }, (_, originalLine) => {
215
+ const line = sourceLines[originalLine] ?? "";
216
+ let previousGeneratedColumn = 0;
217
+ const segments = [];
218
+ const emit = (column) => {
219
+ segments.push(encodeVlq(column - previousGeneratedColumn) + encodeVlq(0) + encodeVlq(originalLine - previousOriginalLine) + encodeVlq(column - previousOriginalColumn));
220
+ previousGeneratedColumn = column;
221
+ previousOriginalLine = originalLine;
222
+ previousOriginalColumn = column;
223
+ };
224
+ emit(0);
225
+ for (let column = 1; column < line.length; column++) if (isTokenBoundary(line, column)) emit(column);
226
+ return segments.join(",");
227
+ }).join(";"),
228
+ names: [],
229
+ sources: file ? [file] : []
230
+ };
231
+ }
232
+ function offsetSourceMap(map, lineOffset, fileName) {
233
+ if (lineOffset <= 0) return fileName ? {
234
+ ...map,
235
+ file: fileName
236
+ } : map;
237
+ return {
238
+ ...map,
239
+ file: fileName ?? map.file,
240
+ mappings: `${";".repeat(lineOffset)}${map.mappings}`
241
+ };
242
+ }
243
+ function isAppSource(source) {
244
+ return !source.includes("node_modules") && !source.includes("\0") && !source.startsWith("virtual:");
245
+ }
246
+ function stripVendorSourcesContent(map) {
247
+ const sources = map.sources ?? [];
248
+ return {
249
+ ...map,
250
+ sourcesContent: sources.map((source, index) => isAppSource(source ?? "") ? map.sourcesContent?.[index] ?? null : null)
251
+ };
252
+ }
253
+ function toInlineSourceMappingUrl(map) {
254
+ return `data:application/json;charset=utf-8;base64,${Buffer.from(JSON.stringify(map)).toString("base64")}`;
255
+ }
256
+ //#endregion
257
+ //#region src/build/bundle.ts
258
+ function isChunk(item) {
259
+ return item.type === "chunk";
260
+ }
261
+ function isAsset(item) {
262
+ return item.type === "asset";
263
+ }
264
+ //#endregion
265
+ //#region src/build/css.ts
266
+ 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)
121
273
  }
122
- return definedGrants;
274
+ })`;
275
+ function collectImportedCss(chunk, bundle, seen = /* @__PURE__ */ new Set()) {
276
+ const files = [...chunk.viteMetadata?.importedCss ?? []];
277
+ for (const imported of chunk.imports) {
278
+ if (seen.has(imported)) continue;
279
+ const dep = bundle[imported];
280
+ if (!dep || !isChunk(dep) || dep.isEntry) continue;
281
+ seen.add(imported);
282
+ files.push(...collectImportedCss(dep, bundle, seen));
283
+ }
284
+ return files;
123
285
  }
286
+ function collectCss(chunk, bundle) {
287
+ const files = [...new Set(collectImportedCss(chunk, bundle))];
288
+ if (!files.length) return {
289
+ css: "",
290
+ files
291
+ };
292
+ return {
293
+ css: files.map((file) => {
294
+ const asset = bundle[file];
295
+ return asset && isAsset(asset) ? String(asset.source) : "";
296
+ }).filter(Boolean).join("\n"),
297
+ files
298
+ };
299
+ }
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`;
305
+ }
306
+ //#endregion
307
+ //#region src/build/iife.ts
308
+ const SOURCE_MAPPING_URL_RE = /\/\/[#@]\s*sourceMappingURL=\S+/g;
309
+ function stripSourceMappingUrl(code) {
310
+ return code.replace(SOURCE_MAPPING_URL_RE, "");
311
+ }
312
+ function stripImports(code) {
313
+ return code.replace(/(^|\n)import(?:\s+type)?(?:\s+[\s\S]*?\s+|\s+)from\s*["'][^"']+["']\s*;?/g, "$1").replace(/(^|\n)import\s*["'][^"']+["']\s*;?/g, "$1");
314
+ }
315
+ function stripExports(code) {
316
+ return code.replace(/^export\s+\{[\s\S]*?\}\s+from\s+["'][^"']+["']\s*;?\s*$/gm, "").replace(/^export\s+\*\s+from\s+["'][^"']+["']\s*;?\s*$/gm, "").replace(/^export\s+\{[\s\S]*?\};?\s*$/gm, "").replace(/^export\s+default\s+/gm, "").replace(/^export\s+async\s+function/gm, "async function").replace(/^export\s+function/gm, "function").replace(/^export\s+class/gm, "class").replace(/^export\s+(const|let|var)/gm, "$1");
317
+ }
318
+ function stripModuleSyntax(code) {
319
+ return stripExports(stripImports(code));
320
+ }
321
+ function isAlreadyIife(code) {
322
+ return !/^\s*(?:import|export)\s/m.test(code) && /\(\s*(?:async\s+)?function\b/.test(code);
323
+ }
324
+ function ensureIife(code) {
325
+ const withoutMap = stripSourceMappingUrl(code);
326
+ if (isAlreadyIife(withoutMap)) return withoutMap.endsWith("\n") ? withoutMap : `${withoutMap}\n`;
327
+ const stripped = stripModuleSyntax(withoutMap);
328
+ return `(${/\bawait\b/.test(stripped) ? "async function" : "function"} () {\n${stripped}\n})();\n`;
329
+ }
330
+ //#endregion
331
+ //#region src/build/proxy.ts
332
+ function toFileRequireUrl(absPath) {
333
+ return pathToFileURL(absPath).href;
334
+ }
335
+ function toRequireList(value) {
336
+ if (value == null) return [];
337
+ return Array.isArray(value) ? value.map(String) : [String(value)];
338
+ }
339
+ function createWatchProxyHeader(script, jsAbsPath) {
340
+ const header = withServeGrants({ ...script.header });
341
+ return {
342
+ ...header,
343
+ require: [...toRequireList(header.require), toFileRequireUrl(jsAbsPath)]
344
+ };
345
+ }
346
+ function generateWatchProxy(script, jsAbsPath) {
347
+ return generateHeader(createWatchProxyHeader(script, jsAbsPath), {
348
+ align: script.align,
349
+ autoMetaUrls: false,
350
+ fileName: script.fileName,
351
+ generate: script.generate,
352
+ mode: "serve"
353
+ });
354
+ }
355
+ function toProxyFileName(fileName) {
356
+ return `${fileName}.proxy.user.js`;
357
+ }
358
+ function toRequireFileName(fileName) {
359
+ return `${fileName}.js`;
360
+ }
361
+ //#endregion
362
+ //#region src/build/apply.ts
363
+ function importedChunkIds(chunk) {
364
+ return [...chunk.imports, ...chunk.dynamicImports ?? []];
365
+ }
366
+ function inlineImportedChunks(chunk, bundle, seen = /* @__PURE__ */ new Set()) {
367
+ let prelude = "";
368
+ for (const imported of chunk.imports) {
369
+ if (seen.has(imported)) continue;
370
+ const dep = bundle[imported];
371
+ if (!dep || !isChunk(dep) || dep.isEntry) continue;
372
+ seen.add(imported);
373
+ prelude += inlineImportedChunks(dep, bundle, seen);
374
+ prelude += dep.code.endsWith("\n") ? dep.code : `${dep.code}\n`;
375
+ }
376
+ return prelude;
377
+ }
378
+ function collectImportedChunks(chunk, bundle) {
379
+ const files = /* @__PURE__ */ new Set();
380
+ const walk = (current) => {
381
+ for (const imported of importedChunkIds(current)) {
382
+ if (files.has(imported)) continue;
383
+ const dep = bundle[imported];
384
+ if (!dep || !isChunk(dep) || dep.isEntry) continue;
385
+ files.add(imported);
386
+ walk(dep);
387
+ }
388
+ };
389
+ walk(chunk);
390
+ return files;
391
+ }
392
+ function collectImportedCssFiles(fileNames, bundle) {
393
+ const files = /* @__PURE__ */ new Set();
394
+ for (const fileName of fileNames) {
395
+ const item = bundle[fileName];
396
+ if (!item || !isChunk(item)) continue;
397
+ for (const cssFile of item.viteMetadata?.importedCss ?? []) files.add(cssFile);
398
+ }
399
+ return files;
400
+ }
401
+ function withSourceMaps(fileNames) {
402
+ const files = [];
403
+ for (const fileName of fileNames) files.push(fileName, `${fileName}.map`);
404
+ return files;
405
+ }
406
+ function findScriptForChunk(chunk, fileName, scripts) {
407
+ return scripts.find((script) => chunk.name === script.fileName || fileName === `${script.fileName}.js` || fileName === `${script.fileName}.user.js`);
408
+ }
409
+ function deleteBundleFiles(bundle, fileNames) {
410
+ for (const fileName of fileNames) delete bundle[fileName];
411
+ }
412
+ function applyUserscriptBundle(bundle, config, context) {
413
+ const { emitFile } = context;
414
+ const userscriptEntries = [];
415
+ const otherEntryFiles = [];
416
+ for (const [fileName, item] of Object.entries(bundle)) {
417
+ if (!isChunk(item) || !item.isEntry) continue;
418
+ const script = findScriptForChunk(item, fileName, config.scripts);
419
+ if (script) userscriptEntries.push({
420
+ fileName,
421
+ chunk: item,
422
+ script
423
+ });
424
+ else otherEntryFiles.push(fileName);
425
+ }
426
+ const keptChunks = new Set(otherEntryFiles);
427
+ for (const fileName of otherEntryFiles) {
428
+ const chunk = bundle[fileName];
429
+ if (chunk && isChunk(chunk)) for (const dep of collectImportedChunks(chunk, bundle)) keptChunks.add(dep);
430
+ }
431
+ const keptCss = collectImportedCssFiles(keptChunks, bundle);
432
+ const leftoverChunks = [];
433
+ const leftoverAssets = [];
434
+ for (const { fileName, chunk, script } of userscriptEntries) {
435
+ const inlined = stripSourceMappingUrl(inlineImportedChunks(chunk, bundle));
436
+ const { css, files: cssFiles } = collectCss(chunk, bundle);
437
+ for (const cssFile of cssFiles) if (!keptCss.has(cssFile)) leftoverAssets.push(...withSourceMaps([cssFile]));
438
+ const cssPrelude = css ? createCssInject(css, script.cssInject) : "";
439
+ const body = `${inlined}${chunk.code}`;
440
+ const wrapped = ensureIife(body);
441
+ const extraGrants = css && script.cssInject === "auto" ? ["GM_addStyle"] : [];
442
+ const code = `${cssPrelude}${wrapped}`;
443
+ const emitFileProxy = Boolean(context.emitProxy && context.outDir && script.server.file);
444
+ leftoverAssets.push(`${fileName}.map`);
445
+ if (emitFileProxy) {
446
+ const requireName = toRequireFileName(script.fileName);
447
+ let nextCode = code.endsWith("\n") ? code : `${code}\n`;
448
+ if (chunk.map) {
449
+ const wrapOffset = isAlreadyIife(stripSourceMappingUrl(body)) ? 0 : 1;
450
+ const lineOffset = countHeaderLines(cssPrelude) + wrapOffset + countHeaderLines(inlined);
451
+ chunk.map = stripVendorSourcesContent(offsetSourceMap(chunk.map, lineOffset, requireName));
452
+ nextCode = `${stripSourceMappingUrl(nextCode).replace(/\n+$/g, "\n")}//# sourceMappingURL=${toInlineSourceMappingUrl(chunk.map)}\n`;
453
+ }
454
+ chunk.code = nextCode;
455
+ chunk.fileName = requireName;
456
+ emitFile(toProxyFileName(script.fileName), `${generateWatchProxy(script, resolve(context.outDir, requireName))}\n`);
457
+ continue;
458
+ }
459
+ const headerConfig = withBuildGrants(script.header, wrapped, extraGrants);
460
+ const prefix = `${generateHeader(headerConfig, {
461
+ align: script.align,
462
+ autoMetaUrls: script.autoMetaUrls,
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,
479
+ autoMetaUrls: script.autoMetaUrls,
480
+ fileName: script.fileName,
481
+ generate: script.generate,
482
+ mode: "meta"
483
+ }));
484
+ }
485
+ for (const [fileName, item] of Object.entries(bundle)) if (isChunk(item) && !item.isEntry && !keptChunks.has(fileName)) leftoverChunks.push(fileName);
486
+ leftoverAssets.push(...withSourceMaps(leftoverChunks));
487
+ deleteBundleFiles(bundle, leftoverChunks);
488
+ deleteBundleFiles(bundle, leftoverAssets);
489
+ }
490
+ //#endregion
491
+ //#region src/client.ts
492
+ const VIRTUAL_MODULE_ID = "virtual:vite-userscript-plugin";
493
+ const RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
494
+ function createClientSnapshot(scripts, command) {
495
+ return scripts.map((script) => {
496
+ let suffix = ".dev.user.js";
497
+ if (command === "build") suffix = ".user.js";
498
+ else if (script.server.file) suffix = ".proxy.user.js";
499
+ return {
500
+ name: script.header.name,
501
+ version: script.header.version,
502
+ file: `${script.fileName}${suffix}`
503
+ };
504
+ });
505
+ }
506
+ function renderVirtualModule(scripts) {
507
+ return `export const scripts = ${JSON.stringify(scripts)}\n`;
508
+ }
509
+ //#endregion
510
+ //#region src/constants.ts
511
+ const PLUGIN_NAME = "vite-userscript-plugin";
512
+ const FAQ_URL = "https://github.com/greasify/vite-userscript-plugin#faq";
513
+ const GM_NAMESPACE = "__viteUserscriptGM__";
514
+ const VITE_CLIENT_FLAG = "__viteUserscriptViteClient__";
515
+ const REACT_PREAMBLE_PATH = `/${PLUGIN_NAME}/react-preamble.js`;
516
+ const REACT_BOOTSTRAP_PATH = `/${PLUGIN_NAME}/react-bootstrap.js`;
517
+ const REACT_REFRESH_PLUGIN_NAMES = /* @__PURE__ */ new Set(["vite:react-refresh", "vite:react-virtual-preamble"]);
518
+ //#endregion
519
+ //#region src/html.ts
520
+ function isHtmlPath(file) {
521
+ return /\.html?$/i.test(file.split("?")[0] ?? file);
522
+ }
523
+ function entryNameFromPath(file) {
524
+ return basename(file.split("?")[0] ?? file).replace(/\.html?$/i, "") || "index";
525
+ }
526
+ function readUserBuildInput(userConfig) {
527
+ return userConfig.build?.rolldownOptions?.input ?? userConfig.build?.rollupOptions?.input;
528
+ }
529
+ function normalizeInput(input) {
530
+ if (input == null) return {};
531
+ if (typeof input === "string") return { [entryNameFromPath(input)]: input };
532
+ if (Array.isArray(input)) {
533
+ const result = {};
534
+ for (const item of input) if (typeof item === "string") result[entryNameFromPath(item)] = item;
535
+ return result;
536
+ }
537
+ if (typeof input === "object") {
538
+ const result = {};
539
+ for (const [key, value] of Object.entries(input)) if (typeof value === "string") result[key] = value;
540
+ return result;
541
+ }
542
+ return {};
543
+ }
544
+ function collectHtmlEntries(root, userInput) {
545
+ const html = {};
546
+ for (const [key, value] of Object.entries(userInput)) if (isHtmlPath(value)) html[key] = value;
547
+ if (!Object.values(html).some((value) => basename(value.split("?")[0] ?? value) === "index.html") && existsSync(resolve(root, "index.html"))) html.index = "index.html";
548
+ return html;
549
+ }
550
+ function mergePluginInput(scripts, userInput, htmlEntries) {
551
+ const input = {
552
+ ...userInput,
553
+ ...htmlEntries
554
+ };
555
+ for (const script of scripts) {
556
+ const existing = input[script.fileName];
557
+ if (existing != null && existing !== script.entry) throw new Error(`[${PLUGIN_NAME}] HTML entry "${script.fileName}" collides with userscript fileName "${script.fileName}". Rename the userscript fileName.`);
558
+ input[script.fileName] = script.entry;
559
+ }
560
+ return input;
561
+ }
562
+ function resolvePluginBuildInput(userConfig, scripts) {
563
+ const root = resolve(userConfig.root ?? process.cwd());
564
+ const userInput = normalizeInput(readUserBuildInput(userConfig));
565
+ const htmlEntries = collectHtmlEntries(root, userInput);
566
+ return {
567
+ input: mergePluginInput(scripts, userInput, htmlEntries),
568
+ hasHtml: Object.keys(htmlEntries).length > 0,
569
+ root
570
+ };
571
+ }
572
+ //#endregion
573
+ //#region src/resolve.ts
574
+ function isEmptyHeaderField(value) {
575
+ if (value == null || value === "") return true;
576
+ return Array.isArray(value) && value.length === 0;
577
+ }
578
+ function assertHeader(header, label) {
579
+ for (const field of [
580
+ "name",
581
+ "version",
582
+ "match"
583
+ ]) if (isEmptyHeaderField(header[field])) throw new Error(`[${PLUGIN_NAME}] ${label} is missing required header.${field}`);
584
+ }
585
+ function toResolvedScript(config) {
586
+ if (!config.entry) throw new Error(`[${PLUGIN_NAME}] Provide an "entry" for each userscript`);
587
+ assertHeader(config.header ?? {}, config.entry);
588
+ const fileName = sanitizeFileName(config.fileName ?? config.header.name);
589
+ return {
590
+ entry: config.entry,
591
+ fileName,
592
+ iifeName: toIdentifier(fileName),
593
+ header: config.header,
594
+ server: {
595
+ open: config.server?.open ?? false,
596
+ prefix: config.server?.prefix ?? "server:",
597
+ file: config.server?.file ?? false
598
+ },
599
+ cssInject: config.cssInject ?? "auto",
600
+ align: config.align ?? 1,
601
+ generate: config.generate,
602
+ autoMetaUrls: config.autoMetaUrls ?? false,
603
+ metaFile: config.metaFile ?? true
604
+ };
605
+ }
606
+ function collectAutoMetaUrlsWarnings(config) {
607
+ const warnings = [];
608
+ for (const script of config.scripts) {
609
+ if (!script.autoMetaUrls) continue;
610
+ if (!script.metaFile) warnings.push(`[${PLUGIN_NAME}] autoMetaUrls is enabled but metaFile is false for "${script.fileName}" — @updateURL points at a .meta.js that will not be emitted`);
611
+ if (!resolveHomePage(script.header)) warnings.push(`[${PLUGIN_NAME}] autoMetaUrls is enabled but "${script.fileName}" has no homepage, homepageURL, website, or source`);
612
+ }
613
+ return warnings;
614
+ }
615
+ function resolvePluginConfig(config) {
616
+ const items = Array.isArray(config) ? config : [config];
617
+ if (!items.length) throw new Error(`[${PLUGIN_NAME}] Provide a userscript config or a non-empty array`);
618
+ const scripts = items.map((item) => toResolvedScript(item));
619
+ const names = /* @__PURE__ */ new Set();
620
+ for (const script of scripts) {
621
+ if (names.has(script.fileName)) throw new Error(`[${PLUGIN_NAME}] Duplicate fileName "${script.fileName}"`);
622
+ names.add(script.fileName);
623
+ }
624
+ return { scripts };
625
+ }
626
+ //#endregion
627
+ //#region src/serve/gm-shim.ts
628
+ function shouldShimModule(id) {
629
+ const cleanId = id.split("\0").pop() ?? id;
630
+ if (cleanId.includes("node_modules")) return false;
631
+ if (/\.(?:css|scss|sass|less|styl|stylus|pcss)(?:$|\?)/i.test(cleanId)) return false;
632
+ if (/[?&](?:vue|svelte)&type=style/.test(cleanId)) return false;
633
+ if (/[?&](?:raw|url)(?:&|$)/.test(cleanId)) return false;
634
+ if (/\.(?:m|c)?[jt]sx?(?:$|\?)/.test(cleanId)) return true;
635
+ if (/[?&]vue&type=script/.test(cleanId) || cleanId.endsWith(".vue")) return true;
636
+ if (/[?&]svelte&type=script/.test(cleanId) || cleanId.endsWith(".svelte")) return true;
637
+ return false;
638
+ }
639
+ function createGmShimPrelude() {
640
+ return `const { ${gmIdentifiers.join(", ")} } = globalThis.${GM_NAMESPACE} ?? globalThis;\n`;
641
+ }
642
+ function shimModule(code, id) {
643
+ const prelude = createGmShimPrelude();
644
+ return {
645
+ code: `${prelude}${code}`,
646
+ map: offsetSourceMap(identitySourceMap(code, id), countHeaderLines(prelude))
647
+ };
648
+ }
649
+ //#endregion
650
+ //#region src/serve/logger.ts
651
+ function formatInstallLine(installUrl) {
652
+ const coloredUrl = styleText("cyan", installUrl.replace(/:(\d+)\//, (_match, port) => `:${styleText("bold", port)}/`));
653
+ return ` ${styleText("green", "➜")} ${styleText("bold", "Userscript")}: ${coloredUrl}`;
654
+ }
655
+ function formatRebuildLine(elapsedMs) {
656
+ return `${styleText("green", "Userscript rebuilt")} ${styleText("dim", `(${elapsedMs}ms)`)}`;
657
+ }
658
+ function formatFaqHint() {
659
+ return `${` ${styleText("green", "➜")} ${styleText("bold", "FAQ")}: `}${styleText("cyan", FAQ_URL)}\n`;
660
+ }
661
+ function stripAnsi(text) {
662
+ return text.replace(/\u001B\[[0-9;]*m/g, "");
663
+ }
664
+ function isViteLocalUrlLine(message) {
665
+ return /Local:\s/.test(stripAnsi(message));
666
+ }
667
+ function createAfterLocalLogger(info, localCount, onAfterLocal) {
668
+ let remaining = localCount;
669
+ let printed = false;
670
+ const flush = () => {
671
+ if (printed) return;
672
+ printed = true;
673
+ onAfterLocal();
674
+ };
675
+ return {
676
+ info: (msg, options) => {
677
+ info(msg, options);
678
+ if (remaining > 0 && isViteLocalUrlLine(String(msg))) {
679
+ remaining -= 1;
680
+ if (remaining === 0) flush();
681
+ }
682
+ },
683
+ flush
684
+ };
685
+ }
686
+ //#endregion
687
+ //#region src/serve/react.ts
688
+ function hasReactRefreshPlugin(plugins) {
689
+ return plugins.some((plugin) => REACT_REFRESH_PLUGIN_NAMES.has(plugin.name));
690
+ }
691
+ function matchReactPreamble(url) {
692
+ return (url.split("?")[0] ?? "") === REACT_PREAMBLE_PATH;
693
+ }
694
+ function matchReactBootstrap(url) {
695
+ return (url.split("?")[0] ?? "") === REACT_BOOTSTRAP_PATH;
696
+ }
697
+ function resolveBootstrapEntry(url) {
698
+ const query = url.includes("?") ? url.slice(url.indexOf("?") + 1) : "";
699
+ const entry = new URLSearchParams(query).get("entry");
700
+ if (!entry?.startsWith("/") || entry.startsWith("//")) return;
701
+ return entry;
702
+ }
703
+ const REACT_PREAMBLE_MODULE = `import { injectIntoGlobalHook } from "/@react-refresh";
704
+ injectIntoGlobalHook(window);
705
+ window.$RefreshReg$ = () => {};
706
+ window.$RefreshSig$ = () => (type) => type;
707
+ window.__vite_plugin_react_preamble_installed__ = true;
708
+ `;
709
+ function createReactBootstrapModule(entryPath) {
710
+ return `import "/@vite/client";
711
+ import ${JSON.stringify(REACT_PREAMBLE_PATH)};
712
+ import ${JSON.stringify(entryPath)};
713
+ `;
714
+ }
715
+ //#endregion
716
+ //#region src/serve/wrapper.ts
717
+ function matchDevUserscript(url, fileName) {
718
+ return (url.split("?")[0] ?? "") === `/${fileName}.dev.user.js`;
719
+ }
720
+ function matchProxyUserscript(url, fileName) {
721
+ return (url.split("?")[0] ?? "") === `/${toProxyFileName(fileName)}`;
722
+ }
723
+ function toInstallUrl(origin, fileName, file = false) {
724
+ const name = file ? toProxyFileName(fileName) : `${fileName}.dev.user.js`;
725
+ return `${origin.replace(/\/$/, "")}/${name}`;
726
+ }
727
+ function toServeEntryPath(root, entry) {
728
+ const absolute = resolve(root, entry);
729
+ return `/${relative(root, absolute).split(sep).join(posix.sep)}`;
730
+ }
731
+ function applyServeHeader(header, prefix) {
732
+ return withServeGrants({
733
+ ...header,
734
+ name: prefix === false ? header.name : `${prefix}${header.name}`
735
+ });
736
+ }
737
+ function generateDevWrapper(options) {
738
+ const clientUrl = `${options.origin}/@vite/client`;
739
+ const entryUrl = `${options.origin}${options.entryPath}`;
740
+ const bootstrapUrl = `${options.origin}${REACT_BOOTSTRAP_PATH}?entry=${encodeURIComponent(options.entryPath)}`;
741
+ const copies = gmIdentifiers.map((id) => `if (typeof ${id} !== 'undefined') gm.${id} = ${id};`).join("\n ");
742
+ const injectTarget = options.reactPreamble ? bootstrapUrl : entryUrl;
743
+ const clientInject = options.reactPreamble ? "" : `
744
+ if (!root.${VITE_CLIENT_FLAG}) {
745
+ root.${VITE_CLIENT_FLAG} = true;
746
+ inject(${JSON.stringify(clientUrl)});
747
+ }`;
748
+ return `(function () {
749
+ var root = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
750
+ var gm = root.${GM_NAMESPACE} || {};
751
+ ${copies}
752
+ root.${GM_NAMESPACE} = gm;
124
753
 
125
- // src/index.ts
126
- function UserscriptPlugin(config) {
127
- try {
128
- let pluginConfig;
129
- let isBuildWatch;
130
- let socketConnection = null;
131
- const fileName = config.fileName ?? config.header.name;
132
- const logger = createLogger("info", {
133
- prefix: `[${pluginName}]`,
134
- allowClearScreen: true
135
- });
136
- const httpServer = createServer((req, res) => {
137
- return serveHandler(req, res, {
138
- public: pluginConfig.build.outDir
139
- });
140
- });
141
- const WebSocketServer = server;
142
- const ws = new WebSocketServer({ httpServer });
143
- ws.on("request", (request) => {
144
- socketConnection = request.accept(null, request.origin);
145
- });
146
- return {
147
- name: pluginName,
148
- apply: "build",
149
- config() {
150
- return {
151
- build: {
152
- target: "esnext",
153
- minify: false,
154
- lib: {
155
- name: fileName,
156
- entry: config.entry,
157
- formats: ["iife"],
158
- fileName: () => `${fileName}.js`
159
- },
160
- rollupOptions: {
161
- output: {
162
- extend: true
163
- }
164
- }
165
- }
166
- };
167
- },
168
- async configResolved(userConfig) {
169
- pluginConfig = userConfig;
170
- isBuildWatch = userConfig.build.watch ?? false;
171
- config.entry = resolve(userConfig.root, config.entry);
172
- Array.from([
173
- "match",
174
- "require",
175
- "include",
176
- "exclude",
177
- "resource",
178
- "connect"
179
- ]).forEach((key) => {
180
- const value = config.header[key];
181
- config.header[key] = removeDuplicates(value);
182
- });
183
- config.server = {
184
- port: await getPort(),
185
- open: false,
186
- ...config.server
187
- };
188
- },
189
- async writeBundle(output, bundle) {
190
- const { open, port } = config.server;
191
- const sanitizedFilename = output.sanitizeFileName(fileName);
192
- const userFilename = `${sanitizedFilename}.user.js`;
193
- const proxyFilename = `${sanitizedFilename}.proxy.user.js`;
194
- const metaFilename = `${sanitizedFilename}.meta.js`;
195
- for (const [fileName2] of Object.entries(bundle)) {
196
- if (regexpScripts.test(fileName2)) {
197
- const rootDir = pluginConfig.root;
198
- const outDir = pluginConfig.build.outDir;
199
- const outPath = resolve(rootDir, outDir, fileName2);
200
- const userFilePath = resolve(rootDir, outDir, userFilename);
201
- const proxyFilePath = resolve(rootDir, outDir, proxyFilename);
202
- const metaFilePath = resolve(rootDir, outDir, metaFilename);
203
- const wsPath = resolve(pluginDir, `ws-${sanitizedFilename}.js`);
204
- try {
205
- let source = readFileSync(outPath, "utf8");
206
- source = await transform(
207
- {
208
- minify: !isBuildWatch,
209
- file: source,
210
- name: fileName2,
211
- loader: "js"
212
- },
213
- config.esbuildTransformOptions
214
- );
215
- config.header.grant = removeDuplicates(
216
- isBuildWatch ? grants : [...defineGrants(source), ...config.header.grant ?? []]
217
- );
218
- if (isBuildWatch) {
219
- const wsFile = readFileSync(resolve(pluginDir, "ws.js"), "utf8");
220
- const wsScript = await transform(
221
- {
222
- minify: !isBuildWatch,
223
- file: wsFile.replace("__WS__", `ws://localhost:${port}`),
224
- name: wsPath,
225
- loader: "js"
226
- },
227
- config.esbuildTransformOptions
228
- );
229
- writeFileSync(wsPath, wsScript);
230
- writeFileSync(
231
- proxyFilePath,
232
- new Banner({
233
- ...config.header,
234
- require: [
235
- ...config.header.require ?? [],
236
- "file://" + wsPath,
237
- "file://" + outPath
238
- ]
239
- }).generate()
240
- );
241
- }
242
- const banner = new Banner(config.header).generate();
243
- writeFileSync(outPath, source);
244
- writeFileSync(metaFilePath, banner);
245
- writeFileSync(userFilePath, `${banner}
246
-
247
- ${source}`);
248
- } catch (err) {
249
- console.log(err);
250
- }
251
- }
252
- }
253
- if (isBuildWatch && !httpServer.listening) {
254
- const link = `http://localhost:${port}`;
255
- httpServer.listen(port, () => {
256
- logger.clearScreen("info");
257
- logger.info(
258
- colors.bold(
259
- `${colors.cyan(">>> [vite-userscript-plugin]")} ${colors.gray(
260
- link
261
- )}`
262
- )
263
- );
264
- });
265
- if (open) {
266
- await openLink(`${link}/${proxyFilename}`);
267
- }
268
- } else if (!isBuildWatch) {
269
- httpServer.close();
270
- process.exit(0);
271
- }
272
- },
273
- buildEnd() {
274
- if (isBuildWatch) {
275
- logger.clearScreen("info");
276
- if (socketConnection) {
277
- socketConnection.sendUTF(
278
- JSON.stringify({
279
- message: "reload"
280
- })
281
- );
282
- }
283
- }
284
- }
285
- };
286
- } catch (err) {
287
- console.error(err);
288
- return {
289
- name: pluginName
290
- };
754
+ function inject(src) {
755
+ var script = document.createElement('script');
756
+ script.type = 'module';
757
+ script.src = src;
758
+ (document.head || document.documentElement).appendChild(script);
291
759
  }
760
+ ${clientInject}
761
+ inject(${JSON.stringify(injectTarget)});
762
+ })();
763
+ `;
764
+ }
765
+ function generateDevUserscript(options) {
766
+ return `${generateHeader(applyServeHeader(options.script.header, options.prefix), {
767
+ ...options.headerOptions,
768
+ fileName: options.script.fileName,
769
+ mode: "serve"
770
+ })}\n\n${generateDevWrapper({
771
+ origin: options.origin,
772
+ entryPath: toServeEntryPath(options.root, options.script.entry),
773
+ reactPreamble: options.reactPreamble
774
+ })}`;
775
+ }
776
+ function findDevScript(url, scripts) {
777
+ return scripts.find((script) => !script.server.file && matchDevUserscript(url, script.fileName));
778
+ }
779
+ function findProxyScript(url, scripts) {
780
+ return scripts.find((script) => script.server.file && matchProxyUserscript(url, script.fileName));
781
+ }
782
+ function createDevUserscript(options) {
783
+ return generateDevUserscript({
784
+ script: options.script,
785
+ origin: options.origin,
786
+ root: options.root,
787
+ prefix: options.script.server.prefix,
788
+ headerOptions: {
789
+ align: options.script.align,
790
+ autoMetaUrls: options.script.autoMetaUrls,
791
+ generate: options.script.generate
792
+ },
793
+ reactPreamble: options.reactPreamble
794
+ });
292
795
  }
293
- export {
294
- UserscriptPlugin as default
796
+ //#endregion
797
+ //#region src/serve/middleware.ts
798
+ const DEV_SCRIPT_HEADERS = {
799
+ "Content-Type": "text/javascript; charset=utf-8",
800
+ "Cache-Control": "no-store"
295
801
  };
802
+ function resolveServerOrigin(urls) {
803
+ const url = urls?.local[0] ?? urls?.network[0];
804
+ return url ? url.replace(/\/$/, "") : "http://localhost:5173";
805
+ }
806
+ function writeScript(res, body) {
807
+ for (const [key, value] of Object.entries(DEV_SCRIPT_HEADERS)) res.setHeader(key, value);
808
+ res.end(body);
809
+ }
810
+ function configureDevServer(server, resolved, reactPreamble) {
811
+ server.middlewares.use((req, res, next) => {
812
+ const url = req.url ?? "";
813
+ if (matchReactPreamble(url)) {
814
+ writeScript(res, REACT_PREAMBLE_MODULE);
815
+ return;
816
+ }
817
+ if (matchReactBootstrap(url)) {
818
+ const entry = resolveBootstrapEntry(url);
819
+ if (!entry) {
820
+ res.statusCode = 400;
821
+ res.end();
822
+ return;
823
+ }
824
+ writeScript(res, createReactBootstrapModule(entry));
825
+ return;
826
+ }
827
+ const proxyScript = findProxyScript(url, resolved.scripts);
828
+ if (proxyScript) {
829
+ writeScript(res, `${generateWatchProxy(proxyScript, resolve(server.config.root, server.config.build.outDir, toRequireFileName(proxyScript.fileName)))}\n`);
830
+ return;
831
+ }
832
+ const script = findDevScript(url, resolved.scripts);
833
+ if (!script) {
834
+ next();
835
+ return;
836
+ }
837
+ writeScript(res, createDevUserscript({
838
+ origin: resolveServerOrigin(server.resolvedUrls),
839
+ root: server.config.root,
840
+ script,
841
+ reactPreamble
842
+ }));
843
+ });
844
+ const printUrls = server.printUrls.bind(server);
845
+ server.printUrls = () => {
846
+ const urls = server.resolvedUrls;
847
+ const info = server.config.logger.info.bind(server.config.logger);
848
+ let origins = [];
849
+ if (urls) origins = urls.local.length ? urls.local : urls.network;
850
+ const printInstall = () => {
851
+ for (const origin of origins) for (const script of resolved.scripts) info(formatInstallLine(toInstallUrl(origin, script.fileName, script.server.file)));
852
+ info(formatFaqHint());
853
+ };
854
+ const logger = createAfterLocalLogger(info, urls?.local.length ?? 0, printInstall);
855
+ const previousInfo = server.config.logger.info;
856
+ server.config.logger.info = logger.info;
857
+ try {
858
+ printUrls();
859
+ } finally {
860
+ server.config.logger.info = previousInfo;
861
+ logger.flush();
862
+ }
863
+ };
864
+ server.httpServer?.once("listening", () => {
865
+ const toOpen = resolved.scripts.filter((script) => script.server.open);
866
+ if (!toOpen.length) return;
867
+ queueMicrotask(() => {
868
+ const origin = resolveServerOrigin(server.resolvedUrls);
869
+ for (const script of toOpen) openLink(toInstallUrl(origin, script.fileName, script.server.file));
870
+ });
871
+ });
872
+ }
873
+ //#endregion
874
+ //#region src/plugin.ts
875
+ function absolutizeEntries(config, root) {
876
+ return { scripts: config.scripts.map((script) => ({
877
+ ...script,
878
+ entry: resolve(root, script.entry)
879
+ })) };
880
+ }
881
+ function UserscriptPlugin(config) {
882
+ let resolved = resolvePluginConfig(config);
883
+ let reactPreamble = false;
884
+ let command = "serve";
885
+ let isWatch = false;
886
+ let mode = "production";
887
+ let outDir = "";
888
+ let fileWatchStarted = false;
889
+ const shouldEmitProxy = () => {
890
+ return isWatch || mode === "development";
891
+ };
892
+ const startFileWatchBuild = async (server) => {
893
+ if (command === "build" || fileWatchStarted) return;
894
+ fileWatchStarted = true;
895
+ const outDirAbs = resolve(server.config.root, server.config.build.outDir);
896
+ let firstBuild = true;
897
+ const run = async () => {
898
+ const isRebuild = !firstBuild;
899
+ const started = Date.now();
900
+ await build({
901
+ configFile: server.config.configFile ?? false,
902
+ root: server.config.root,
903
+ mode: server.config.mode,
904
+ logLevel: "silent",
905
+ clearScreen: false,
906
+ plugins: server.config.configFile ? void 0 : [UserscriptPlugin(config)],
907
+ build: {
908
+ outDir: server.config.build.outDir,
909
+ emptyOutDir: firstBuild,
910
+ minify: server.config.build.minify,
911
+ sourcemap: server.config.build.sourcemap,
912
+ write: true,
913
+ reportCompressedSize: false
914
+ }
915
+ });
916
+ firstBuild = false;
917
+ if (isRebuild) server.config.logger.info(formatRebuildLine(Date.now() - started), { timestamp: true });
918
+ };
919
+ try {
920
+ await run();
921
+ } catch (error) {
922
+ fileWatchStarted = false;
923
+ server.config.logger.error(`[${PLUGIN_NAME}] Failed to start file-mode watch build`);
924
+ server.config.logger.error(String(error));
925
+ return;
926
+ }
927
+ let timer;
928
+ const onChange = (file) => {
929
+ if (file.startsWith(outDirAbs)) return;
930
+ clearTimeout(timer);
931
+ timer = setTimeout(() => {
932
+ run().catch((error) => {
933
+ server.config.logger.error(String(error));
934
+ });
935
+ }, 80);
936
+ };
937
+ server.watcher.on("change", onChange);
938
+ server.watcher.on("add", onChange);
939
+ const closeServer = server.close.bind(server);
940
+ server.close = async () => {
941
+ server.watcher.off("change", onChange);
942
+ server.watcher.off("add", onChange);
943
+ clearTimeout(timer);
944
+ return closeServer();
945
+ };
946
+ };
947
+ return [
948
+ {
949
+ name: `${PLUGIN_NAME}:config`,
950
+ config(userConfig) {
951
+ const { input, hasHtml } = resolvePluginBuildInput(userConfig, resolved.scripts);
952
+ const scriptNames = new Set(resolved.scripts.map((script) => script.fileName));
953
+ userConfig.build ??= {};
954
+ userConfig.build.rolldownOptions ??= {};
955
+ userConfig.build.rolldownOptions.input = input;
956
+ const userOutput = userConfig.build.rolldownOptions.output;
957
+ const userEntryFileNames = userOutput && !Array.isArray(userOutput) ? userOutput.entryFileNames : void 0;
958
+ return {
959
+ appType: userConfig.appType ?? (hasHtml ? "spa" : "custom"),
960
+ optimizeDeps: {
961
+ entries: Object.values(input),
962
+ exclude: [VIRTUAL_MODULE_ID]
963
+ },
964
+ server: { cors: userConfig.server?.cors ?? true },
965
+ build: {
966
+ minify: userConfig.build?.minify ?? false,
967
+ assetsInlineLimit: userConfig.build?.assetsInlineLimit ?? Number.MAX_SAFE_INTEGER,
968
+ rolldownOptions: {
969
+ input,
970
+ output: {
971
+ format: "es",
972
+ entryFileNames: (chunkInfo) => {
973
+ if (scriptNames.has(chunkInfo.name)) return "[name].js";
974
+ if (typeof userEntryFileNames === "function") return userEntryFileNames(chunkInfo);
975
+ if (typeof userEntryFileNames === "string") return userEntryFileNames;
976
+ return "assets/[name]-[hash].js";
977
+ }
978
+ }
979
+ }
980
+ }
981
+ };
982
+ },
983
+ configResolved(viteConfig) {
984
+ command = viteConfig.command;
985
+ resolved = absolutizeEntries(resolved, viteConfig.root);
986
+ reactPreamble = hasReactRefreshPlugin(viteConfig.plugins);
987
+ for (const message of collectAutoMetaUrlsWarnings(resolved)) viteConfig.logger.warn(message);
988
+ }
989
+ },
990
+ {
991
+ name: `${PLUGIN_NAME}:virtual`,
992
+ resolveId: (id) => {
993
+ if (id === "virtual:vite-userscript-plugin") return RESOLVED_VIRTUAL_MODULE_ID;
994
+ },
995
+ load: (id) => {
996
+ if (id === RESOLVED_VIRTUAL_MODULE_ID) return renderVirtualModule(createClientSnapshot(resolved.scripts, command));
997
+ }
998
+ },
999
+ {
1000
+ name: `${PLUGIN_NAME}:serve`,
1001
+ apply: "serve",
1002
+ configureServer(server) {
1003
+ configureDevServer(server, resolved, reactPreamble);
1004
+ if (resolved.scripts.some((script) => script.server.file)) startFileWatchBuild(server);
1005
+ }
1006
+ },
1007
+ {
1008
+ name: `${PLUGIN_NAME}:gm-shim`,
1009
+ apply: "serve",
1010
+ transform: {
1011
+ filter: { id: { exclude: [/node_modules/] } },
1012
+ handler(code, id) {
1013
+ if (!shouldShimModule(id)) return null;
1014
+ return shimModule(code, id);
1015
+ }
1016
+ }
1017
+ },
1018
+ {
1019
+ name: `${PLUGIN_NAME}:build`,
1020
+ apply: "build",
1021
+ enforce: "post",
1022
+ configResolved(viteConfig) {
1023
+ isWatch = Boolean(viteConfig.build.watch);
1024
+ mode = viteConfig.mode;
1025
+ outDir = resolve(viteConfig.root, viteConfig.build.outDir);
1026
+ },
1027
+ generateBundle(_options, bundle) {
1028
+ isWatch = this.meta.watchMode || isWatch;
1029
+ applyUserscriptBundle(bundle, resolved, {
1030
+ emitFile: (fileName, source) => {
1031
+ this.emitFile({
1032
+ type: "asset",
1033
+ fileName,
1034
+ source
1035
+ });
1036
+ },
1037
+ emitProxy: shouldEmitProxy(),
1038
+ outDir
1039
+ });
1040
+ }
1041
+ }
1042
+ ];
1043
+ }
1044
+ //#endregion
1045
+ export { Header, UserscriptPlugin as default, generateHeader, resolveHomePage, resolvePublicFileUrl };