vite-userscript-plugin 2.3.0 → 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 +25 -2
- package/dist/index.d.ts +49 -4
- package/dist/index.js +321 -100
- 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
|
|
|
@@ -133,7 +135,7 @@ See [examples/sourcemap](./examples/sourcemap).
|
|
|
133
135
|
| Option | Default | Description |
|
|
134
136
|
| --- | --- | --- |
|
|
135
137
|
| `entry` | — | Userscript entry. Required. |
|
|
136
|
-
| `header` | — | Metablock. Required: `name`, `version`, `match`. Relative `icon` / `require` / `resource` / `supportURL` / `updateURL` / `downloadURL` join `homepage` (`homepageURL` / `website` / `source`). Absolute `http(s):` URLs stay as-is. |
|
|
138
|
+
| `header` | — | Metablock. Required: `name`, `version`, `match`. Relative `icon` / `require` / `resource` / `supportURL` / `updateURL` / `downloadURL` join `homepage` (`homepageURL` / `website` / `source`). Absolute `http(s):` URLs stay as-is. `updateURL` / `downloadURL` of `none` disable updates and are not joined. |
|
|
137
139
|
| `fileName` | sanitized `header.name` | Output base name (`{fileName}.user.js`). |
|
|
138
140
|
| `server.open` | `false` | Open the install target when Vite starts (`true`, `'user'`, `'proxy'`). HMR: `.dev.user.js`. `file`: `.user.js` or `.proxy.user.js`. Vite opens one URL. |
|
|
139
141
|
| `server.prefix` | `'server:'` | Prefix for `@name` in serve mode. `false` disables it. |
|
|
@@ -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
|
@@ -63,18 +63,33 @@ type HeaderConfig = {
|
|
|
63
63
|
*/
|
|
64
64
|
'author'?: string;
|
|
65
65
|
/**
|
|
66
|
+
* Script homepage. Also the base for relative `icon`, `iconURL`,
|
|
67
|
+
* `defaulticon`, `icon64`, `icon64URL`, `require`, `resource`, `supportURL`,
|
|
68
|
+
* `updateURL`, and `downloadURL`. Absolute `http(s):` URLs stay as-is.
|
|
69
|
+
* `updateURL` / `downloadURL` of `none` are not joined.
|
|
70
|
+
*
|
|
71
|
+
* Fallback order: `homepage`, `homepageURL`, `website`, `source`.
|
|
72
|
+
*
|
|
66
73
|
* @see https://www.tampermonkey.net/documentation.php#meta:homepage
|
|
67
74
|
*/
|
|
68
75
|
'homepage'?: string;
|
|
69
76
|
/**
|
|
77
|
+
* Alias of `homepage`. Used as the relative-URL base when `homepage` is empty.
|
|
78
|
+
*
|
|
70
79
|
* @see https://www.tampermonkey.net/documentation.php#meta:homepage
|
|
71
80
|
*/
|
|
72
81
|
'homepageURL'?: string;
|
|
73
82
|
/**
|
|
83
|
+
* Alias of `homepage`. Used as the relative-URL base when `homepage` and
|
|
84
|
+
* `homepageURL` are empty.
|
|
85
|
+
*
|
|
74
86
|
* @see https://www.tampermonkey.net/documentation.php#meta:homepage
|
|
75
87
|
*/
|
|
76
88
|
'website'?: string;
|
|
77
89
|
/**
|
|
90
|
+
* Alias of `homepage`. Used as the relative-URL base when `homepage`,
|
|
91
|
+
* `homepageURL`, and `website` are empty.
|
|
92
|
+
*
|
|
78
93
|
* @see https://www.tampermonkey.net/documentation.php#meta:homepage
|
|
79
94
|
*/
|
|
80
95
|
'source'?: string;
|
|
@@ -124,13 +139,17 @@ type HeaderConfig = {
|
|
|
124
139
|
*/
|
|
125
140
|
'noframes'?: boolean;
|
|
126
141
|
/**
|
|
142
|
+
* `none` disables update checks. It is not joined with homepage.
|
|
143
|
+
*
|
|
127
144
|
* @see https://www.tampermonkey.net/documentation.php#meta:updateURL
|
|
128
145
|
*/
|
|
129
|
-
'updateURL'?: string;
|
|
146
|
+
'updateURL'?: 'none' | (string & {});
|
|
130
147
|
/**
|
|
148
|
+
* `none` disables update checks (pinned / non-latest installs). It is not joined with homepage.
|
|
149
|
+
*
|
|
131
150
|
* @see https://www.tampermonkey.net/documentation.php#meta:downloadURL
|
|
132
151
|
*/
|
|
133
|
-
'downloadURL'?: string;
|
|
152
|
+
'downloadURL'?: 'none' | (string & {});
|
|
134
153
|
/**
|
|
135
154
|
* @see https://www.tampermonkey.net/documentation.php#meta:supportURL
|
|
136
155
|
*/
|
|
@@ -176,6 +195,22 @@ interface HeaderGenerateContext {
|
|
|
176
195
|
userscript: string;
|
|
177
196
|
mode: HeaderMode;
|
|
178
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
|
+
}
|
|
179
214
|
/**
|
|
180
215
|
* One userscript. Pass an object, or an array of these, to {@link UserscriptPluginConfig}.
|
|
181
216
|
*/
|
|
@@ -224,12 +259,21 @@ interface UserscriptConfig {
|
|
|
224
259
|
* @default true
|
|
225
260
|
*/
|
|
226
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;
|
|
227
272
|
}
|
|
228
273
|
type UserscriptPluginConfig = UserscriptConfig | UserscriptConfig[];
|
|
229
274
|
interface ResolvedScript {
|
|
230
275
|
entry: string;
|
|
231
276
|
fileName: string;
|
|
232
|
-
iifeName: string;
|
|
233
277
|
header: HeaderConfig;
|
|
234
278
|
server: {
|
|
235
279
|
open: ResolvedServerOpen;
|
|
@@ -240,9 +284,10 @@ interface ResolvedScript {
|
|
|
240
284
|
generate?: (ctx: HeaderGenerateContext) => string;
|
|
241
285
|
autoMetaUrls: boolean;
|
|
242
286
|
metaFile: boolean;
|
|
287
|
+
external: ResolvedExternal[];
|
|
243
288
|
}
|
|
244
289
|
//#endregion
|
|
245
290
|
//#region src/plugin.d.ts
|
|
246
291
|
declare function UserscriptPlugin(config: UserscriptPluginConfig): Plugin[];
|
|
247
292
|
//#endregion
|
|
248
|
-
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
|
|
@@ -118,16 +114,17 @@ function resolveHomePage(header) {
|
|
|
118
114
|
const trimmed = homePage.trim();
|
|
119
115
|
return trimmed === "" ? void 0 : trimmed;
|
|
120
116
|
}
|
|
121
|
-
function isResolvableHeaderPath(value) {
|
|
117
|
+
function isResolvableHeaderPath(value, field) {
|
|
122
118
|
const trimmed = value.trim();
|
|
123
119
|
if (!trimmed) return false;
|
|
120
|
+
if ((field === "updateURL" || field === "downloadURL") && trimmed.toLowerCase() === "none") return false;
|
|
124
121
|
if (ABSOLUTE_URL_RE.test(trimmed) || trimmed.startsWith("//") || trimmed.startsWith("/")) return false;
|
|
125
122
|
return true;
|
|
126
123
|
}
|
|
127
|
-
function containsResolvableHeaderPath(value) {
|
|
128
|
-
if (typeof value === "string") return isResolvableHeaderPath(value);
|
|
124
|
+
function containsResolvableHeaderPath(value, field) {
|
|
125
|
+
if (typeof value === "string") return isResolvableHeaderPath(value, field);
|
|
129
126
|
if (!Array.isArray(value)) return false;
|
|
130
|
-
return value.some((item) => typeof item === "string" && isResolvableHeaderPath(item));
|
|
127
|
+
return value.some((item) => typeof item === "string" && isResolvableHeaderPath(item, field));
|
|
131
128
|
}
|
|
132
129
|
function containsResolvableResourcePath(resource) {
|
|
133
130
|
if (!Array.isArray(resource)) return false;
|
|
@@ -137,7 +134,7 @@ function containsResolvableResourcePath(resource) {
|
|
|
137
134
|
}
|
|
138
135
|
function listHomepageRelativeFields(header) {
|
|
139
136
|
const fields = [];
|
|
140
|
-
for (const key of HOMEPAGE_RELATIVE_FIELDS) if (containsResolvableHeaderPath(header[key])) fields.push(key);
|
|
137
|
+
for (const key of HOMEPAGE_RELATIVE_FIELDS) if (containsResolvableHeaderPath(header[key], key)) fields.push(key);
|
|
141
138
|
if (containsResolvableResourcePath(header.resource)) fields.push("resource");
|
|
142
139
|
return fields;
|
|
143
140
|
}
|
|
@@ -148,12 +145,12 @@ function resolvePublicFileUrl(header, fileName) {
|
|
|
148
145
|
return new URL(fileName, ensureTrailingSlash(homePage)).href;
|
|
149
146
|
} catch {}
|
|
150
147
|
}
|
|
151
|
-
function resolveHeaderUrlValue(header, value) {
|
|
148
|
+
function resolveHeaderUrlValue(header, value, field) {
|
|
152
149
|
if (typeof value === "string") {
|
|
153
|
-
if (!isResolvableHeaderPath(value)) return value;
|
|
150
|
+
if (!isResolvableHeaderPath(value, field)) return value;
|
|
154
151
|
return resolvePublicFileUrl(header, value.trim()) ?? value;
|
|
155
152
|
}
|
|
156
|
-
if (Array.isArray(value)) return value.map((item) => typeof item === "string" ? resolveHeaderUrlValue(header, item) : item);
|
|
153
|
+
if (Array.isArray(value)) return value.map((item) => typeof item === "string" ? resolveHeaderUrlValue(header, item, field) : item);
|
|
157
154
|
return value;
|
|
158
155
|
}
|
|
159
156
|
function resolveResourceUrls(header, resource) {
|
|
@@ -169,7 +166,7 @@ function resolveResourceUrls(header, resource) {
|
|
|
169
166
|
function applyHomepageRelativeUrls(header) {
|
|
170
167
|
if (!resolveHomePage(header)) return header;
|
|
171
168
|
const next = { ...header };
|
|
172
|
-
for (const key of HOMEPAGE_RELATIVE_FIELDS) if (next[key] != null) Object.assign(next, { [key]: resolveHeaderUrlValue(next, next[key]) });
|
|
169
|
+
for (const key of HOMEPAGE_RELATIVE_FIELDS) if (next[key] != null) Object.assign(next, { [key]: resolveHeaderUrlValue(next, next[key], key) });
|
|
173
170
|
if (next.resource != null) next.resource = resolveResourceUrls(next, next.resource);
|
|
174
171
|
return next;
|
|
175
172
|
}
|
|
@@ -311,21 +308,86 @@ function isAsset(item) {
|
|
|
311
308
|
return item.type === "asset";
|
|
312
309
|
}
|
|
313
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
|
|
314
382
|
//#region src/build/css.ts
|
|
315
383
|
const defaultCssInjector = `(function (css) {
|
|
316
384
|
var style = document.createElement('style')
|
|
317
385
|
style.textContent = css
|
|
318
386
|
;(document.head || document.documentElement).appendChild(style)
|
|
319
387
|
})`;
|
|
320
|
-
function collectImportedCss(chunk, bundle
|
|
388
|
+
function collectImportedCss(chunk, bundle) {
|
|
321
389
|
const files = [...chunk.viteMetadata?.importedCss ?? []];
|
|
322
|
-
for (const
|
|
323
|
-
if (seen.has(imported)) continue;
|
|
324
|
-
const dep = bundle[imported];
|
|
325
|
-
if (!dep || !isChunk(dep) || dep.isEntry) continue;
|
|
326
|
-
seen.add(imported);
|
|
327
|
-
files.push(...collectImportedCss(dep, bundle, seen));
|
|
328
|
-
}
|
|
390
|
+
for (const { chunk: dep } of walkImportedChunks(chunk, bundle)) files.push(...dep.viteMetadata?.importedCss ?? []);
|
|
329
391
|
return files;
|
|
330
392
|
}
|
|
331
393
|
function collectCss(chunk, bundle) {
|
|
@@ -346,6 +408,94 @@ function createCssInject(css) {
|
|
|
346
408
|
return `${defaultCssInjector}(${JSON.stringify(css)});\n`;
|
|
347
409
|
}
|
|
348
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
|
|
349
499
|
//#region src/build/iife.ts
|
|
350
500
|
const SOURCE_MAPPING_URL_RE = /\/\/[#@]\s*sourceMappingURL=\S+/g;
|
|
351
501
|
function stripSourceMappingUrl(code) {
|
|
@@ -357,11 +507,15 @@ function stripImports(code) {
|
|
|
357
507
|
function stripExports(code) {
|
|
358
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");
|
|
359
509
|
}
|
|
510
|
+
function stripDynamicImports(code) {
|
|
511
|
+
return code.replace(/\bimport\s*\(\s*(?:\/\*[\s\S]*?\*\/\s*)?["'][^"']+["']\s*\)/g, "Promise.resolve({})");
|
|
512
|
+
}
|
|
360
513
|
function stripModuleSyntax(code) {
|
|
361
|
-
return stripExports(stripImports(code));
|
|
514
|
+
return stripExports(stripImports(stripDynamicImports(code)));
|
|
362
515
|
}
|
|
363
516
|
function isAlreadyIife(code) {
|
|
364
|
-
|
|
517
|
+
const trimmed = code.trimStart();
|
|
518
|
+
return !/^\s*(?:import|export)\s/m.test(code) && /^\(\s*(?:async\s+)?function\b/.test(trimmed);
|
|
365
519
|
}
|
|
366
520
|
function ensureIife(code) {
|
|
367
521
|
const withoutMap = stripSourceMappingUrl(code);
|
|
@@ -379,7 +533,7 @@ function toRequireList(value) {
|
|
|
379
533
|
return Array.isArray(value) ? value.map(String) : [String(value)];
|
|
380
534
|
}
|
|
381
535
|
function createWatchProxyHeader(script, jsAbsPath) {
|
|
382
|
-
const header = withServeGrants(
|
|
536
|
+
const header = withServeGrants(withExternalRequires(script.header, script.external));
|
|
383
537
|
return {
|
|
384
538
|
...header,
|
|
385
539
|
require: [...toRequireList(header.require), toFileRequireUrl(jsAbsPath)]
|
|
@@ -402,35 +556,6 @@ function toRequireFileName(fileName) {
|
|
|
402
556
|
}
|
|
403
557
|
//#endregion
|
|
404
558
|
//#region src/build/apply.ts
|
|
405
|
-
function importedChunkIds(chunk) {
|
|
406
|
-
return [...chunk.imports, ...chunk.dynamicImports ?? []];
|
|
407
|
-
}
|
|
408
|
-
function inlineImportedChunks(chunk, bundle, seen = /* @__PURE__ */ new Set()) {
|
|
409
|
-
let prelude = "";
|
|
410
|
-
for (const imported of chunk.imports) {
|
|
411
|
-
if (seen.has(imported)) continue;
|
|
412
|
-
const dep = bundle[imported];
|
|
413
|
-
if (!dep || !isChunk(dep) || dep.isEntry) continue;
|
|
414
|
-
seen.add(imported);
|
|
415
|
-
prelude += inlineImportedChunks(dep, bundle, seen);
|
|
416
|
-
prelude += dep.code.endsWith("\n") ? dep.code : `${dep.code}\n`;
|
|
417
|
-
}
|
|
418
|
-
return prelude;
|
|
419
|
-
}
|
|
420
|
-
function collectImportedChunks(chunk, bundle) {
|
|
421
|
-
const files = /* @__PURE__ */ new Set();
|
|
422
|
-
const walk = (current) => {
|
|
423
|
-
for (const imported of importedChunkIds(current)) {
|
|
424
|
-
if (files.has(imported)) continue;
|
|
425
|
-
const dep = bundle[imported];
|
|
426
|
-
if (!dep || !isChunk(dep) || dep.isEntry) continue;
|
|
427
|
-
files.add(imported);
|
|
428
|
-
walk(dep);
|
|
429
|
-
}
|
|
430
|
-
};
|
|
431
|
-
walk(chunk);
|
|
432
|
-
return files;
|
|
433
|
-
}
|
|
434
559
|
function collectImportedCssFiles(fileNames, bundle) {
|
|
435
560
|
const files = /* @__PURE__ */ new Set();
|
|
436
561
|
for (const fileName of fileNames) {
|
|
@@ -448,8 +573,13 @@ function withSourceMaps(fileNames) {
|
|
|
448
573
|
function findScriptForChunk(chunk, fileName, scripts) {
|
|
449
574
|
return scripts.find((script) => chunk.name === script.fileName || fileName === `${script.fileName}.js` || fileName === `${script.fileName}.user.js`);
|
|
450
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
|
+
}
|
|
451
581
|
function createHeadedUserscript(script, options) {
|
|
452
|
-
const headerConfig = withBuildGrants(script.header, options.wrapped);
|
|
582
|
+
const headerConfig = withBuildGrants(withExternalRequires(script.header, script.external), options.wrapped);
|
|
453
583
|
const prefix = `${generateHeader(headerConfig, {
|
|
454
584
|
align: script.headerAlign,
|
|
455
585
|
autoMetaUrls: script.autoMetaUrls,
|
|
@@ -460,9 +590,14 @@ function createHeadedUserscript(script, options) {
|
|
|
460
590
|
const nextFileName = `${script.fileName}.user.js`;
|
|
461
591
|
let nextCode = `${prefix}${options.code}`;
|
|
462
592
|
if (options.map) {
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
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
|
+
});
|
|
466
601
|
nextCode = `${stripSourceMappingUrl(nextCode).replace(/\n+$/g, "\n")}//# sourceMappingURL=${toInlineSourceMappingUrl(map)}\n`;
|
|
467
602
|
}
|
|
468
603
|
return {
|
|
@@ -490,7 +625,7 @@ function applyUserscriptBundle(bundle, config, context) {
|
|
|
490
625
|
const keptChunks = new Set(otherEntryFiles);
|
|
491
626
|
for (const fileName of otherEntryFiles) {
|
|
492
627
|
const chunk = bundle[fileName];
|
|
493
|
-
if (chunk && isChunk(chunk)) for (const dep of
|
|
628
|
+
if (chunk && isChunk(chunk)) for (const dep of collectImportedChunkIds(chunk, bundle)) keptChunks.add(dep);
|
|
494
629
|
}
|
|
495
630
|
const keptCss = collectImportedCssFiles(keptChunks, bundle);
|
|
496
631
|
const leftoverChunks = [];
|
|
@@ -500,7 +635,7 @@ function applyUserscriptBundle(bundle, config, context) {
|
|
|
500
635
|
const { css, files: cssFiles } = collectCss(chunk, bundle);
|
|
501
636
|
for (const cssFile of cssFiles) if (!keptCss.has(cssFile)) leftoverAssets.push(...withSourceMaps([cssFile]));
|
|
502
637
|
const cssPrelude = css ? createCssInject(css) : "";
|
|
503
|
-
const body = `${inlined}${chunk.code}
|
|
638
|
+
const body = rewriteExternalImports(rewriteInlinedDynamicImports(`${inlined}${chunk.code}`, chunk, bundle), script.external);
|
|
504
639
|
const wrapped = ensureIife(body);
|
|
505
640
|
const code = `${cssPrelude}${wrapped}`;
|
|
506
641
|
const emitFileProxy = Boolean(context.emitProxy && context.outDir && script.server.file);
|
|
@@ -517,9 +652,13 @@ function applyUserscriptBundle(bundle, config, context) {
|
|
|
517
652
|
const requireName = toRequireFileName(script.fileName);
|
|
518
653
|
let nextCode = code.endsWith("\n") ? code : `${code}\n`;
|
|
519
654
|
if (chunk.map) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
655
|
+
chunk.map = offsetUserscriptMap({
|
|
656
|
+
map: chunk.map,
|
|
657
|
+
fileName: requireName,
|
|
658
|
+
cssPrelude,
|
|
659
|
+
inlined,
|
|
660
|
+
body
|
|
661
|
+
});
|
|
523
662
|
nextCode = `${stripSourceMappingUrl(nextCode).replace(/\n+$/g, "\n")}//# sourceMappingURL=${toInlineSourceMappingUrl(chunk.map)}\n`;
|
|
524
663
|
}
|
|
525
664
|
chunk.code = nextCode;
|
|
@@ -651,7 +790,6 @@ function toResolvedScript(config) {
|
|
|
651
790
|
return {
|
|
652
791
|
entry: config.entry,
|
|
653
792
|
fileName,
|
|
654
|
-
iifeName: toIdentifier(fileName),
|
|
655
793
|
header: config.header,
|
|
656
794
|
server: {
|
|
657
795
|
open: resolveServerOpen(config.server?.open, file),
|
|
@@ -661,7 +799,8 @@ function toResolvedScript(config) {
|
|
|
661
799
|
headerAlign: config.headerAlign ?? 1,
|
|
662
800
|
generate: config.generate,
|
|
663
801
|
autoMetaUrls: config.autoMetaUrls ?? false,
|
|
664
|
-
metaFile: config.metaFile ?? true
|
|
802
|
+
metaFile: config.metaFile ?? true,
|
|
803
|
+
external: resolveExternals(config.external)
|
|
665
804
|
};
|
|
666
805
|
}
|
|
667
806
|
function collectAutoMetaUrlsWarnings(config) {
|
|
@@ -713,6 +852,28 @@ function resolvePluginConfig(config) {
|
|
|
713
852
|
return { scripts };
|
|
714
853
|
}
|
|
715
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
|
|
716
877
|
//#region src/serve/gm-shim.ts
|
|
717
878
|
function shouldShimModule(id) {
|
|
718
879
|
const cleanId = id.split("\0").pop() ?? id;
|
|
@@ -812,14 +973,17 @@ import ${JSON.stringify(entryPath)};
|
|
|
812
973
|
}
|
|
813
974
|
//#endregion
|
|
814
975
|
//#region src/serve/wrapper.ts
|
|
976
|
+
function matchUserscriptPath(url, fileName, suffix) {
|
|
977
|
+
return (url.split("?")[0] ?? "") === `/${fileName}${suffix}`;
|
|
978
|
+
}
|
|
815
979
|
function matchDevUserscript(url, fileName) {
|
|
816
|
-
return (url
|
|
980
|
+
return matchUserscriptPath(url, fileName, ".dev.user.js");
|
|
817
981
|
}
|
|
818
982
|
function matchProxyUserscript(url, fileName) {
|
|
819
|
-
return (url
|
|
983
|
+
return matchUserscriptPath(url, fileName, ".proxy.user.js");
|
|
820
984
|
}
|
|
821
985
|
function matchFileUserscript(url, fileName) {
|
|
822
|
-
return (url
|
|
986
|
+
return matchUserscriptPath(url, fileName, ".user.js");
|
|
823
987
|
}
|
|
824
988
|
function toInstallFileName(fileName, kind) {
|
|
825
989
|
return {
|
|
@@ -838,17 +1002,17 @@ function toServeEntryPath(root, entry) {
|
|
|
838
1002
|
const absolute = resolve(root, entry);
|
|
839
1003
|
return `/${relative(root, absolute).split(sep).join(posix.sep)}`;
|
|
840
1004
|
}
|
|
841
|
-
function applyServeHeader(header, prefix) {
|
|
842
|
-
return withServeGrants({
|
|
1005
|
+
function applyServeHeader(header, prefix, externals = []) {
|
|
1006
|
+
return withServeGrants(withExternalRequires({
|
|
843
1007
|
...header,
|
|
844
1008
|
name: prefix === false ? header.name : `${prefix}${header.name}`
|
|
845
|
-
});
|
|
1009
|
+
}, externals));
|
|
846
1010
|
}
|
|
847
1011
|
function generateDevWrapper(options) {
|
|
848
1012
|
const clientUrl = `${options.origin}/@vite/client`;
|
|
849
1013
|
const entryUrl = `${options.origin}${options.entryPath}`;
|
|
850
1014
|
const bootstrapUrl = `${options.origin}${REACT_BOOTSTRAP_PATH}?entry=${encodeURIComponent(options.entryPath)}`;
|
|
851
|
-
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 ");
|
|
852
1016
|
const injectTarget = options.reactPreamble ? bootstrapUrl : entryUrl;
|
|
853
1017
|
const clientInject = options.reactPreamble ? "" : `
|
|
854
1018
|
if (!root.${VITE_CLIENT_FLAG}) {
|
|
@@ -873,14 +1037,15 @@ ${clientInject}
|
|
|
873
1037
|
`;
|
|
874
1038
|
}
|
|
875
1039
|
function generateDevUserscript(options) {
|
|
876
|
-
return `${generateHeader(applyServeHeader(options.script.header, options.prefix), {
|
|
1040
|
+
return `${generateHeader(applyServeHeader(options.script.header, options.prefix, options.script.external), {
|
|
877
1041
|
...options.headerOptions,
|
|
878
1042
|
fileName: options.script.fileName,
|
|
879
1043
|
mode: "serve"
|
|
880
1044
|
})}\n\n${generateDevWrapper({
|
|
881
1045
|
origin: options.origin,
|
|
882
1046
|
entryPath: toServeEntryPath(options.root, options.script.entry),
|
|
883
|
-
reactPreamble: options.reactPreamble
|
|
1047
|
+
reactPreamble: options.reactPreamble,
|
|
1048
|
+
externals: options.script.external
|
|
884
1049
|
})}`;
|
|
885
1050
|
}
|
|
886
1051
|
function findDevScript(url, scripts) {
|
|
@@ -994,6 +1159,49 @@ function configureDevServer(server, resolved, reactPreamble) {
|
|
|
994
1159
|
};
|
|
995
1160
|
}
|
|
996
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
|
|
997
1205
|
//#region src/plugin.ts
|
|
998
1206
|
function absolutizeEntries(config, root) {
|
|
999
1207
|
return { scripts: config.scripts.map((script) => ({
|
|
@@ -1047,30 +1255,34 @@ function UserscriptPlugin(config) {
|
|
|
1047
1255
|
server.config.logger.error(String(error));
|
|
1048
1256
|
return;
|
|
1049
1257
|
}
|
|
1050
|
-
|
|
1258
|
+
const scheduler = createDebouncedSingleFlight(run, 80, (error) => {
|
|
1259
|
+
server.config.logger.error(String(error));
|
|
1260
|
+
});
|
|
1051
1261
|
const onChange = (file) => {
|
|
1052
1262
|
if (file.startsWith(outDirAbs)) return;
|
|
1053
|
-
|
|
1054
|
-
timer = setTimeout(() => {
|
|
1055
|
-
run().catch((error) => {
|
|
1056
|
-
server.config.logger.error(String(error));
|
|
1057
|
-
});
|
|
1058
|
-
}, 80);
|
|
1263
|
+
scheduler.schedule();
|
|
1059
1264
|
};
|
|
1060
1265
|
server.watcher.on("change", onChange);
|
|
1061
1266
|
server.watcher.on("add", onChange);
|
|
1062
|
-
|
|
1063
|
-
|
|
1267
|
+
let cleaned = false;
|
|
1268
|
+
const cleanup = () => {
|
|
1269
|
+
if (cleaned) return;
|
|
1270
|
+
cleaned = true;
|
|
1271
|
+
scheduler.cancel();
|
|
1064
1272
|
server.watcher.off("change", onChange);
|
|
1065
1273
|
server.watcher.off("add", onChange);
|
|
1066
|
-
|
|
1274
|
+
};
|
|
1275
|
+
server.httpServer?.once("close", cleanup);
|
|
1276
|
+
const closeServer = server.close.bind(server);
|
|
1277
|
+
server.close = async () => {
|
|
1278
|
+
cleanup();
|
|
1067
1279
|
return closeServer();
|
|
1068
1280
|
};
|
|
1069
1281
|
};
|
|
1070
1282
|
return [
|
|
1071
1283
|
{
|
|
1072
1284
|
name: `${PLUGIN_NAME}:config`,
|
|
1073
|
-
config(userConfig) {
|
|
1285
|
+
config(userConfig, env) {
|
|
1074
1286
|
const { input, hasHtml } = resolvePluginBuildInput(userConfig, resolved.scripts);
|
|
1075
1287
|
const scriptNames = new Set(resolved.scripts.map((script) => script.fileName));
|
|
1076
1288
|
userConfig.build ??= {};
|
|
@@ -1080,11 +1292,25 @@ function UserscriptPlugin(config) {
|
|
|
1080
1292
|
const userEntryFileNames = userOutput && !Array.isArray(userOutput) ? userOutput.entryFileNames : void 0;
|
|
1081
1293
|
const openScript = resolved.scripts.find((script) => script.server.open);
|
|
1082
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);
|
|
1083
1309
|
return {
|
|
1084
1310
|
appType: userConfig.appType ?? (hasHtml ? "spa" : "custom"),
|
|
1085
1311
|
optimizeDeps: {
|
|
1086
1312
|
entries: Object.values(input),
|
|
1087
|
-
exclude: [VIRTUAL_MODULE_ID]
|
|
1313
|
+
exclude: [VIRTUAL_MODULE_ID, ...specifiers]
|
|
1088
1314
|
},
|
|
1089
1315
|
server: {
|
|
1090
1316
|
cors: userConfig.server?.cors ?? true,
|
|
@@ -1093,18 +1319,7 @@ function UserscriptPlugin(config) {
|
|
|
1093
1319
|
build: {
|
|
1094
1320
|
minify: userConfig.build?.minify ?? false,
|
|
1095
1321
|
assetsInlineLimit: userConfig.build?.assetsInlineLimit ?? Number.MAX_SAFE_INTEGER,
|
|
1096
|
-
rolldownOptions
|
|
1097
|
-
input,
|
|
1098
|
-
output: {
|
|
1099
|
-
format: "es",
|
|
1100
|
-
entryFileNames: (chunkInfo) => {
|
|
1101
|
-
if (scriptNames.has(chunkInfo.name)) return "[name].js";
|
|
1102
|
-
if (typeof userEntryFileNames === "function") return userEntryFileNames(chunkInfo);
|
|
1103
|
-
if (typeof userEntryFileNames === "string") return userEntryFileNames;
|
|
1104
|
-
return "assets/[name]-[hash].js";
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1322
|
+
rolldownOptions
|
|
1108
1323
|
}
|
|
1109
1324
|
};
|
|
1110
1325
|
},
|
|
@@ -1119,9 +1334,15 @@ function UserscriptPlugin(config) {
|
|
|
1119
1334
|
name: `${PLUGIN_NAME}:virtual`,
|
|
1120
1335
|
resolveId: (id) => {
|
|
1121
1336
|
if (id === "virtual:vite-userscript-plugin") return RESOLVED_VIRTUAL_MODULE_ID;
|
|
1337
|
+
if (command === "serve" && findResolvedExternal(resolved.scripts, id)) return toExternalModuleId(id);
|
|
1122
1338
|
},
|
|
1123
1339
|
load: (id) => {
|
|
1124
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);
|
|
1125
1346
|
}
|
|
1126
1347
|
},
|
|
1127
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"
|