vite-userscript-plugin 2.3.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -1
- package/dist/index.d.ts +28 -2
- package/dist/index.js +311 -91
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -111,6 +111,8 @@ import './style.css'
|
|
|
111
111
|
|
|
112
112
|
`index.html` is a normal Vite app next to the userscript. `vite` serves it at `/`. `vite build` writes it to `dist/` beside `{fileName}.user.js`.
|
|
113
113
|
|
|
114
|
+
The plugin sets `build.assetsInlineLimit` very high so userscript assets become data URLs. The HTML app’s assets will too, unless you set `build.assetsInlineLimit` yourself.
|
|
115
|
+
|
|
114
116
|
> [!WARNING]
|
|
115
117
|
> Keep the page's `<script>` entries distinct from `entry`.
|
|
116
118
|
|
|
@@ -142,10 +144,30 @@ See [examples/sourcemap](./examples/sourcemap).
|
|
|
142
144
|
| `generate` | — | Rewrite the generated metablock. |
|
|
143
145
|
| `autoMetaUrls` | `false` | Fill empty `updateURL` / `downloadURL` from `homepage` / `homepageURL` / `website` / `source`. |
|
|
144
146
|
| `metaFile` | `true` | Emit `{fileName}.meta.js`. |
|
|
147
|
+
| `external` | — | Keep these packages out of the bundle and load them via `@require`. Keys are specifiers (`jquery`, `vue`). A string value is the CDN URL (global name from the specifier). Pass `{ global, url }` for `$` / `Vue`. Install `@types/…` for `tsc`; do not install the runtime package. See [examples/external-cdn](./examples/external-cdn). |
|
|
145
148
|
|
|
146
149
|
Everything else on `header` follows the manager metablock (`@grant`, `@require`, `@connect`, …).
|
|
147
150
|
|
|
148
|
-
|
|
151
|
+
```ts
|
|
152
|
+
userscript({
|
|
153
|
+
entry: 'src/index.ts',
|
|
154
|
+
header: {
|
|
155
|
+
name: pkg.name,
|
|
156
|
+
version: pkg.version,
|
|
157
|
+
match: 'https://example.com/*',
|
|
158
|
+
},
|
|
159
|
+
external: {
|
|
160
|
+
jquery: {
|
|
161
|
+
global: '$',
|
|
162
|
+
url: 'https://cdn.jsdelivr.net/npm/jquery@4.0.0/dist/jquery.min.js',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
For types without bundling the package: add `@types/jquery` (not `jquery`) and a `d.ts` that references those types. The import type-checks; the plugin maps it to the CDN global. Full setup: [examples/external-cdn](./examples/external-cdn).
|
|
169
|
+
|
|
170
|
+
In serve mode the header lists every grant. In production the plugin scans the bundle and writes only the grants in use. `window.focus`, `window.close`, and `window.onurlchange` are **not** auto-detected (they collide with DOM APIs) — list them in `header.grant` when you need them. `grant: "none"` disables GM APIs and is never mixed with the scan.
|
|
149
171
|
|
|
150
172
|
> [!WARNING]
|
|
151
173
|
> Keep `metaFile: true` if you use `autoMetaUrls`. Otherwise `@updateURL` points at a file that is not emitted.
|
|
@@ -161,6 +183,7 @@ In serve mode the header lists every grant. In production the plugin scans the b
|
|
|
161
183
|
| [multiple-entries](./examples/multiple-entries) | Two scripts. |
|
|
162
184
|
| [sourcemap](./examples/sourcemap) | Inline map, HTML page, virtual module. |
|
|
163
185
|
| [serve-file](./examples/serve-file) | `server.file`, install `.user.js` or the proxy from the printed URLs. |
|
|
186
|
+
| [external-cdn](./examples/external-cdn) | `@types/jquery` only; runtime `$` from a CDN `@require`. |
|
|
164
187
|
|
|
165
188
|
## FAQ
|
|
166
189
|
|
package/dist/index.d.ts
CHANGED
|
@@ -195,6 +195,22 @@ interface HeaderGenerateContext {
|
|
|
195
195
|
userscript: string;
|
|
196
196
|
mode: HeaderMode;
|
|
197
197
|
}
|
|
198
|
+
interface ExternalRequire {
|
|
199
|
+
/**
|
|
200
|
+
* Global name the `@require` script assigns (e.g. `$`, `Vue`).
|
|
201
|
+
*/
|
|
202
|
+
global: string;
|
|
203
|
+
/**
|
|
204
|
+
* Absolute URL added to `@require`.
|
|
205
|
+
*/
|
|
206
|
+
url: string;
|
|
207
|
+
}
|
|
208
|
+
type UserscriptExternal = Record<string, string | ExternalRequire>;
|
|
209
|
+
interface ResolvedExternal {
|
|
210
|
+
specifier: string;
|
|
211
|
+
global: string;
|
|
212
|
+
url: string;
|
|
213
|
+
}
|
|
198
214
|
/**
|
|
199
215
|
* One userscript. Pass an object, or an array of these, to {@link UserscriptPluginConfig}.
|
|
200
216
|
*/
|
|
@@ -243,12 +259,21 @@ interface UserscriptConfig {
|
|
|
243
259
|
* @default true
|
|
244
260
|
*/
|
|
245
261
|
metaFile?: boolean;
|
|
262
|
+
/**
|
|
263
|
+
* Keep these packages out of the bundle and load them via `@require`.
|
|
264
|
+
* Keys are specifiers (`jquery`, `vue`). A string value is the CDN URL; the
|
|
265
|
+
* global name is derived from the specifier. Pass `{ global, url }` for `$` / `Vue`.
|
|
266
|
+
*
|
|
267
|
+
* Install `@types/…` (or a types-only package) for `tsc`. Do not install the
|
|
268
|
+
* runtime package — serve maps the specifier to the CDN global, build rewrites
|
|
269
|
+
* the import. See `examples/external-cdn`.
|
|
270
|
+
*/
|
|
271
|
+
external?: UserscriptExternal;
|
|
246
272
|
}
|
|
247
273
|
type UserscriptPluginConfig = UserscriptConfig | UserscriptConfig[];
|
|
248
274
|
interface ResolvedScript {
|
|
249
275
|
entry: string;
|
|
250
276
|
fileName: string;
|
|
251
|
-
iifeName: string;
|
|
252
277
|
header: HeaderConfig;
|
|
253
278
|
server: {
|
|
254
279
|
open: ResolvedServerOpen;
|
|
@@ -259,9 +284,10 @@ interface ResolvedScript {
|
|
|
259
284
|
generate?: (ctx: HeaderGenerateContext) => string;
|
|
260
285
|
autoMetaUrls: boolean;
|
|
261
286
|
metaFile: boolean;
|
|
287
|
+
external: ResolvedExternal[];
|
|
262
288
|
}
|
|
263
289
|
//#endregion
|
|
264
290
|
//#region src/plugin.d.ts
|
|
265
291
|
declare function UserscriptPlugin(config: UserscriptPluginConfig): Plugin[];
|
|
266
292
|
//#endregion
|
|
267
|
-
export { type HeaderConfig, type HeaderGenerateContext, type ResolvedScript, type ResolvedServerOpen, type ServerConfig, type ServerOpen, type UserscriptConfig, type UserscriptPluginConfig, UserscriptPlugin as default };
|
|
293
|
+
export { type ExternalRequire, type HeaderConfig, type HeaderGenerateContext, type ResolvedExternal, type ResolvedScript, type ResolvedServerOpen, type ServerConfig, type ServerOpen, type UserscriptConfig, type UserscriptExternal, type UserscriptPluginConfig, UserscriptPlugin as default };
|
package/dist/index.js
CHANGED
|
@@ -54,7 +54,7 @@ const gmIdentifiers = [
|
|
|
54
54
|
];
|
|
55
55
|
//#endregion
|
|
56
56
|
//#region src/grants/scan.ts
|
|
57
|
-
const grantMatchers = grants.map((grant) => ({
|
|
57
|
+
const grantMatchers = grants.filter((grant) => !grant.startsWith("window.")).map((grant) => ({
|
|
58
58
|
grant,
|
|
59
59
|
pattern: new RegExp(`\\b${grant.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`)
|
|
60
60
|
}));
|
|
@@ -74,15 +74,11 @@ function withServeGrants(header) {
|
|
|
74
74
|
grant: [.../* @__PURE__ */ new Set([...header.grant ?? [], ...grants])]
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
-
function withBuildGrants(header, code
|
|
77
|
+
function withBuildGrants(header, code) {
|
|
78
78
|
if (header.grant === "none") return header;
|
|
79
79
|
return {
|
|
80
80
|
...header,
|
|
81
|
-
grant: removeDuplicates([
|
|
82
|
-
...defineGrants(code),
|
|
83
|
-
...removeDuplicates(header.grant),
|
|
84
|
-
...extraGrants
|
|
85
|
-
])
|
|
81
|
+
grant: removeDuplicates([...defineGrants(code), ...removeDuplicates(header.grant)])
|
|
86
82
|
};
|
|
87
83
|
}
|
|
88
84
|
//#endregion
|
|
@@ -312,21 +308,86 @@ function isAsset(item) {
|
|
|
312
308
|
return item.type === "asset";
|
|
313
309
|
}
|
|
314
310
|
//#endregion
|
|
311
|
+
//#region src/build/graph.ts
|
|
312
|
+
function importedChunkIds(chunk, includeDynamic = true) {
|
|
313
|
+
if (!includeDynamic) return [...chunk.imports];
|
|
314
|
+
return [...chunk.imports, ...chunk.dynamicImports ?? []];
|
|
315
|
+
}
|
|
316
|
+
function walkImportedChunks(chunk, bundle, options = {}) {
|
|
317
|
+
const includeDynamic = options.includeDynamic ?? true;
|
|
318
|
+
const result = [];
|
|
319
|
+
const seen = /* @__PURE__ */ new Set();
|
|
320
|
+
const visit = (current) => {
|
|
321
|
+
for (const fileName of importedChunkIds(current, includeDynamic)) {
|
|
322
|
+
if (seen.has(fileName)) continue;
|
|
323
|
+
const dep = bundle[fileName];
|
|
324
|
+
if (!dep || !isChunk(dep) || dep.isEntry) continue;
|
|
325
|
+
seen.add(fileName);
|
|
326
|
+
visit(dep);
|
|
327
|
+
result.push({
|
|
328
|
+
fileName,
|
|
329
|
+
chunk: dep
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
visit(chunk);
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
function collectImportedChunkIds(chunk, bundle) {
|
|
337
|
+
return new Set(walkImportedChunks(chunk, bundle).map((item) => item.fileName));
|
|
338
|
+
}
|
|
339
|
+
function inlineImportedChunks(chunk, bundle) {
|
|
340
|
+
return walkImportedChunks(chunk, bundle).map(({ chunk: dep }) => dep.code.endsWith("\n") ? dep.code : `${dep.code}\n`).join("");
|
|
341
|
+
}
|
|
342
|
+
function namespaceLiteral(chunk) {
|
|
343
|
+
return `{ ${(chunk.exports ?? []).map((name) => {
|
|
344
|
+
if (name === "default") return "default: undefined";
|
|
345
|
+
return /^[a-z_$][\w$]*$/i.test(name) ? name : `${JSON.stringify(name)}: undefined`;
|
|
346
|
+
}).join(", ")} }`;
|
|
347
|
+
}
|
|
348
|
+
function replaceDynamicImportCalls(code, fileName, namespace) {
|
|
349
|
+
const replacement = `Promise.resolve(${namespace})`;
|
|
350
|
+
const baseName = fileName.split("/").pop() ?? fileName;
|
|
351
|
+
const candidates = [
|
|
352
|
+
fileName,
|
|
353
|
+
`./${fileName}`,
|
|
354
|
+
baseName,
|
|
355
|
+
`./${baseName}`
|
|
356
|
+
];
|
|
357
|
+
let next = code;
|
|
358
|
+
for (const name of new Set(candidates)) {
|
|
359
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
360
|
+
next = next.replace(new RegExp(`\\bimport\\s*\\(\\s*(?:/\\*[\\s\\S]*?\\*/\\s*)?(['"])${escaped}\\1\\s*\\)`, "g"), replacement);
|
|
361
|
+
}
|
|
362
|
+
return next;
|
|
363
|
+
}
|
|
364
|
+
function rewriteInlinedDynamicImports(code, chunk, bundle) {
|
|
365
|
+
let next = code;
|
|
366
|
+
const seen = /* @__PURE__ */ new Set();
|
|
367
|
+
const visit = (current) => {
|
|
368
|
+
for (const fileName of current.dynamicImports ?? []) {
|
|
369
|
+
if (seen.has(fileName)) continue;
|
|
370
|
+
seen.add(fileName);
|
|
371
|
+
const dep = bundle[fileName];
|
|
372
|
+
if (!dep || !isChunk(dep)) continue;
|
|
373
|
+
visit(dep);
|
|
374
|
+
next = replaceDynamicImportCalls(next, fileName, namespaceLiteral(dep));
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
visit(chunk);
|
|
378
|
+
if (next.includes("__vitePreload")) return `function __vitePreload(fn){return fn();}\n${next}`;
|
|
379
|
+
return next;
|
|
380
|
+
}
|
|
381
|
+
//#endregion
|
|
315
382
|
//#region src/build/css.ts
|
|
316
383
|
const defaultCssInjector = `(function (css) {
|
|
317
384
|
var style = document.createElement('style')
|
|
318
385
|
style.textContent = css
|
|
319
386
|
;(document.head || document.documentElement).appendChild(style)
|
|
320
387
|
})`;
|
|
321
|
-
function collectImportedCss(chunk, bundle
|
|
388
|
+
function collectImportedCss(chunk, bundle) {
|
|
322
389
|
const files = [...chunk.viteMetadata?.importedCss ?? []];
|
|
323
|
-
for (const
|
|
324
|
-
if (seen.has(imported)) continue;
|
|
325
|
-
const dep = bundle[imported];
|
|
326
|
-
if (!dep || !isChunk(dep) || dep.isEntry) continue;
|
|
327
|
-
seen.add(imported);
|
|
328
|
-
files.push(...collectImportedCss(dep, bundle, seen));
|
|
329
|
-
}
|
|
390
|
+
for (const { chunk: dep } of walkImportedChunks(chunk, bundle)) files.push(...dep.viteMetadata?.importedCss ?? []);
|
|
330
391
|
return files;
|
|
331
392
|
}
|
|
332
393
|
function collectCss(chunk, bundle) {
|
|
@@ -347,6 +408,94 @@ function createCssInject(css) {
|
|
|
347
408
|
return `${defaultCssInjector}(${JSON.stringify(css)});\n`;
|
|
348
409
|
}
|
|
349
410
|
//#endregion
|
|
411
|
+
//#region src/build/external.ts
|
|
412
|
+
function resolveExternals(external) {
|
|
413
|
+
if (!external) return [];
|
|
414
|
+
return Object.entries(external).map(([specifier, value]) => {
|
|
415
|
+
if (typeof value === "string") return {
|
|
416
|
+
specifier,
|
|
417
|
+
url: value,
|
|
418
|
+
global: toIdentifier(specifier)
|
|
419
|
+
};
|
|
420
|
+
return {
|
|
421
|
+
specifier,
|
|
422
|
+
url: value.url,
|
|
423
|
+
global: value.global
|
|
424
|
+
};
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
function toRequireList$1(value) {
|
|
428
|
+
if (value == null) return [];
|
|
429
|
+
return Array.isArray(value) ? value.map(String) : [String(value)];
|
|
430
|
+
}
|
|
431
|
+
function mergeRequireUrls(header, urls) {
|
|
432
|
+
if (!urls.length) return header;
|
|
433
|
+
const merged = [...toRequireList$1(header.require)];
|
|
434
|
+
for (const url of urls) if (!merged.includes(url)) merged.push(url);
|
|
435
|
+
return {
|
|
436
|
+
...header,
|
|
437
|
+
require: merged
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function withExternalRequires(header, externals) {
|
|
441
|
+
return mergeRequireUrls(header, externals.map((item) => item.url));
|
|
442
|
+
}
|
|
443
|
+
function normalizeNamedImports(inner) {
|
|
444
|
+
return inner.split(",").map((part) => {
|
|
445
|
+
const trimmed = part.trim();
|
|
446
|
+
if (!trimmed) return trimmed;
|
|
447
|
+
const asMatch = trimmed.match(/^([a-z_$][\w$]*)\s+as\s+([a-z_$][\w$]*)$/i);
|
|
448
|
+
if (asMatch) return `${asMatch[1]}: ${asMatch[2]}`;
|
|
449
|
+
return trimmed;
|
|
450
|
+
}).filter(Boolean).join(", ");
|
|
451
|
+
}
|
|
452
|
+
const IDENT = "([a-z_$][\\w$]*)";
|
|
453
|
+
const IDENT_FLAGS = "i";
|
|
454
|
+
function rewriteImportClause(clause, globalName) {
|
|
455
|
+
const trimmed = clause.trim();
|
|
456
|
+
const star = trimmed.match(new RegExp(`^\\*\\s+as\\s+${IDENT}$`, IDENT_FLAGS));
|
|
457
|
+
if (star?.[1]) return star[1] === globalName ? "" : `const ${star[1]} = ${globalName};`;
|
|
458
|
+
const defaultStar = trimmed.match(new RegExp(`^${IDENT}\\s*,\\s*\\*\\s+as\\s+${IDENT}$`, IDENT_FLAGS));
|
|
459
|
+
if (defaultStar?.[1] && defaultStar[2]) return [defaultStar[1] === globalName ? "" : `const ${defaultStar[1]} = ${globalName};`, defaultStar[2] === globalName ? "" : `const ${defaultStar[2]} = ${globalName};`].filter(Boolean).join(" ");
|
|
460
|
+
const defaultNamed = trimmed.match(new RegExp(`^${IDENT}\\s*,\\s*\\{([^}]+)\\}$`, IDENT_FLAGS));
|
|
461
|
+
if (defaultNamed?.[1] && defaultNamed[2] != null) {
|
|
462
|
+
const named = `const { ${normalizeNamedImports(defaultNamed[2])} } = ${globalName};`;
|
|
463
|
+
if (defaultNamed[1] === globalName) return named;
|
|
464
|
+
return `const ${defaultNamed[1]} = ${globalName}; ${named}`;
|
|
465
|
+
}
|
|
466
|
+
const named = trimmed.match(/^\{([^}]+)\}$/);
|
|
467
|
+
if (named?.[1] != null) return `const { ${normalizeNamedImports(named[1])} } = ${globalName};`;
|
|
468
|
+
if (/^[a-z_$][\w$]*$/i.test(trimmed)) return trimmed === globalName ? "" : `const ${trimmed} = ${globalName};`;
|
|
469
|
+
return `const ${trimmed} = ${globalName};`;
|
|
470
|
+
}
|
|
471
|
+
function rewriteExternalImports(code, externals) {
|
|
472
|
+
let next = code;
|
|
473
|
+
for (const { specifier, global: globalName } of externals) {
|
|
474
|
+
const quoted = `["']${specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`;
|
|
475
|
+
next = next.replace(new RegExp(`(^|\\n)import\\s*${quoted}\\s*;?`, "g"), "$1");
|
|
476
|
+
next = next.replace(new RegExp(`(^|\\n)import\\s+type\\s+[\\s\\S]*?\\s+from\\s*${quoted}\\s*;?`, "g"), "$1");
|
|
477
|
+
next = next.replace(new RegExp(`(^|\\n)import\\s+([\\s\\S]*?)\\s+from\\s*${quoted}\\s*;?`, "g"), (_match, lead, clause) => {
|
|
478
|
+
const rewritten = rewriteImportClause(clause, globalName);
|
|
479
|
+
return rewritten ? `${lead}${rewritten}` : lead;
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
return next;
|
|
483
|
+
}
|
|
484
|
+
function mergeRolldownExternal(user, specifiers) {
|
|
485
|
+
if (!specifiers.length) return user;
|
|
486
|
+
if (typeof user === "function") {
|
|
487
|
+
const specSet = new Set(specifiers);
|
|
488
|
+
return (id, importer, isResolved) => {
|
|
489
|
+
if (specSet.has(id)) return true;
|
|
490
|
+
return user(id, importer, isResolved);
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
let list = [];
|
|
494
|
+
if (Array.isArray(user)) list = [...user];
|
|
495
|
+
else if (user != null) list = [user];
|
|
496
|
+
return [...list, ...specifiers];
|
|
497
|
+
}
|
|
498
|
+
//#endregion
|
|
350
499
|
//#region src/build/iife.ts
|
|
351
500
|
const SOURCE_MAPPING_URL_RE = /\/\/[#@]\s*sourceMappingURL=\S+/g;
|
|
352
501
|
function stripSourceMappingUrl(code) {
|
|
@@ -358,11 +507,15 @@ function stripImports(code) {
|
|
|
358
507
|
function stripExports(code) {
|
|
359
508
|
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");
|
|
360
509
|
}
|
|
510
|
+
function stripDynamicImports(code) {
|
|
511
|
+
return code.replace(/\bimport\s*\(\s*(?:\/\*[\s\S]*?\*\/\s*)?["'][^"']+["']\s*\)/g, "Promise.resolve({})");
|
|
512
|
+
}
|
|
361
513
|
function stripModuleSyntax(code) {
|
|
362
|
-
return stripExports(stripImports(code));
|
|
514
|
+
return stripExports(stripImports(stripDynamicImports(code)));
|
|
363
515
|
}
|
|
364
516
|
function isAlreadyIife(code) {
|
|
365
|
-
|
|
517
|
+
const trimmed = code.trimStart();
|
|
518
|
+
return !/^\s*(?:import|export)\s/m.test(code) && /^\(\s*(?:async\s+)?function\b/.test(trimmed);
|
|
366
519
|
}
|
|
367
520
|
function ensureIife(code) {
|
|
368
521
|
const withoutMap = stripSourceMappingUrl(code);
|
|
@@ -380,7 +533,7 @@ function toRequireList(value) {
|
|
|
380
533
|
return Array.isArray(value) ? value.map(String) : [String(value)];
|
|
381
534
|
}
|
|
382
535
|
function createWatchProxyHeader(script, jsAbsPath) {
|
|
383
|
-
const header = withServeGrants(
|
|
536
|
+
const header = withServeGrants(withExternalRequires(script.header, script.external));
|
|
384
537
|
return {
|
|
385
538
|
...header,
|
|
386
539
|
require: [...toRequireList(header.require), toFileRequireUrl(jsAbsPath)]
|
|
@@ -403,35 +556,6 @@ function toRequireFileName(fileName) {
|
|
|
403
556
|
}
|
|
404
557
|
//#endregion
|
|
405
558
|
//#region src/build/apply.ts
|
|
406
|
-
function importedChunkIds(chunk) {
|
|
407
|
-
return [...chunk.imports, ...chunk.dynamicImports ?? []];
|
|
408
|
-
}
|
|
409
|
-
function inlineImportedChunks(chunk, bundle, seen = /* @__PURE__ */ new Set()) {
|
|
410
|
-
let prelude = "";
|
|
411
|
-
for (const imported of chunk.imports) {
|
|
412
|
-
if (seen.has(imported)) continue;
|
|
413
|
-
const dep = bundle[imported];
|
|
414
|
-
if (!dep || !isChunk(dep) || dep.isEntry) continue;
|
|
415
|
-
seen.add(imported);
|
|
416
|
-
prelude += inlineImportedChunks(dep, bundle, seen);
|
|
417
|
-
prelude += dep.code.endsWith("\n") ? dep.code : `${dep.code}\n`;
|
|
418
|
-
}
|
|
419
|
-
return prelude;
|
|
420
|
-
}
|
|
421
|
-
function collectImportedChunks(chunk, bundle) {
|
|
422
|
-
const files = /* @__PURE__ */ new Set();
|
|
423
|
-
const walk = (current) => {
|
|
424
|
-
for (const imported of importedChunkIds(current)) {
|
|
425
|
-
if (files.has(imported)) continue;
|
|
426
|
-
const dep = bundle[imported];
|
|
427
|
-
if (!dep || !isChunk(dep) || dep.isEntry) continue;
|
|
428
|
-
files.add(imported);
|
|
429
|
-
walk(dep);
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
walk(chunk);
|
|
433
|
-
return files;
|
|
434
|
-
}
|
|
435
559
|
function collectImportedCssFiles(fileNames, bundle) {
|
|
436
560
|
const files = /* @__PURE__ */ new Set();
|
|
437
561
|
for (const fileName of fileNames) {
|
|
@@ -449,8 +573,13 @@ function withSourceMaps(fileNames) {
|
|
|
449
573
|
function findScriptForChunk(chunk, fileName, scripts) {
|
|
450
574
|
return scripts.find((script) => chunk.name === script.fileName || fileName === `${script.fileName}.js` || fileName === `${script.fileName}.user.js`);
|
|
451
575
|
}
|
|
576
|
+
function offsetUserscriptMap(options) {
|
|
577
|
+
const wrapOffset = isAlreadyIife(stripSourceMappingUrl(options.body)) ? 0 : 1;
|
|
578
|
+
const lineOffset = countHeaderLines(options.headerPrefix ?? "") + countHeaderLines(options.cssPrelude) + wrapOffset + countHeaderLines(options.inlined);
|
|
579
|
+
return stripVendorSourcesContent(offsetSourceMap(options.map, lineOffset, options.fileName));
|
|
580
|
+
}
|
|
452
581
|
function createHeadedUserscript(script, options) {
|
|
453
|
-
const headerConfig = withBuildGrants(script.header, options.wrapped);
|
|
582
|
+
const headerConfig = withBuildGrants(withExternalRequires(script.header, script.external), options.wrapped);
|
|
454
583
|
const prefix = `${generateHeader(headerConfig, {
|
|
455
584
|
align: script.headerAlign,
|
|
456
585
|
autoMetaUrls: script.autoMetaUrls,
|
|
@@ -461,9 +590,14 @@ function createHeadedUserscript(script, options) {
|
|
|
461
590
|
const nextFileName = `${script.fileName}.user.js`;
|
|
462
591
|
let nextCode = `${prefix}${options.code}`;
|
|
463
592
|
if (options.map) {
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
593
|
+
const map = offsetUserscriptMap({
|
|
594
|
+
map: options.map,
|
|
595
|
+
fileName: nextFileName,
|
|
596
|
+
cssPrelude: options.cssPrelude,
|
|
597
|
+
inlined: options.inlined,
|
|
598
|
+
body: options.body,
|
|
599
|
+
headerPrefix: prefix
|
|
600
|
+
});
|
|
467
601
|
nextCode = `${stripSourceMappingUrl(nextCode).replace(/\n+$/g, "\n")}//# sourceMappingURL=${toInlineSourceMappingUrl(map)}\n`;
|
|
468
602
|
}
|
|
469
603
|
return {
|
|
@@ -491,7 +625,7 @@ function applyUserscriptBundle(bundle, config, context) {
|
|
|
491
625
|
const keptChunks = new Set(otherEntryFiles);
|
|
492
626
|
for (const fileName of otherEntryFiles) {
|
|
493
627
|
const chunk = bundle[fileName];
|
|
494
|
-
if (chunk && isChunk(chunk)) for (const dep of
|
|
628
|
+
if (chunk && isChunk(chunk)) for (const dep of collectImportedChunkIds(chunk, bundle)) keptChunks.add(dep);
|
|
495
629
|
}
|
|
496
630
|
const keptCss = collectImportedCssFiles(keptChunks, bundle);
|
|
497
631
|
const leftoverChunks = [];
|
|
@@ -501,7 +635,7 @@ function applyUserscriptBundle(bundle, config, context) {
|
|
|
501
635
|
const { css, files: cssFiles } = collectCss(chunk, bundle);
|
|
502
636
|
for (const cssFile of cssFiles) if (!keptCss.has(cssFile)) leftoverAssets.push(...withSourceMaps([cssFile]));
|
|
503
637
|
const cssPrelude = css ? createCssInject(css) : "";
|
|
504
|
-
const body = `${inlined}${chunk.code}
|
|
638
|
+
const body = rewriteExternalImports(rewriteInlinedDynamicImports(`${inlined}${chunk.code}`, chunk, bundle), script.external);
|
|
505
639
|
const wrapped = ensureIife(body);
|
|
506
640
|
const code = `${cssPrelude}${wrapped}`;
|
|
507
641
|
const emitFileProxy = Boolean(context.emitProxy && context.outDir && script.server.file);
|
|
@@ -518,9 +652,13 @@ function applyUserscriptBundle(bundle, config, context) {
|
|
|
518
652
|
const requireName = toRequireFileName(script.fileName);
|
|
519
653
|
let nextCode = code.endsWith("\n") ? code : `${code}\n`;
|
|
520
654
|
if (chunk.map) {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
655
|
+
chunk.map = offsetUserscriptMap({
|
|
656
|
+
map: chunk.map,
|
|
657
|
+
fileName: requireName,
|
|
658
|
+
cssPrelude,
|
|
659
|
+
inlined,
|
|
660
|
+
body
|
|
661
|
+
});
|
|
524
662
|
nextCode = `${stripSourceMappingUrl(nextCode).replace(/\n+$/g, "\n")}//# sourceMappingURL=${toInlineSourceMappingUrl(chunk.map)}\n`;
|
|
525
663
|
}
|
|
526
664
|
chunk.code = nextCode;
|
|
@@ -652,7 +790,6 @@ function toResolvedScript(config) {
|
|
|
652
790
|
return {
|
|
653
791
|
entry: config.entry,
|
|
654
792
|
fileName,
|
|
655
|
-
iifeName: toIdentifier(fileName),
|
|
656
793
|
header: config.header,
|
|
657
794
|
server: {
|
|
658
795
|
open: resolveServerOpen(config.server?.open, file),
|
|
@@ -662,7 +799,8 @@ function toResolvedScript(config) {
|
|
|
662
799
|
headerAlign: config.headerAlign ?? 1,
|
|
663
800
|
generate: config.generate,
|
|
664
801
|
autoMetaUrls: config.autoMetaUrls ?? false,
|
|
665
|
-
metaFile: config.metaFile ?? true
|
|
802
|
+
metaFile: config.metaFile ?? true,
|
|
803
|
+
external: resolveExternals(config.external)
|
|
666
804
|
};
|
|
667
805
|
}
|
|
668
806
|
function collectAutoMetaUrlsWarnings(config) {
|
|
@@ -714,6 +852,28 @@ function resolvePluginConfig(config) {
|
|
|
714
852
|
return { scripts };
|
|
715
853
|
}
|
|
716
854
|
//#endregion
|
|
855
|
+
//#region src/serve/external.ts
|
|
856
|
+
const EXTERNAL_MODULE_PREFIX = `\0${PLUGIN_NAME}:external:`;
|
|
857
|
+
function toExternalModuleId(specifier) {
|
|
858
|
+
return `${EXTERNAL_MODULE_PREFIX}${specifier}`;
|
|
859
|
+
}
|
|
860
|
+
function matchExternalModuleId(id) {
|
|
861
|
+
if (!id.startsWith(EXTERNAL_MODULE_PREFIX)) return;
|
|
862
|
+
return id.slice(EXTERNAL_MODULE_PREFIX.length);
|
|
863
|
+
}
|
|
864
|
+
function findResolvedExternal(scripts, specifier) {
|
|
865
|
+
for (const script of scripts) {
|
|
866
|
+
const hit = script.external.find((item) => item.specifier === specifier);
|
|
867
|
+
if (hit) return hit;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
function listExternalSpecifiers(scripts) {
|
|
871
|
+
return [...new Set(scripts.flatMap((script) => script.external.map((item) => item.specifier)))];
|
|
872
|
+
}
|
|
873
|
+
function renderExternalModule(globalName) {
|
|
874
|
+
return `const api = globalThis[${JSON.stringify(globalName)}];\nexport default api;\n`;
|
|
875
|
+
}
|
|
876
|
+
//#endregion
|
|
717
877
|
//#region src/serve/gm-shim.ts
|
|
718
878
|
function shouldShimModule(id) {
|
|
719
879
|
const cleanId = id.split("\0").pop() ?? id;
|
|
@@ -813,14 +973,17 @@ import ${JSON.stringify(entryPath)};
|
|
|
813
973
|
}
|
|
814
974
|
//#endregion
|
|
815
975
|
//#region src/serve/wrapper.ts
|
|
976
|
+
function matchUserscriptPath(url, fileName, suffix) {
|
|
977
|
+
return (url.split("?")[0] ?? "") === `/${fileName}${suffix}`;
|
|
978
|
+
}
|
|
816
979
|
function matchDevUserscript(url, fileName) {
|
|
817
|
-
return (url
|
|
980
|
+
return matchUserscriptPath(url, fileName, ".dev.user.js");
|
|
818
981
|
}
|
|
819
982
|
function matchProxyUserscript(url, fileName) {
|
|
820
|
-
return (url
|
|
983
|
+
return matchUserscriptPath(url, fileName, ".proxy.user.js");
|
|
821
984
|
}
|
|
822
985
|
function matchFileUserscript(url, fileName) {
|
|
823
|
-
return (url
|
|
986
|
+
return matchUserscriptPath(url, fileName, ".user.js");
|
|
824
987
|
}
|
|
825
988
|
function toInstallFileName(fileName, kind) {
|
|
826
989
|
return {
|
|
@@ -839,17 +1002,17 @@ function toServeEntryPath(root, entry) {
|
|
|
839
1002
|
const absolute = resolve(root, entry);
|
|
840
1003
|
return `/${relative(root, absolute).split(sep).join(posix.sep)}`;
|
|
841
1004
|
}
|
|
842
|
-
function applyServeHeader(header, prefix) {
|
|
843
|
-
return withServeGrants({
|
|
1005
|
+
function applyServeHeader(header, prefix, externals = []) {
|
|
1006
|
+
return withServeGrants(withExternalRequires({
|
|
844
1007
|
...header,
|
|
845
1008
|
name: prefix === false ? header.name : `${prefix}${header.name}`
|
|
846
|
-
});
|
|
1009
|
+
}, externals));
|
|
847
1010
|
}
|
|
848
1011
|
function generateDevWrapper(options) {
|
|
849
1012
|
const clientUrl = `${options.origin}/@vite/client`;
|
|
850
1013
|
const entryUrl = `${options.origin}${options.entryPath}`;
|
|
851
1014
|
const bootstrapUrl = `${options.origin}${REACT_BOOTSTRAP_PATH}?entry=${encodeURIComponent(options.entryPath)}`;
|
|
852
|
-
const copies = gmIdentifiers.map((id) => `if (typeof ${id} !== 'undefined') gm.${id} = ${id};`).join("\n ");
|
|
1015
|
+
const copies = [...gmIdentifiers.map((id) => `if (typeof ${id} !== 'undefined') gm.${id} = ${id};`), ...(options.externals ?? []).map((item) => `if (typeof ${item.global} !== 'undefined') root.${item.global} = ${item.global};`)].join("\n ");
|
|
853
1016
|
const injectTarget = options.reactPreamble ? bootstrapUrl : entryUrl;
|
|
854
1017
|
const clientInject = options.reactPreamble ? "" : `
|
|
855
1018
|
if (!root.${VITE_CLIENT_FLAG}) {
|
|
@@ -874,14 +1037,15 @@ ${clientInject}
|
|
|
874
1037
|
`;
|
|
875
1038
|
}
|
|
876
1039
|
function generateDevUserscript(options) {
|
|
877
|
-
return `${generateHeader(applyServeHeader(options.script.header, options.prefix), {
|
|
1040
|
+
return `${generateHeader(applyServeHeader(options.script.header, options.prefix, options.script.external), {
|
|
878
1041
|
...options.headerOptions,
|
|
879
1042
|
fileName: options.script.fileName,
|
|
880
1043
|
mode: "serve"
|
|
881
1044
|
})}\n\n${generateDevWrapper({
|
|
882
1045
|
origin: options.origin,
|
|
883
1046
|
entryPath: toServeEntryPath(options.root, options.script.entry),
|
|
884
|
-
reactPreamble: options.reactPreamble
|
|
1047
|
+
reactPreamble: options.reactPreamble,
|
|
1048
|
+
externals: options.script.external
|
|
885
1049
|
})}`;
|
|
886
1050
|
}
|
|
887
1051
|
function findDevScript(url, scripts) {
|
|
@@ -995,6 +1159,49 @@ function configureDevServer(server, resolved, reactPreamble) {
|
|
|
995
1159
|
};
|
|
996
1160
|
}
|
|
997
1161
|
//#endregion
|
|
1162
|
+
//#region src/serve/watch-queue.ts
|
|
1163
|
+
function createDebouncedSingleFlight(task, delay, onError) {
|
|
1164
|
+
let timer;
|
|
1165
|
+
let running = false;
|
|
1166
|
+
let queued = false;
|
|
1167
|
+
const run = async () => {
|
|
1168
|
+
if (running) {
|
|
1169
|
+
queued = true;
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
running = true;
|
|
1173
|
+
try {
|
|
1174
|
+
while (true) {
|
|
1175
|
+
queued = false;
|
|
1176
|
+
await task();
|
|
1177
|
+
if (!queued) break;
|
|
1178
|
+
}
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
onError(error);
|
|
1181
|
+
} finally {
|
|
1182
|
+
running = false;
|
|
1183
|
+
if (queued) {
|
|
1184
|
+
queued = false;
|
|
1185
|
+
run();
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
return {
|
|
1190
|
+
schedule: () => {
|
|
1191
|
+
clearTimeout(timer);
|
|
1192
|
+
timer = setTimeout(() => {
|
|
1193
|
+
timer = void 0;
|
|
1194
|
+
run();
|
|
1195
|
+
}, delay);
|
|
1196
|
+
},
|
|
1197
|
+
cancel: () => {
|
|
1198
|
+
clearTimeout(timer);
|
|
1199
|
+
timer = void 0;
|
|
1200
|
+
},
|
|
1201
|
+
pending: () => running || queued || timer != null
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
//#endregion
|
|
998
1205
|
//#region src/plugin.ts
|
|
999
1206
|
function absolutizeEntries(config, root) {
|
|
1000
1207
|
return { scripts: config.scripts.map((script) => ({
|
|
@@ -1048,30 +1255,34 @@ function UserscriptPlugin(config) {
|
|
|
1048
1255
|
server.config.logger.error(String(error));
|
|
1049
1256
|
return;
|
|
1050
1257
|
}
|
|
1051
|
-
|
|
1258
|
+
const scheduler = createDebouncedSingleFlight(run, 80, (error) => {
|
|
1259
|
+
server.config.logger.error(String(error));
|
|
1260
|
+
});
|
|
1052
1261
|
const onChange = (file) => {
|
|
1053
1262
|
if (file.startsWith(outDirAbs)) return;
|
|
1054
|
-
|
|
1055
|
-
timer = setTimeout(() => {
|
|
1056
|
-
run().catch((error) => {
|
|
1057
|
-
server.config.logger.error(String(error));
|
|
1058
|
-
});
|
|
1059
|
-
}, 80);
|
|
1263
|
+
scheduler.schedule();
|
|
1060
1264
|
};
|
|
1061
1265
|
server.watcher.on("change", onChange);
|
|
1062
1266
|
server.watcher.on("add", onChange);
|
|
1063
|
-
|
|
1064
|
-
|
|
1267
|
+
let cleaned = false;
|
|
1268
|
+
const cleanup = () => {
|
|
1269
|
+
if (cleaned) return;
|
|
1270
|
+
cleaned = true;
|
|
1271
|
+
scheduler.cancel();
|
|
1065
1272
|
server.watcher.off("change", onChange);
|
|
1066
1273
|
server.watcher.off("add", onChange);
|
|
1067
|
-
|
|
1274
|
+
};
|
|
1275
|
+
server.httpServer?.once("close", cleanup);
|
|
1276
|
+
const closeServer = server.close.bind(server);
|
|
1277
|
+
server.close = async () => {
|
|
1278
|
+
cleanup();
|
|
1068
1279
|
return closeServer();
|
|
1069
1280
|
};
|
|
1070
1281
|
};
|
|
1071
1282
|
return [
|
|
1072
1283
|
{
|
|
1073
1284
|
name: `${PLUGIN_NAME}:config`,
|
|
1074
|
-
config(userConfig) {
|
|
1285
|
+
config(userConfig, env) {
|
|
1075
1286
|
const { input, hasHtml } = resolvePluginBuildInput(userConfig, resolved.scripts);
|
|
1076
1287
|
const scriptNames = new Set(resolved.scripts.map((script) => script.fileName));
|
|
1077
1288
|
userConfig.build ??= {};
|
|
@@ -1081,11 +1292,25 @@ function UserscriptPlugin(config) {
|
|
|
1081
1292
|
const userEntryFileNames = userOutput && !Array.isArray(userOutput) ? userOutput.entryFileNames : void 0;
|
|
1082
1293
|
const openScript = resolved.scripts.find((script) => script.server.open);
|
|
1083
1294
|
const openPath = openScript?.server.open ? toInstallPath(openScript.fileName, openScript.server.open) : void 0;
|
|
1295
|
+
const specifiers = listExternalSpecifiers(resolved.scripts);
|
|
1296
|
+
const rolldownOptions = {
|
|
1297
|
+
input,
|
|
1298
|
+
output: {
|
|
1299
|
+
format: "es",
|
|
1300
|
+
entryFileNames: (chunkInfo) => {
|
|
1301
|
+
if (scriptNames.has(chunkInfo.name)) return "[name].js";
|
|
1302
|
+
if (typeof userEntryFileNames === "function") return userEntryFileNames(chunkInfo);
|
|
1303
|
+
if (typeof userEntryFileNames === "string") return userEntryFileNames;
|
|
1304
|
+
return "assets/[name]-[hash].js";
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
if (env.command === "build" && specifiers.length) rolldownOptions.external = mergeRolldownExternal(userConfig.build.rolldownOptions.external, specifiers);
|
|
1084
1309
|
return {
|
|
1085
1310
|
appType: userConfig.appType ?? (hasHtml ? "spa" : "custom"),
|
|
1086
1311
|
optimizeDeps: {
|
|
1087
1312
|
entries: Object.values(input),
|
|
1088
|
-
exclude: [VIRTUAL_MODULE_ID]
|
|
1313
|
+
exclude: [VIRTUAL_MODULE_ID, ...specifiers]
|
|
1089
1314
|
},
|
|
1090
1315
|
server: {
|
|
1091
1316
|
cors: userConfig.server?.cors ?? true,
|
|
@@ -1094,18 +1319,7 @@ function UserscriptPlugin(config) {
|
|
|
1094
1319
|
build: {
|
|
1095
1320
|
minify: userConfig.build?.minify ?? false,
|
|
1096
1321
|
assetsInlineLimit: userConfig.build?.assetsInlineLimit ?? Number.MAX_SAFE_INTEGER,
|
|
1097
|
-
rolldownOptions
|
|
1098
|
-
input,
|
|
1099
|
-
output: {
|
|
1100
|
-
format: "es",
|
|
1101
|
-
entryFileNames: (chunkInfo) => {
|
|
1102
|
-
if (scriptNames.has(chunkInfo.name)) return "[name].js";
|
|
1103
|
-
if (typeof userEntryFileNames === "function") return userEntryFileNames(chunkInfo);
|
|
1104
|
-
if (typeof userEntryFileNames === "string") return userEntryFileNames;
|
|
1105
|
-
return "assets/[name]-[hash].js";
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1322
|
+
rolldownOptions
|
|
1109
1323
|
}
|
|
1110
1324
|
};
|
|
1111
1325
|
},
|
|
@@ -1120,9 +1334,15 @@ function UserscriptPlugin(config) {
|
|
|
1120
1334
|
name: `${PLUGIN_NAME}:virtual`,
|
|
1121
1335
|
resolveId: (id) => {
|
|
1122
1336
|
if (id === "virtual:vite-userscript-plugin") return RESOLVED_VIRTUAL_MODULE_ID;
|
|
1337
|
+
if (command === "serve" && findResolvedExternal(resolved.scripts, id)) return toExternalModuleId(id);
|
|
1123
1338
|
},
|
|
1124
1339
|
load: (id) => {
|
|
1125
1340
|
if (id === RESOLVED_VIRTUAL_MODULE_ID) return renderVirtualModule(createClientSnapshot(resolved.scripts, command));
|
|
1341
|
+
const specifier = matchExternalModuleId(id);
|
|
1342
|
+
if (!specifier) return;
|
|
1343
|
+
const external = findResolvedExternal(resolved.scripts, specifier);
|
|
1344
|
+
if (!external) return;
|
|
1345
|
+
return renderExternalModule(external.global);
|
|
1126
1346
|
}
|
|
1127
1347
|
},
|
|
1128
1348
|
{
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-userscript-plugin",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.4.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Vitalij Ryndin",
|
|
7
7
|
"url": "https://github.com/crashmax-dev"
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"eslint-plugin-format": "2.0.1",
|
|
53
53
|
"prettier-plugin-css-order": "2.2.0",
|
|
54
54
|
"svelte": "5.57.0",
|
|
55
|
-
"
|
|
55
|
+
"taze": "21.1.0",
|
|
56
|
+
"tsdown": "0.23.0",
|
|
56
57
|
"turbo": "2.10.12",
|
|
57
58
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
58
59
|
"vite": "8.2.2",
|
|
@@ -66,7 +67,8 @@
|
|
|
66
67
|
"build:examples": "turbo run build --filter=./examples/*",
|
|
67
68
|
"test": "vitest",
|
|
68
69
|
"test:ui": "vitest --ui --watch",
|
|
69
|
-
"type-check": "tsc --noEmit -p tsconfig.json &&
|
|
70
|
+
"type-check": "tsc --noEmit -p tsconfig.json && turbo run type-check --filter=./examples/*",
|
|
71
|
+
"check-update": "taze major -Irl",
|
|
70
72
|
"sync-types": "node scripts/sync-types.ts",
|
|
71
73
|
"lint": "eslint . --cache",
|
|
72
74
|
"lint:fix": "eslint . --fix --cache"
|