UNPKG

vite-plugin-monkey

Version:

A vite plugin server and build your.user.js for userscript engine like Tampermonkey and Violentmonkey and Greasemonkey

1,556 lines (1,555 loc) 71.1 kB
import { t as __exportAll } from "./chunk-CfYAbeIz.mjs"; import module from "node:module"; import * as acornWalk from "acorn-walk"; import { build, normalizePath } from "vite"; import { resolve } from "import-meta-resolve"; import fs from "node:fs/promises"; import path, { join } from "node:path"; import { pathToFileURL } from "node:url"; import { DomUtils, ElementType, parseDocument } from "htmlparser2"; import crypto from "node:crypto"; import MagicString from "magic-string"; import { exec } from "node:child_process"; import open from "open"; import spawn from "cross-spawn"; import colors from "picocolors"; //#region src/node/utils/gmApi.ts const gmIdentifiers = [ "GM_addElement", "GM_addStyle", "GM_addValueChangeListener", "GM_cookie", "GM_deleteValue", "GM_deleteValues", "GM_download", "GM_getResourceText", "GM_getResourceURL", "GM_getTab", "GM_getTabs", "GM_getValue", "GM_getValues", "GM_info", "GM_listValues", "GM_log", "GM_notification", "GM_openInTab", "GM_registerMenuCommand", "GM_removeValueChangeListener", "GM_saveTab", "GM_setClipboard", "GM_setValue", "GM_setValues", "GM_unregisterMenuCommand", "GM_webRequest", "GM_xmlhttpRequest", "GM_audio" ]; const gmMembers = [ "GM.addElement", "GM.addStyle", "GM.addValueChangeListener", "GM.cookie", "GM.deleteValue", "GM.deleteValues", "GM.download", "GM.getResourceText", "GM.getResourceUrl", "GM.getTab", "GM.getTabs", "GM.getValue", "GM.getValues", "GM.info", "GM.listValues", "GM.log", "GM.notification", "GM.openInTab", "GM.registerMenuCommand", "GM.removeValueChangeListener", "GM.saveTab", "GM.setClipboard", "GM.setValue", "GM.setValues", "GM.unregisterMenuCommand", "GM.webRequest", "GM.xmlHttpRequest", "GM.audio" ]; const othersGrantNames = [ "unsafeWindow", "window.close", "window.focus", "window.onurlchange" ]; const grantNames = [ ...gmMembers, ...gmIdentifiers, ...othersGrantNames ]; //#endregion //#region src/node/userscript/index.ts const finalMonkeyOptionToComment = async (option, collectGrantSet, mode) => { const { userscript, collectRequireUrls, collectResource } = option; let attrList = []; const { name, namespace, version, author, description, license, copyright, icon, iconURL, icon64, icon64URL, defaulticon, homepage, homepageURL, website, source, supportURL, downloadURL, updateURL, include, match, exclude, require, "exclude-match": excludeMatch, "inject-into": injectInto, "run-at": runAt, compatible, incompatible, antifeature, contributionAmount, contributionURL, connect, sandbox, tag, resource, grant, noframes, unwrap, webRequest, $extra } = userscript; Object.entries({ namespace, version, author, license, copyright, icon, iconURL, icon64, icon64URL, defaulticon, homepage, homepageURL, website, source, supportURL, downloadURL, updateURL, "inject-into": injectInto, "run-at": runAt, compatible, incompatible, contributionAmount, contributionURL, sandbox }).forEach(([k, v]) => { if (typeof v == "string") attrList.push([k, v]); }); Object.entries(name).forEach(([k, v]) => { if (k == "") attrList.push(["name", v]); else attrList.push(["name:" + k, v]); }); Object.entries(description).forEach(([k, v]) => { if (k == "") attrList.push(["description", v]); else attrList.push(["description:" + k, v]); }); Object.entries({ include, match, exclude, "exclude-match": excludeMatch }).forEach(([k, v]) => { v.forEach((v2) => { attrList.push([k, v2]); }); }); [...require, ...collectRequireUrls].forEach((s) => { attrList.push(["require", s]); }); Object.entries({ ...resource, ...collectResource }).forEach(([k, v]) => { attrList.push([ "resource", k, v ]); }); connect.forEach((s) => { attrList.push(["connect", s]); }); tag.forEach((s) => { attrList.push(["tag", s]); }); webRequest.forEach((s) => { attrList.push(["webRequest", s]); }); if (grant.has("none")) attrList.push(["grant", "none"]); else if (grant.has("*")) grantNames.forEach((s) => { attrList.push(["grant", s]); }); else new Set([...Array.from(collectGrantSet.values()).flat(), ...grant]).forEach((s) => { if (!s.trim()) return; attrList.push(["grant", s]); }); antifeature.forEach(({ description, type, tag }) => { attrList.push([ tag ? `antifeature:${tag}` : "antifeature", type, description ]); }); if (noframes) attrList.push(["noframes"]); if (unwrap) attrList.push(["unwrap"]); attrList.push(...$extra); attrList = defaultSortFormat(attrList); if (option.align >= 1) { const formatKey = (subAttrList) => { if (subAttrList.length == 0) return; const maxLen = Math.max(...subAttrList.map((s) => s[1].length)); subAttrList.forEach((s) => { s[1] = s[1].padEnd(option.align + maxLen); }); }; formatKey(attrList.filter((s) => s[0] == "resource")); formatKey(attrList.filter((s) => s[0] == "antifeature" || s[0].startsWith("antifeature:"))); const maxLen = Math.max(...attrList.map((s) => s[0].length)); attrList.forEach((s) => { s[0] = s[0].padEnd(option.align + maxLen); }); } const uString = [ "==UserScript==", ...attrList.map((attr) => "@" + attr.map((v) => { return v.endsWith(" ") ? v : v + " "; }).join("").trimEnd()), "==/UserScript==" ].map((s) => "// " + s).join("\n"); return option.generate({ userscript: uString, mode }); }; const stringSort = (a, b) => { const minLen = Math.min(a.length, b.length); for (let i = 0; i < minLen; i++) if (a[i] > b[i]) return 1; else if (a[i] < b[i]) return -1; if (a.length > b.length) return 1; else if (a.length < b.length) return -1; return 0; }; const defaultSortFormat = (p0) => { const filter = (predicate) => { const notMatchList = []; const matchList = []; p0.forEach((value, index) => { if (!predicate(value, index)) notMatchList.push(value); else matchList.push(value); }); p0 = notMatchList; return matchList; }; return [ filter(([k]) => k == "name"), filter(([k]) => k.startsWith("name:")), filter(([k]) => k == "namespace"), filter(([k]) => k == "version"), filter(([k]) => k == "author"), filter(([k]) => k == "description"), filter(([k]) => k.startsWith("description:")), filter(([k]) => k == "license"), filter(([k]) => k == "copyright"), filter(([k]) => k == "icon"), filter(([k]) => k == "iconURL"), filter(([k]) => k == "icon64"), filter(([k]) => k == "icon64URL"), filter(([k]) => k == "defaulticon"), filter(([k]) => k == "homepage"), filter(([k]) => k == "homepageURL"), filter(([k]) => k == "website"), filter(([k]) => k == "source"), filter(([k]) => k == "supportURL"), filter(([k]) => k == "downloadURL"), filter(([k]) => k == "updateURL"), filter(([k]) => k == "include"), filter(([k]) => k == "match"), filter(([k]) => k == "exclude"), filter(([k]) => k == "exclude-match"), filter(([k]) => k == "webRequest"), filter(([k]) => k == "require"), filter(([k]) => k == "resource").sort(stringSort), filter(([k]) => k == "sandbox"), filter(([k]) => k == "tag"), filter(([k]) => k == "connect"), filter(([k]) => k == "grant").sort(stringSort), filter(([k]) => k == "inject-into"), filter(([k]) => k == "run-at"), filter(([k]) => k == "compatible"), filter(([k]) => k == "incompatible"), filter(([k]) => k == "antifeature").sort(stringSort), filter(([k]) => k.startsWith("antifeature:")).sort(stringSort), filter(([k]) => k == "contributionAmount"), filter(([k]) => k == "contributionURL"), filter(([k]) => k == "noframes"), filter(([k]) => k == "unwrap"), p0 ].flat(1); }; //#endregion //#region src/node/utils/grant.ts const collectGrant = (context, chunks, injectCssCode, minify) => { const codes = /* @__PURE__ */ new Set(); if (injectCssCode) codes.add(injectCssCode); for (const chunk of chunks) { if (minify) Object.values(chunk.modules).forEach((m) => { const code = m.code; if (code) codes.add(code); }); codes.add(chunk.code); } const unusedMembers = new Set(grantNames.filter((s) => s.includes(`.`))); const endsWithWin = (a, b) => { if (a.endsWith(b)) return a === "monkeyWindow." + b || a === "_monkeyWindow." + b; return false; }; const memberHandleMap = Object.fromEntries(grantNames.filter((s) => s.startsWith("window.")).map((name) => [name, (v) => endsWithWin(v, name.split(".")[1])])); const unusedIdentifiers = new Set(grantNames.filter((s) => !s.includes(`.`))); const usedGm = /* @__PURE__ */ new Set(); const matchIdentifier = (name) => { if (unusedIdentifiers.has(name)) { usedGm.add(name); unusedIdentifiers.delete(name); return true; } return false; }; const matchMember = (name) => { for (const unusedName of unusedMembers.values()) if (name.endsWith(unusedName) || memberHandleMap[unusedName]?.(name)) { usedGm.add(unusedName); unusedMembers.delete(unusedName); return true; } return false; }; for (const code of codes) { if (!code.trim()) continue; const ast = context.parse(code); acornWalk.simple(ast, { MemberExpression(node) { if (unusedMembers.size === 0) return; if (node.computed || node.object.type !== "Identifier" || node.property.type !== "Identifier") return; if (node.object.name === "monkeyWindow" || node.object.name === "_monkeyWindow") { if (matchIdentifier(node.property.name)) return; } matchMember(node.object.name + "." + node.property.name); }, Identifier(node) { matchIdentifier(node.name); } }, { ...acornWalk.base }); if (unusedMembers.size == 0 && unusedIdentifiers.size == 0) break; } return usedGm; }; //#endregion //#region src/node/utils/others.ts const isFirstBoot = () => { return (Reflect.get(globalThis, "__vite_start_time") ?? 0) < 1e3; }; const compatResolve = (id) => { return resolve(id, pathToFileURL(process.cwd() + "/any.js").href); }; const existFile = async (path) => { try { return (await fs.stat(path)).isFile(); } catch { return false; } }; const moduleExportExpressionWrapper = (expression) => { let n = 0; let identifier = ``; while (expression.includes(identifier)) { identifier = `_${(n || ``).toString(16)}`; n++; } return `(()=>{const ${identifier}=${expression};('default' in ${identifier})||(${identifier}.default=${identifier});return ${identifier}})()`; }; async function* walk(dirPath) { const pathnames = (await fs.readdir(dirPath)).map((s) => path.join(dirPath, s)); while (pathnames.length > 0) { const pathname = pathnames.pop(); const state = await fs.lstat(pathname); if (state.isFile()) yield pathname; else if (state.isDirectory()) pathnames.push(...(await fs.readdir(pathname)).map((s) => path.join(pathname, s))); } } const stringifyFunction = (fn, ...args) => { return `;(${fn})(${args.map((v) => JSON.stringify(v)).join(",")});`; }; const dataJsUrl = (code) => { return "data:application/javascript," + encodeURIComponent(code); }; function dataUrl(p0, ...args) { if (typeof p0 == "string") return dataJsUrl(p0); return dataJsUrl(stringifyFunction(p0, ...args)); } const parserHtmlScriptResult = (html) => { const doc = parseDocument(html); return DomUtils.getElementsByTagType(ElementType.Script, doc).map((p) => { const src = p.attribs.src ?? ""; const textNode = p.firstChild; let text = ""; if (textNode?.type == ElementType.Text) text = textNode.data ?? ""; if (src) return { src, text }; else return { src: "", text }; }); }; const simpleHash = (str) => { return crypto.createHash("md5").update(str || "").digest("base64url").substring(0, 16); }; const safeURL = (url, base) => { if (!url) return void 0; try { return new URL(url, base); } catch {} }; const getSafeIdentifier = (prefix, code, others) => { let n = 0; let identifier = prefix; while (code.includes(identifier) || others && others.some((c) => c.includes(identifier))) { n++; identifier = `${prefix}${n.toString(16)}`; } return identifier; }; const getProgramImportNodes = (program) => { const nodes = []; acornWalk.simple(program, { ImportDeclaration(node) { const s = node.source; if (s.type === "Literal") { const value = s.value; if (!value) return; if (typeof value !== "string") return; nodes.push({ node, value }); } }, ImportExpression(node) { const s = node.source; if (s.type === "Literal") { const value = s.value; if (!value) return; if (typeof value !== "string") return; nodes.push({ node, value }); } else if (s.type === "TemplateLiteral") { if (s.expressions.length) return; if (s.quasis.length !== 1) return; const value = s.quasis[0].value.cooked; if (!value) return; if (typeof value !== "string") return; nodes.push({ node, value }); } } }); return nodes; }; const nameReg = /[0-9a-zA-Z_]+/g; const autoPreUnderline = (v) => { if (!v) return "_"; return Number.isInteger(Number(v[0])) ? `_${v}` : v; }; const getUpperCaseName = (value) => { if (!value) return; const list = value.match(nameReg); if (!list?.length) return; return list.map((v, i) => { if (i === 0) return autoPreUnderline(v); return v[0].toUpperCase() + v.substring(1); }).join(""); }; const defaultCssSideEffects = (c) => { if (typeof GM_addStyle === "function") GM_addStyle(c); else (document.head || document.documentElement).appendChild(document.createElement("style")).append(c); }; const getCssModuleCode = (f) => { f ??= defaultCssSideEffects; return ` const s = new Set; export const _css = async (t) => { if (s.has(t)) return; s.add(t); ${`(${f})(t);`} }; `.trimStart(); }; //#endregion //#region src/node/utils/systemjs.ts const _require = module.createRequire(import.meta.url); const systemjsPkg = _require(`systemjs/package.json`); const systemjsSubPaths = ["dist/system.min.js", "dist/extras/named-register.min.js"]; const customSystemInstanceCode = `;(typeof System!='undefined')&&(System=new System.constructor());`; const systemjsAbsolutePaths = systemjsSubPaths.map((s) => { return _require.resolve(`systemjs/` + s); }); const getSystemjsTexts = async () => { return Promise.all(systemjsAbsolutePaths.map((s) => fs.readFile(s, "utf-8").then((s) => s.trim().replace(/^\/\*[\s\S]*?\*\//, "").replace(/\/\/.*map$/, "").trim())).concat([Promise.resolve(customSystemInstanceCode)])); }; const getSystemjsRequireUrls = (fn) => { return systemjsSubPaths.map((p) => { return fn(systemjsPkg.version, systemjsPkg.name, p, p); }).concat([dataUrl(customSystemInstanceCode)]); }; //#endregion //#region src/node/utils/topLevelAwait.ts const awaitOffset = `await`.length; const initTlaIdentifier = `_TLA_`; const getSafeTlaIdentifier = (rawBundle) => { const codes = []; for (const chunk of Object.values(rawBundle)) if (chunk.type == "chunk") codes.push(chunk.code); let x = 0; let identifier = initTlaIdentifier; while (codes.some((code) => code.includes(identifier))) { x++; identifier = initTlaIdentifier + x.toString(36); } return identifier; }; const startWith = (text, searchString, position = 0, ignoreString) => { for (let i = position; i < text.length; i++) { if (ignoreString.includes(text[i])) continue; return text.startsWith(searchString, i); } return false; }; const includes = (str, start, end, substr) => { const i = str.indexOf(substr, start); return i >= 0 && i + substr.length < end; }; const transformTlaToIdentifier = (context, chunk, identifier) => { if (chunk.type == "chunk") { const code = chunk.code; if (!code.includes(`await`)) return; const ast = context.parse(code); const tlaNodes = []; const tlaForOfNodes = []; acornWalk.simple(ast, { AwaitExpression(node) { tlaNodes.push(node); }, ForOfStatement(node) { if (node.await === true) tlaForOfNodes.push(node); } }, { ...acornWalk.base, Function: () => {} }); if (tlaNodes.length > 0 || tlaForOfNodes.length > 0) { const ms = new MagicString(code); tlaNodes.forEach((node) => { if (!startWith(chunk.code, "(", node.start + awaitOffset, " \r\n")) { ms.appendLeft(node.start + awaitOffset, `(`); ms.appendRight(node.end, `)`); } ms.update(node.start, node.start + awaitOffset, identifier); }); tlaForOfNodes.forEach((node) => { ms.appendLeft(node.start, `${identifier + `FOR`}((async()=>{`); ms.appendRight(node.end, `})());`); }); return { code: ms.toString(), map: ms.generateMap() }; } } }; const transformIdentifierToTla = (context, chunk, identifier) => { if (chunk.type == "chunk") { if (!chunk.code.includes(identifier)) return; const forIdentifier = identifier + `FOR`; const ast = context.parse(chunk.code); const tlaCallNodes = []; const forTlaCallNodes = []; const topFnNodes = []; acornWalk.ancestor(ast, { CallExpression(node, _state, ancestors) { if ("name" in node.callee) { const { name, type } = node.callee; if (type === `Identifier`) { if (name === identifier) { let needsParens = false; if (ancestors.length >= 2) { const parent = ancestors[ancestors.length - 2]; needsParens = parent.type === "MemberExpression" && parent.object === node || parent.type === "CallExpression" && parent.callee === node || parent.type === "TaggedTemplateExpression" && parent.tag === node; } tlaCallNodes.push({ node: { ...node, callee: node.callee }, needsParens }); } else if (name === forIdentifier) forTlaCallNodes.push({ ...node, callee: node.callee }); } } } }, { ...acornWalk.base, Function: (node, state, callback) => { if (topFnNodes.length == 0) topFnNodes.push(node); if (includes(chunk.code, node.start, node.end, identifier)) return acornWalk.base.Function?.(node, state, callback); } }); if (tlaCallNodes.length > 0 || forTlaCallNodes.length > 0) { const ms = new MagicString(chunk.code, {}); tlaCallNodes.forEach(({ node, needsParens }) => { const callee = node.callee; ms.update(callee.start, callee.end, "await"); if (needsParens) { ms.appendLeft(node.start, "("); ms.appendRight(node.end, ")"); } }); forTlaCallNodes.forEach((node) => { const arg0 = node.arguments[0]; ms.update(node.start, arg0.start, "await"); ms.update(arg0.end, node.end, ""); }); topFnNodes.forEach((node) => { ms.appendLeft(node.start, `async\x20`); }); chunk.code = ms.toString(); } } }; //#endregion //#region src/node/plugins/css.ts const cssModuleId = "virtual:monkey-css"; const virtualCssModuleId = "\0" + cssModuleId; const styleExts = [ ".css", ".less", ".sass", ".scss", ".styl", ".stylus", ".pcss", ".postcss", ".sss" ]; const exludeModuleCssExts = styleExts.map((v) => ".module" + v); const appendInline = (value) => { return value + "?inline"; }; const exlcudeChars = [ "\0", "?", "&" ]; const exlcudeModuleNames = ["uno.css"]; const filterAsync = async (arr, predicate) => { const results = await Promise.all(arr.map(predicate)); return arr.filter((_, index) => results[index]); }; const staticCssIdSuffix = "__monkey-css"; const staticCssTemplate = ` import {0} from '{1}'; import { _css } from '${cssModuleId}'; {0} && _css({0}); export default undefined; `.trimStart(); const cssFactory = (getOption) => { let option; const isCssImport = async (context, importer, value) => { if (!value) return false; if (value.startsWith("virtual:")) return false; if (exlcudeModuleNames.includes(value)) return false; if (exlcudeChars.some((c) => value.includes(c))) return false; if (exludeModuleCssExts.some((c) => value.endsWith(c))) return false; if (option.build.externalResource[value]) return false; const resolvedId = (await context.resolve(value, importer))?.id; if (!resolvedId) return false; if (exlcudeChars.some((c) => resolvedId.includes(c))) return false; if (exludeModuleCssExts.some((c) => resolvedId.endsWith(c))) return false; if (!styleExts.some((e) => resolvedId.endsWith(e))) return false; return fs.access(resolvedId).then(() => true).catch(() => false); }; return { name: "monkey:css", apply: "build", enforce: "post", async config() { option = await getOption(); return { build: { rolldownOptions: { external: [cssModuleId] } } }; }, resolveId(source) { if (source.endsWith(staticCssIdSuffix)) return source; }, load(id) { if (!id.endsWith(staticCssIdSuffix)) return; const staticId = id.slice(0, -12); return staticCssTemplate.replaceAll("{0}", getUpperCaseName(staticId.split("/").at(-1)) || "css").replaceAll("{1}", appendInline(staticId)); }, async transform(code, id) { if (new URLSearchParams(id.split("?")[1] || "").has("inline")) return; if (!code.includes("import")) return; if (!styleExts.some((e) => code.includes(e))) return; const importedCssNodes = await filterAsync(getProgramImportNodes(this.parse(code)).filter((n) => { if (n.node.type === "ImportDeclaration" && n.node.specifiers.length) return false; return true; }), async (n) => isCssImport(this, id, n.value)); if (!importedCssNodes.length) return; const ms = new MagicString(code); const loadName = getSafeIdentifier("_css", code); const importList = [`import {_css as ${loadName}} from '${cssModuleId}';`]; const nameCache = {}; for (const n of importedCssNodes) if (n.node.type === "ImportExpression") { const inlineCssId = appendInline(n.value); if (!nameCache[inlineCssId]) { const cssName = getSafeIdentifier(getUpperCaseName(n.value) || "css", code, importList); nameCache[inlineCssId] = cssName; importList.push(`import ${cssName} from '${inlineCssId}';`); } ms.update(n.node.start, n.node.end, `${loadName}(${nameCache[inlineCssId]})`); } else { const resolved = (await this.resolve(n.value, id))?.id; if (!resolved) continue; const staticCssId = resolved + staticCssIdSuffix; ms.update(n.node.start, n.node.end, `import '${staticCssId}';`); } ms.prepend(importList.join("\n")); return { code: ms.toString(), map: ms.generateMap() }; } }; }; //#endregion //#region src/node/plugins/buildBundle.ts const __entry_name = `__monkey.entry.js`; const cssModuleEntryId = `virtual:monkey-css-entry`; const virtualCssModuleEntryId = "\0" + cssModuleEntryId; const polyfillId = "\0vite/legacy-polyfills"; const systemJsImportMapPrefix = `user`; const buildBundleFactory = (getOption) => { let option; let viteConfig; return { name: "monkey:buildBundle", apply: "build", enforce: "post", async config() { option = await getOption(); }, async configResolved(resolvedConfig) { viteConfig = resolvedConfig; }, async generateBundle(_, rawBundle) { const entryChunks = []; const chunks = []; Object.values(rawBundle).forEach((chunk) => { if (chunk.type == "chunk") { if (chunk.facadeModuleId != polyfillId) chunks.push(chunk); if (chunk.isEntry) if (chunk.facadeModuleId == polyfillId) entryChunks.unshift(chunk); else entryChunks.push(chunk); } }); const fristEntryChunk = entryChunks.find((s) => s.facadeModuleId != polyfillId); const cssCode = Object.entries(rawBundle).map(([k, v]) => { if (v.type == "asset" && k.endsWith(".css")) { delete rawBundle[k]; return v.source.toString(); } }).filter(Boolean).join("").trim(); let cssJsCode = ""; const entryCode = (() => { const e = Array.from(entryChunks); const codes = []; if (cssCode) { if (e[0].facadeModuleId === polyfillId) { codes.push(`import ${JSON.stringify(`./${e[0].fileName}`)};`); e.shift(); } codes.push(`import '${cssModuleEntryId}';`); } codes.push(...e.map((c) => `import './${c.fileName}';`)); return codes.join("\n"); })(); const hasDynamicImport = entryChunks.some((e) => { if (e.dynamicImports.length) return true; if (!e.code.includes("import")) return false; let a = Boolean(false); const ast = this.parse(e.code); try { acornWalk.simple(ast, { ImportExpression() { a = true; throw new Error("stop"); } }); } catch {} return a; }); const usedModules = /* @__PURE__ */ new Set(); const tlaIdentifier = getSafeTlaIdentifier(rawBundle); let finalJsCode = ``; const mockPlugin = { name: "monkey:mock", resolveId: { order: "pre", handler: (source, importer, options) => { if (!importer && options.isEntry) return "\0" + source; if (source === cssModuleEntryId) return virtualCssModuleEntryId; if (source === "virtual:monkey-css") return virtualCssModuleId; if (Object.values(rawBundle).find((chunk) => chunk.type == "chunk" && source.endsWith(chunk.fileName))) return "\0" + source; } }, load: { order: "pre", handler: (id) => { if (!id.startsWith("\0")) return; if (id === virtualCssModuleEntryId) return `import { _css } from '${cssModuleId}'; _css(${JSON.stringify(" " + cssCode + " ")});`; if (id === "\0virtual:monkey-css") { cssJsCode = getCssModuleCode(option.cssSideEffects); return cssJsCode; } if (id.endsWith(__entry_name)) return entryCode; const [k, chunk] = Object.entries(rawBundle).find(([_, chunk]) => id.endsWith(chunk.fileName)) ?? []; if (chunk && chunk.type == "chunk" && k) { usedModules.add(k); if (!hasDynamicImport) { const ch = transformTlaToIdentifier(this, chunk, tlaIdentifier); if (ch) return ch.code; } return chunk.code; } } } }; const minify = viteConfig.build.minify ?? false; if (hasDynamicImport) { const { rollup } = await import("rollup"); const chunks = (await (await rollup({ logLevel: "silent", external: Object.keys(option.globalsPkg2VarName), input: __entry_name, plugins: [mockPlugin] })).generate({ globals: option.globalsPkg2VarName, format: "systemjs", sourcemap: false, strict: true, compact: true })).output.flat(); const systemJsModules = []; let entryName = ""; chunks.forEach((chunk) => { if (chunk.type == "chunk") { const name = JSON.stringify(`./` + chunk.fileName); systemJsModules.push(chunk.code.trimStart().replace(/^System\.register\(/, `System.register(${name}, `)); if (chunk.isEntry) entryName = name; } }); systemJsModules.push(`System.import(${entryName}, "./");`); finalJsCode = systemJsModules.map((v) => v.trim()).join("\n"); const usedModuleIds = Array.from(this.getModuleIds()).filter((d) => d in option.globalsPkg2VarName); const importsMap = usedModuleIds.reduce((p, c) => { p[c] = `${systemJsImportMapPrefix}:${c}`; return p; }, {}); finalJsCode = [ Object.keys(importsMap).length > 0 ? `System.addImportMap({ imports: ${JSON.stringify(importsMap)} });` : ``, ...usedModuleIds.map((id) => `System.set(${JSON.stringify(`${systemJsImportMapPrefix}:${id}`)}, ${moduleExportExpressionWrapper(option.globalsPkg2VarName[id])});`), finalJsCode ].filter((s) => s.trim()).join("\n"); if (typeof option.systemjs == "function") option.collectRequireUrls.push(...getSystemjsRequireUrls(option.systemjs)); else finalJsCode = (await getSystemjsTexts()).join("\n") + "\n" + finalJsCode; } else { const rolldownMinify = (viteConfig.build.minify === void 0 || viteConfig.build.minify === "oxc") && !Array.isArray(viteConfig.build.rolldownOptions?.output) ? viteConfig.build.rolldownOptions.output?.minify : void 0; (await build({ logLevel: "error", configFile: false, build: { write: false, minify, terserOptions: viteConfig.build.terserOptions, target: "esnext", modulePreload: false, rolldownOptions: { external: Object.keys(option.globalsPkg2VarName), output: { comments: false, strict: false, intro: `'use strict'`, ...viteConfig.build.rolldownOptions.output, ...rolldownMinify !== void 0 ? { minify: rolldownMinify } : {}, globals: option.globalsPkg2VarName }, experimental: { attachDebugInfo: "none" } }, lib: { entry: __entry_name, formats: ["iife"], name: "__expose__", fileName: () => "__entry.js" } }, plugins: [{ ...mockPlugin, generateBundle: { order: "pre", handler(_, iifeBundle) { Object.entries(iifeBundle).forEach(([_, chunk]) => { transformIdentifierToTla(this, chunk, tlaIdentifier); }); } } }] }))[0].output.flat().forEach((chunk) => { if (chunk.type == "chunk" && chunk.isEntry) finalJsCode = chunk.code; }); } usedModules.forEach((k) => { if (fristEntryChunk != rawBundle[k]) delete rawBundle[k]; }); let collectGrantSet; if (option.build.autoGrant) collectGrantSet = collectGrant(this, chunks, cssJsCode, viteConfig.build.minify !== false); else collectGrantSet = /* @__PURE__ */ new Set(); const mergedCode = [await finalMonkeyOptionToComment(option, collectGrantSet, "build"), finalJsCode].filter((s) => s).join(`\n\n`).trimEnd() + "\n"; if (fristEntryChunk) { fristEntryChunk.fileName = option.build.fileName; fristEntryChunk.code = mergedCode; } else this.emitFile({ type: "asset", fileName: option.build.fileName, source: mergedCode }); if (option.build.metaFileName) this.emitFile({ type: "asset", fileName: option.build.metaFileName(), source: await finalMonkeyOptionToComment(option, collectGrantSet, "meta") }); } }; }; //#endregion //#region src/node/plugins/config.ts const configFactory = (getOption) => { let option; return { name: "monkey:config", async config(userConfig) { option = await getOption(); return { resolve: { alias: { [option.clientAlias]: "vite-plugin-monkey/dist/client" } }, build: { assetsInlineLimit: Number.MAX_SAFE_INTEGER, chunkSizeWarningLimit: Number.MAX_SAFE_INTEGER, assetsDir: "./", cssCodeSplit: false, minify: userConfig.build?.minify ?? false, cssMinify: userConfig.build?.cssMinify ?? true, sourcemap: false, rolldownOptions: { input: option.entry, onLog(level, log, defaultHandler) { if (level === "warn" && log.code === "TOLERATED_TRANSFORM" && log.message.includes("Top-level await is not available")) return; defaultHandler(level, log); }, experimental: { attachDebugInfo: "none" } } } }; } }; }; //#endregion //#region src/node/utils/pkg.ts const getProjectPkg = async () => { const rawPkg = await fs.readFile(path.resolve(process.cwd(), "package.json"), "utf-8").then(JSON.parse).catch(() => {}); const pkg = {}; if (!rawPkg) return pkg; Object.entries(rawPkg).forEach(([k, v]) => { if (typeof v == "string") Reflect.set(pkg, k, v); }); if (typeof rawPkg.author === "object" && typeof rawPkg.author?.name == "string") pkg.author = rawPkg.author.name; if (typeof rawPkg.bugs === "object" && typeof rawPkg.bugs?.url == "string") pkg.bugs = rawPkg.bugs.url; if (typeof rawPkg.repository === "object" && typeof rawPkg.repository?.url == "string") { const { url } = rawPkg.repository; if (url.startsWith("http")) pkg.repository = url; else if (url.startsWith("git+http")) pkg.repository = url.substring(4); } return pkg; }; const isScopePkg = (name) => name.startsWith("@"); const resolveModuleFromPath = async (subpath) => { const p = normalizePath(process.cwd()).split("/"); for (let i = p.length; i > 0; i--) { const p2 = `${p.slice(0, i).join("/")}/node_modules/${subpath}`; if (await existFile(p2)) return p2; } }; const compatResolveModulePath = async (id) => { try { return compatResolve(id); } catch (e) { const r = await resolveModuleFromPath(id); if (!r) throw e; return r; } }; const getModuleRealInfo = async (importName) => { const nameNoQuery = normalizePath(importName.split("?")[0]); const resolveName = await (async () => { const n = normalizePath(await compatResolveModulePath(nameNoQuery)).replace(/.*\/node_modules\/[^/]+\//, ""); if (isScopePkg(importName)) return n.split("/").slice(1).join("/"); return n; })(); let version = void 0; const nameList = nameNoQuery.split("/"); let name = nameNoQuery; while (nameList.length > 0) { name = nameList.join("/"); const filePath = await (async () => { const p = await resolveModuleFromPath(`${name}/package.json`); if (p) return p; try { return compatResolve(`${name}/package.json`); } catch { return; } })(); if (filePath === void 0 || !await existFile(filePath)) { nameList.pop(); continue; } version = JSON.parse(await fs.readFile(filePath, "utf-8")).version; break; } if (version === void 0) { console.warn(`[plugin-monkey] not found module ${nameNoQuery} version, use ${nameNoQuery}@latest`); name = nameNoQuery; version = "latest"; } return { version, name, resolveName }; }; //#endregion //#region src/node/plugins/externalGlobals.ts const externalGlobalsFactory = (getOption) => { let option; return { name: "monkey:externalGlobals", enforce: "pre", apply: "build", async config() { option = await getOption(); for (const [moduleName, varName2LibUrl] of option.build.externalGlobals) { const { name, version } = await getModuleRealInfo(moduleName); if (typeof varName2LibUrl == "string") option.globalsPkg2VarName[moduleName] = varName2LibUrl; else if (typeof varName2LibUrl == "function") option.globalsPkg2VarName[moduleName] = await varName2LibUrl(version, name, moduleName); else if (varName2LibUrl instanceof Array) { const [varName, ...libUrlList] = varName2LibUrl; if (typeof varName == "string") option.globalsPkg2VarName[moduleName] = varName; else if (typeof varName == "function") option.globalsPkg2VarName[moduleName] = await varName(version, name, moduleName); for (const libUrl of libUrlList) if (typeof libUrl == "string") option.requirePkgList.push({ url: libUrl, moduleName }); else if (typeof libUrl == "function") option.requirePkgList.push({ url: await libUrl(version, name, moduleName), moduleName }); } } return { build: { rolldownOptions: { external: Object.keys(option.globalsPkg2VarName) } } }; }, async generateBundle() { const usedModIdSet = new Set(Array.from(this.getModuleIds()).map((s) => normalizePath(s))); option.collectRequireUrls = option.requirePkgList.filter((p) => usedModIdSet.has(p.moduleName)).map((p) => p.url); } }; }; //#endregion //#region src/node/plugins/externalResource.ts const loaderModuleCode = ` import { GM_addStyle, GM_getResourceText, GM_getResourceURL } from 'vite-plugin-monkey/dist/client'; export const cssLoader = (name) => GM_addStyle(GM_getResourceText(name)); export const jsonLoader = (name) => JSON.parse(GM_getResourceText(name)); export const rawLoader = (name) => GM_getResourceText(name); export const urlLoader = (name, type) => { return GM_getResourceURL(name, false).replace( /^data:application;base64,/, 'data:' + type + ';base64', ); }; `; const loaderModId = "virtual:monkey-loader"; const virtualloaderModId = "\0" + loaderModId; const getExportModuleCode = (name, dynamic, params) => { const valueLiteral = `(${name}(${params.map((v) => JSON.stringify(v)).join(",")}))`; return [ `import {${name}} from '${loaderModId}'`, dynamic ? `let cache; export const _ = () => cache ?? (cache = ${valueLiteral})` : `export const _ = ${valueLiteral}`, `export default _` ].join(";"); }; const resPrefix = "virtual:monkey-resource-"; const resDynamicPrefix = "virtual:monkey-resource-dynamic-"; const isRawResId = (id) => { return id.startsWith(resPrefix) || id.startsWith(resDynamicPrefix); }; const getValueResId = (value, dynamic = false) => { return (dynamic ? resDynamicPrefix : resPrefix) + encodeURIComponent(value); }; const getVirtualResId = (id) => { return "\0" + id + "\0"; }; const getResNameTuple = (id) => { if (id.startsWith("\0") && id.endsWith("\0")) { if (id.startsWith(resDynamicPrefix, 1)) return [decodeURIComponent(id.slice(33, -1)), true]; else if (id.startsWith(resPrefix, 1)) return [decodeURIComponent(id.slice(25, -1)), false]; } }; const externalResourceFactory = (getOption) => { let option; let viteConfig; let mrmime; const resourceRecord = {}; let resKeys; return { name: "monkey:externalResource", enforce: "post", apply: "build", async config() { option = await getOption(); mrmime = await import("mrmime"); resKeys = Object.keys(option.build.externalResource); }, configResolved(config) { viteConfig = config; }, resolveId(id) { if (id === loaderModId) return virtualloaderModId; if (isRawResId(id)) return getVirtualResId(id); }, async transform(code) { if (!code.includes("import")) return; if (!resKeys.some((k) => code.includes(k))) return; const nodes = getProgramImportNodes(this.parse(code)).filter((n) => resKeys.includes(n.value)); if (!nodes.length) return; const ms = new MagicString(code); const importCodes = []; for (const { node, value } of nodes) if (node.type === "ImportDeclaration") ms.update(node.source.start, node.source.end, JSON.stringify(getValueResId(value))); else { const loadName = getSafeIdentifier(getUpperCaseName(value) || "r", code, importCodes); importCodes.push(`import { _ as ${loadName}} from ${JSON.stringify(getValueResId(value, true))};`); ms.update(node.start, node.end, `${loadName}()`); } ms.prepend(importCodes.join("\n")); return { code: ms.toString(), map: ms.generateMap() }; }, async load(id) { if (id === virtualloaderModId) return loaderModuleCode; const [importName, dynamic] = getResNameTuple(id) || []; if (dynamic === void 0) return; if (!importName) return; const pkg = await getModuleRealInfo(importName); const resOption = option.build.externalResource[importName]; const resourceName = await resOption.resourceName({ ...pkg, importName }); const resourceUrl = await resOption.resourceUrl({ ...pkg, importName }); resourceRecord[importName] = { resourceName, resourceUrl }; const loaderParam = { ...pkg, resourceName, resourceUrl, importName, dynamic }; if (resOption.nodeLoader) return resOption.nodeLoader(loaderParam); else if (resOption.loader) { const valueLiteral = `((${resOption.loader})(${JSON.stringify(loaderParam)}))`; return (dynamic ? `let cache; export const _ async()=>cache??(cache=${valueLiteral})` : `export const _ ${valueLiteral}`) + `\nexport default _`; } const [resourcePath, query] = importName.split("?", 2); const ext = resourcePath.split(".").at(-1) ?? ""; const mimeType = mrmime.lookup(ext) ?? "application/octet-stream"; const suffixSet = new URLSearchParams(query); return (() => { if (suffixSet.has("inline") && ext === "css") return getExportModuleCode("rawLoader", dynamic, [resourceName]); else if (suffixSet.has("url") || suffixSet.has("inline")) return getExportModuleCode("urlLoader", dynamic, [resourceName, mimeType]); else if (suffixSet.has("raw")) return getExportModuleCode("rawLoader", dynamic, [resourceName]); else if (ext == "json") return getExportModuleCode("jsonLoader", dynamic, [resourceName]); else if (ext == "css") return getExportModuleCode("cssLoader", dynamic, [resourceName]); else if (viteConfig.assetsInclude(resourcePath)) return getExportModuleCode("urlLoader", dynamic, [resourceName, mimeType]); else throw new Error(`module: ${importName} not found loader`); })(); }, generateBundle() { const usedModIdSet = /* @__PURE__ */ new Set(); Array.from(this.getModuleIds()).forEach((id) => { const name = getResNameTuple(id)?.[0]; if (name) usedModIdSet.add(name); }); const collectResource = {}; Object.entries(resourceRecord).forEach(([importName, { resourceName, resourceUrl }]) => { if (usedModIdSet.has(importName)) collectResource[resourceName] = resourceUrl; }); option.collectResource = collectResource; } }; }; //#endregion //#region src/node/plugins/fixAssetUrl.ts /** * convert `export default "/src/assets/a.png"` to `export default new URL("/src/assets/a.png", import.meta['url']).href` */ const fixAssetUrlFactory = () => { let viteConfig; return { name: "monkey:fixAssetUrl", apply: "serve", async configResolved(resolvedConfig) { viteConfig = resolvedConfig; }, async transform(code, id) { const [_, query = "url"] = id.split("?", 2); if ((query.split("&").includes("url") || viteConfig.assetsInclude(id)) && code.match(/^\s*export\s+default/)) { const defaultNode = this.parse(code).body[0]; if (defaultNode?.type == "ExportDefaultDeclaration") { const childNode = defaultNode?.declaration; if (childNode?.type == "Literal" && typeof childNode.value == "string" && childNode.value[0] === "/") return `export default new URL(${JSON.stringify(childNode.value)}, import.meta['url']).href`; } } } }; }; //#endregion //#region src/node/plugins/fixClient.ts const fixClientFactory = () => { return { name: "monkey:fixClient", apply: "serve", async transform(code, id) { if (id.endsWith("node_modules/vite/dist/client/client.mjs")) return code.replaceAll("__BASE__", `new URL(__BASE__ || '/', import.meta['url']).href`); } }; }; //#endregion //#region src/node/plugins/fixCssUrl.ts const fixCssUrlFactory = () => { return { name: "monkey:fixCssUrl", apply: "serve", async config() { const postUrl = (await import("postcss-url")).default; return { css: { postcss: { plugins: [postUrl({ url: "inline" })] } } }; } }; }; //#endregion //#region src/node/plugins/fixWorker.ts const isWorkerRequest = (id) => { const queryIndex = id.indexOf("?"); if (queryIndex === -1) return false; const query = new URLSearchParams(id.slice(queryIndex + 1)); return query.has("worker") && !query.has("url"); }; const workerWrapper = ` const dataUri = \`data:text/javascript;charset=utf-8,\${encodeURIComponent( \`import \${JSON.stringify( new URL('?worker_file&type=module', import.meta['url']).href, )};\`, )}\`; export default function WorkerWrapper(options) { return new Worker(dataUri, { type: 'module', name: options?.name, }); } `.trimStart(); const fixWorkerFactory = () => { return { name: "monkey:fixWorker", enforce: "pre", apply: "serve", load(id) { if (isWorkerRequest(id)) return workerWrapper; } }; }; //#endregion //#region src/node/utils/template.ts const htmlText = ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="https://vite.dev/logo.svg" /> <title>Vite</title> </head> <script type="module" data-source="vite-plugin-monkey"> __CODE__ <\/script> </html> `.trimStart(); const fcToHtml = (fn, ...args) => { return htmlText.replace(`__CODE__`, stringifyFunction(fn, ...args)); }; const serverInjectFn = (entrySrc, key) => { window.GM; document[key] = window; const script = document.createElement("script"); script.type = "module"; if (window.trustedTypes) script.src = window.trustedTypes.createPolicy(key, { createScriptURL: (input) => input }).createScriptURL(entrySrc); else script.src = entrySrc; (document.head || document.documentElement).append(script); }; const mountGmApiFn = (key, apiNames) => { const monkeyWindow = document[key]; if (!monkeyWindow) { console.warn(`[vite-plugin-monkey] not found monkeyWindow`); return; } window.unsafeWindow = window; apiNames.push("GM"); apiNames.forEach((apiName) => { if (monkeyWindow[apiName]) window[apiName] = monkeyWindow[apiName]; }); }; const virtualHtmlTemplate = async (url) => { const delay = (n = 0) => new Promise((res) => setTimeout(res, n)); await delay(); const u = new URL(url, location.origin); u.searchParams.set("origin", u.origin); if (window == window.parent) { location.href = u.href; await delay(500); window.close(); return; } const style = document.createElement("style"); document.head.append(style); style.innerText = ` body { font-family: Arial, sans-serif; margin: 0; } .App { margin: 25px; } p { font-size: 1.5em; } a { color: blue; text-decoration: none; font-size: 1.5em; } a:hover { text-decoration: underline; } `.trim(); document.body.innerHTML = ` <div class="App"> <h1>PREVIEW PAGE</h1> <p>Click the links below to install userscripts:</p> <a target="_blank"></a></th> </div> `.trim(); await delay(); const a = document.querySelector("a"); a.href = location.href; a.text = location.href; }; const previewTemplate = async (urls) => { const delay = (n = 0) => new Promise((res) => setTimeout(res, n)); await delay(); const style = document.createElement("style"); document.head.append(style); style.innerText = ` body { font-family: Arial, sans-serif; margin: 0; } .App { margin: 25px; } p { font-size: 1.5em; } table { width: 100%; border-collapse: collapse; font-size: 1.5em; } th, td { border: 1px solid black; padding: 8px; text-align: left; } th { background-color: #f2f2f2; } a { color: blue; text-decoration: none; } a:hover { text-decoration: underline; } `.trim(); if (window == window.parent && urls.length == 1) { const u = new URL(urls[0], location.origin); location.href = u.href; await delay(500); window.close(); return; } else if (urls.length == 0) { document.body.innerHTML = ` <div class="App"> <h1> There is no script to install </h1> </div> `.trim(); return; } else { document.body.innerHTML = ` <div class="App"> <h1>PREVIEW PAGE</h1> <p>Click the links below to install userscripts:</p> <table> <tr> <th>No.</th> <th>Install Link</th> </tr> </table> </div> `.trim(); await delay(); const table = document.querySelector(`table`); urls.sort().forEach((u, index) => { const tr = document.createElement("tr"); const td1 = document.createElement("td"); const td2 = document.createElement("td"); const a = document.createElement("a"); td1.innerText = `${index + 1}`; if (window != window.parent) a.target = "_blank"; a.href = u; a.textContent = new URL(u, location.origin).href; td2.append(a); tr.append(td1); tr.append(td2); table.append(tr); }); } }; //#endregion //#region src/node/plugins/perview.ts const perviewFactory = () => { let viteConfig; return { name: "monkey:perview", apply: "serve", configResolved(config) { viteConfig = config; }, async configurePreviewServer(server) { server.middlewares.use(async (req, res, next) => { if (["/", "/index.html"].includes((req.url ?? "").split("?")[0])) { const distDirPath = path.join(process.cwd(), viteConfig.build.outDir); const urls = []; for await (const pathname of walk(distDirPath)) if (pathname.endsWith(".user.js")) { const fileName = normalizePath(path.relative(distDirPath, pathname)); urls.push(`/` + fileName); } res.setHeader("content-type", "text/html; charset=utf-8"); res.end(fcToHtml(previewTemplate, urls)); return; } next(); }); } }; }; //#endregion //#region src/node/plugins/removePreload.ts const removePreloadFactory = () => { return { name: "monkey:removeVitePreload", apply: "build", config() { return { build: { modulePreload: false } }; }, configResolved(config) { const plugin = config.plugins.find((p) => p.name === "native:import-analysis-build"); if (plugin) plugin.applyToEnvironment = void 0; } }; }; //#endregion //#region src/node/plugins/redirectClient.ts const clientSourceId = "vite-plugin-monkey/dist/client"; const clientId = "\0" + clientSourceId; const redirectClientFactory = () => { return { name: "monkey:redirectClient", enforce: "pre", apply: "build", resolveId(source) { if (source === clientSourceId) return clientId; }, load(id) { if (id == clientId) { const identifiers = [ "GM", ...gmIdentifiers, "unsafeWindow" ]; const declarations = identifiers.map((v) => { return `var _${v} = /* @__PURE__ */ (() => typeof ${v} != "undefined" ? ${v} : undefined)();`; }).concat("var _monkeyWindow = /* @__PURE__ */ (() => window)();"); const exportIdentifiers = identifiers.concat("monkeyWindow"); return declarations.join("\n") + `\nexport {${exportIdentifiers.map((v) => ` _${v} as ${v},`).join("\n")}};`; } } }; }; //#endregion //#region src/node/utils/openBrowser.ts const VITE_PACKAGE_DIR = path.dirname(compatResolve("vite/package.json")); /** * Reads the BROWSER environment variable and decides what to do with it. */ function openBrowser(url, opt) { const browser = typeof opt === "string" ? opt : process.env.BROWSER || ""; if (browser.toLowerCase().endsWith(".js")) executeNodeScript(browser, url); else if (browser.toLowerCase() !== "none") startBrowserProcess(browser, process.env.BROWSER_ARGS ? process.env.BROWSER_ARGS.split(" ") : [], url); } function executeNodeScript(scriptPath, url) { const extraArgs = process.argv.slice(2); spawn(process.execPath, [ scriptPath, ...extraArgs, url ], { stdio: "inherit" }).on("close", (code) => { if (code !== 0) console.error("[plugin-monkey] " + colors.red(`\nThe script specified as BROWSER environment variable failed.\n\n${colors.cyan(scriptPath)} exited with code ${code}.`), { error: null }); }); } const supportedChromiumBrowsers = [ "Google Chrome Canary", "Google Chrome Dev", "Google Chrome Beta", "Google Chrome", "Microsoft Edge", "Brave Browser", "Vivaldi", "Chromium" ]; async function startBrowserProcess(browser, browserArgs, url) { const preferredOSXBrows