Files
QZMusic-Web/src/plugins/nodePolyfills.ts

523 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 浏览器端 Node.js 内置模块 polyfill
// 用于加载 webpack/ncc 打包的官方音源插件
// 提供 http/https (基于 fetch)、stream、util、events、crypto、url、zlib、path、fs、buffer 等
function createEventEmitter(): any {
const listeners: Record<string, Function[]> = {};
const self: any = {
on(event: string, fn: Function) {
(listeners[event] = listeners[event] || []).push(fn);
return self;
},
once(event: string, fn: Function) {
const wrapper = (...args: any[]) => {
self.off(event, wrapper);
fn(...args);
};
return self.on(event, wrapper);
},
off(event: string, fn: Function) {
if (listeners[event]) {
listeners[event] = listeners[event].filter((f: Function) => f !== fn);
}
return self;
},
emit(event: string, ...args: any[]) {
const fns = listeners[event] || [];
for (const fn of fns) {
try { fn.apply(null, args); } catch (_) {}
}
return true;
},
addListener: function() { return self.on.apply(self, arguments as any); },
removeListener: function() { return self.off.apply(self, arguments as any); },
removeAllListeners(event?: string) {
if (event) delete listeners[event];
else { for (const k in listeners) delete listeners[k]; }
return self;
},
listeners(event: string) { return listeners[event] || []; },
};
return self;
}
function createStreamBase(): any {
function Stream() {}
Stream.prototype.pipe = function(dest: any) { return dest; };
Stream.prototype.on = function(_e: string, _fn: Function) { return this; };
Stream.prototype.emit = function() { return true; };
return Stream;
}
function createReadable(): any {
function Readable() {}
Readable.prototype.pipe = function(dest: any) { return dest; };
Readable.prototype.on = function() { return this; };
Readable.prototype.once = function() { return this; };
Readable.prototype.emit = function() { return true; };
Readable.prototype.read = function() { return null; };
Readable.prototype.pause = function() { return this; };
Readable.prototype.resume = function() { return this; };
Readable.prototype.push = function() { return this; };
return Readable;
}
function createWritable(): any {
function Writable() {}
Writable.prototype.write = function() { return true; };
Writable.prototype.end = function() { return this; };
Writable.prototype.on = function() { return this; };
Writable.prototype.once = function() { return this; };
Writable.prototype.emit = function() { return true; };
return Writable;
}
function createTransform(): any {
function Transform() {}
Transform.prototype.pipe = function(dest: any) { return dest; };
Transform.prototype.write = function() { return true; };
Transform.prototype.end = function() { return this; };
Transform.prototype.on = function() { return this; };
Transform.prototype.once = function() { return this; };
Transform.prototype.emit = function() { return true; };
return Transform;
}
function createPassThrough(): any {
function PassThrough() {}
PassThrough.prototype.pipe = function(dest: any) { return dest; };
PassThrough.prototype.write = function() { return true; };
PassThrough.prototype.end = function() { return this; };
PassThrough.prototype.on = function() { return this; };
PassThrough.prototype.once = function() { return this; };
PassThrough.prototype.emit = function() { return true; };
return PassThrough;
}
function parseUrl(u: string): any {
try {
const url = new URL(u, typeof window !== 'undefined' ? window.location.href : 'http://localhost/');
return {
protocol: url.protocol,
hostname: url.hostname,
host: url.host,
port: url.port || (url.protocol === 'https:' ? '443' : '80'),
pathname: url.pathname,
path: url.pathname + url.search,
search: url.search,
query: Object.fromEntries(url.searchParams.entries()),
hash: url.hash,
href: url.href,
auth: url.username ? url.username + (url.password ? ':' + url.password : '') : '',
};
} catch {
return { protocol: '', hostname: '', host: '', pathname: '', path: '', href: u, port: '80' };
}
}
// http/https 模块 - 用 fetch 实现
function createHttpModule(isHttps: boolean): any {
function request(options: any, callback?: (res: any) => void): any {
let url: string;
let opts: any = typeof options === 'string' ? {} : { ...options };
if (typeof options === 'string') {
url = options;
} else if (options && typeof options === 'object') {
const protocol = opts.protocol || (isHttps ? 'https:' : 'http:');
const host = opts.hostname || opts.host || 'localhost';
const port = opts.port ? ':' + opts.port : '';
const path = opts.path || opts.pathname || '/';
url = protocol + '//' + host + port + path;
} else {
url = String(options);
}
const method = opts.method || 'GET';
const headers = opts.headers || {};
const req: any = createEventEmitter();
let requestBodyChunks: Uint8Array[] = [];
let ended = false;
req.write = function(chunk: any) {
if (chunk) requestBodyChunks.push(typeof chunk === 'string' ? new TextEncoder().encode(chunk) : chunk);
return true;
};
req.end = function(chunk: any) {
if (chunk) req.write(chunk);
if (ended) return;
ended = true;
let body: any = undefined;
if (method !== 'GET' && method !== 'HEAD' && requestBodyChunks.length > 0) {
if (requestBodyChunks.length === 1) body = requestBodyChunks[0];
else {
const total = requestBodyChunks.reduce((a, b) => a + b.length, 0);
const merged = new Uint8Array(total);
let offset = 0;
for (const c of requestBodyChunks) { merged.set(c, offset); offset += c.length; }
body = merged;
}
}
fetch(url, {
method,
headers,
body,
signal: opts.signal,
mode: 'cors',
credentials: 'omit',
} as any).then((resp: any) => {
const res: any = createEventEmitter();
res.statusCode = resp.status;
res.statusMessage = resp.statusText;
res.headers = {};
try {
resp.headers.forEach((v: string, k: string) => { res.headers[k.toLowerCase()] = v; });
} catch {}
res.setEncoding = function() { return res; };
res.pipe = function(dest: any) {
resp.arrayBuffer().then((buf: ArrayBuffer) => {
if (dest && typeof dest.write === 'function') dest.write(new Uint8Array(buf));
if (dest && typeof dest.end === 'function') dest.end();
res.emit('end');
}).catch((err: Error) => res.emit('error', err));
return dest;
};
res.text = () => resp.text();
res.json = () => resp.json();
res.body = resp.body;
if (callback) callback(res);
req.emit('response', res);
// 如果没有 callback等待 body 然后发射 data/end 事件
if (!callback) {
resp.arrayBuffer().then((buf: ArrayBuffer) => {
res.emit('data', new Uint8Array(buf));
res.emit('end');
}).catch((err: Error) => res.emit('error', err));
}
}).catch((err: Error) => {
req.emit('error', err);
});
return req;
};
req.abort = function() { req.emit('abort'); };
req.setTimeout = function() { return req; };
req.setHeader = function(name: string, value: string) { (headers as any)[name.toLowerCase()] = value; return req; };
req.getHeader = function(name: string) { return (headers as any)[name.toLowerCase()]; };
req.removeHeader = function(name: string) { delete (headers as any)[name.toLowerCase()]; };
// 如果用户立即调用 req.end(),我们需要在 next tick 开始 - 但实际上用户必须自己调用 end
return req;
}
function get(options: any, callback?: (res: any) => void): any {
const req = request(options, callback);
req.end();
return req;
}
return {
request,
get,
createServer: () => ({ listen: () => {}, close: () => {} }),
Agent: function() { this.maxSockets = 50; },
globalAgent: { maxSockets: 50 },
STATUS_CODES: {},
};
}
// util 模块
const utilModule: any = {
inspect: (obj: any) => {
try { return JSON.stringify(obj); } catch { return String(obj); }
},
format: function() { return Array.prototype.slice.call(arguments).join(' '); },
inherits: function(ctor: any, superCtor: any) {
if (!superCtor || !superCtor.prototype) return;
ctor.super_ = superCtor;
const origProto = ctor.prototype;
ctor.prototype = Object.create(superCtor.prototype, {
constructor: { value: ctor, enumerable: false, writable: true, configurable: true },
});
// 保留原型上原有的属性
if (origProto) {
for (const k in origProto) {
if (Object.prototype.hasOwnProperty.call(origProto, k)) {
ctor.prototype[k] = origProto[k];
}
}
}
},
promisify: function(fn: any) {
return function() {
const args = Array.prototype.slice.call(arguments);
return new Promise((resolve, reject) => {
args.push((err: any, ...rest: any[]) => {
if (err) reject(err);
else resolve(rest.length === 1 ? rest[0] : rest);
});
try { fn.apply(null, args); } catch (e) { reject(e); }
});
};
},
callbackify: function(fn: any) {
return function() {
const args = Array.prototype.slice.call(arguments);
const cb = args.pop();
fn.apply(null, args).then((r: any) => cb(null, r)).catch((e: Error) => cb(e));
};
},
TextDecoder: typeof (globalThis as any).TextDecoder !== 'undefined' ? (globalThis as any).TextDecoder : function(this: any) { this.decode = function() { return ''; }; },
TextEncoder: typeof (globalThis as any).TextEncoder !== 'undefined' ? (globalThis as any).TextEncoder : function(this: any) { this.encode = function(s: string) { return s; }; },
types: { isDate: (x: any) => x instanceof Date, isRegExp: (x: any) => x instanceof RegExp, isError: (x: any) => x instanceof Error, isArray: Array.isArray, isFunction: (x: any) => typeof x === 'function' },
};
// crypto 模块
const cryptoModule: any = {
createHash: function(algo: string) {
const chunks: string[] = [];
return {
update: function(chunk: any) { chunks.push(String(chunk)); return this; },
digest: function(encoding?: string) {
// 简化:不做真正的哈希,返回一个稳定的伪随机值
const s = algo + ':' + chunks.join('');
let h = 0;
for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
const hex = Math.abs(h).toString(16).padStart(16, '0') + Math.abs(h * 31).toString(16).padStart(16, '0');
if (encoding === 'hex') return hex;
if (encoding === 'base64') return btoa(hex);
return hex;
},
};
},
createHmac: function(algo: string, key: any) {
const chunks: string[] = [];
return {
update: function(chunk: any) { chunks.push(String(chunk)); return this; },
digest: function(encoding?: string) {
const s = 'hmac:' + algo + ':' + key + ':' + chunks.join('');
let h = 0;
for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
const hex = Math.abs(h).toString(16).padStart(16, '0') + Math.abs(h * 17).toString(16).padStart(16, '0');
if (encoding === 'hex') return hex;
if (encoding === 'base64') return btoa(hex);
return hex;
},
};
},
randomBytes: function(n: number, cb?: (err: Error | null, buf: any) => void) {
const arr = new Uint8Array(n);
if (typeof (globalThis as any).crypto !== 'undefined' && (globalThis as any).crypto.getRandomValues) {
(globalThis as any).crypto.getRandomValues(arr);
} else {
for (let i = 0; i < n; i++) arr[i] = Math.floor(Math.random() * 256);
}
const buf: any = {
buffer: arr,
length: n,
toString: function(enc?: string) {
if (enc === 'hex') {
let s = '';
for (let i = 0; i < n; i++) s += arr[i].toString(16).padStart(2, '0');
return s;
}
if (enc === 'base64') {
let s = '';
for (let i = 0; i < n; i++) s += String.fromCharCode(arr[i]);
return btoa(s);
}
return String.fromCharCode.apply(null, arr as any);
},
};
if (cb) { setTimeout(() => cb(null, buf), 0); return undefined; }
return buf;
},
randomUUID: function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
},
};
// 完整的 polyfill 注册表
export function createNodePolyfills(): Record<string, any> {
const streamMod: any = {
Stream: createStreamBase(),
Readable: createReadable(),
Writable: createWritable(),
Duplex: createTransform(),
Transform: createTransform(),
PassThrough: createPassThrough(),
pipeline: (_a: any, b: any, cb?: () => void) => { if (cb) cb(); return b; },
finished: (_s: any, cb: () => void) => { cb(); },
};
streamMod.Stream.prototype = Object.create(createEventEmitter());
streamMod.Readable.prototype = Object.create(streamMod.Stream.prototype);
streamMod.Writable.prototype = Object.create(streamMod.Stream.prototype);
streamMod.Duplex.prototype = Object.create(streamMod.Stream.prototype);
streamMod.Transform.prototype = Object.create(streamMod.Duplex.prototype);
streamMod.PassThrough.prototype = Object.create(streamMod.Transform.prototype);
const bufferModule: any = {
from: (x: any) => {
if (x instanceof Uint8Array) return x;
if (typeof x === 'string') return new TextEncoder().encode(x);
if (Array.isArray(x)) return new Uint8Array(x);
return new Uint8Array(x || 0);
},
alloc: (n: number) => new Uint8Array(n),
allocUnsafe: (n: number) => new Uint8Array(n),
isBuffer: () => false,
concat: (arrs: any[]) => {
const total = arrs.reduce((a, b) => a + (b?.length || 0), 0);
const merged = new Uint8Array(total);
let offset = 0;
for (const c of arrs) { if (c) { merged.set(c, offset); offset += c.length; } }
return merged;
},
byteLength: (x: any) => typeof x === 'string' ? new TextEncoder().encode(x).length : (x?.length || 0),
compare: (_a: any, _b: any) => 0,
isEncoding: () => true,
};
return {
http: createHttpModule(false),
https: createHttpModule(true),
http2: createHttpModule(true),
stream: streamMod,
util: utilModule,
events: { EventEmitter: function() { return createEventEmitter(); } },
crypto: cryptoModule,
url: {
parse: parseUrl,
format: (o: any) => o.href || `${o.protocol}//${o.host}${o.path || o.pathname || ''}`,
resolve: (_from: string, to: string) => { try { return new URL(to, _from).href; } catch { return to; } },
URL: typeof (globalThis as any).URL !== 'undefined' ? (globalThis as any).URL : function() {},
URLSearchParams: typeof (globalThis as any).URLSearchParams !== 'undefined' ? (globalThis as any).URLSearchParams : function() {},
},
zlib: {
gunzipSync: (d: any) => d,
inflateSync: (d: any) => d,
deflateSync: (d: any) => d,
gzipSync: (d: any) => d,
gunzip: (d: any, cb: (err: Error | null, out: any) => void) => { setTimeout(() => cb(null, d), 0); },
inflate: (d: any, cb: (err: Error | null, out: any) => void) => { setTimeout(() => cb(null, d), 0); },
createGunzip: () => { const p = Object.create(streamMod.Transform.prototype); p.write = () => true; p.end = function() { this.emit && this.emit('end'); return this; }; return p; },
createInflate: () => { const p = Object.create(streamMod.Transform.prototype); p.write = () => true; p.end = function() { this.emit && this.emit('end'); return this; }; return p; },
createGzip: () => { const p = Object.create(streamMod.Transform.prototype); p.write = () => true; p.end = function() { this.emit && this.emit('end'); return this; }; return p; },
},
path: {
join: function() { return Array.prototype.slice.call(arguments).filter(Boolean).join('/').replace(/\/+/g, '/'); },
resolve: function() { return Array.prototype.slice.call(arguments).filter(Boolean).join('/'); },
dirname: (p: string) => String(p).split('/').slice(0, -1).join('/') || '/',
basename: (p: string, ext?: string) => { const base = String(p).split('/').pop() || ''; return ext && base.endsWith(ext) ? base.slice(0, -ext.length) : base; },
extname: (p: string) => { const m = String(p).match(/\.[^.]*$/); return m ? m[0] : ''; },
sep: '/', delimiter: ':',
parse: (p: string) => { const parts = String(p).split('/'); return { root: '/', dir: parts.slice(0, -1).join('/'), base: parts[parts.length - 1], ext: '', name: parts[parts.length - 1].replace(/\.[^.]*$/, '') }; },
normalize: (p: string) => p,
isAbsolute: (p: string) => p && p[0] === '/',
relative: (_from: string, to: string) => to,
},
fs: {
readFileSync: () => '',
writeFileSync: () => {},
existsSync: () => false,
createWriteStream: () => { const s = Object.create(streamMod.Writable.prototype); s.write = () => true; s.end = function() { this.emit && this.emit('finish'); return this; }; return s; },
createReadStream: () => { const s = Object.create(streamMod.Readable.prototype); s.pipe = (d: any) => { s.emit && s.emit('end'); return d; }; return s; },
readFile: (_path: any, enc: any, cb: (err: Error | null, data: any) => void) => { if (typeof enc === 'function') cb = enc; setTimeout(() => cb(null, ''), 0); },
writeFile: (_p: any, _d: any, cb?: (err: Error | null) => void) => { if (typeof cb === 'function') setTimeout(() => cb(null), 0); },
mkdirSync: () => {},
statSync: () => ({ isDirectory: () => false, isFile: () => true, size: 0, mtime: new Date() }),
unlinkSync: () => {},
promises: {
readFile: async () => '',
writeFile: async () => {},
mkdir: async () => {},
},
},
os: {
platform: () => 'browser',
type: () => 'Browser',
release: () => '1.0',
tmpdir: () => '/tmp',
homedir: () => '/',
EOL: '\n',
arch: () => 'x64',
cpus: () => [],
totalmem: () => 0,
freemem: () => 0,
},
assert: {
ok: function() {},
equal: function() {},
deepEqual: function() {},
strictEqual: function() {},
throws: function() {},
notEqual: function() {},
ifError: function() {},
},
dns: {
lookup: (_host: string, cb: (err: Error | null, addr: string, family: number) => void) => { setTimeout(() => cb(null, '127.0.0.1', 4), 0); },
resolve: (_host2: string, cb: (err: Error | null, addrs: string[]) => void) => { setTimeout(() => cb(null, ['127.0.0.1']), 0); },
},
buffer: bufferModule,
Buffer: bufferModule,
string_decoder: {
StringDecoder: function() {
this.write = function(x: any) { return typeof x === 'string' ? x : new TextDecoder().decode(x); };
this.end = function(x: any) { return x ? this.write(x) : ''; };
},
},
querystring: {
parse: (s: string) => {
const out: Record<string, string> = {};
const q = String(s || '').replace(/^\?/, '');
if (!q) return out;
for (const pair of q.split('&')) {
const [k, v] = pair.split('=');
if (k) out[decodeURIComponent(k)] = v !== undefined ? decodeURIComponent(v) : '';
}
return out;
},
stringify: (obj: any) => Object.entries(obj || {}).map(([k, v]) => encodeURIComponent(String(k)) + '=' + encodeURIComponent(String(v))).join('&'),
},
tty: { isatty: () => false, ReadStream: function() {}, WriteStream: function() {} },
net: {
createServer: () => ({ listen: () => {}, close: () => {}, on: function() { return this; } }),
createConnection: () => createEventEmitter(),
Socket: function() { return createEventEmitter(); },
},
tls: {
connect: () => createEventEmitter(),
TLSSocket: function() { return createEventEmitter(); },
createSecureContext: () => ({}),
},
child_process: {
spawn: () => createEventEmitter(),
exec: (_cmd: string, cb: (err: Error | null, stdout: string, stderr: string) => void) => setTimeout(() => cb(null, '', ''), 0),
execSync: () => '',
},
cluster: { isWorker: false, isMaster: true, fork: function() {}, on: function() { return this; } },
module: { Module: function() {} },
vm: { runInNewContext: (code: string, sandbox: any) => { const fn = new Function(...Object.keys(sandbox || {}), code); return fn(...Object.values(sandbox || {})); } },
perf_hooks: { performance: typeof (globalThis as any).performance !== 'undefined' ? (globalThis as any).performance : { now: () => Date.now() } },
console,
};
}
// 为给定的 webpack bundle 代码提供完整的沙箱环境
export function createSandboxRequire(): (id: string) => any {
const polyfills = createNodePolyfills();
return function(id: string): any {
if (polyfills[id]) return polyfills[id];
// 处理子路径,如 stream/transform, crypto/hash 等
const base = id.split('/')[0];
if (polyfills[base]) return polyfills[base];
return {};
};
}