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